目录
[1. 进程状态的概念](#1. 进程状态的概念)
[1.1 课本上的说法:名词提炼](#1.1 课本上的说法:名词提炼)
[1.2 运行,阻塞和挂起](#1.2 运行,阻塞和挂起)
[1.2.1 什么叫做运行状态(running)?](#1.2.1 什么叫做运行状态(running)?)
[1.2.2 什么叫做阻塞状态(sleeping)?](#1.2.2 什么叫做阻塞状态(sleeping)?)
[1.2.3 什么叫做挂起状态(pending)?](#1.2.3 什么叫做挂起状态(pending)?)
[2. linux下的进程状态](#2. linux下的进程状态)
[2.1 R运⾏状态(running)](#2.1 R运⾏状态(running))
[2.2 S睡眠状态(sleeping)](#2.2 S睡眠状态(sleeping))
[2.3 D磁盘休眠状态(深度睡眠)(Disk sleep)](#2.3 D磁盘休眠状态(深度睡眠)(Disk sleep))
[2.4 T/t暂停状态(stopped)](#2.4 T/t暂停状态(stopped))
[2.5 X死亡状态(dead)](#2.5 X死亡状态(dead))
[2.6 Z僵尸状态(zombie)](#2.6 Z僵尸状态(zombie))
[3. 孤儿进程](#3. 孤儿进程)
1. 进程状态的概念
⼀个进程可以有多个状态(在Linux内核⾥,进程有时候也叫做任务),打个比方:我们现在正在上课,这叫做上课中,上完课回到宿舍睡觉,这叫做休息中,休息完跑到操场是跑步,这叫做运动中
所以每一个人都有自己对应的状态,状态决定了我们当前正在做什么事情,以及系统应该如何看待我们:比如你每天准时上课,那么你就是一个好学生
对于进程来说,这个进程当前是需要被使用,还是正在休眠,正在等待,所以进程的状态决定了当前进程在系统里应该被如何处理
进程状态的本质其实就是task_state结构体内部的一个整数1.1 课本上的说法:名词提炼
由上图可以得知:进程具有多种状态,状态之间是可以相互转化的
1.2 运行,阻塞和挂起
PCB 即属于双链表又属于队列**,但是我们可以把队列也看作是一个双链表,只不过这个双链表遵守的是尾进头出**
在这里有一个既不属于Linux也不使用其他操作系统的调度算法之一的 ------ FIFO(先进先出)****,也就是说在这个队列当中在头部的优先级高,尾部的优先级低,优先级按照顺序排
我们下面这张图就是CPU调度 按照顺序****来依次调度
1.2.1 什么叫做运行状态(running)?
当一个进程只有CPU在跑的时候,它就是运行状态,但是,在当代计算机里面,只要一个进程在调度队列当中,它就是运行状态(running)
处于running状态的进程,也么是正在被运行,要么就是已经准备好了,随时都可以被调度
运行状态对于的是创建,就绪和运行,其中运行和就绪可以当作一种状态
1.2.2 什么叫做阻塞状态(sleeping)?
举个例子:C语言当中的scanf函数,当我们scanf的时候,其实我们并不是在等待用户输入内容,而是在等待键盘硬件就绪,也就是在等待键盘上有按键被按下了,如果没有被按下就称之为键盘不就绪,那么scanf就要等
阻塞就是等待某种设备或者资源就绪,在等待的期间如果一直不就绪,那么我的进程就不会被调动,那么就会卡在哪里不动
我们以一个问题来理解一下:操作系统os是怎么对软硬件进行管理的?答案是:先描述,再组织我们先拿硬件来举例子:实际上,因为操作系统要管理硬件,所以操作系统os也要创建数据结构(struct_device)
这个数据结构里包含的就是目标设备的所有属性,那么就说明struct_device可以直接或者间接获得我们对应的数据
上面这张图可以称之为设备队列,而下面的图片叫做运行队列
我们都知道,在内存当中,每一种设备都要对应一种struct_device结构体,当我们读磁盘读网卡的时候如果设备上对应的设备没有就绪,那么我们的进程就要阻塞等待了,那么在操作系统中我们如何理解阻塞等待呢?我们可以在数据结构体里加上一个等待队列
所以我们对应的每一个设备它都有一个等待队列
++我们假设我们的CPU正在运行,当运行的时候执行我们的代码(假设是scanf)需要就绪读取,读取的时候OS发现需要去读取键盘,然后OS去检查键盘的状态,然后发现键盘没有任何活跃的状态,那么你这个进程无法读到键盘的任何数据进程就无法继续执行了,所以操作系统把这个进程从cpu上拿下来啊,并且把这个进程从运行队列当中移走然后把它链入到我们对应的特定设备的等待队列当中,那么这个进程就不会再被调度了,那么这个进程此时就处于阻塞状态++
只有在运行队列里的进程才会被CPU调度,不在运行队列里转而在设备队列里等待,那么这个进程就叫做阻塞
1.2.3 什么叫做挂起状态(pending)?
当我们有一个进程处于阻塞状态的时候, 如果这个时候又来了几个进程, 而这个时候操作系统的内存不足了,那么这个时候, 操作系统就需要在保持正常的情况下节省出来内存资源
而我们知道一个进程如果在阻塞状态, 那么它的代码和数据就是处于一个空闲的状态。 这个时候操作系统就会将进程的PCB保留, 而进程的代码和数据就会被放到外设当中
当下次资源就绪的时候,进程就会被重新唤醒。 那么这个时候代码和数据再次从磁盘放到内存中!而这个过程就叫做换出和换入操作,而换出后, 也就是代码和数据在磁盘时, 就叫做挂起状态,这种挂起状态称为阻塞挂起状态,那么当它换入时,我们就可以将阻塞挂起状态重新改为运行状态,那么该进程就可以程序被调度了
2. linux下的进程状态
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 */
};
2.1 R运⾏状态(running)
运行状态并不意味着进程⼀定在运⾏中,它表明进程要么是在运⾏中要么在运⾏队列⾥
我们先简单创建个代码
运行结果
我们再打开一个xshell来查看一下我们的进程
我们再给一个命令让它一直跑起来,每隔一秒查一次
但是我们就发现了,为什么进程里面都是S呢?答案就是因为我们的代码里面有prinft函数,如果进程运行需要1秒的话,假设prinft函数运行需要1纳秒,那么1秒减去1纳秒剩下的时间进程就在等待IO,所以就是S状态,所以我们当前这个进程正在运行队列和等待队列来回切换,也就是正在阻塞状态
解决方法就是将printf去掉, 那么就不用打印到外设上, 就不需要等待。 我们再查看进程状态, 进程的状态就是R运行了
我们可以看到图中的R后面还有一个 + ,这个 + 的意思是我们的确这个进程启动是在前台启动的
"R (running)", /*0 */
我们可以看到,R为0,那么我们只需要在PCB当中将当前状态设置为对应的整数就可以保证为R
"S (sleeping)", /*1 */
同理S为0,那么我们也只需要在PCB当中将当前状态设置为对应的整数就可以保证为S,其他的几个状态也是如此
2.2 S睡眠状态(sleeping)
睡眠状态意味着进程在等待事件完成(这⾥的睡眠有时候也叫做可中断睡眠(interruptible sleep))
在Linux当中,操作系统理论里我们的阻塞状态叫做阻塞,而在Linux内核当中,我们的阻塞状态叫做S,我们看到一个进程卡住不动了,其实就是这个进程当前没有被调度,这个进程正在等待键盘有数据,所以这个状态叫做阻塞状态
2.3 D磁盘休眠状态(深度睡眠)(Disk sleep)
"D (disk sleep)", /*2 */
深度睡眠状态有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束 ,也叫深度睡眠,其实也是一种阻塞状态,我们前面的S也叫做浅度睡眠
浅度睡眠状态也叫做可中断休眠状态,简单来说就是:如果一个进程处于S状态,那么我们可以直接把这个进程杀掉,这个进程会响应我们杀掉它的动作
深度睡眠其实就是一种不响应操作系统任何请求的状态, 一般的阻塞状态, 当进程停止等待外设后就会进入运行状态, 但是深度睡眠只有当完成了特定的任务, 否则不会响应操作系统的任何命令, 即便系统关机了, 这个进程仍然会自己跑
D状态产生的原因是因为进程向磁盘中写入数据,如果我们想要将1G数据写到磁盘中,但是对于磁盘来说, 磁盘写入数据是有可能失败的(磁盘满了或者其他原因), 所以对于进程来说, 他写入数据就不能将数据交给磁盘后不管了, 他要等待磁盘写入的结果。 如果没有写入成功, 可以再写入一次, 或者写入不成功后想其他办法处理数据
那么进程就将数据交给磁盘,但是对于进程来说这个期间等待的时间是非常长的,如果这个时候内存满了, 操作系统管理内存, 就势必要杀掉一部分进程,这个过程是必须等
而我们的进程在等待磁盘的过程中会处于闲置的状态, 那么就容易被操作系统杀掉,而被操作系统杀掉后, 磁盘就不会找不到进程, 那么他就不会在进程写入数据了(数据丢失)所以, 为了避免这种情况, 就出现了深度睡眠------D状态,只要进程处于D状态,那么这个进程就不可能被杀掉,即便操作系统关机, 只要电源存在, 就可以一直进入,想要结束D状态,只有关掉电源
D状态其实也是一种阻塞状态
2.4 T/t暂停状态(stopped)
"T (stopped)", /*4 */ "t (tracing stop)", /*8 */
暂停状态(stopped)可以通过发送 SIGSTOP 信号给进程来暂停(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行
暂停状态属于Linux的一种特有状态
t是有特殊用途的,主要是用来做debug的
2.5 X死亡状态(dead)
"X (dead)", /*16 */
死亡状态(dead)这个状态只是⼀个返回状态,你不会在任务列表里看到这个状态
死亡状态其实对应的就是我们系统中的结束状态
2.6 Z僵尸状态(zombie)
"Z (zombie)", /*32 */
1. 僵死状态(Zombies)是⼀个⽐较特殊的状态。当进程退出并且⽗进程(使⽤wait()系统调⽤,后⾯讲)没有读取到⼦进程退出的返回代码时就会产⽣僵死(⼫)进程
2. 僵死进程会以终⽌状态保持在进程表中,并且会⼀直在等待⽗进程读取退出状态代码
3. 所以,只要⼦进程退出,⽗进程还在运⾏,但⽗进程没有读取⼦进程状态,⼦进程进⼊Z状态
举个例子:比如有一天我们在路上走路,然后我们看到有一个人躺在路边,我们靠近之后发现这个人已经走了好一会了,然后我们就报警打110,警察来了之后就开始封锁现场,警察过来的时候可能还要把法医带过来,然后在这个人身上做一些采样拿回去做化验拿几根头发,什么的,用于判断这个人是自杀还是谋杀还是自然死亡,当法医查完之后,他把在这个人身上获取了法医想获取的有效信息之后,法医就说可以撤了。这个时候呢我们警察才会通知家属啊,准备后事,然后就把人就拉走了
那么在他死亡之后到被抬走之前这段时间这个人一直在地上躺着,那么这个人在这段时间里所处的状态叫做僵尸状态,为什么要让这个人处于僵尸状态呢?,是为了获得这个人死亡(退出)时的死亡信息(自杀还是谋杀还是自然死亡),当这个人被警察抬走的时候就变成了X状态
//创建维持30秒的僵死进程例⼦ #include <stdio.h> #include <stdlib.h> int main() { pid_t id = fork(); if (id < 0) { perror("fork"); return 1; } else if (id > 0) { //parent printf("parent[%d] is sleeping...\n", getpid()); sleep(30); } else { printf("child[%d] is begin Z...\n", getpid()); sleep(5); exit(EXIT_SUCCESS); } return 0; }
如何模拟Z状态
我们模拟Z状态的前提条件是我们必须得有父子进程,而且要让这个子进程在退的时候,父进程什么都不干,我们先暂时先不讲父进程怎么解决僵尸状态,我们只需要父进程什么都不管,然后子进程直接退出,如果我们也不获取子进程的退出的信息,那么子进程就必须一直把自己维持在Z状态
就好比在路边啊,一个人倒下了那么打110没人来获取这个人的退休结果信息,那么此时,这个人就只能一直在路边躺着,所以我要模拟验证Z状态,我们就要创建父子进程,让Z进程在一定程度上直接退出,子进程退出之后我们就可以查到我们子进程的相关信息了
观察上面的两张图片,我们发现前面子进程还在正常运行,但是后面就只剩下一个父进程了,观察我们状态的那张图片,我们发现状态里有Z状态
以上这个情况就是说, 对于一个进程来说, 它在退出的时候, 一定要维持一段时间的僵尸进程, 而只有当父进程或则其他关系进程子进程的资源将这个进程的情况读取到了, 才会去释放这个子进程的资源。 也就是说, 对于僵尸进程来说, 如果父进程一直不去释放它, 那么它就会一直占用着资源,也就导致了内存泄漏
僵尸进程危害
1. 进程的退出状态必须被维持下去,因为他要告诉关⼼它的进程(⽗进程),你交给我的任务,我办的怎么样了。可⽗进程如果⼀直不读取,那⼦进程就⼀直处于Z状态?是的!
2. 维护退出状态本⾝就是要⽤数据维护,也属于进程基本信息,所以保存task_struct(PCB)中,换句话说,Z状态⼀直不退出,PCB⼀直都要维护?是的!
3. 那⼀个⽗进程创建了很多⼦进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本⾝就要占⽤内存,想想C中定义⼀个结构体变量(对象),是要在内存的某个位置进⾏开辟空间
浅谈
我们目前所讲到的6种状态其实都是某一个进程的子进程,可能是我们自己进程的子进程,也有可能是bash进程的子进程
3. 孤儿进程
如果一个父进程先退出,那么子进程就会被操作系统领养,子进程的父进程会变成操作系统,这个时候子进程就称之为孤儿进程,这个过程就叫做领养
为什么需要领养操作?答案是一个进程想要被释放, 就需要父进程,如果没有父进程的话,子进程本身是一个进程,它也需要被释放, 否则就会发生内存泄漏, 所以就需要被领养
如果父进程被终止, 子进程仍然会运行
完结撒花~