推荐阅读:通俗易懂帮你理清操作系统
接着上回说到了操作系统的先描述再组织,在操作系统内部用一个个的结构体来专门管理这些硬件,以及我们写好一个程序它要从磁盘加载到内存也是一个一个的PCB结构体来管理和维护的,
所谓进程状态就是进程内部的属性,这些属性都在task_struct内通过一些int类型的整数来表示例如:1代表运行状态、2代表停止状态、3代表挂起状态、4表示死亡状态等等。就绪和新建比较简单说的是这个进程PCB刚刚被创建好。
那么有这么多需要管理的硬件和进程,一般我们只有一个CPU怎么办呢?
其实CPU维护了一个进程队列,让这个进程在CPU上运行本质上是将该进程的task_struct结构体对象放入运行队列中!进程PCB在这个运行队列runqueue里,就是运行状态,不是这个进程正在CPU上运行,才是运行状态。
我们知道CPU很快但是硬件很慢,而很多的进程又或多或少的要访问硬件(printf访问显示器、文件操作要访问磁盘等等),不能认为进程只会等待(占用)CPU资源,进程也可能随时随地要外设资源。但硬件也少,很多进程都要访问同一个硬件怎么办呢?其实每一个硬件对应的结构体里也有等待队列,当CPU的一个进程假如说(fwrite)需要访问磁盘的时候,而恰好这个时候别的进程也在访问,磁盘还没有被准备好去让fwrite去访问,那么这个fwrite对应的PCB结构体对象就只能从CPU的运行队列里剥离出来,去磁盘的等待队列里这个PCB结构体对象此时的状态,所以这种进程PCB结构体对象等待外设的状态就是所谓的阻塞状态。
等磁盘准备好了之后,操作系统需要把fwrite对应的PCB结构体对象的状态改为运行状态,然后再把它放到CPU的运行队列里,此时操作系统就不需要管了,接下来的工作由CPU去做了。
假如说由于下面三个进程因为外设没有准备好而处于阻塞状态,短期之内这些代码和数据就不会被执行,也就是不会被CPU调度,需要等待外设资源就绪才可以,那么加载进来的代码和数据依旧要占用内存,那么万一内存空间不够了怎么办?把代码和数据暂时保存回磁盘上,但他的内核数据结构PCB不大,还在内存中。此时就节省出来一部分空间再去运行别的程序了,所以一个进程暂时把它的代码和数据换出到磁盘的这种进程我们叫做该进程被挂起了,等什么时候这个进程可以被执行了,那么就可以再把这部分磁盘中的代码和数据再换回到内存中,把PCB再改为运行状态,进入CPU的运行队列里,再被CPU运行。把这种将进程的相关数据,加载或者保存到磁盘的行为叫做内存数据的换入换出。
所以所谓的进程不同的状态,本质是进程在不同的队列中,等待某种资源
其实Linux进程PCB是一个比较大的结构体,这里就把它内部的表示状态的内容节选下来。
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 */
};
首先我们写一个这样的程序,查看现象:
上面这个死循环没有访问外设,运行起来查看他进程的状是R状态(运行状态)。
还是上面的那个程序我们把表示注释的//去掉,让它持续不断地打印1,就会发现这时候进程的状态就变成了S状态,因为printf本身要访问显示器是外设,是外设就比较慢,本身打印的话很快就打印了,但是等显示器就绪要花比较长的时间,这样的进程有99%的时间都是在等IO就绪,1%在执行打印代码,所以其实大部分访问外设的进程的状态基本都是S状态。
在命令行输入kill -l
,就可以看到kill下其实是有很多选项的
其中第19个选项,找到对应的pid号输入kill -19 pid号
或者ctrl+z都是使当前进程停下来的方法。
查看进程的状态的时候就发现它是T状态。
然后再使用kill -18 pid号
可以再让这个进程运行起来,
但是此时无法再被暂停的,在命令行输入命令也无济于事,细心的小伙伴可以发现虽然暂停前和暂停后都是S状态,但之前的S后面跟另一个‘+’,后面那个却没有’+',那么当前没有‘+’的状态就是后台进程,有‘+’的就是前台进程。这样的后台进程只能用kill -9 pid号
来杀掉当前进程。
至于上面说到的挂起状态,可以看到似乎并没有一种叫做挂起的状态,那是因为操作系统认为挂起状态不需要暴露给用户,用户只需要知道进程在不在运行、是不是休眠、是不是暂停、是不是退出了也就可以了,就算让用户知道也没有任何意义和作用,所以在这么多状态里就没有一种叫做挂起的状态。
其他状态:
从上面你可以看出来那么R就是运行状态、S是一种浅度睡眠其实它就是一种阻塞状态、D也是一种阻塞状态,t、T它都是阻塞状态只不过这些阻塞状态是为了满足于不同的应用场景。
僵尸状态Z:进程退出的时候,不能立即释放该进程对应的资源!需要保存一段时间,让父进程或者0S来进行读取!这一段时间内这个进程的状态就叫僵尸状态。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态,是把进程PCB回收之后的状态。也就是说一个进程正常退出的时候是先是Z状态,然后是X状态。
下面这幅图来描述这几个状态之间的关系。
当一个进程运行完毕时,它所占用的内存资源和执行的退出状态需要被它的父进程回收。如果父进程没有回收这些东西,这个进程就得是一个僵尸状态。如果父进程长时间不回收进程数据,那么这个进程将长期处于僵尸状态,其PCB和代码依旧存储在内存中,占用空间但是不会被执行。
我们写一个代码:
父子进程同时运行,此时杀掉子进程,子进程就停止运行了,但此时父进程还在忙着执行自己的代码,也就回收不了子进程的空间与返回状态,所以此时子进程就处于僵尸状态了,注意下面大写Z。
对于僵尸进程的理解:
孤儿进程正好反过来了是指父进程先于子进程退出,父进程死亡,子进程就没有了父进程,这时的子进程就会变成孤儿进程。但是父进程的死亡不代表孤儿进程就没有了父进程,每一个孤儿进程都会被一号进程也就是操作系统领养。这种被领养的进程就叫孤儿进程。
用上面相同的代码,这次我们干掉父进程,同时查看进程状态。 我们可以发现子进程的父进程pid变为了1,也就是说子进程会被1号进程收养,并在子进程执行完毕后回收所对应的资源。
同时我们也可以看到,子进程状态从之前的S+变为S,也就是说从这个被收养的子进程变为了一个后台进程了,也就是不能用ctrl+z暂停,只能用kill -9
来杀掉这个进程。