Linux-Linux的进程状态
摘要:本文深入解析Linux操作系统中进程的各种状态及其实际应用。从运行状态(R状态)到休眠状态(S状态),再到深度睡眠状态(D状态)、暂停状态(T状态)、追踪状态(t状态),以及进程终止相关的X状态和Z状态(僵尸状态),每个状态都通过代码示例和实际操作进行详细演示。文章还探讨了孤儿进程的形成机制、进程状态查看方法,并分析了僵尸进程的危害。通过理论与实践相结合的方式,帮助读者全面理解Linux进程状态管理的核心概念。
目录
1.Linux的进程状态
在Linux内核源代码中搜索:
static const char *task_state_array
会有下面结果:

之前我们讲过进程有很多状态,如:运行状态(R状态)等,Linux内核状态与之前我们所讲的操作系统的状态有一定程度的关系!
那么如何查看Linux的这些状态呢,或如何模拟这些状态呢?
2.R状态
我们在fork.c中写下如下代码:
#include<stdio.h>
#include<unistd.h>
int main()
{
while(1)
{
printf("I am a process\n");
sleep(1);
}
return 0;
}
然后gcc编译后运行,并在另外一个机器上输入以下指令:
ps ajx | head -1 && ps axj | grep myfork
能观察到:

在之前的进程的学习中我们知道,PPID和PID和TTY的意思,STAT表示:进程对应的状态!
而此时我们正在运行./myfork进程,此时对应的状态是S+,但是运行的状态不应该为R状态吗,为啥为S+?
我们把代码改一下,把while循环里面的所有代码那一行注释掉变成这样:
#include<stdio.h>
#include<unistd.h>
int main()
{
while(1)
{
// printf("I am a process\n");
}
return 0;
}
然后再次执行刚刚的操作有:

此时变成了R+,这种状态叫做当前Linux处于运行状态!(+先不管)
3.S状态
为什么刚刚加了打印语句对应的状态是S状态呢(+先忽略)?S是什么状态?
我们把代码改为:
#include<stdio.h>
#include<unistd.h>
int main()
{
int a = 0;
scanf("%d",&a);
printf("%d\n",a);
return 0;
}
此时make,并./myfork有:

我们注意到:此时的./myfork进程处于S状态,之前我们了解到,scanf是导致进程卡住的,即阻塞状态,在Linux系统里,S叫做休眠状态(sleeping),准确来讲,它叫浅度休眠!
而浅度休眠状态就算我们在操作系统的状态里学到的:阻塞状态。本质上和之前学的阻塞状态是一个道理!只是在Linux的表述下这个状态叫休眠状态,在操作系统表述下叫做:阻塞状态,叫法不一样而已!具体的状态数字为1(见第一个图)
之前我们说过操作系统上面的理论是指导思想,Linux是具体的操作系统,Linux会根据你的指导思想来实现进程状态,而在Linux上具体的阻塞状态叫做S状态!
那为啥我们在printf打印时是S状态呢,为什么?
因为printf在打印时要访问硬件!printf在高频对显示器做打印,打印的时候显示器有缓冲区(进度条就是这样,printf的结果不会直接显示,而是要等sleep结束后再打印出来,具体见之前博客),所以高频向缓冲区写入数据,缓冲区的数据一定能够让你写进去吗?有没有可能进程在打印的时候大部分时间其实都是显示器不就绪的状态。虽然看似我们打印出来了,但是计算机一秒能打印上亿次,在显示器上打印出来的才多少次?其实我们的进程在运行的时候它大部分时间是在运行队列和显示器的等待队列里,来回的把进程在调度队列和阻塞队列放,只不过几乎99%的情况是它的S状态,因为外设太慢了!CPU很快。
总结一下:printf打印时本质是访问外设,当你打印时外设不一定就绪,所以进程在大部分查的时间都是S状态,当去掉printf打印时,while循环一直在检测为真,此时就算纯CPU运行了。如果只要没有访问外设资源,只访问CPU资源,全都是R状态!
为什么S叫做浅度休眠呢?它不就是sleep吗?
我们能通过Ctrl+C把当前的进程直接杀掉,也就是说当前进程处于S状态。比如之前的scanf那个代码,只要我们一直不输入就是S状态,现在只要我们不输入,它就是阻塞(S)状态的,它不是在CPU运行队列里等待,而是在我们的键盘等待队列里等待,同样Ctrl+C能终止掉这个进程,也就是说这种处于S状态的进程 -------------- 它能够响应外部的事件!比如说:我摁键盘或Ctrl+C能够改变进程对应的状态,这种状态就叫做浅度睡眠,有浅度必定有深度睡眠:

这张图对应的D(disk sleep)就是深度睡眠!
4.D状态
D状态在Linux下叫做:磁盘休眠(深度睡眠)状态!只有把深度睡眠理解了,浅度睡眠就更好理解了!
前提:深度睡眠在这里一般做不到,可能机器会被挂掉!
未来进程正在向磁盘里写入,假设未来进程想向磁盘写1GB数据,如进程把数据从内存写到磁盘上。在内存中还存在操作系统,对内存进行管理!

进程A让磁盘保存1GB数据,磁盘先找该把这些数据放哪里,磁盘会跟进程说拷贝成功与否,如果拷贝失败,磁盘会给进程说拷贝失败的原因,由进程反馈给用户失败原因,在拷贝的过程中,进程以S状态等待磁盘拷贝完成,但操作系统发现:内存已经严重不足了,而看到进程A还在等待,但是我把进程A的代码和数据就算放到了磁盘里,内存还是不够,操作系统马上就要挂掉了,所以我要把你这个处于S状态的进程杀掉。
操作系统在内存资源严重不足的时候,极端情况:操作系统要通过杀掉进程来保全自己!如:手机上启动的应用程序过多,有些APP会闪退,闪退不一定是APP的bug,有可能是对应的操作系统把你的进程杀掉了,因为操作系统要保全自己!
磁盘此时已经知道拷贝成功与否了,就在准备找进程A反馈结果,发现进程A不在了,如果此时磁盘拷贝失败了,那么这个1GB数据该怎么处理磁盘不知道了,磁盘可能会把数据直接丢弃掉,也可能再次尝试写入。但是磁盘觉得:我都让进程A等了,它不等了,那没办法,其他的进程可能还让我忙,让我写入数据,我不管这1GB数据了,我把这1GB数据干掉了。
如果此时这个恰好是银行的1GB数据,这些数据是一天之内所有客户的转账记录,这个时候给银行造成重大损失了。银行行长找到磁盘、操作系统、进程A,磁盘觉得数据写入失败是情有可原的,而且都让进程A等了,它就算不等,所以磁盘觉得自己没问题;进程A觉得我正常给磁盘交代任务,让它写1GB数据,我都交给磁盘了,但是我在休眠的过程中我被操作系统杀掉了,觉得不是自己问题;操作系统觉得自己在内存资源严重不足的时候是能杀掉一些进程的,虽然杀掉了进程A,但是保全了其他的进程,虽然损失了一部分,但是功大于过,如果不把进程A杀掉操作系统就会挂掉,操作系统没了那这个银行也开不下去!
为了避免下一次再出现类似的问题,银行行长给进程A一个特权:从今天开始,如果你要进行磁盘级的IO,不要把自己的状态设置为S,而设置为D状态!对操作系统说:状态为S的进程你随便杀掉,但是D状态的进程不能杀掉!
D状态:深度睡眠状态,不对任何事件进行任何响应,操作系统也杀不掉,除非进程自己醒来,否则重启也没用!如果D状态一直存在,想要杀掉不可能!如果有进程一直处于D状态,那么你的系统只能挂掉!
D状态是:进程为了和磁盘进行数据访问的时候(进行磁盘级访问,因为磁盘太重要了),所以在进程往磁盘写入时有短暂的D状态(我们看不到)!一般如果在电脑里查到D状态,大概率磁盘要挂掉了,因为往磁盘写入时短暂的D状态一般不会很久,但一旦系统出现了小时乃至几天的D状态进程,磁盘就快挂掉了!此时系统就会挂掉了!如:公司一些机器很久不被响应,大概率磁盘以及老化了,就会挂掉了,此时就需要更换机器了!
这和我们之前阻塞挂起状态有关联,是阻塞挂起的一个分支,我们不考虑它。
深度睡眠和浅度睡眠合称:阻塞状态。
如果就想看到D状态,“用dd状态模拟实现D状态”这句话问AI大模型即可!
S状态:浅度睡眠状态,等待键盘输入;D状态:深度睡眠状态,等待磁盘的回复。
5.T状态
T状态(stopped状态):

把代码改为:
#include<stdio.h>
#include<unistd.h>
int main()
{
while(1)
{
//printf("I am a process\n");
//sleep(1);
}
return 0;
}
此时就是R状态:

我们可以通过kill命令来给目标进程发一个东西(信号),如:我们观察到./myfork的PID为14096,如果:
kill -9 14096
此时就能杀掉14096这个进程(在右边机器上输入):

此时没杀掉是因为我们没权限,建议使用tan登录右边以及左边这两个机器就行:

此时执行刚刚的指令即可:

这种杀掉进程的方法我们叫做:信号!后面会专门来讲。
如果想要给目标进程发信号有很多做法,我们可以通过:
kill -l
查看所有信号:

19)SIGSTOP,我们再把进程运行起来,然后查编号有:

此时PID为14116,此时执行:
kill -19 14116
有:

我们是不是之前在哪里见过这个?
之前在vim演示时,如果用Ctrl+Z退出也行:

此时我们再用ps axj这个指令查就有了:

此时的STAT是T就代表为T状态!
T状态(stopped):暂停状态(实现方法:vim时Ctrl+Z或者kill -19 PID)。
进程的暂停和休眠有区别,如果想要继续运行暂停的进程,我们给14116再发一个信号:

18)SIGCONT让进程继续运行
所以直接:
kill -18 14116
有:

但是这个时候我们看不到,但是用ps去查此时的状态为R:

对于vim的用Ctrl+Z退出的,直接输入fg回车就行:

为什么开始我们为R+状态:

现在却是R状态呢?即+代表什么意思呢?
+代表该进程是前台进程!前台进程可以用Ctrl+C终止掉,这叫做前台进程。一旦进程暂停了,这个进程自动变成后台了,此时就算我们频繁在刚刚运行./myfork的机器上Ctrl+C也无法杀掉刚刚的那个14116进程!这个时候要杀掉这个进程,只能使用kill -9才行!
什么时候该stop呢,这时候我们需要了解一下t状态了!
6.t状态
t(tracing stop):追踪状态,只是在Linux内核里把它也称为T状态而已:
把fork.c的代码改为:
#include<stdio.h>
#include<unistd.h>
int main()
{
while(1)
{
printf("I am a process\n");
sleep(1);
}
return 0;
}
并打开Makefile,改为如下样式:

加了一个-g选项代表当前进程允许被调试了!
此时我们make clean后再make有:

此时我们:
gdb myfork
然后用ps指令去查有:

此时因为gdb也是一个进程,所以有一个gdb,但是当前我们的程序并没有运行,此时处于S状态。
然后我们在第9行打一个断点:b 9;然后再r运行:

此时代码运行到了第14行(我的实际代码有注释),然后用ps指令去查有:

此时我们发现ps产生的结果第三行对应的STAT是t,也就是说现在处于t状态。
当我们r运行时,这个![]()
该进程会被新建起来,而当前gdb所建的进程PID为14183,但是我的父进程14173就算gdb的PID,所以gdb在r时本质是gdb创建了一个子进程,让子进程执行自己的代码!
所以在gdb调试的场景当中,你的进程状态就叫做t状态!
t状态表示的是:当进程因为被gdb追踪,进程因为断点而停下来,此时的状态就是t状态!
为什么要有T状态,它和阻塞状态有什么区别?
我们的进程有时候要访问某种资源,比如说:向显示器打印,但是有时候我们不想要它在显示器上打印。如:一个进程想要从键盘读取数据,有时候我们不准这个进程从键盘读数据,可是你这个进程非得要从键盘读,那这个时候进程就访问了它不该访问的资源了,可是操作系统觉得你这种情况不是那么严重,即不想杀掉这个进程,即这个读取数据并不会带来太多危害,所以操作系统就把这个进程暂停了,暂停了就算告诉用户:你当前的行为有非法操作,你的进程被暂停了!
阻塞状态是操作系统让进程等待就绪,而暂停状态本身是因为进程进行了非法操作,进程不想要杀掉你这个进程,因此就暂停了你这个进程!(如:野指针这种就需要直接杀掉这个进程了)
7.X状态和Z状态
在操作系统里,进程结束就只有一种状态;而在Linux当中,进程结束有两种状态(X状态、Z状态),X(dead)表明进程真正结束了,把进程的代码和数据在内存释放掉,曾经申请的空间也要全部释放掉,换句话说:进程结束了,是进程创建的反过程!
但是进程结束,没有那么简单的就结束了!
你把一个进程创建出来,你为什么要创建这个进程呢?
进程被创建出来,目的就是为了完成某种任务的。
进程结束时,把任务完成得怎么样,我该怎么知道呢?
如果我们只把最后的结果打印出来,我们能够知道正确的结果应该是什么,然后通过对比来知道我们的进程是否成功执行,但是万一结果不能显示出来呢?此时该怎么证明程序正确运行了呢?
为啥我们学C语言main函数最后要return 0,没有人告诉我们为什么要return 0才行,为什么要main函数要返回0,为什么不能返回其他数字?
main函数的返回值将来会返回给系统,其实就是返回给未来的父进程,父进程就可以通过返回的数字来鉴别你把任务完成得怎么样,0往往表示的是:成功,所以我们往往写的是return 0。只不过我们到现在还没有主动抓取子进程的退出信息,也就是main函数的return值我们从来没有关注过!
结论:进程结束,不能立即释放该进程的所有资源!
进程结束了,操作系统不能让进程直接处于死亡状态,即不能让进程立即销毁,因为这个进程退出信息就没法拿到了,而是让进程先处于一种状态(Z状态僵尸状态)。
∴进程结束,要先处于一种“僵尸状态”,这种僵尸状态往往会把当前代码和数据释放掉,因为代码和数据不会被调度了,但是会把task_struct(进程控制块)保留,task_struct会自动记录下来当前进程退出的时候的退出信息!
task_struct里面有个int类型的变量,变量名为exit_code(退出码):

该图片为Linux内核源码中task_struct结构体里面的一部分!
所以此时main函数的返回值就被写到task_struct里,将来进程退出了,就把状态设置为Z状态,把进程退出信息记录下来,方便父进程读取退出码(讲到进程控制再详细讲)!父进程一旦读取,此时子进程的状态才由Z状态变为X状态,此时操作系统才进行进一步的销毁。
总结:进程退出时,进程的PCB不会立即被释放掉,而是一直处于Z状态。
如何模拟出这种状态,该如何做?
我们可以想办法创建一个子进程,然后让子进程直接退出,退出之后,父进程什么都不做,此时我们就能看到子进程处于僵尸了!
左边机器上的代码为:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t id = fork();
if(id==0)
{
//子进程
//让它跑5秒后退出
int cnt = 5;
while(cnt--)
{
printf("我是子进程,pid:%d\n",getpid());
sleep(1);
}
printf("子进程退出了\n");
exit(1);//包含于头文件stdlib.h
}
//父进程
while(1)
{
printf("我是父进程,pid:%d\n",getpid());
sleep(1);
}
return 0;
}
再在右边机器上运行如下指令:
while :; do ps ajx | head -1 && ps ajx | grep myfork; sleep 1; done
运行结果有:

我们注意到这些变化:

首先,子进程退出的时候,它会从S+状态变为Z+状态,且最后还有一个<defunct>的出现,defunct:

表明当前的进程已经死了。
Z状态:当进程结束时,会先处于这种状态,Z状态会把自己所匹配的代码和数据如果有它就释放掉了,如果没有,它就会把自己的PCB(task_struct)保留下来,PCB会存在当前进程的返回值(退出结果),而exit(1)就是我们将来用来决定未来进程的执行结果了!
结论:如果一个进程被结束了,它的代码和数据可以被释放了,因为它已经不会被调度了,但是当前进程一定要把task_struct保留下来,保留下来这是为了让父进程来读取子进程的退出信息,从而给用户一个交代,让我们知道,你把任务完成得怎么样,未来会讲到一些wait接口,可以通过这些接口来获取子进程的退出信息。我们把这种进程结束了,但是保留PCB的情况我们称之为:僵尸进程!
一旦进程僵尸了,而父进程一直不读取即父进程一直不来处理这个僵尸进程,这个僵尸进程会一直存在!(默认情况)也就意味着PCB一直存在,也就意味着PCB占用的内存无法被释放,会带来内存泄漏!
那父进程必须要处理子进程吗?
必须的,因为子进程是你创建的!你创建的子进程目的就是为了完成这个任务的,所以这个子进程把任务完成得怎么样,你这个父进程必须要知道。
一旦将来父进程回收了子进程的退出信息,那么子进程的状态就会由Z变为X,一旦变为X,操作系统才会把进程的PCB给释放掉!
X状态看不到,X状态是一种瞬时状态!
8.其他状态
在操作系统的状态里,我们看到了创建状态,但是在Linux系统里并没有对应的状态;就绪状态也不存在;挂起状态我们也看不到,因为挂起的状态完全是操作系统自己做的,和用户无关,不需要告诉用户某个进程已经挂起了。
进程=内核数据结构(task_struct)+代码和数据。
当创建进程时,先创建内核数据结构还是先创建代码和数据呢?
一定是先创建内核数据结构,再加载代码和数据!这就是为什么有些操作系统有一个新建状态,新建状态就是先把内核数据结构创建完但是代码和数据没加载完时的状态!进程就要处于这个状态,只是Linux不需要,因为Linux有虚拟地址空间!
之前我们说过,子进程一旦结束就会处于僵尸状态,但是如果父进程先退出了呢?
9.孤儿进程
把代码改为:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t id = fork();
if(id==0)
{
//子进程
while(1)
{
printf("我是子进程,pid:%d\n",getpid());
sleep(1);
}
printf("子进程退出了\n");
exit(1);//包含于头文件stdlib.h
}
//父进程
int count = 5;
while(count--)
{
printf("我是父进程,pid:%d\n",getpid());
sleep(1);
}
return 0;
}

可以看到有如下变化:

首先:父进程退了,只有子进程,子进程还在运行,而且观察右边发现,子进程在父进程退出后它的PPID从1824变为了1,STAT变为了S。
所以,如果父进程先退出,子进程还会运行,该子进程就会被1号进程领养!我们把这种被1号进程领养的进程叫做:孤儿进程!
那么有几个问题:
①1号是谁啊?
②为什么要被领养啊?
③父进程呢?不是说好了僵尸吗?
①我们使用top指令查看一下:

发现PID为1的进程是systemd,在比较老的内核里它叫initd,我们可以把systemd理解成操作系统(的一部分)!换句话说,父进程退了,子进程会被操作系统领养
②因为孤儿进程也会退出,但是它的父进程没了!也就代表没人会回收子进程的退出信息,只要孤儿进程一旦结束,就会变成僵尸状态,所以被系统领养,是为了回收孤儿,防止内存泄漏!
③如果一个进程已经变成了孤儿进程,刚刚我们发现它是S状态,也就是后台进程,想要杀掉这个进程只能用kill命令杀掉。
我们查看刚刚的图片:

发现父进程退出之前的PPID即父进程的PID为1111,我们用以下指令查看一下PID为1111是什么:
ps ajx | grep 1111

发现1111实际上对应的是bash,我们在命令行启动任务的时候,所有的任务都是bash的子进程,而我在代码里创建的子进程是bash的孙子进程,如果bash的子进程任务结束了,那么bash就会自动回收子进程,所以刚刚的父进程就被回收了,因此我们看不到!
答案是:父进程不是没有僵尸,而是你这里对应的父进程被bash进程自动回收了!怎么回收的,后面会讲!
10.进程状态的查看
ps aux / ps axj 命令
来⼀个创建维持30秒的僵死进程例⼦:
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id > 0)
{
//parent
printf("parent[%d] is sleeping...\n", getpid());
sleep(30);
}
else
{
printf("child[%d] is begin Z...\n", getpid());
sleep(5);
exit(EXIT_SUCCESS);
}
return 0;
}
11.僵尸进程的危害
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)