一.进程
1.基本概念与基本操作
客观概念:程序的一次执行实例,正在运行的程序。
从操作系统角度:进程是一个负责和管理系统资源的实体。
Linux认为一个进程等于:
task_struct + 代码 + 数据
其中task_struct相当于进程的身份证 + 档案
里面记录:
- 谁是这个进程
- 当前状态
- 优先级
- 内存在哪里
- 打开了哪些文件
2.进程PCB
进程信息被放在⼀个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
操作系统为了管理进程,会给每个进程建立一个结构体。
task_struct 是PCB的一种
3.task_struct
内容分类:
1. 标识符(PID)
作用:区分不同进程。
例如:
PID=1001 微信
PID=1002 浏览器
PID=1003 QQ
Linux查看:ps aux
可以看到PID
2. 状态
表示进程当前情况。
Linux常见:
| 状态 | 含义 |
|---|---|
| R | 运行 |
| S | 睡眠 |
| D | 不可中断睡眠 |
| T | 停止 |
| Z | 僵尸 |
3. 优先级
CPU资源有限。
多个进程竞争CPU。
例如:
游戏 优先级高
后台更新 优先级低
调度器根据优先级决定谁先运行。
4. 程序计数器(PC)
保存下一条要执行的指令地址。
例如程序:
1 printf("hello");
2 printf("world");
3 return 0;
执行到第二行
CPU中:PC ---> 第二行地址
5. 内存指针
记录进程代码和数据在哪里。
6. 上下文数据
CPU运行进程时:
需要保存:
寄存器数据
程序计数器
栈指针
例如:进程A运行:
CPU
|
寄存器
|
A的数据
突然切换运行进程B
CPU必须保存A当前状态
以后回来恢复A继续执行
这个保存的信息叫:上下文
类似玩游戏存档。
退出游戏保存:
等级
装备
位置
下次继续。
7. IO状态信息
记录:进程使用的输入输出资源。
例如:打开文件:test.txt
task_struct记录:
打开了哪些文件
使用哪些设备
8. 记账信息
记录资源使用情况。
例如:
CPU运行时间
占用内存大小
运行次数
方便性能统计和资源限制。
进程的组织
Linux中:
所有进程不是乱放的。
而是使用:双向链表组织
结构类似:
task_struct
|
v
+------+ +------+ +------+
|进程1 |<--->|进程2 |<--->|进程3 |
+------+ +------+ +------+
4.查看进程
1. 通过 /proc 查看进程
Linux里面有一个特殊目录:/proc
它不是普通磁盘文件夹,而是:
内核提供的虚拟文件系统
里面保存了当前系统运行进程的信息。
例如查看:ls /proc
你会看到:
1
2
3
500
1000
...
这些数字就是:PID
比如:/proc/1
表示查看PID为1的进程信息。
进入:cd /proc/1
查看:ls
会看到:
cmdline
status
cwd
exe
fd
maps
...
2. 使用ps查看进程
ps 是最常用的查看进程命令。
例如:ps
显示:
PID TTY TIME CMD
1234 pts/0 00:00 bash
但是默认只显示当前终端进程。
一般使用:ps aux或者ps ajx查看所有进程。
3. 使用top查看进程
top类似Windows任务管理器。
输入:top
显示:
PID USER CPU MEM COMMAND
1 root 0 1% systemd
2330 whb 0 0% test
可以实时看到:
- CPU占用
- 内存占用
- 进程状态
退出:按q
5.通过系统调用获取进程标示符
Linux中,每一个进程都有自己的身份信息:
- PID(Process ID)
- 进程编号
- 唯一标识一个进程
- PPID(Parent Process ID)
- 父进程编号
- 表示是谁创建了当前进程
Linux中进程不是凭空产生的。
一般父进程创建子进程
例如:
打开终端:
systemd
|
bash
|
./test
那么bash的PPID = systemd的PID
test的PPID = bash的PID
6.通过系统调用创建进程-fork
fork() 是 Linux 中创建进程的系统调用。
作用:创建一个新的进程。
调用fork之前:只有一个进程
父进程 PID=1000
调用fork()之后:
fork()
|
----------------
| |
父进程 子进程
PID=1000 PID=1001
原来的进程叫:父进程(father)
新创建的进程叫:子进程(child)
fork有两个返回值
很多函数一般一个函数调用,一个返回值。
但是fork特殊。
原因:fork创建了一个新的执行流。
fork执行以后:
实际上有两个进程继续往下运行:
fork()
|
----------------
| |
父进程 子进程
所以两个进程都需要知道fork的结果。
因此fork返回两次。
fork返回值
情况1:返回给父进程
父进程中:ret = 子进程PID
例如:
父进程:PID=3000
创建:子进程PID=3001
父进程得到:ret=3001
也就是:父进程拿到子进程的PID
情况2:返回给子进程
子进程:ret=0
例如:
子进程: PID=3001 ret=0
为什么不是自己的PID?
因为:子进程可以通过:getpid()
得到自己PID。
但是:子进程需要知道:我是不是子进程?
所以规定:fork返回0表示自己是子进程
父子进程代码共享,数据独立
fork之后:
父子共享代码
例如:
printf()
代码只有一份。
但是数据各自一份。
例如:
int a=10;
fork();
a++;
fork后:
父进程: a=10 a++
子进程: a=10 a++
最终:
父: a=11 子: a=11
互不影响。
二.进程状态
1.进程状态
1. R运行状态(Running)
表示:进程正在运行,或者等待运行。
注意:
R不一定代表正在占用CPU。
因为CPU一次只能运行一个进程。
例如有三个进程:
CPU
运行
|
进程A
运行队列:
进程B
进程C
此时:
- A正在CPU执行
- B、C也属于R状态
因为它们:已经准备好了,等待CPU调度。
所以:R = 正在运行 + 等待运行
2. S睡眠状态(Sleeping)
表示:进程正在等待某个事件发生。
例如程序:
while(1)
{
sleep(1);
}
执行:
运行
|
sleep()
|
等待1秒
|
唤醒
sleep期间:进程状态:S
因为它没有事情做,让CPU去执行其他程序。
可中断睡眠
S也叫:可中断睡眠(interruptible sleep)
意思:睡眠中的进程可以被信号唤醒。
例如程序:
pause();
等待信号:
睡眠
|
收到信号
|
继续运行
3. D磁盘休眠状态
也叫:不可中断睡眠
表示:进程正在等待IO完成。
例如:
程序读取硬盘:
read(file);
过程:
进程
|
请求硬盘数据
|
等待磁盘
|
D状态
|
数据返回
|
继续运行
为什么叫不可中断?
因为如果磁盘IO过程中被强制打断,可能导致:
- 数据错误
- 文件损坏
所以Linux一般不允许随便终止。
4. T停止状态(Stopped)
表示:进程暂停执行。
常见方式:
发送:
SIGSTOP
例如:
查看进程:
ps aux
暂停:
kill -19 PID
进程:
运行
|
T状态
恢复:
发送:
SIGCONT
命令:
kill -18 PID
状态:
T
↓
R
继续运行。
5. X死亡状态(Dead)
表示:进程已经死亡。
但是:这个状态非常短暂。
过程:
运行
↓
退出
↓
释放资源
↓
消失
所以使用:
ps
通常看不到X状态。
2.进程状态查看 ------ ps命令
作用:查看当前系统中的进程信息。
常用:
ps aux
或者:
ps axj
ps aux 命令
格式:
ps aux
三个参数:
- a
- u
- x
1. a 参数
a:显示所有终端中的进程
默认
ps
只能查看当前终端自己的进程。
例如:
ps
可能:
PID CMD
3000 bash
4000 ./test
ps a
可以看到:
其他用户以及其他终端的进程。
例如:
USER PID CMD
root 1 systemd
whb 3000 bash
tom 5000 vim
2. x 参数
x:显示没有控制终端的进程
什么叫控制终端?
比如你打开终端:
终端窗口
|
|
bash
|
程序
这个程序有终端。
但是后台程序:
例如:
服务器程序
nginx
mysql
systemd
没有终端。
这些叫:守护进程(daemon)
默认ps看不到。
加:
ps x
就可以显示。
3. u 参数
u:以用户为中心显示详细信息
例如:
ps u
显示:
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
解释:
| 字段 | 含义 |
|---|---|
| USER | 进程属于哪个用户 |
| PID | 进程ID |
| %CPU | CPU占用比例 |
| %MEM | 内存占用比例 |
| VSZ | 虚拟内存大小 |
| RSS | 实际占用内存 |
| TTY | 终端 |
| STAT | 进程状态 |
| START | 启动时间 |
| TIME | CPU使用时间 |
| COMMAND | 执行程序 |
ps axj 命令
格式:
ps axj
参数:
- a
- x
- j
j 参数
j:显示进程关系信息
包括:
-
PPID(父进程ID)
-
PGID(进程组ID)
-
SID(会话ID)
例如:
PPID PID PGID SID COMMAND
1 2000 2000 2000 bash
2000 3000 3000 2000 ./test
表示:
bash
|
|
test
3.僵尸进程
僵尸进程是 Linux 进程状态中比较特殊的一种:
状态:z
正常情况下:
子进程退出:
子进程结束
|
|
父进程回收
|
|
资源释放
但是如果:
子进程退出
↓
父进程还在运行
↓
父进程没有调用 wait() 获取子进程退出信息
↓
子进程变成 Z状态
这就是僵尸进程
Linux中进程退出时,并不是立即删除所有信息。
它会保留一部分信息:
例如:
- 退出码
- 退出原因
- PID
这些信息需要告诉父进程。
例如:
子进程:
return 5;
系统需要保存:退出状态 = 5
等父进程:
wait();
来读取。
wait()作用
父进程调用:
wait();
作用:获取子进程退出状态,并释放子进程资源。
僵尸进程的危害
僵尸进程虽然不运行,但是它的进程信息还保存在内核中,占用系统资源。
task_struct本身也是一个结构体。
需要占用内存。
同理:
Linux每个进程都有:struct task_struct
僵尸进程虽然没有运行
但是还存在
所以占用内存。
僵尸进程是一种内存泄漏。
它不是普通程序里的泄漏。
例如C语言:
malloc(100);
没有:
free();
这是用户空间内存泄漏。
僵尸进程:
父进程没有回收子进程
↓
内核没有释放task_struct
↓
资源泄漏
属于:进程资源泄漏。
4.孤儿进程
孤儿进程和僵尸进程正好相反。
简单理解:父进程先退出,子进程还在运行,这个子进程就叫孤儿进程。
正常情况下:
父进程
|
|
子进程
父进程负责:
- 创建子进程
- 管理子进程
- 回收子进程退出状态
但是如果:
父进程提前结束:
父进程退出
X
子进程继续运行
子进程失去了父亲。
所以叫:孤儿进程
孤儿进程被1号进程init/systemd领养。
Linux启动后:
第一个启动的用户空间进程:
PID:1
现代Linux:
一般是:
systemd
以前叫:
init
结构:
原来:
父进程
|
|
子进程
父进程退出:
systemd(PID=1)
|
|
子进程
此时子进程的PPID
会变成:1
为什么要让systemd领养?
因为:
最终子进程也可能退出。
例如:
systemd
|
|
孤儿进程
以后孤儿进程退出
那么需要有人调用:
wait()
回收它。
这个责任交给:
PID=1的systemd
三.进程优先级
1.基本概念
Linux是一个多任务操作系统,系统中同时存在很多进程,但是:
CPU资源有限,一个CPU同一时间只能执行一个进程。
所以Linux需要决定:
哪个进程先运行,哪个进程后运行。
这个决定依据就是:
进程优先级(priority)
1.基本概念
简单理解:CPU分配资源的先后顺序。
例如:
现在有两个进程:
进程A:游戏
进程B:后台下载
如果:游戏优先级高:
CPU
先运行游戏
再运行下载
如果下载优先级高:
CPU
先运行下载
再运行游戏
指定CPU运行(CPU绑定)
Linux还可以:
让某个进程固定运行在某个CPU上。
例如电脑:
CPU0
CPU1
CPU2
CPU3
可以设置:
普通程序 → CPU0
重要程序 → CPU1
这样减少不同CPU之间切换,提高性能。
2. 查看进程优先级
命令:
ps -l
查看:
例如:
UID PID PPID PRI NI CMD
root 1000 1 80 0 bash
whb 2000 1000 90 5 test
重点字段:
1.UID
表示:进程属于哪个用户
例如:
root
表示root用户创建。
2. PID
进程编号:
例如:
2000
唯一标识进程。
3. PPID
父进程ID。
表示:当前进程是谁创建的。
例如:
PPID=1000
表示:
PID 1000创建了它。
4. PRI(priority)
PRI:
表示:进程实际优先级。
特点 :数值越小,优先级越高。
例如:
进程A PRI=50
进程B PRI=90
CPU:
优先考虑:
A
因为:50 < 90
5. NI(nice值)
nice:
表示:对进程优先级进行修正的值。
范围:-20 ~ 19
共40个等级。
2.PRI和NI关系
重点公式:
PRI(new)=PRI(old)+nicePRI(new)=PRI(old)+nice
例如:
原来:
PRI=80
设置:
nice=5
那么:
PRI=80+5
=85
PRI变大:
优先级降低。
如果:
nice=-10
那么:
PRI=80-10
=70
PRI变小:
优先级升高。
PRI是:
真正参与CPU调度的优先级。
特点:越小越优先
NI是调整PRI的修正值。
不是最终优先级。
关系:
PRI = 原始优先级 + NI
所以:NI影响PRI。
3.修改进程优先级
方法1:top
输入:
top
进入后:
按:
r
输入:
PID
再输入:
nice值
例如:
PID:
2000
nice:
10
修改。
方法2:nice命令
启动程序时设置:
例如:
nice -n 5 ./test
表示:
以nice=5运行。
方法3:renice
修改已经运行的进程:
例如:
renice 10 -p 2000
修改:
PID=2000的nice值。
系统函数
Linux提供:
获取:
getpriority()
设置:
setpriority()
例如:
int getpriority(int which,int who);
获取进程优先级。
int setpriority(int which,int who,int prio);
设置优先级。
4.竞争、独立、并行、并发
1. 竞争性(Competition)
原因:
进程很多
CPU很少
例如:
CPU一个
进程100个
大家竞争:CPU资源
所以需要:优先级
2. 独立性(Independence)
多个进程运行:
彼此互不影响。
例如:
微信进程
浏览器进程
游戏进程
微信崩溃:不会影响浏览器。
原因:
每个进程有自己的:
- 地址空间
- 数据
- 资源
3. 并行(Parallel)
条件: 多个CPU
例如:
4核CPU:
CPU1 → 微信
CPU2 → 浏览器
CPU3 → 游戏
CPU4 → 音乐
多个进程真正同时运行。
叫并行。
4. 并发(Concurrency)
条件: 一个CPU
CPU:快速切换进程。
例如:
时间:
0ms 微信
10ms 浏览器
20ms 游戏
30ms 微信
看起来同时运行。
实际上CPU不断切换。
叫并发。
四.进程切换
进程切换就是CPU从执行一个进程,切换到执行另一个进程的过程。
这个过程叫:上下文切换(Context Switch)
1.CPU与runqueue(运行队列)
什么是runqueue?
CPU不能同时运行所有进程。
所以:
每个CPU都有一个:runqueue(运行队列)
里面保存:等待CPU执行的进程。
例如:
一个CPU:
CPU0
runqueue
进程A
进程B
进程C
CPU按照调度规则:
从runqueue中选择一个进程运行。
多CPU情况
例如:
4核CPU:
CPU0 → runqueue0
CPU1 → runqueue1
CPU2 → runqueue2
CPU3 → runqueue3
问题:
如果:
CPU0:
100个进程
CPU1:
2个进程
那么:
CPU0压力很大,
CPU1很空闲。
所以需要:负载均衡(load balance)
让进程在CPU之间迁移。
2 .进程优先级
Linux进程优先级分两类:
1. 普通优先级
范围:100 ~ 139
普通进程:
比如:
- 浏览器
- 微信
- 编辑器
都属于这个范围。
2. 实时优先级
范围:0 ~ 99
实时进程:
例如:
- 工业控制
- 音频处理
特点:
优先级非常高
为什么普通优先级是100~139?
之前学习nice值:
-20 ~ 19
一共:
40个等级。
对应:
100 ~ 139
关系:
nice=-20
优先级=100
nice=19
优先级=139
注意:
还是:数字越小,优先级越高。
3.活动队列(active queue)
什么是活动队列?
活动队列保存: 时间片还没有结束的进程。
例如:
进程:
A 剩余时间片5ms
B 剩余时间片10ms
C 剩余时间片2ms
它们进入:
active队列。
active队列结构
Linux设计:
queue[140]
表示:
140个优先级队列。
结构:
queue[0]
queue[1]
queue[2]
...
queue[139]
每一个数组元素:
就是一个进程队列。
例如:
优先级:
105
那么放入:
queue[105]
相同优先级如何排队?
采用:FIFO
First In First Out
先进先出。
例如:
queue120:
A → B → C
调度:先A再B再C
nr_active
表示:当前活动队列中有多少个运行状态进程。
例如:
nr_active=50
表示:有50个进程等待调度。
CPU如何选择进程?
过程:
第一步遍历:
queue[140]
从:
0
开始。
第二步:
找到第一个非空队列。
例如:
queue[0] 空
queue[1] 空
queue[2] 有进程
那么选择:
queue[2]
因为数字越小:
优先级越高。
第三步:
取该队列第一个进程。
例如:
queue[2]
A → B → C
选择:A
开始运行。
但我们通常不使用这种方法
虽然遍历140个队列:
数量固定。
时间复杂度:
O(140)
看起来是常数。
但是Linux追求极致性能。
如果每次调度都扫描:
还是浪费。
所以使用bitmap。
bitmap优化
bitmap:位图。
140个优先级:
需要:140个标记。
例如:
queue:
0 空
1 空
2 有
3 空
4 有
bitmap:
0010100
表示:
哪些队列非空。
Linux使用:
5 × 32bit
因为:
5*32=160
大于140。
这样可以快速找到第一个非空队列。
4 .过期队列(expired queue)
除了:
active队列
还有:
expired队列。
active队列:
保存:时间片没有用完的进程。
expired队列:
保存:时间片已经耗尽的进程。
例如进程A:
时间片:
10ms
运行10ms以后:
时间片结束。
进入:
expired队列
为什么需要expired队列?
避免某些进程一直占用CPU。
例如三个进程:
A
B
C
如果A一直优先:
可能:B、C饿死。
所以时间片结束:
换队列。
5 .active指针和expired指针
系统维护两个指针:
active
expired
分别指向两个队列。
例如:
active指针
|
↓
active队列
expired指针
|
↓
expired队列
调度过程
开始:
active:
A B C
expired:
D E F
CPU运行:
A、B、C
时间片耗尽:
移动到expired。
变成:
active:
空
expired:
D E F A B C
交换指针
当active空了。
不用搬数据
只需要交换两个指针。
之前:
active ---> 空
expired ---> D E F
交换:
active ---> D E F
expired ---> 空
瞬间又有新的活动队列。
效率非常高。