进程状态
我们人无时无刻都处在不同的状态,可能这时候在学习,那就是学习状态,学完了去睡觉,那就是休息状态。进程也是有多种状态,下面来一一讲解。
在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 */ };
1.运行状态(R状态)
操作系统在对众多进程进行管理时,往往会建立一些数据结构来进行管理,在操作系统中,这个数据结构就是队列 + 双链表。
调度器所管理的数据结构叫做运行队列,
其中,这个运行队列对应的结构体为:
struct RunQueue { struct task_struct *head; struct task_struct *tail; };
实际上,进入运行队列排队的不是进程的代码和数据在排队,而是PCB数据结构在排队,因为PCB本身就是一个数据结构, 里面有各种指针,可以链接起来。
所以,运行队列的head
指针直接指向第一个PCB对象,tail
指针指向最后一个PCB对象即可完成队列的排队。
前面说过一个概念叫做调度器,这个调度器其实就是一个函数,可以接收一些进程的参数来获取运行队列中有多少个进程在排队等信息。
所以,我们把在运行队列中的进程状态叫做运行态,即R状态
。
这里有个问题,为什么在运行队列的进程就是R状态,而不是被CPU调度运行的进程才是R状态吗?
我们知道,一个进程既然已经在运行队列中排队等待了,那就说明该进程已经准备好了!所以,在该运行队列排队的进程,就是处于R状态
。
将来如果一个新的进程想要被调度运行,它只需要进入运行队列中排队等待,CPU会根据排队顺序一个个地运行,所以,在运行队列的进程状态就是运行态。
这里还有第二个问题,一个进程被放到CPU上面运行,是不是要等到进程运行完之后才被放下来?
很显然不是。因为如果我写了一个死循环,被CPU调度运行起来,那岂不是我整个电脑的其他程序都得等到循环结束才能运行其他进程?死循环是不会结束的。
根据生活经验来看,我们执行一个死循环,其他程序一样会正常跑起来。
这是因为每个进程都有它执行的时间片。
这个时间片是一个进程会被放到CPU上调度执行的时间!
假如一个进程的时间片是10ms,那不管这个进程是不是死循环,它只能跑最多10ms就会被弹出来,到下一个进程被CPU调度运行。
所以,只要有了这个时间片的概念,死循环就不会一直被执行,就没有一个进程过长时间占用一个CPU的情况出现了。
注意:一个CPU,只有一个运行队列。
2. 阻塞状态(S状态)
2.1 浅度睡眠状态(S状态)
在操作系统内部,OS
对各种外设进行管理时,因为一切外设都是文件,所以操作系统对外设进行管理同样是先描述,再组织
。操作系统对外设管理时,先将外设描述起来,就形成一个个task_struct
结构体,然后再进行组织,这个组织的过程,就是将各种PCB结构体连接起来形成链表。
我们知道,一个进程是由PCB数据结构对象和该进程所对应的数据和代码组成的。
所以在进程的PCB结构体内部会有它所包含的各种指针信息,这些指针信息会指向外设所对应的等待队列中。
- 而对于一个进程来讲,它可能会需要从外设读取数据,也就是从键盘,硬盘等外设获取信息。假如一个进程需要等待键盘输入数据,而键盘一直不输入,那么该进程就会被放到键盘所对应的等待队列中。
因为这个时候,这个进程一定是没有准备好的!
- 注意:每一个外设都会有对应的等待队列,就连进程之间,也会有各种等待队列。
一个外设可能会有多个进程在排队等待从该外设中获取数据。这些进程会在该外设的等待队列中排队等待,如下图:
如果一个进程想要从硬盘获取数据,又想从键盘获取数据,那么它可能需要在几个等待队列中进行排队。
所以,我们把这种在等待队列中排队的进程对应的状态叫做阻塞状态(S状态),也叫做浅度睡眠状态。
如果一个进程处于阻塞状态,即正在等待数据到位,当我们从键盘中输入数据时,该进程所获取的数据就达到了,此时CPU就会将该进程唤醒,并将该进程从等待队列放到运行队列中,即从S状态变成R状态。
阻塞状态的本质就是等待某种资源就绪。
综合运行状态和阻塞状态来看,进程之间的各种状态,无非就是将进程从一个队列放到另一个队列中罢了。
- 注意:上面所有运行队列,等待队列,实际上都是进程的PCB在排队。
下面给一个例子感受一下S状态和R状态的问题:
code.c 1 #include<stdio.h> 2 #include<unistd.h> 3 4 int main() 5 { 6 while(1) 7 { 8 } 9 return 0; 10 }
当我们创建一个code.c文件,编译运行上面的代码后,然后查看进程的状态,结果如下:
为什么code程序会是S状态?
它可是一直在显示屏中每隔1秒打印一次。
这是因为我们的CPU运行速度实在是太快了,一个进程被放到运行队列等待的时间加上运行时间都比显示器文件资源准备就绪时间还要很多。导致显示器的显式跟不上CPU调度进程,导致进程有99%以上的时间处于阻塞状态,也就是在等待显示器资源准备就绪,只有不到%1的时间处于运行状态。
前台进程和后台进程
细心的你会发现,我们说的S状态,在上面的例子中,并不是S状态
,而是S+状态
,S+状态和S状态的区别是:
- S+状态是处于前台进程,S状态是处于后台进程。
前台进程是指:在我们肉眼可见的地方运行该程序,我们可以对该程序进行中止。
比如按下ctrl + c,就可以让该循环终止,这个就叫前台进程。
而后台进程是无法通过
ctrl + c
,或者ctrl + d
操作进行终止的。我们只需要执行
./code &
加上一个取地址符号即可让该进程处于后台运行状态。如果想要终止后台进程,则需要通过
kill
指令杀掉该进程。
kill -9 + 对应进程的pid
即可杀掉后台进程。
- 注意:上面说的浅度睡眠状态,是可以被唤醒的。
- 下面所说的深度睡眠状态,无法被唤醒。
2.2深度睡眠状态(D状态)
下面以一个小故事来帮助理解:
假如一个进程,它有1GB数据要写入磁盘中,进程说:磁盘啊,你帮我把这1GB数据放进你那里。磁盘慢悠悠地看了一下说:好的,等我一下。然后这个进程,就坐在那里慢悠悠地等着,(因为磁盘的写入速度是比较慢的)此时,操作系统不知道因为什么原因,突然出现大量进程占用CPU资源,内存资源极度匮乏,操作系统为了补救把能换出的数据全都换出了,还是没多大效果。然后操作系统看到了进程这货在这满面春风,心里气不打一处来,对着进程说:我这里都火上浇油了,你还有心思搁这喝茶??还没来得及等进程解释,这个进程就直接被操作系统干掉了,来缓解内存压力。然后操作系统就走了,等到磁盘把数据写完,慢悠悠地过来想告诉进程时,发现进程不见了,找都找不到。无奈,磁盘不知道怎么处理那1GB数据,毕竟后面还有那么多进程排队等着我去写入数据呢,所以磁盘只能把这1GB数据给丢了。可它却不知道,这1GB数据是银行里面各种百万级别用户存的资金!
为了防止再出现上面的悲剧,程序员只能想出一个办法,当进程在等待磁盘写入数据时,**不能让任何人打扰到进程的等待,即让进程处于D状态!**包括操作系统在内!这样做就能够让进程等待直到获取到磁盘的反馈结果!
这就相当于进程得到免死金牌一样了。
那如果有多个进程都处于D状态呢?
实际上,只要有一个进程处于D状态,操作系统就在处于崩溃的边缘了。如果有两三个D状态,操作系统基本上就完了。
3.挂起状态(无需暴露给用户)
在平常使用电脑时,可能会有这样一种情况:操作系统内存严重不足这样的情况可能会发送在我们打开了大量的软件,并且这些软件都是大量占用CPU资源的。
这些进程的数据和代码量庞大,是占用CPU资源的主要因素,所以操作系统想出了一个办法:既然这些进程在运行队列,等待队列中排队都是该进程的PCB对象在排队,那它们对应的数据和代码为何不放在硬盘中,等到该进程被调度时,再把该进程对应的数据和代码从硬盘中拿出来呢?
这样做我们就能缓解操作系统内存严重不足的情况。
其中:
进程的数据和代码被放到硬盘这个过程叫做换出
从硬盘中读取回到操作系统的过程叫做换入
所以,只有进程的PCB在队列中排队,该进程的数据和代码被放在硬盘中的这个状态叫做挂起状态。
4.僵尸进程(Z状态)
这个进程状态听起来还比较吓人,它具体的意思是:
- 当一个子进程退出程序后,父进程如果不关心子进程,也就是父进程没有回收子进程的空间,资源等。子进程就会处于僵尸状态,即
Z状态
。 - 僵尸进程会以终止状态保存在进程表中,并一直等待父进程读取它的退出返回码。
下面有一个例子:
1 #include<stdio.h> 2 #include<unistd.h> 3 4 int main() 5 { 6 //父进程 7 pid_t id = fork(); 8 if(id > 0) 9 { 10 int cnt = 100; 11 while(cnt--) 12 { 13 printf("我是父进程,我的pid是%d,我的ppid是%d\n",getpid(),getppid()); 14 sleep(1); 15 } 16 } 17 //子进程 18 else if(id == 0) 19 { 20 int cnt = 5; 21 while(cnt--) 22 { 23 printf("我是子进程,我的pid是%d,我的ppid是%d\n",getpid(),getppid()); 24 sleep(1); 25 } 26 } 27 return 0; 28 }
通过上面代码及运行结果可知,当子进程没有退出时,父子进程都处于S状态,这个好理解,因为父进程要等到显示器资源就绪,它才会从等待队列被放到运行队列中,(时间极短,无法展示)当子进程退出程序时,处于Z状态。
这是因为当子进程退出时,父进程没有对子进程的资源进行回收释放,不关心子进程。
接下来就必须说到僵尸进程的危害了。
僵尸进程的危害
- 子进程是被父进程创建出来的,自然要执行一些父进程交代的任务,可如果子进程退出了,它就必须把任务执行得怎么样了反馈给父进程,所以它会一直维持退出状态,等待父进程来读取,如果父进程不来读取,子进程一直处于
Z状态
。维持一个进程是需要消耗内存资源的,一个进程维持在某种状态,本质上是该进程的PCB数据结构在某个队列中排队等待!这就要一直维护该进程的PCB! - 所以,如果一个父进程创建了大量子进程,并且都不回收,那这些子进程都会处于
Z状态
,它们的PCB数据结构一直在一个队列中排队等待,意味着它们一直在吃内存资源,就会造成内存泄露!
那为什么父进程退出时没有处于Z状态?
- 因为父进程的父亲是bash进程,父进程一退出,bash进程就立刻对父进程回收了。
由于每个进程只会对父进程负责,这个父进程的子进程跟bash进程并没有关系,也就是爷爷进程和孙子进程没啥关系,所以就无法让bash进程也回收孙子进程。 - 另一个原因是孙子进程并不是bash进程创建的,它没有能力对孙子进程回收。
5.孤儿进程
孤儿进程相对于僵尸进程类似,孤儿进程是:
- 如果父进程先退出,那子进程就没有父亲了,子进程就是一个孤儿进程!
孤儿进程重点:
如果父进程先退出了,子进程的父进程的ppid会立刻变成1号进程,即操作系统!
意思就是:父进程退出后,子进程会被操作系统领养!
为什么操作系统要领养子进程呢?
因为以后子进程也要退出,也需要被回收,让操作系统回收最合适不过。
让bash进程回收子进程不行吗?
bash进程只能回收它的子进程,没办法回收孙子进程。
总结
本文章讲述了进程的几个基本状态:运行状态,阻塞状态(深度睡眠状态和浅度睡眠状态),挂起状态,僵尸状态等。以及两个比较重要的进程:僵尸进程和孤儿进程。
到目前为止,所具备的知识还无法解决僵尸进程和孤儿进程的问题,到后面会解决。
进程状态切换的本质是一个进程的PCB从一个队列被放到另一个队列中排队。
本文到这里就结束啦。