进程状态
你真正的理解了进程的状态嘛?特别是操作系统教材中学过的进程状态,你真的理解了吗?
教材上关于进程状态的说明
下面我们以下图为例:
这是教材上对操作系统的说明,但是它并没有详细的说明,这些状态具体是什么?如果我们想要真正的理解这些状态,那么我们就必须谈具体的操作系统。在有些具体的操作系统当中,这些状态要么有差异,要么没有这样的状态。所以这张图呢,就算是套到linux操作系统上也没错,但是就是还是没有说明具体是什么?
其实所谓的进程状态,就是PCB中的一个字段,就是PCB中的一个变量,比如说是int status.
然后呢我们可以用整形值定义一下状态
#define NEW 1
#define RUNNING 2
#define BLOCK 3
我们就可以用上面宏定义的几种状态给对应进程PCB中的状态进行赋值表示,就像pcb->status = NEW;
然后我们在代码中可以这么用:
if(pcb->status==NEW) PCB放入运行队列中之类的
else if(pcb->status==BLOCK) pcb就放入阻塞队列中等等一系列要执行的操作。
所以呢,所谓的状态变化,其实本质就是通过修改整形变量的值。
下面呢,我们来具体的理解以下三种状态:
1.运行状态:
不同的计算机有不同的配置,有的计算机是单CPU,有的计算机是多CPU,我们现在以单CPU为例来进行说明。
什么叫做运行状态:
只要在运行队列中的进程,状态都是运行状态,对于进程来说就是:我已经准备好了,可以随时被调度。
每一个CPU在系统层面都会维护一个运行队列。
2.阻塞状态
假如代码是你写的,当形成的可执行程序被加载到内存中放到运行队列中等待被CPU调度运行的时候, 我们的代码中,一定会或多或少的会访问系统中的某些资源。比如:磁盘,键盘,网卡等各种外设。
我们以最常见的键盘为例,当我们的代码中有scanf()函数,或者cin>>从我们的键盘中读取数据,如果我们就是不输入数据呢?下面我们写如下一段简单的代码:
运行输入一个10,就打印一个10;
如果我们就是不输入值呢?
不输入,说明键盘上的数据是没有准备好,没有就绪。说明我们的进程要访问的资源没有就绪,那就是不具备访问条件,就导致了代码无法继续往后执行。
那么如果我们的进程要访问的资源没有就绪的话,操作系统要不要知道设备的状态呢?操作系统不光要知道,就连键盘不具备访问条件都是操作系统告诉进程的。因为操作系统要管理对应的进程,所以操作系统必须要知道各种硬件设备的状态信息。
既然操作系统要对设备进程管理,那么硬件设备也应该有各个设备对应的一个结构体通过用来描述该硬件设备的状态,类型等各种信息,然后还有结构体指针进行链接,那么操作系统对设备的管理,就转变成了对该链表的增删查改。那么上面谈到的需要键盘输入数据的时候,没有输入,那就是数据没有准备好,在设备结构体中也有设备自己维护的等待队列,运行队列,没有输入数据可以通过识别数据获取状态,如果没有准备好,那么可以把CPU中关联的运行队列中进程对应的PCB移动到该设备下的等待队列中进行等待即可。此时这种状态就叫做阻塞状态。操作系统中会存在非常多的队列,运行队列,等待硬件的设备等待队列等......
因此我们可以得出一个结论:
进程变化的本质:
1.更改pcb status中的整形变量的值
2.将PCB链入不同的队列中。
我们所说的所有过程都只和进程的pcb有关,和进程的代码和数据都无关。
操作系统一定是最先知道它所管理的设备的状态变化的
我们把进程的PCB从运行队列移动到等待队列中排队的过程叫做该进程阻塞了,把进程的PCB从等待队列中移动到运行队列中叫做把该进程唤醒了。
3.挂起状态
如果一个进程当前被阻塞了,注定了,这个进程在它所等待的资源没有就绪的时候,该进程是无法被调度的。如果此时,恰好操作系统内的内存资源已经严重不足了,怎么办?那么此时操作系统就需要解决该问题,操作系统就会对阻塞状态的进程PCB所指向的代码和数据将其交换到磁盘中去,就能够腾出来一段内存空间,因为该进程本来就是阻塞的,暂时不会被调度运行,这个时候我们就把这个进程的 代码和数据交换到磁盘中的进程的状态就叫做挂起。将内存中数据进行置换到外设,针对所有阻塞进程的。我们都知道内存将数据写入磁盘的时候速度都是比较慢的,但是在这种操作系统内存不足,与计算机性能的矛盾下,会优先让操作系统保持正常运行,所以会优先考虑操作系统,所以不用担心慢的问题,因为这个是必然的,主要关心的是,让OS继续执行下去。
其实我们的计算机有一个分区叫swap分区,如果发生上述情况操作系统内的数据会被置换到这里,当进程被调度时,曾经被置换出去的代码和数据又要被重新加载进来。上述全部过程都由操作系统自动执行。
我们上述所说的这种情况这个状态有一个前提条件,那就是进程被阻塞了,所以就叫做阻塞挂起。
linux中进程的具体状态
"R"状态(running):运行状态
我们可以看到下图中linux内核中的源代码怎么说?
/*
* 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 */
};
我们发现,好像没有看到阻塞,挂起的描述,运行倒是看到了。所以我们发现具体的操作系统和我们教材中所说的操作系统差别还挺大的,难道是教材说错了吗?其实并不是,因为我们具体阻塞是什么阻塞,挂起是什么挂起,一定要落实到具体的操作系统上面来。
下面我们来学习linux下具体的进程的几种状态。首先我们来学习"R",也就是running,运行状态。
下面我们通过用一段简单的代码来进行说明:
然后运行起来:
这个进程在运行吗?很明显在运行的,但是我们发现该进程打印出来的进程状态是"S",
我们可以看到这里的S是sleeping, 其实这个S就是一种特殊的阻塞状态,我们在执行这个进程的时候明明是运行状态,怎么会打印出来的全是阻塞状态呢?原因就在于,我们的CPU的运行速度是非常快的,而printf函数在显示器中打印的时候,大部分时候都不是就绪的,而是在阻塞,他都不在运行队列中。我们的这个进程的PCB一直被快速的在等待队列和运行队列中来回调度,大部分状态都是"S"原因就在于我们的IO太慢了,是由于IO影响了进程的状态,那么我们下面把printf函数给去掉,再执行代码:
运行结果
R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
上述两次代码的运行我们发现进程状态S和R后面都有一个”+“号,其实我们是吧这种进程叫做前台进程,就是你在运行当前前台进程的时候不能在输入其他指令,但是比如说我们平时多数都有下载任务,下载的时候我们不会傻傻的在那等吧,如果说那个软件非常的大,我们一般都是把他丢在后台,作为后台进程去下载。
"S"状态(sleeping):休眠状态
浅度睡眠,可以被终止,浅度睡眠会对外部信号做出响应,
"D"状态(dist sleep):休眠状态,深度睡眠
专门针对磁盘来设计的。处于D状态的进程不可被杀掉,OS也没资格。
下面我们可以用一个小故事来让大家理解为什么要有这个状态?
故事时刻:
假如呢,现在有一个进程需要把1GB的数据写入到磁盘中,这1GB的数据是10w个用户的存钱记录,(形象化比喻)进程找到磁盘说:"磁盘,你出来!"。磁盘就探出个脑袋说:"怎么了?你找我有事?",进程就说:"你帮我把这1GB的数据给我写到你那里去",磁盘说:"好嘞,你得稍等一会,因为有可能会写错哦!" 进程就说:"好的!" 此时进程的状态就变成了"S"状态(睡眠状态),刚好这会,操作系统内部内存严重不足,就算把一些阻塞状态进程的代码数据都交换出去了也还是缺少内存,所以操作系统就气冲冲的走了过来看到进程就说:"你在这干嘛呢?没看到我压力更大吗?你还在这休眠 " 操作系统一气之下 ,kill,把进程给干掉了,然后磁盘写到999MB的数据的时候写错了,就探出个脑袋发现:"数据写错了,咦?刚刚那个进程呢?" 然后磁盘由于不止这一个进程需要写入数据,所以它不得已只能把这写错的数据给删了。删了之后呢,这10w个用户存钱的信息不翼而飞了。造成了大量损失,这个时候呢,公司老板把操作系统,进程,磁盘叫到了办公室里面准备挨个问,首先找到最大的操作系统问:"那10w用户存钱记录怎么没了?是不是你干的?" 操作系统说:"老板,你之前可不是这么说的,你之前可是说,公司里面所有的进程都是交给我安排的,我想干掉谁,干掉谁,我系统都快挂了,不干掉进程到时候我系统挂了可就不是你这10w条用户的存钱记录了,那还有更多的数据信息得丢失啊,那我如果挂掉了,你怪我还是怪进程" 老板听到这些话也觉得挺有道理说:"行吧,那不是你的锅",也没办法就找到了进程,指着进程说:"那就是你的问题喽" 进程话都没听老板说完就哭着鼻子说:"老板,你可不能这么不明面是非,我可是受害者,是操作系统他把我干掉了,我的数据被丢了,我能有什么办法,这锅你让我背,你觉得合理吗?" 老板听了之后也觉得很无奈:"那就是磁盘你的问题了喽",磁盘还没等老板话说完就说到:"别看我,别看我啊,我都说了我就是个跑腿的,我都跟每个进程都说了,我可能写失败,写成功数据丢了那是我的锅,但是我没写成功啊,反正我不管,这肯定不是我的锅" 老板说:"算了,看你这样子好像也赔不起"。那这件事就这么算了,但是现在关键的问题就是怎么解决这种事情不再发生呢?为了防止这种问题的再次发生呢? 操作系统就跟进程商量了个事,进程就说:"这样,我们俩也别谁看不上谁了,以后呢,我跟你规定一种状态D(也就是disk sleep深度睡眠),以后呢你操作系统看到我这种状态绕着走,别杀掉我了,你要杀就杀"S"状态的,你杀掉我那就又得出问题了" 操作系统说:"行吧"。
所以呢,进程就出现了一种深度睡眠状态:"D"状态,操作系统也没有资格kill该状态的进程。
所以一般而言,我们很少见到D状态,如果用户看到了"D"状态,那么几乎就是计算机快要挂掉了。
"T" 状态(stopped):暂停状态
接下来呢我们继续运行这段代码:
然后在另一个窗口监视该进程的状态信息:
然后我们打开第三个SSH渠道:输入 kill -l命令查看进程信号
我们可以看到18号和19号对应的是sigcont代表继续,sinstop代表停止
下面我想让该进程停下来该怎么做呢?
输入命令kill -SIGSTOP 进程pid
我们可以发现右边的状态变成了T,同时hello linux!停下来了
我通过再输入kill -SIGCONT 13458发现他又继续跑了,状态又变成了S
现在的问题是,进程为什么要暂停呢?
在进程访问软件资源时,可能暂时不让进程访问,就将进程状态设置为STOP
"t"状态( tracing stop):追踪暂停状态。
用gdb 进行debug程序的时候,追踪程序,遇到断点,进程就暂停了。
一个进程可以等待硬件资源,也可以等待软件资源,还可以等待另一个进程,这种等待资源的状态其实都可以被称之为阻塞状态。
"X"状态(dead):死亡状态。
这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
"Z"状态(zombie):僵尸状态。
下面我们再用一个小故事来理解一下该状态
故事时刻:有一天呢,有一个程序员叫张三,他呢,是一个加班狂,喜欢加班,你呢,年纪轻轻就是一个小领导了,每天早晨呢这个张三他喜欢晨跑,比如说早上六点,你又正在那里慢跑,他呢,跑的比较快,然后呢,在理你五十米的地方呢,倒下去了,倒下去之后呢,醒不来了,有社会责任感的你呢,看到这种情况,你是不是会先打个120叫救护车,然后如果救护车说救不了了那你会再打一个110,警察一过来呢,会不会说吩咐说:"李四王五,你们把他抬走,该通知家属通知家属,该干嘛干嘛?" 你认为警察会不会这么干呢?警察他不会,他也不敢这么干,因为在一个公共场所下有一个人直接倒下了,警察要给社会一个交代,所以呢会把周围的警戒线拉起来,叫上法医,要根据张三的尸体进行信息确认,是他杀呢,还是自杀,又或是正常死亡呢?我们的警察要根据法医的鉴定结果提取证明,然后把采集信息的工作给作为确认了是正常死亡才会说通知家属,把人给送走。在张三倒下到警察采集完各种信息的这段时间把这种状态叫做僵尸状态,抬走张三的时候才叫做死亡状态。
对一个进程来讲,一个进程终止了,那么我们是不是要看这个进程为什么终止了,我们的父进程需要采集该进程的终止信息,是由于什么原因导致的,等到采集完了,该进程才会变成死亡状态然后释放自己的资源。
进程=内核PCB+进程的代码和数据,那么进程都要占据内存空间。而申请到的内存空间到最后都会把内存给还回去,所以呢,有了进程的创建,那么就有进程退出,而进程退出的核心工作之一呢就是将PCB和自己的代码和数据释放掉!
但关键问题就是如何释放。
那么我们又需要弄明白一个问题,那就是为什么我们要创建一个进程?
一定是因为我要完成某种任务!
那么你怎么知道进程把任务完成的怎么样呢:所以,进程退出的时候要有一些推出信息,要表明自己把任务完成的怎么样了!
在我们自己写的c/c++代码里,都要有main函数,而这个main函数为什么最终都要写成return 0;return 1, 2,3可以嘛?为什么?
这些返回值其实是交给进程了,0一般是代表这个程序正常退出了。
你怎么知道进程把任务完成的怎么样呢 这里的你指的是该进程的父进程。
当一个进程在退出的时候,退出信息会由操作系统写入到当前退出进程的PCB中,可以允许进程的代码和数据的空间被释放,但是不允许进程的PCB被立即释放!! 原因就在于,要让操作系统或者父进程,读取退出进程的PCB中的退出信息,得知子进程退出的原因。
进程退出了,但是还没有被父进程或者操作系统读取,操作系统必须维护这个退出进程的PCB结构,此时,这个进程算退出了吗?
算终止但是不算彻底退出,此时该进程的状态就叫Z状态,即僵尸状态。
由父进程或者是操作系统读取之后,PCB状态先被改成X状态才会被释放!
如果一个进程Z状态了,但是父进程就是不回收它,PCB就要一直存在??
是的,如果不及时回收,就会有内存泄漏问题出现。
下面我们通过如下代码来查看僵尸状态:
发现Z状态出现了。
下面是我们的一张状态转化的图:
僵尸进程:
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)
没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
僵尸进程危害
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎
么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话
说,Z状态一直不退出,PCB一直都要维护?是的!
那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构
对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空
间!
内存泄漏?是的!
如何避免?后续文章揭晓
孤儿进程
父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
下面我们通过如下代码进程展示运行效果:
通过两个窗口来监视代码的运行:
可以看到父进程先于子进程先退出。
父进程先退出,子进程就称之为“孤儿进程”
如果孤儿进程没有被领养那么后续等该孤儿进程退出的时候就会变成僵尸进程,僵尸进程的危害,想必通过上述的说明大家都很清楚了吧。
孤儿进程被1号init进程领养,当然要有init进程回收喽。