进程的状态和优先级
-
- Linux的进程状态
-
- [R - running](#R - running)
- [S - sleeping](#S - sleeping)
- [D - disk sleep](#D - disk sleep)
- [T - stopped](#T - stopped)
- [t - tracing stop](#t - tracing stop)
- [Z - zombie 与 X - dead](#Z - zombie 与 X - dead)
- 僵尸进程和孤儿进程
- [运行 阻塞 挂起](#运行 阻塞 挂起)
- 进程的切换
- 进程的优先级
书接上回进程的概念,进程 = PCB + 代码和数据。PCB 中存放着进程的信息,是进程属性的集合,其中包括进程的状态
Linux的进程状态
话不多说,我们直接来看在 Linux 都有哪些状态:
- R - running
- S - sleeping
- D - disk sleep
- T - stopped
- t - tracing stop
- X - dead
- Z - zombie
下面我们来通过代码实际演示,来看看这些状态到底是怎样的。其中 D 状态由于机器限制,X 状态又是瞬时的,这里无法演示
R - running
running,顾名思义,就是运行状态。当进程被 CPU 调度时,进程就处于运行状态
例如,有如下 test.c 文件,写一个死循环,方便我们观察进程的状态
c
int main()
{
while(1)
{
}
return 0;
}
编译得到 procStatus 文件,执行
再使用如下命令查看进程的状态:
shell
while :; do ps ajx | head -1 && ps ajx | grep procStatus | grep -v grep; sleep 1; done
最终我们可以看到的结果:
我们的 procStatus 进程确实在运行中,处于 R 状态,加号不用管
S - sleeping
进程没有在运行,处于睡眠状态,这是一种进程等待资源就绪的状态
以下是进行测试的 test.c 文件
c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
while(1)
{
printf("I am a process, pid: %d\n", getid());
}
return 0;
}
依然是编译并运行,同时使用 ps 命令查看进程的状态:
可以看到,虽然我们的进程在不停地输出,但是我们查到的进程状态却还是处于睡眠状态。原因如下:
进程是在 CPU 上跑的,而 printf 输出的对象是屏幕,也就是外设。输出的信息需要由 CPU 先传到内存中,进而输出到外设
CPU 的处理速度是比外设快得多得多的,这就导致 CPU 上的进程输出了一大堆信息到外设,外设还在慢慢处理,那进程就只能等待外设就绪后再运行了
这种睡眠状态是可以中断的,所以是可中断睡眠。想要中断进程,我们可以:
- CTRL + c
- 使用信号命令,kill -9
D - disk sleep
由于机器限制,我这里就不再演示了,只说理论
D 状态是 Linux 操作系统特有的一种状态,可以应对下面这种情况:
操作系统运行于内存中,现有一个进程 A 也在内存中,它的任务是把一份非常重要的数据交给磁盘,由于磁盘的处理速度较慢,进程 A 需要等待磁盘的反馈信息,进入了 S 状态。
而此时内存压力比较紧张,操作系统快顶不住了,为了保证操作系统的正常运行,操作系统有权杀掉一些进程来释放空间。所以操作系统将处于 S 状态的进程A 杀掉了,之后磁盘恰巧写入失败,来找进程A 反馈信息,但是进程A 已经没了
磁盘不只有进程A 的任务要处理,还有其他进程的任务,所以只能将这份数据抛弃掉,处理其他任务去了。这就导致了数据的丢失,如果这份数据非常重要,如银行用户转账记录,那么损失就大了
所以为了避免这种情况发生,就有了 D 状态,这也是一种睡眠状态,但是不可中断。想要中断有两种办法:
- 等待进程自己醒来
- 重启大法
T - stopped
T/t 状态都是使进程暂停,等待进一步的唤醒。要想将进程暂停,我们可以使用信号命令,使用如下命令来查看可以使用的信号
shell
kill -l
19号信号可以帮我们暂停进程
演示如下,当我们使用kill -19
后,进程的状态由 S 变为了 T
我们还可以使用 18 号信号,唤醒处于 T 状态的进程
t - tracing stop
这种暂停状态,我们平时也会使用到,就是调试。当我们调试程序时,进程就会启动,我们给程序打断点,就会使进程暂停
我们先编译 .c 文件,让程序可以调试
接着进入调试,并查看进程
可以看到有了gdb 进程,但还没有我们的程序进程。接下来给我们的程序打个断点
然后输入 r,让我们的程序跑起来
此时可以看到,我们的程序进程,处于 t 状态。
接着输入 c,到下个断点处
程序进程还是处于 t 状态
Z - zombie 与 X - dead
当一个进程结束后,并不是直接退出。一般是将代码与数据 先释放掉,留下 PCB。PCB 中存着进程退出的信息,将一直处于 Z 状态,直到父进程将这些信息读取,才会退出 Z 状态,变为 X 状态,继而进程将完全退出
僵尸进程和孤儿进程
僵尸进程
当一个进程结束时,需要维持自己的退出信息,在自己的 task_struct(Linux 中的 PCB) 记录相关信息,未来让父进程读取,之后进程就会完全退出
下面我们用代码开一个父进程和一个子进程,让父进程无限循环,子进程运行一会就结束,进程运行期间我们可以查看它们的状态
c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
// child
int cnt = 5; // 运行5秒
while(cnt--)
{
sleep(1);
printf("I am child, pid: %d\n", getpid());
}
}
else
{
// parent
while(1)
{
sleep(1);
printf("I am parent, pid: %d\n", getpid());
}
}
return 0;
}
一开始,两个进程都处于 S 状态
当运行 5 秒后,子进程结束,进入 Z 状态,等待父进程读取退出信息
当父进程结束时,会读取子进程的信息,子进程也就会完全退出
如果父进程不读取子进程的退出信息,那么子进程的 PCB 占用的空间就会一直不释放,造成内存泄漏问题
孤儿进程
如果父进程先退出,那么子进程就会成为孤儿进程
还是用代码开一个父进程和一个子进程,让父进程运行 5 秒,子进程无限循环
c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
// child
while(1)
{
sleep(1);
printf("I am child, pid: %d\n", getpid());
}
}
else
{
// parent
int cnt = 5; // 运行5秒
while(cnt--)
{
sleep(1);
printf("I am parent, pid: %d\n", getpid());
}
}
return 0;
}
可以看到,前五秒有父进程 6315 和子进程 6316,五秒后父进程退出,只剩下子进程,此时子进程就成为了孤儿进程
孤儿进程没有父进程,所以退出信息就没人读取,会造成内存泄露问题。为了避免这种情况,孤儿进程一般都是由1号进程(操作系统)领养,这样就可以保证子进程被正常回收
运行 阻塞 挂起
运行
进程是运行在 CPU 上的,而一个 CPU 同时只能运行一个进程,如果有多个进程都想要运行,那么就只能排队了
每个 CPU 都要维护一个运行队列,运行队列是一个数据结构,里面至少有一个指针 task_struct* head 指向进程队列头部
系统将位于队列头部的进程的代码数据等相关信息加载到 CPU 的寄存器中,CPU 就可以调度进程了
一般来说,当进程被放到 CPU 中时,这个进程的状态就是 R 状态了,也就是运行状态。而在很多教材中,只要进程进入了运行队列,就可以被称为 R 状态,其含义是:进程已经准备好了,可以随时被调度
进程的切换问题
当一个进程被 CPU 调度时,会一直运行到进程结束吗?不会的,如果写一个死循环的程序,那其他进程岂不是永远轮不到了。所以有这样一种调度算法:基于时间片的轮转调度
在一段时间内 ,给运行队列的每个进程分配特定运行时间,一旦时间到了,不管进程有没有结束,都要从 CPU 剥离下来,放到运行队列的尾部,然后 CPU 调度下一个进程。这样让多个进程以切换的方式进行调度,在一段时间内得以同时推进代码 的做法,就叫做并发
如果有多个 CPU 在工作,就存在多个运行队列,不同队列可以同时运行不同的进程 ,这样在任意时刻 ,真的有不同进程运行的做法,叫做并行
阻塞
下面我们还是通过代码来理解阻塞态
c
#include <stdio.h>
int main()
{
int a = 0;
scanf("%d", &a);
printf("%d\n", a);
return 0;
}
编译运行,执行到 scanf 时,程序就会暂停住,如下
此时进程处于阻塞态,对应 Linux 中的睡眠状态,在等待键盘资源的就绪。在冯诺依曼体系中,键盘是属于外设的,进程是软件,它是如何等待外设的呢?
键盘属于外设层,它的上一层是驱动层,再上一层是操作系统层,如图
操作系统是对软硬件进行管理的,如何管理,我们可以先描述,再组织
硬件设备也可以描述成一个数据结构 struct device,其属性如下
c
struct device
{
int type; // 设备类型
int status; // 设备是否已经就绪
// ...设备其他属性
struct device* next; // 指向下一个设备
}
每一个设备在内核中都拥有自己的 struct device,不同设备的 struct device 互相链接在一起,组织成一个设备链表
在每个设备的 struct device 中,也存在一个指针,指向等待此设备的进程 ,这样的进程可能不止一个,所以和运行队列 一样,也存在等待队列 wait_queue
c
struct device
{
int type; // 设备类型
int status; // 设备是否已经就绪
// ...设备其他属性
struct device* next; // 指向下一个设备
task_struct* wait_queue; // 指向等待设备的进程
}
当进程等待设备资源就绪时,就会从运行队列剥离,链入到设备的等待队列。此时进程的状态就不是运行态了,而是阻塞态,不可被调度。直到设备资源就绪,如按下键盘,进程就会被唤醒到运行队列
进程在运行态和阻塞态之间切换时,往往伴随着 PCB 链入不同的队列中。入队列的不是进程的代码和数据,而是进程的 PCB
挂起
我们在装系统的时候,一般都会给磁盘分区,而磁盘中存在一个独立的分区:swap 分区,它的大小一般和系统内存相同,或者是内存的 2 倍,不同的人可能分配的空间大小不同,但是 swap 分区不宜太大也不宜太小
假设现在内存中运行着很多进程,操作系统的内存压力很大,即将调度不过来了,这时有一个进程 1 处于阻塞状态,在 Linux 就是 S 状态或者 D 状态,总之就是这个进程的代码和数据 暂时不会被调度。那么就可以把进程 1 的代码和数据唤出 到 swap 分区,这样就可以腾出一些空间,解决燃眉之急。此时进程 1 就是处于挂起态 ,严格来说是阻塞挂起态,当进程 1 需要调度时,再将其代码数据唤入到内存中
虽然 swap 分区可以减轻内存压力,但不宜设置太大。因为唤入和唤出是有消耗的,是一种用效率换取空间的做法。所以为了操作系统的效率,不宜频繁唤入唤出
进程的切换
在运行部分,我们已经知道,进程的切换是基于时间片的轮转调度,如果一个进程还没运行完毕,时间片结束,如何保证此进程下次运行的时候从中断点继续运行呢?
在 CPU 中,有非常多的寄存器,它们可以保存一些临时数据,如下面这个函数
c
int add(int a, int b)
{
int c = a + b;
return c;
}
int ret = add(1, 1);
c 是临时变量,函数结束就会销毁。实际上,c 的值会被临时保存在寄存器中,这样就可以返回给 ret 了
而在 CPU 上运行的进程也是这样的,寄存器会保存进程的临时数据。CPU 内部所有寄存器中的临时数据,叫做进程的上下文
假设进程 1 正在 CPU 上运行,时间片到了,还没运行完,代码运行到了 20 行,为了保证进程 1 下次运行时从 20 行开始,进程 1 需要将寄存器中的上下文数据存入到 task_struct 中。轮到进程 2 运行时,如果不是第一次运行,就需要将 task_struct 中的上下文数据恢复到寄存器当中,继续运行
所以,进程的切换,重要的是上下文数据的保存与恢复
进程的优先级
什么是优先级
简单来说,优先级就是操作系统中指定进程获取某种资源的先后顺序。在进程的 task_struct 中,进程的优先级用数字表示,或者一个,或者多个
c
struct task_strcut
{
// ...
int prio; // 优先级用数字表示
}
在 Linux 中,优先级数字越小,表示优先级越高
这时可能会有人觉得优先级 与权限的意思有点相像,其实是不一样的
- 权限决定的是能不能获取某资源
- 优先级是在已经能的前提下,获取资源的先后顺序
为什么要有优先级
我们在食堂打饭时通常要排队,是为什么?因为人很多,饭是有限的,排队打饭是相对公平的分配方式
进程调度也是同理,进程要访问的资源(CPU等)通常都是有限的,而进程相对来说是很多的。我们所使用的操作系统 CPU 调度通常都是基于时间片轮转的,也就是分时操作系统,这样可以相对公平地给进程分配系统资源
如果在我们打饭时,不断有人破环规则而插队,那么有人就会因为迟迟打不打饭而饥饿。同样在进程的运行队列中,如果有进程不按特定顺序来访问 CPU 资源,就会导致有的进程一直访问不到 CPU,造成饥饿问题
Linux中的优先级
我们可以使用 ps -al
命令查看进程的情况,可以看到两个属性:PRI 和 NI
- PRI 就是进程的优先级,数字越小,代表进程优先级越高
- NI 是进程优先级的修正数值,被称为 nice 值
PRI 是进程的默认优先级,若想修改进程的优先级,一般是修改 NI 的值,新的优先级 = PRI + NI
下面我们写一个程序并运行,修改一下它的优先级看看
c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("I am a process, pid: %d\n", getpid());
}
}
启动我们的程序后,相应的进程优先级 PRI 为 80,NI 为 0,下面我们来修改进程的优先级
- 输入 top 命令 -> 按下 r -> 输入进程的 pid -> 输入要修改的 NI 值 -> 输入 q 退出
我们先随便输入一个 100 试试,然后查看我们进程的优先级
为什么我们输入的是 100,但是 NI 值却是 19 呢?这是因为 NI 值的范围是 [-20, 19] ,一共40个数。下面我们将 NI 改为 -10
注意,普通用户不能频繁改变进程的优先级,需要 root
为什么进程的优先级不是 99 -10 = 89,而是 70 呢?这是因为每次调整优先级,都是从 80 开始的
通过上面的演示我们可以发现,Linux 中的进程优先级可以调整,但是存在限制,可以调整的范围很小。而进程的优先级也确实不应该轻易人为调整,这些应该交给系统