【计算机系统基石与Linux进程管理深度解析】(三):https://developer.aliyun.com/article/1425715
4.3.1.R运行状态和S睡眠状态
我么先来看一下R运行状态和S睡眠状态,我们先来写一段代码。
make编译之后执行我们的可执行程序
然后我们加入获取pid的代码,然后再次make编译运行,立马输入指令:ps axj | head -1 && ps axj | grep myprocess
我们可以看到状态列:STAT,但是此时我们发现此时进程的状态是S状态,S状态不是我们的睡眠状态嘛,但是我们的程序此刻正在运行,不应该是R运行状态吗???
这是因为我们的代码有一句sleep休眠1秒,注定了当前进程有90.99999999%的可能性大部分处于休眠状态的,只有很少的情况下才会被调度一下,输出"hello myprocess!\n",所以要查到运行状态是很难的,因为只有几毫秒甚至几微米这个进程才会被调度一次,大部分这个进程都是没有被调度的,所以处于S睡眠状态很正常。
我们把我们的sleep语句注释掉,这下应该是R运行状态吧,我们来看看结果。
我们发现此时还是S睡眠状态,这是为什么呢?
这是因为我们的代码有一句printf打印输出的语句,它作用是向显示器打印,而且我们这个代码是在远端服务器上运行的,远端服务器上运行的结果展现在我们电脑的显示器上,printf打印输出它是会访问外设的,此时外设设备不一定已经准备就绪了,第一次输出打印成功,第二次我们就能保证数据立马从缓冲区刷新出来打印了吗?CPU的速度比外设快很多,当时我们打印输出的缓存区的时候,显示器这个外设不一定能立马刷新数据到显示器,外设相比CPU太慢了,所以printf打印输出的语句里面伴生着很多引起我们当前进程处于某种等待这个设备就绪的过程,所以这个进程还是有大量时间在等待,所以还是S睡眠状态。
那我们把我们的printf语句注释掉,这下应该是R运行状态吧,我们来看看结果。
此时终于是我们的R运行状态。这里我们发现grep --color = auto myprocess也是R运行状态,为什么?首先grep也是一个进程,当一个进程被调度的时候,它才是运行状态,也只有grep进程被调度的时候,它才能帮我们过滤process进程信息,只不过这里grep也过滤出了自己。这就相当于我们每天吃饭,看电影和打游戏,前提是自己得运行起来,你要是睡觉肯定就完成不了,所以grep要过滤得前提必须自己是运行状态。
我们先将我们的程序恢复如初,然后我们再来看一下这个'+'是什么意思?
当我们执行程序的时候,期间无论输入什么指令,这些指令都不会执行,这就是我们的前台程序,当为前台程序的时候,会在状态后面加上'+'。
那我们怎么让加号'+'变没呢?我们需要在执行我们的可执行程序后面加上&,这样我们的程序就变成了后台程序,期间无论输入什么指令,这些指令都会执行。
这种后台进程我们无法通过ctrl+c终止,只能通过kill -9 18361杀掉程序。我们再来看一下我们的S休眠状态,它到底是什么呢?我们来写一段代码。
然后我们编译运行我们的程序,当我们不从键盘上输入的时候,我们来查看此时的进程状态。
此时我们的进程正在干嘛呢?此时它正在等待键盘的输入,此时它正在等待我们的外设,当前进程并没有被调度,因为我们的外设资源没有就绪,我们的用户没有按下键盘,所以此时process这个进程处于阻塞状态,所以Linux下的S状态就等同于操作系统学科中的阻塞状态,但是阻塞状态就不只有S状态这一种。我们可以通过ctrl +c终止上面的进程,所以在Linux下S状态也可称为可中断睡眠 - 浅度睡眠。
4.3.2.D磁盘休眠状态
那我们有其他睡眠形式嘛?有,比如我们的D磁盘休眠状态,它是不可中断睡眠 - 深度睡眠,不过这种形式无法用代码演示,这里我们只能举一个例子:
未来当一个进程运行的时候需要向磁盘写入文件(文件比较大),于是进程就隔着老远向磁盘喊:磁盘,你出来,我们有事和你说,磁盘就探出个脑袋说:进程你找我什么事啊!进程说:我这里有100MB的数据哦,很重要,你帮我去写入吧!写入到你的磁盘里,我在内存种等你,你完成的时候,不管是写入成功还是写入失败你都告诉我一声啊!因为我要给用户交代,我要告诉用户结果。于是进程就把数据交给磁盘,让磁盘写入数据,于是磁盘就开始找合适位置开始写了,因为磁盘是外设,所以进程就只能等了,进程就等的很无聊,在内存种搬个小板凳,翘着二郎腿和磕着小瓜子在那等外设磁盘的结果。当时操作系统看到这个进程为你干啥呢?进程说:我在休眠,我在等待磁盘的返会结果。操作系统生气的说:等什么等,你没看到我们忙成什么样子啦,整个内存的资源已经严重吃紧了,我已经唤出了很多进程的代码和数据,你还在这里等!!!你在这里什么都没干,还翘着二郎腿和磕着小瓜子,还占着内存,于是操作系统一怒之下把这个进程回收了。此时磁盘写着数据,突然发现磁盘空间不够了,于是磁盘弹出脑袋说:进程老哥,我空间不够了,写失败了,进程老哥,你人呢?怎么不见呢.?此时磁盘找不到就只能将这个结果给丢弃了。用户一直在那等,发现那个进程已经退出了,就认为文件已经写入到磁盘中了,于是用户就去磁盘中寻找,一看头一炸,发现并没有,更严重的是这个100MB的文件是在某宝上面购买的,而且文件就只有5秒的使用权限,现在5秒时间过了,我文件还没有得到,用户直接崩溃了。用户此时就把操作系统,进程,磁盘拉出来军训了,到底是谁的问题。操作系统在被逼急了得时候,是会杀掉进程得哦!此时操作系统通过唤入已经不能解决这个问题。当我们国庆节访问高铁12123网站买票的时候,此时网站很大几率会被挂掉,因为资源已经非常吃紧了,操作系统已经唤出很多进程的数据了 ,此时操作系统发现资源还是非常吃紧了,操作系统自己马上就挂掉了,于是就把进程直接删掉了。所以我们也就不能访问到网站。此时D磁盘休眠状态就应运而生了,它就相当于一块免死金牌,当操作系统发现进程具有这块免死金牌,就不会杀掉这个进程,此时磁盘返回下载失败的结果也能返回给进程,然后进程就由D状态变为R状态,然后告诉用户说失败了。D磁盘休眠状态也是教材上的阻塞状态,因为它也在等外设资源就绪。
4.3.3.T停止状态(stopped)
一步一个jio印,我们在来学习一下T停止状态(stopped)。我们先看现象,后看结论。
首先我们先修改我们的myprocess.c代码
运行一下没有问题
然后我们学习一下kill -l
kill -l 是在Linux操作系统中用来列出所有可用的信号的命令。信号是一种进程间通信的方式,用于通知进程执行某种动作或事件。kill 命令除了用于终止进程外,还可以向进程发送不同的信号,这些信号有各种不同的作用。
以下是一些常见的信号及其含义:
- SIGINT (2): 中断。通常通过按下 Ctrl+C 产生,用于终止当前运行的程序。
- SIGKILL (9): 杀死。立即终止进程,无法被捕获或忽略。
- SIGCONT (18): 继续。继续执行停止的进程。
- SIGSTOP (19): 停止。停止进程的执行,但不终止它。
所以我们可以通过:SIGSTOP (19),停止。停止进程的执行,但不终止它。
此时这个进程就变成了T停止状态,并且后面的'+'没有了。T停止状态:让进程处于暂停状态。此时我们使用ctrl+c无法终止程序,因为一旦把一个进程暂停了,这个进程立马就由前台进程转为后台进程,只能通过kill -9 16659杀掉进程。
再来看一下t(tracing stop)状态,我们先来修改一下我们的process.c和makefile
然后开始进入我们的gdb调试界面。
此时我们就发现进程处于t(tracing stop)状态,它在等gdb进程发生下一个指令。当我们输入r的时候,这个调式进程就运行了,进程在断点处停下来的本质是以t(tracing stop)状态等待用户的下一条指令,由于此时gdb进程没有得到用户的输入,自己也就处于阻塞状态了。这里暂停状态程序什么都没有做,那么可以理解成暂停状态也属于教材上的阻塞状态!!!
4.3.4.X死亡状态(dead)和Z僵尸状态(zomble)
当一个进程在退出之后,它所对应的资源当中,代码和数据可以被直接释放,因为此时代码和数据肯定不会执行了,因为进程已经不执行了,但是当前进程的PCB,描述改进程的状态信息不应该立马被释放,应该缓一缓,此时让系统或者其他进程知道这个进程的退出数据,只有拿完之后这个进程才能算作X死亡状态(dead),此时PCB才能被释放,我们把一个进程已经执行完毕,但是当前并没有会去获取它进程的退出的相关数据时,此时就是Z僵尸状态(zomble)。可以理解人不在了但是精神还在影响着我们!!!现在我来看一下这种状态。
Z(zombie)-僵尸进程
- 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲) 没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
- 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
- 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
此时父进程是没有读取子进程相关退出信息的。
此时我们就可以看到僵尸状态,同时子程序此时后面还有一个单词:defunct
那我们怎么可以观察到死亡状态呢?我们先来学习一下wait()
。
wait()
获取子进程的终止状态: 当一个子进程终止时,它的终止状态会被保存,父进程通过 wait()
调用可以获取到这个子进程的终止状态信息。这包括子进程的退出码(Exit Code)等信息。
我们首先来看一下这个代码的含义:让子进程执行5秒,父进程执行10秒,前5秒父子进程共同运行,后5秒只有父进程运行,中间5秒子进程处于僵尸状态,10秒之后父进程就结束了,我们让父进程等待 wait() 获取一下子进程的终止状态信息,然后我们就可以看到子进程已经会销毁了。
前五秒的结果:
后五秒结果:
父进程获取子进程死亡信息,子进程被销毁
随后父进程也被退出了
上面子进程确实已经死亡了,已经时死亡状态了,只不过子进程死亡的时间很短,我们没有检测到。这里我们提一个问题:为什么要有Z僵尸状态?创建进程是希望这个进程给用户完成工作的,子进程就必须必须产生结果返回给用户,进程退出时这些结果都放在PCB里,必须要等到父进程拿到PCB里面的结果,僵尸状态的子进程才能被释放。什么时Z僵尸状态?子进程已经退出,但是当前的进程的状态需要自己维护的特性,供父进程读取,此时就必须处于Z僵尸状态。如果我作为父进程不读取呢?Z僵尸状态就会一直存在,也就意味着PCB一直存在,此时就要占用内存,而内存不释放就会发生内存泄漏的问题。这里的父进程为什么查看不到Z僵尸状态呢?因为bash自动读取了父进程的PCB信息,读取完是一瞬间的事,也就看不到父进程的Z僵尸状态。
僵尸进程危害
- 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎 么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话 说,Z状态一直不退出,PCB一直都要维护?是的!
- 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构 对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空 间!
- 内存泄漏?是的!
- 如何避免?后面讲
4.4.5.进程状态总结
这就是Linux下的进程状态转化图
至此,值得关注的进程状态全部讲解完成,下面来认识另一种进程
4.4.6.孤儿进程
先不解是什么孤儿进程,我们先来改一下我们的代码。
我们让子进程执行500秒,让父进程执行5秒。
前5秒结果:
后面时间结果:
毫无疑问程序肯定是父进程先退出,此时父进程退出后,bash获取到父进程的PCB后,父进程就销毁了。父进程先退出,此时子进程就称之为“孤儿进程”。
问题来了,那么父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?这里我们发现父进程退出之后,此时父进程的id就变为1了,1号pid是谁呢?我们来看一看。
父进程如果提前退出,那么子进程后退出,进入Z之后,此时子进程被1号init进程领养,当然要有init进程回收喽。同时我们还可以发现孤儿进程还是一个后台进程。