ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux-Linux的进程状态

Linux-Linux的进程状态 摘要本文深入解析Linux操作系统中进程的各种状态及其实际应用。从运行状态R状态到休眠状态S状态再到深度睡眠状态D状态、暂停状态T状态、追踪状态t状态以及进程终止相关的X状态和Z状态僵尸状态每个状态都通过代码示例和实际操作进行详细演示。文章还探讨了孤儿进程的形成机制、进程状态查看方法并分析了僵尸进程的危害。通过理论与实践相结合的方式帮助读者全面理解Linux进程状态管理的核心概念。目录1.Linux的进程状态2.R状态3.S状态4.D状态5.T状态6.t状态7.X状态和Z状态8.其他状态9.孤儿进程10.进程状态的查看11.僵尸进程的危害1.Linux的进程状态在Linux内核源代码中搜索static const char *task_state_array会有下面结果之前我们讲过进程有很多状态如运行状态R状态等Linux内核状态与之前我们所讲的操作系统的状态有一定程度的关系那么如何查看Linux的这些状态呢或如何模拟这些状态呢2.R状态我们在fork.c中写下如下代码#includestdio.h #includeunistd.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循环里面的所有代码那一行注释掉变成这样#includestdio.h #includeunistd.h int main() { while(1) { // printf(I am a process\n); } return 0; }然后再次执行刚刚的操作有此时变成了R这种状态叫做当前Linux处于运行状态先不管3.S状态为什么刚刚加了打印语句对应的状态是S状态呢先忽略S是什么状态我们把代码改为#includestdio.h #includeunistd.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吗我们能通过CtrlC把当前的进程直接杀掉也就是说当前进程处于S状态。比如之前的scanf那个代码只要我们一直不输入就是S状态现在只要我们不输入它就是阻塞S状态的它不是在CPU运行队列里等待而是在我们的键盘等待队列里等待同样CtrlC能终止掉这个进程也就是说这种处于S状态的进程 -------------- 它能够响应外部的事件比如说我摁键盘或CtrlC能够改变进程对应的状态这种状态就叫做浅度睡眠有浅度必定有深度睡眠这张图对应的Ddisk 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状态把代码改为#includestdio.h #includeunistd.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查看所有信号19SIGSTOP我们再把进程运行起来然后查编号有此时PID为14116此时执行kill -19 14116有我们是不是之前在哪里见过这个之前在vim演示时如果用CtrlZ退出也行此时我们再用ps axj这个指令查就有了此时的STAT是T就代表为T状态T状态stopped暂停状态实现方法vim时CtrlZ或者kill -19 PID。进程的暂停和休眠有区别如果想要继续运行暂停的进程我们给14116再发一个信号18SIGCONT让进程继续运行所以直接kill -18 14116有但是这个时候我们看不到但是用ps去查此时的状态为R对于vim的用CtrlZ退出的直接输入fg回车就行为什么开始我们为R状态现在却是R状态呢即代表什么意思呢代表该进程是前台进程前台进程可以用CtrlC终止掉这叫做前台进程。一旦进程暂停了这个进程自动变成后台了此时就算我们频繁在刚刚运行./myfork的机器上CtrlC也无法杀掉刚刚的那个14116进程这个时候要杀掉这个进程只能使用kill -9才行什么时候该stop呢这时候我们需要了解一下t状态了6.t状态t(tracing stop)追踪状态只是在Linux内核里把它也称为T状态而已把fork.c的代码改为#includestdio.h #includeunistd.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状态Xdead表明进程真正结束了把进程的代码和数据在内存释放掉曾经申请的空间也要全部释放掉换句话说进程结束了是进程创建的反过程但是进程结束没有那么简单的就结束了你把一个进程创建出来你为什么要创建这个进程呢进程被创建出来目的就是为了完成某种任务的。进程结束时把任务完成得怎么样我该怎么知道呢如果我们只把最后的结果打印出来我们能够知道正确的结果应该是什么然后通过对比来知道我们的进程是否成功执行但是万一结果不能显示出来呢此时该怎么证明程序正确运行了呢为啥我们学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状态。如何模拟出这种状态该如何做我们可以想办法创建一个子进程然后让子进程直接退出退出之后父进程什么都不做此时我们就能看到子进程处于僵尸了左边机器上的代码为#includestdio.h #includeunistd.h #includestdlib.h int main() { pid_t id fork(); if(id0) { //子进程 //让它跑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状态会把自己所匹配的代码和数据如果有它就释放掉了如果没有它就会把自己的PCBtask_struct保留下来,PCB会存在当前进程的返回值退出结果而exit(1)就是我们将来用来决定未来进程的执行结果了结论如果一个进程被结束了它的代码和数据可以被释放了因为它已经不会被调度了但是当前进程一定要把task_struct保留下来保留下来这是为了让父进程来读取子进程的退出信息从而给用户一个交代让我们知道你把任务完成得怎么样未来会讲到一些wait接口可以通过这些接口来获取子进程的退出信息。我们把这种进程结束了但是保留PCB的情况我们称之为僵尸进程一旦进程僵尸了而父进程一直不读取即父进程一直不来处理这个僵尸进程这个僵尸进程会一直存在默认情况也就意味着PCB一直存在也就意味着PCB占用的内存无法被释放会带来内存泄漏那父进程必须要处理子进程吗必须的因为子进程是你创建的你创建的子进程目的就是为了完成这个任务的所以这个子进程把任务完成得怎么样你这个父进程必须要知道。一旦将来父进程回收了子进程的退出信息那么子进程的状态就会由Z变为X一旦变为X操作系统才会把进程的PCB给释放掉X状态看不到X状态是一种瞬时状态8.其他状态在操作系统的状态里我们看到了创建状态但是在Linux系统里并没有对应的状态就绪状态也不存在挂起状态我们也看不到因为挂起的状态完全是操作系统自己做的和用户无关不需要告诉用户某个进程已经挂起了。进程内核数据结构task_struct代码和数据。当创建进程时先创建内核数据结构还是先创建代码和数据呢一定是先创建内核数据结构再加载代码和数据这就是为什么有些操作系统有一个新建状态新建状态就是先把内核数据结构创建完但是代码和数据没加载完时的状态进程就要处于这个状态只是Linux不需要因为Linux有虚拟地址空间之前我们说过子进程一旦结束就会处于僵尸状态但是如果父进程先退出了呢9.孤儿进程把代码改为#includestdio.h #includeunistd.h #includestdlib.h int main() { pid_t id fork(); if(id0) { //子进程 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变为了1STAT变为了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 命令a显⽰⼀个终端所有的进程包括其他⽤⼾的进程。x显⽰没有控制终端的进程例如后台运⾏的守护进程。j显⽰进程归属的进程组ID、会话ID、⽗进程ID以及与作业控制相关的信息u以⽤⼾为中⼼的格式显⽰进程信息提供进程的详细信息如⽤⼾、CPU和内存使⽤情况等Linux进程状态图僵死状态Zombies是⼀个⽐较特殊的状态。当进程退出并且⽗进程使⽤wait()系统调⽤,后⾯讲没有读取到⼦进程退出的返回代码时就会产⽣僵死(⼫)进程僵死进程会以终⽌状态保持在进程表中并且会⼀直在等待⽗进程读取退出状态代码。所以只要⼦进程退出⽗进程还在运⾏但⽗进程没有读取⼦进程状态⼦进程进⼊Z状态来⼀个创建维持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.僵尸进程的危害①进程的退出状态必须被维持下去因为他要告诉关⼼它的进程⽗进程你交给我的任务我办的怎么样了。可⽗进程如果⼀直不读取那⼦进程就⼀直处于Z状态是的②维护退出状态本⾝就是要⽤数据维护也属于进程基本信息所以保存在task_struct(PCB)中换句话说Z状态⼀直不退出PCB⼀直都要维护是的③那⼀个⽗进程创建了很多⼦进程就是不回收是不是就会造成内存资源的浪费是的因为数据结构对象本⾝就要占⽤内存想想C中定义⼀个结构体变量对象是要在内存的某个位置进⾏开辟空间如何避免僵尸进程后面再讲进程还需要至少三篇博客才能讲透下讲再见
返回列表