前言
在前面的学习中,我们已经学习了进程的概念和基本创建,以及如何通过相关的系统调用创建进程和获取进程标识符。
那为了弄明白正在运行的进程是什么意思,我们需要了解进程的不同状态
1. 准备工作
先问大家一个问题:我们使用一个应用的时候,比如我们打开电脑上的爱奇艺看电影,那在看电影的过程中这个应用对应的进程是否是一直在不停的运行呢?
🆗,那其实呢它并不是一直在不停运行的。 我们来举个极端一点的场景: 假设现在这里只有一个CPU,但是我们同时打开了多个进程,比如QQ、微信、爱奇艺、网易云音乐等,然后浏览器还有一些下载任务。
那这么多的进程在操作系统内被CPU调度运行的时候呢其实并不是从一个进程运行开始,一直不停直到运行结束的,而是每个进程被CPU运行一会儿,操作系统都会把它从操作系统上拿下来,然后把另一个放上来运行,这样重复的快速交替运行的。 一般呢我们把它叫做基于进程切换的分时操作系统,即不同的进程快速切换交替运行,同一时间段内它们的代码都可以得以推进,使得用户感觉多个应用程序几乎同时在运行,因为我们的感官和CPU的运行速度差的是很大的。
所以进程在运行的时候是可以被操作系统管理和调度的:
那这样的话就涉及一个问题,就是在某个时刻操作系统凭什么调度这个进程,让这个进程在CPU上运行而不是其它的进程呢?
那这就取决于进程状态相关的概念。 那在正式学习进程状态之前,我们先来了解两个概念——阻塞和挂起。
2. 阻塞、挂起状态的了解
2.1 阻塞
那我们先来了解一下阻塞:
阻塞即进程因为正在等待某种条件就绪,而导致的一种不推进(不被调度)的状态。
这样说呢,大家可能不太理解:
比如现在有一个进程被创建了(我们打开一个应用或运行一个程序),但是一直没有被CPU执行,那大家想一下这种情况在我们用户层面看到的是一个什么情况呢? 再比如我们有时候在Windows上启动了好多个程序,就可能会出现“卡”的情况。 那这种情况呢其实就可能是进程太多了,操作系统调度不过来了,目前操作系统正在调度的,就正在运行,没有被调度的,就卡在那了。 所以呢,说成大白话,阻塞就是进程“卡住”了。
再比如呢:
我们下载一些东西的时候,如果出现了断网或者0KB了,那这个时候这个下载的进度条就也卡住了。当然这个卡跟我们上面说的有的不一样。 但是这种情况其实也可以认为是阻塞状态。 所以,我们又得出: 阻塞一定是在等待某种资源。
那如何理解这里的等待某种资源呢?
首先这里等待的资源可能是什么呢? 比如:磁盘、网卡、显卡各种外设等。 举个例子: 我们在下载某个东西的时候,突然断网了,那对应的进程就会被设置成阻塞状态了,CPU就不会再继续执行你了,你这个进程就要等到网络好了的时候才会被操作系统调度,被CPU继续执行。 就好比你去银行办理某个业务,办理之前你需要填一个单子,但是此时单子用完了,相应的工作人员去取了,然后你所在的这个柜台的工作人员对你说,那您先去旁边等一会吧,先让后面的人办理它们的业务,等您拿到单子填好之后再来办理您的业务吧。
那现实生活中的等待我们可能很好理解,那你就搬个凳子坐那里等一会呗,可是这里等待某种资源,它具体是如何等待呢?
首先,对于这些资源,操作系统肯定要管理起来,怎么管理的? 先描述,再组织! 这是我们之前讲过的。先搞一个结构体把它们的属性啥的都封装起来,然后在搞一个链表或其它高效的数据结构组织起来。 那进程呢?操作系统里面可能存在很多进程,那也要管理起来,如何管理? 先描述,再组织。 那就是一个task_struct的链表。 那某个进程在等待某种资源的时候呢其实就是把自己的task_struct放到对应资源的等待队列中。 在操作系统中,每个资源对应的描述数据结构通常会包含一个等待队列。这个等待队列用于存储等待该资源的进程或线程。 当一个进程请求某个资源时,如果资源当前不可用,操作系统会将该进程标记为阻塞状态,并将其对应的 PCB(task_struct) 移动到相应资源的等待队列后面。这样,CPU就可以调度其他可执行的进程来继续执行。
2.2 挂起
那下面我们再来了解一下挂起
还是通过一个例子给大家讲解:
假设现在有一个下载任务因为断网进入了阻塞状态
而此时呢操作系统的内存资源又特别紧张,那此时呢操作系统可能会做这样的事情,就是把这些阻塞状态的进程的代码和数据先交换到磁盘上,因为你这些阻塞的进程不被调度,但是你的代码和数据还放在内存里,那就太占用资源了。 然后等到这些阻塞的进程等待的资源就绪的时候,再把它们的代码和数据交换回内存,然后被CPU运行。 那其中操作系统把某些进程的代码和数据交换到磁盘上,此时就可以认为这些进程处在挂起状态。 严格意义来讲,我们这里说的这种挂起状态全称可以叫做阻塞挂起状态,可以将挂起理解为一种特殊的阻塞状态
那为什么要先给大家说一下这两个概念呢?
因为这两个状态是操作系统中进程比较核心的两个状态,当然还有一个运行状态它相对比较好理解,我们后面针对具体的Linux操作系统去讲解。
当然如果我们去看一些操作系统的书籍或去网上搜进程的状态:
可能大部分都是这种
有的可能会有挂起状态。 而我们上面了解的内容其实就是基于操作系统这门课程来说的,可以认为它对于所有具体的操作系统都是成立的,可能比较抽象。
而我们下面呢,要针对一款具体的操作系统——Linux来学习一下进程的状态。
3. 看看Linux内核源代码怎么说
一个进程可以有多个状态(在Linux内核里,进程有时候也叫做任务),那首先我们可以来看一下在kernel源代码里关于进程状态的定义:
代码语言:javascript
复制
/*
* 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.
* 翻译:任务/进程状态数组是一种奇特的"位图",用于表示睡眠的原因。
* 因此,"运行中"对应的位为零,你可以使用简单的位测试来检查
* 其他组合的状态。
*/
static const char * const task_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运行状态(running)
R表示运行状态,那我问大家,如果一个进程是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这种休眠状态被称为可中断休眠
我们CTRL+c就可以终止该进程
那现在大家再回过头来看S状态的概念:
S休眠状态(sleeping): 意味着进程在等待事件完成(这里的休眠有时候也叫做可中断修眠(interruptible sleep))。
就应该比较好理解了
那我们继续
6. D不可中断休眠状态
D 磁盘休眠状态(Disk sleep)也叫不可中断休眠状态(uninterruptible sleep),在这个状态的进程通常要等待IO的结束。 也算是一种阻塞状态。
那么D 状态呢?
其实我们平时大概率是遇不到的,一般是那些做系统管理的,运维的等等这些人员可能遇到的会比较多。
那该如何理解这个D 状态呢?我们来看这样一个场景:
假设现在呢这个进程想往磁盘上写100MB的数据。 那它就告诉磁盘,我想往你身上写100MB的数据;然后磁盘说,那我的速度比较慢,你要等等我;那此时进程就被设置成了阻塞状态,就去磁盘的等待队列里面排队了。 那CPU呢就对这个进程说,那你先写数据吧,我先运行其它的进程,等你资源准备就绪了,我再调度你。 此时,操作系统路过。 作为系统的管理者,它发现此时系统的内存资源已经非常紧张了,如果再有进程就要挂了,但是此时操作系统却发现你这个进程却不在运行队列里,而是啥也不干在这里等。 那操作系统就想,那我把你杀掉吧,于是,这个进程就被干掉了。 与此同时呢,又发生了新的状况,这个进程被干掉了,此时磁盘还在写数据,但是,由于某些原因,数据写失败了。 那然后呢磁盘就要给这个进程说,你的这些数据写入失败了,但是此时却发现人不见了,找不到这个进程了。我还想着告诉你,然后让你反馈给用户呢! 那现在进程被杀死了,就只剩磁盘拿着这100mb的数据,不知该怎么处理了。 可以直接把这些数据丢掉嘛,如果这些数据很重要呢!
那此时这种情况该如何处理呢?
上面的故事呢,涉及3个比较关键的角色——操作系统、进程、磁盘。 那么请问:导致上面那样的情况出现,是谁的锅呢? 那我们分析一下它们做的好像都是合理的。 那怎么办呢?如何避免这种情况的出现呢? 那其实只要我们保证这个进程不能被杀死就行了啊。
所以呢,就有了这样一种休眠状态:
即D状态——不可中断休眠状态。 如果一个进程处在这种状态,它就无法被杀死,操作系统也不行。 所以如果出现D状态的话,那你的机器可能就快要宕机了,因为此时磁盘的压力可能已经非常大了,严重变慢,才导致出现这种情况。 所以我们才说这种状态我们大概率遇不到。
7. T暂停状态(stopped)
T暂停状态呢其实也是一种阻塞状态:
可以通过发送 SIGSTOP 信号给进程来暂停(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
我们再来修改一下代码:
我们make并运行一下
当然现在如果我们查他的状态是S
那我们怎么让它暂停呢?
再来学习一个新命令
还是kill命令,后面我们还会详细讲,现在先用。 然后呢-19这个选项,加上对应进程的PID,他就可以暂停一个进程。
我们来试一下:
目前它的状态是S
我们看到执行之后这个进程就停止了,而且我们再去查看进程状态就变成T暂停状态了。
那我想让他继续运行呢?
再学一个命令
kill -18 就可以让指定的进程继续执行
我们看到执行之后它就重新运行起来了 此时再去查看进程状态
就又变回S了。
但是呢!
此时我们去CTRL+c无法终止这个进程了。 那除此之外不知道大家有没有注意到一个现象 前面我们查看的状态字母后面还有一个“+”加号,但是自从上面变成T状态之后,就没有+了
那进程状态后面的+表示什么呢?
🆗,如果带+的话,表明该进程是在前台运行的,CTRL+c可以终止掉它; 如果没有+,就表明这个进程变成了在后台运行,后台运行的时候我们可以去正常执行我们的shell指令
但是它在后台还会一直运行,且CTRL+c终止不了 那有办法杀掉它吗,再来认识一个命令
kill -9
就可以杀掉这个后台进程(当然前台的也可以)
8. t 追踪暂停状态 (tracing stop)
然后我们看到,除了T之外:
还有一个t——追踪暂停状态
那其实我们GDB调式程序的是时候,如果打了断点,程序在断点处停下来,此时程序就会停止执行进入t状态
我们来演示一下:
那首先Makefile里面我们要加一个-g选项让它以debug版本生成,这是我们之前讲过的内容
然后我们GBD调式一下
查看一下代码
那我现在在第12行打个断点
然后我们r开始调式
它就在12行停下来了
那此时我们去查看它的状态
就能看到对应进程的状态就变成t了 如果我们退出gdb
此时再查就没有了
那这就是追踪暂停状态
9. X死亡状态(dead)
X死亡状态(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死亡状态。(如何回收我们后面会讲)
那处在僵尸状态的进程就叫做僵尸进程,关于僵尸进程的进一步理解我们下篇文章讲解…