【Linux系统编程】进程状态

张开发
2026/4/12 0:04:30 15 分钟阅读
【Linux系统编程】进程状态
文章目录前言1. 准备工作2. 阻塞、挂起状态的了解2.1 阻塞2.2 挂起3. 看看Linux内核源代码怎么说4. R运行状态running5. S休眠状态sleeping)6. D不可中断休眠状态7. T暂停状态stopped8. t 追踪暂停状态 (tracing stop)9. X死亡状态dead10. Z僵尸状态 (zombie)前言在前面的学习中我们已经学习了进程的概念和基本创建以及如何通过相关的系统调用创建进程和获取进程标识符。那为了弄明白正在运行的进程是什么意思我们需要了解进程的不同状态1. 准备工作先问大家一个问题我们使用一个应用的时候比如我们打开电脑上的爱奇艺看电影那在看电影的过程中这个应用对应的进程是否是一直在不停的运行呢那其实呢它并不是一直在不停运行的。我们来举个极端一点的场景假设现在这里只有一个CPU但是我们同时打开了多个进程比如QQ、微信、爱奇艺、网易云音乐等然后浏览器还有一些下载任务。那这么多的进程在操作系统内被CPU调度运行的时候呢其实并不是从一个进程运行开始一直不停直到运行结束的而是每个进程被CPU运行一会儿操作系统都会把它从CPU上拿下来然后把另一个放上来运行这样重复的快速交替运行的。一般呢我们把它叫做基于进程切换的分时操作系统即不同的进程快速切换交替运行同一时间段内它们的代码都可以得以推进使得用户感觉多个应用程序几乎同时在运行因为我们的感官和CPU的运行速度差的是很大的。所以进程在运行的时候是可以被操作系统管理和调度的那这样的话就涉及一个问题就是在某个时刻操作系统凭什么调度这个进程让这个进程在CPU上运行而不是其它的进程呢那这就取决于进程状态相关的概念。那在正式学习进程状态之前我们先来了解两个概念——阻塞和挂起。2. 阻塞、挂起状态的了解2.1 阻塞那我们先来了解一下阻塞阻塞即进程因为正在等待某种条件就绪而导致的一种不推进不被调度的状态。这样说呢大家可能不太理解比如现在有一个进程被创建了我们打开一个应用或运行一个程序但是一直没有被CPU执行那大家想一下这种情况在我们用户层面看到的是一个什么情况呢再比如我们有时候在Windows上启动了好多个程序就可能会出现“卡”的情况。那这种情况呢其实就可能是进程太多了操作系统调度不过来了目前操作系统正在调度的就正在运行没有被调度的就卡在那了。所以呢说成大白话阻塞就是进程“卡住”了。再比如呢我们下载一些东西的时候如果出现了断网或者0KB了那这个时候这个下载的进度条就也卡住了等待网络资源恢复。当然这个卡跟我们上面说的有点不一样。但是这种情况其实也可以认为是阻塞状态。所以我们又得出阻塞一定是在等待某种资源。那如何理解这里的等待某种资源呢首先这里等待的资源可能是什么呢比如磁盘、网卡、显卡各种外设等。举个例子我们在下载某个东西的时候突然断网了那对应的进程就会被设置成阻塞状态了CPU就不会再继续执行你了你这个进程就要等到网络好了的时候才会被操作系统调度被CPU继续执行。就好比你去银行办理某个业务办理之前你需要填一个单子但是此时单子用完了相应的工作人员去取了然后你所在的这个柜台的工作人员对你说那您先去旁边等一会吧先让后面的人办理它们的业务等您拿到单子填好之后再来办理您的业务吧。那现实生活中的等待我们可能很好理解那你就搬个凳子坐那里等一会呗可是这里等待某种资源它具体是如何等待呢首先对于这些资源操作系统肯定要管理起来怎么管理的先描述再组织这是我们之前讲过的。先搞一个结构体把它们的属性啥的都封装起来然后在搞一个链表或其它高效的数据结构组织起来。那进程呢操作系统里面可能存在很多进程那也要管理起来如何管理先描述再组织。那就是一个task_struct的链表。那某个进程在等待某种资源的时候呢其实就是把自己的task_struct放到对应资源的等待队列中。在操作系统中每个资源对应的描述数据结构通常会包含一个等待队列。这个等待队列用于存储等待该资源的进程或线程。当一个进程请求某个资源时如果资源当前不可用操作系统会将该进程标记为阻塞状态并将其对应的 PCBtask_struct 移动到相应资源的等待队列后面。这样CPU就可以调度其他可执行的进程来继续执行。2.2 挂起那下面我们再来了解一下挂起还是通过一个例子给大家讲解假设现在有一个下载任务因为断网进入了阻塞状态而此时呢操作系统的内存资源又特别紧张那此时呢操作系统可能会做这样的事情就是把这些阻塞状态的进程的代码和数据先交换到磁盘上因为你这些阻塞的进程不被调度但是你的代码和数据还放在内存里那就太占用资源了。然后等到这些阻塞的进程等待的资源就绪的时候再把它们的代码和数据交换回内存然后被CPU运行。那其中操作系统把某些进程的代码和数据交换到磁盘上此时就可以认为这些进程处在挂起状态。严格意义来讲我们这里说的这种挂起状态全称可以叫做阻塞挂起状态,可以将挂起理解为一种特殊的阻塞状态那为什么要先给大家说一下这两个概念呢因为这两个状态是操作系统中进程比较核心的两个状态当然还有一个运行状态它相对比较好理解我们后面针对具体的Linux操作系统去讲解。当然如果我们去看一些操作系统的书籍或去网上搜进程的状态可能大部分都是这种有的可能会有挂起状态。而我们上面了解的内容其实就是基于操作系统这门课程来说的可以认为它对于所有具体的操作系统都是成立的可能比较抽象。而我们下面呢要针对一款具体的操作系统——Linux来学习一下进程的状态。3. 看看Linux内核源代码怎么说一个进程可以有多个状态在Linux内核里进程有时候也叫做任务那首先我们可以来看一下在kernel源代码里关于进程状态的定义/* * The task state array is a strange bitmap of * reasons to sleep. Thus running is zero, and * you can test for combinations of others with * simple bit tests. * 翻译任务/进程状态数组是一种奇特的位图用于表示睡眠的原因。 * 因此运行中对应的位为零你可以使用简单的位测试来检查 * 其他组合的状态。 */staticconstchar*consttask_state_array[]{R (running),/* 0 */S (sleeping),/* 1 */D (disk sleep),/* 2 */T (stopped),/* 4 */t (tracing stop),/* 8 */X (dead),/* 16 */Z (zombie),/* 32 */};那这里的进程状态呢其实就是这几个一共有7种进程状态的变化其实就是改每个状态对应的那个数值就行了接下来我们就来一一学习这几种状态4. R运行状态runningR表示运行状态那我问大家如果一个进程是R状态那么它一定是在CPU上运行吗那其实是不一定的也就是说操作系统里可能有10个8个状态为R的进程但是它们之中可能只有几个是正在CPU上运行的。所以其实操作系统维护调度进程也有相应的队列运行队列运行队列通常根据不同的调度策略进行管理处在运行队列中的进程它的状态就是R所以总结一下R运行状态running: 并不意味着进程一定在运行中它表明进程要么是在运行中要么在运行队列里那下面我们来写一个代码观察观察这样一个代码再写一个Makefile然后我们来make生成可执行程序并执行一下大家看现在这个进程在运行吗当然我们肉眼可见它在不停的运行。那我们来查看一下前面学过查看进程的命令我们看到查出来的进程的状态是s后面那个号我们先不管那s对应的状态是啥呢s呢其实是休眠状态后面我们就会学。那这里怎么回事啊为什么我们查到的状态是s休眠呢虽然s状态我们还不太了解但我知道它不是R运行状态那这是怎么回事呢至于原因呢先不急我呢现在把我们上面的代码修改一下我把这句代码注释掉。然后我们重写make并运行这次再查看大家看这次就是R运行状态了那这是怎么回事呢我们第一次的代码是不是一个死循环然后里面一个打印语句所以它运行的时候是不是要不断的频繁访问显示器这个外设啊但是我们每次访问显示器的时候他一定是就绪的吗如果不是就绪的话那我们的进程是不是就不在进程的运行队列里面排队了而是在显示器这个外设资源的等待队列里面排队。所以我们才查到了S状态它也是阻塞状态的一种。所以这个进程并不是一直在CPU的运行队列里面的而是在运行队列和外设资源的等待队列里面不断切换的。那为什么我们查到的是S状态而不是R状态呢我们这里只有一行代码CPU执行的时候是很快的而等待外设的过程相对于CPU执行代码的速度是非常慢的。所以我们ps命令查的时候会发现查到的差不多都是s状态可能查1万次能有一两次是R状态。那为什么我们后面把打印注释掉查看到的就是R状态了那也很好解释因为我们把它注释掉程序里面就没有访问资源的代码。只有while循环判断while循环判断就是纯计算所以它不需要访问外设那只要被调度就一直处在运行队列里所以我们查到它的状态总是R状态。那下面我们就来学习下一个状态——S状态5. S休眠状态sleeping)概念大家可以先看一下S休眠状态sleeping): 意味着进程在等待事件完成这里的休眠有时候也叫做可中断修眠interruptible sleep。那首先呢可以告诉大家S状态就是一种阻塞状态我们再来修改一下代码改成这样我们来运行一下其实就是一个死循环嘛我们输入一个数他就给我们打印一个数那我们来查一下这个进程的状态我们看到此时它的状态就是S。因为它此时正在等待资源啊等待我们通过键盘输入数据。所以它此时就不在CPU的运行队列里没有被调度而是在键盘资源的等待队列那就是我们上面说的阻塞状态。而我们说了S休眠状态其实就是阻塞的一种而且S这种休眠状态被称为可中断休眠我们CTRLc就可以终止该进程那现在大家再回过头来看S状态的概念S休眠状态sleeping): 意味着进程在等待事件完成这里的休眠有时候也叫做可中断修眠interruptible sleep/浅度睡眠。就应该比较好理解了那我们继续6. D不可中断休眠状态D 磁盘休眠状态Disk sleep也叫不可中断休眠状态uninterruptible sleep/深度睡眠在这个状态的进程通常要等待IO的结束。也算是一种阻塞状态。那么D 状态呢其实我们平时大概率是遇不到的一般是那些做系统管理的运维的等等这些人员可能遇到的会比较多。那该如何理解这个D 状态呢我们来看这样一个场景假设现在呢这个进程想往磁盘上写100MB的数据。那它就告诉磁盘我想往你身上写100MB的数据然后磁盘说那我的速度比较慢你要等等我那此时进程就被设置成了阻塞状态就去磁盘的等待队列里面排队了。那CPU呢就对这个进程说那你先写数据吧我先运行其它的进程等你资源准备就绪了我再调度你。此时操作系统路过。作为系统的管理者它发现此时系统的内存资源已经非常紧张了如果再有进程就要挂了但是此时操作系统却发现你这个进程却不在运行队列里而是啥也不干在这里等。那操作系统就想那我把你杀掉吧于是这个进程就被干掉了。与此同时呢又发生了新的状况这个进程被干掉了此时磁盘还在写数据但是由于某些原因数据写失败了。那然后呢磁盘就要给这个进程说你的这些数据写入失败了但是此时却发现人不见了找不到这个进程了。我还想着告诉你然后让你反馈给用户呢那现在进程被杀死了就只剩磁盘拿着这100mb的数据不知该怎么处理了。可以直接把这些数据丢掉嘛如果这些数据很重要呢那此时这种情况该如何处理呢上面的故事呢涉及3个比较关键的角色——操作系统、进程、磁盘。那么请问导致上面那样的情况出现是谁的锅呢那我们分析一下它们做的好像都是合理的。那怎么办呢如何避免这种情况的出现呢那其实只要我们保证这个进程不能被杀死就行了啊。所以呢就有了这样一种休眠状态即D状态——不可中断休眠状态/深度睡眠状态。如果一个进程处在这种状态它就无法被杀死你按CTRLc也没用处在深度睡眠状态的进程不对任何外部事件进行响应操作系统也杀不掉。所以如果出现长时间D状态的话正常的io进程处于D状态不会维持特别久我们一般看不到那你的机器可能就快要宕机了因为此时磁盘的压力可能已经非常大了严重变慢才导致出现这种情况。所以我们才说这种状态我们大概率遇不到。7. T暂停状态stoppedT暂停状态呢其实也是一种阻塞状态可以通过发送 SIGSTOP 信号给进程来暂停T进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。我们再来修改一下代码我们make并运行一下当然现在如果我们查他的状态是那我们怎么让它暂停呢?再来学习一个新命令还是kill命令后面我们还会详细讲现在先用。然后呢-19这个选项加上对应进程的PID他就可以暂停一个进程。我们来试一下目前它的状态是S我们看到执行之后这个进程就停止了而且我们再去查看进程状态就变成T暂停状态了。那我想让他继续运行呢再学一个命令kill -18 就可以让指定的进程继续执行我们看到执行之后它就重新运行起来了此时再去查看进程状态就又变回S了。但是呢此时我们去CTRLc无法终止这个进程了。那除此之外不知道大家有没有注意到一个现象前面我们查看的状态字母后面还有一个“”加号但是自从上面变成T状态之后就没有了那进程状态后面的表示什么呢如果带的话表明该进程是在前台运行的CTRLc可以终止掉它如果没有就表明这个进程变成了在后台运行后台运行的时候我们可以去正常执行我们的shell指令但是它在后台还会一直运行且CTRLc终止不了那有办法杀掉它吗再来认识一个命令kill -9就可以杀掉这个后台进程当然前台的也可以那一个进程什么时候会暂停什么情况下应该进入暂停状态呢来看看下面这种状态8. t 追踪暂停状态 (tracing stop)然后我们看到除了T之外还有一个t——追踪暂停状态那其实我们GDB调式程序的时候打了一个断点我需要程序在断点处停下来此时程序就会停止执行进入t状态这就是需要让进程进入暂停状态的一种情况我们来演示一下那首先Makefile里面我们要加一个-g选项让它以debug版本生成这是我们之前讲过的内容然后我们GBD调式一下查看一下代码那我现在在第12行打个断点然后我们r开始调式它就在12行停下来了那此时我们去查看它的状态就能看到对应进程的状态就变成t了并且我们发现我们启动gdb进行调式就会新启动一个gdb的进程并且我们输入r开始调试之后我们调式的这个程序对应的进程也会被新建出来r之前是没有的而这个进程是gdb对应进程的子进程。即我们输入r进行调式的时候gdb会创建一个子进程来执行我们要调式的代码。如果我们退出gdb此时再查就没有了这就是追踪暂停状态9. X死亡状态deadX死亡状态dead这个状态只是一个返回状态你不会在任务列表里看到这个状态。它是一个瞬时状态我们也很难查到不过与之相关的还有一个重点状态——Z (zombie)僵尸状态那这两个我们放到一块讲10. Z僵尸状态 (zombie)首先问大家一个问题我们为什么要去创建进程那其实就是为了让进程帮我们办事嘛完成某个任务。那对于进程执行的结果我们有时候可能是关心的有时候可能并不关心。那我们来讲一个东西我们平时写的C/C 代码main函数里面最后一般都要有一个返回值return 0那大家可能不是特别清楚为什么main函数要有一个返回值这个返回值是做什么的呢那这个返回值呢其实叫做进程退出码。另外呢我们之前有讲过任何命令行上启动的进程都是bash的子进程所以我们运行一个程序的时候可以认为是父进程bash创建了一个子进程让这个子进程去帮忙办事。那你事办的怎么样结果如何父进程是怎么知道的呢它是通过退出码来获悉的。那如何获取一个进程的退出码呢echo $?就可以获取进程的退出码不过呢我们上面的代码没什么意义如果我们关心进程的执行结果比如有一个算法判断它执行的结果对不对。我们改一下代码假设正确结果是0如果算法返回值等于0就返回0否则返回3代表返回结果不正确。那我们再来运行查看一下退出码是0表示结果正确如果运算结果不对那么退出码就变成了3那关于上面讲的这个退出码大家先了解一下后面还会说。那再回到我们上面讲的我们创建进程帮我们做事有时候是关心结果的那如何获取这个结果呢其中一个方式就是通过退出码那么如果一个进程退出了立即变成了X死亡状态那父进程bash有没有机会拿到这个退出结果呢所以为了方便子进程退出后父进程或操作系统获取该进程的退出结果Linux进程退出时进程一般不会立即死亡而是要维持一个Z状态即——僵尸状态。等这个进程真正被回收了它的状态就会变成X死亡状态。如何回收我们后面会讲那处在僵尸状态的进程就叫做僵尸进程关于僵尸进程的进一步理解我们下篇文章讲解…

更多文章