深入解析进程状态:从运行到阻塞的奥秘
进程状态
我们之前讲解了一些task_struct,只是简单的讲解,接下来要进入到里面,一个一个的解决,了解内部,接下来要了解一下进程状态
什么是状态呢?就比如现在我在上比特课程,状态就是上课中!在宿舍睡觉,状态就是休息中,在操场跑步,状态就是运动中!所以每一个活着的人都会有各自的状态!状态决定着你现在在干的事,以及系统如何看待你,你认真上课,学校认为你是个好学生,你天天在宿舍睡觉不学习,学校认为你不思进取,所以进程状态决定着当前进程在系统层面,系统要如何处理这个进程!
进程状态本质就是在task_struct中的一个整型变量,在操作系统内部通过#define宏定义将不同状态定义为0、1、2这样的整数,之后在task_struct中定义整型变量,放在其内部,将来进程什么状态,就在这个状态整型变量中填入对应的状态整数值,这个就是状态,未来OS是要选择调度进程还是选择将这个进程挂起,都是根据这个整型值,来控制进程!
课本上的说法 —— 名词提炼

进程状态有多种(运行、就绪、阻塞、创建、结束、挂起),而运行状态是真正意义上持有CPU的,不同状态间也可以进行转换的!
运行 && 阻塞 && 挂起
在计算机中,我们只有少量的CPU,而进程往往会很多,一个进程想要在CPU上运行,本质是每一个CPU需要在系统内部维护一个调度队列! 即我们在学习进程状态之前,要知道是一个什么样背景下学习,以及什么样的调度结构!
CPU选择一个进程去运行,本质不是选择这个进程的代码和数据运行,而是选择这个进程的PCB来运行,我们都知道task_struct内部会有内存指针,会直接或间接的帮助CPU找到这些代码和数据,所以说一个进程要被调度到CPU上跑,本质是把这个进程的task_struct给CPU,CPU有了task_struct也就意味着有了代码和数据
而每一个CPU都要维护一个队列,这个队列为调度队列(runqueue),队列元素为进程PCB,runqueue类型为struct task_struct *帮助CPU找到对应PCB;好,停一下!我们之前学习进程的时候不是说过,进程PCB是通过链入一个双链表(循环链表)存储在内核的吗?怎么这里又有用队列数据结构来处理PCB呢 ?
我们之前学习数据结构的时候,自己实现的数据结构,结构内我们new或malloc出来的数据结构的节点只属于当前这个结构,但是Linux内核对PCB的维护,采用的做法是一个task_struct节点,既可以放在全局的一个双链表里,也可以把相关进程放到一个全局的队列里!简单说一个节点既可以属于A数据结构也可以属于B数据结构,如何做到的?稍后在Linux进程状态部分之前讲!
这里知道这个队列结构是不同于进程储存的双链表,OS内部会专门为一个CPU设计了一个队列!进程PCB既可以属于全局双链表,也可以属于这里的全局调度队列即可!
接着上面,我们继续来说调度队列! 我们之前也写过队列的实现,队列是先进先出(first in first out) 的规则,而队列无非是用数组或链表来实现, 所以我们可以认为这个调度队列的物理结构为一个双链表,但是要求这个链表是从头出,从尾进!所以实际意义为一个队列!
我们现在并没有学习调度相关知识,但是有一个调度算法确实我FIFO!这个算法就是对调度队列头部优先级比较高,尾部的优先级比较低!所以我们今天的学习背景就是CPU调度是对这样的调度队列按照顺序依次选择一个task_struct进程来进行调度执行
选择我们初步的了解了进程的调度结构,以及是甚麽样的调度背景,接下来我们就可以好好了解进程状态了
运行状态
一个进程在CPU上跑的时候才叫运行状态,这样认为是没错的,但是在当代计算机中, 只要一个进程在调度队列中就称该进程为运行状态!
运行:进程在调度队列中,进程的状态都running,并不是一个进程持有CPU运行的才叫进程运行状态,而是只要早进程调度队列中的进程都是运行的,队列里的进程要么已经被CPU调度运行,要么就是随时做好了被CPU调度运行的准备,所以这里的运行状态我们可以认为上图的运行和就绪两个状态的总和
阻塞状态
我们实际使用计算机的时候,什么情景是阻塞状态呢?
我们以前学习C/C++语言,其中的
scnaf / cin语句,在程序运行的时候,此时程序必然是进程,而进程运行到这个语句的时候,我们自己的程序就停下来了,严格意义上来说,是我们自己的进程停下来了!那停下来干什么呢?停下来是在等用户输入信息的!输入了数据,让scanf / cin 拿到了数据,程序就会继续运行下去了✨但其实有不一样的理解!
其实上面并不是程序停止,并不是等待用户输入,而是等待键盘硬件就绪(就是等待键盘硬件有键盘被按下),那么用户没有按下键盘时,我们称为键盘文件未就绪,竟然键盘未就绪,那么要scanf / cin 的进程也无法读取数据,那么这个进程必须要等!
阻塞:等待某种设备或者资源就绪(在等期间,如果这个进程没就绪,我的进程就不会被调度,这个进程卡在哪里,那不就是不动了吗?)
进一步的理解
OS要对计算机的软硬件进行管理,硬件资源中 键盘、显示器、网卡、磁盘、话筒... 这些设备不一定是就绪的,比如进程scanf / cin 需要键盘输入,但键盘不一定就绪的,即可能没有被按下,这个时候及键盘没就行,而进程必然阻塞;比如进程需要用显示器打印,但是显示器被占用了,显示器来不及显示,这个时候进程就需要等
OS要管理系统中的各种硬件资源,先描述、再组织!
OS要管理硬件,就必然要先描写,在OS内部会有struct device数据结构来描述硬件设备,其中的属性有(ID、vender(厂商)、stasus(状态)、void data(对应的数据)、struct device next ),通过next 指针把这些单个的对象组织为为一个链表数据结构,和进程类似!
OS会对每个设备以struct device 结构创建节点,之后在OS内部把所有节点通过next 指针链接起来,每个节点都对应不同的底层硬件,从而讲OS对硬件的管理转换成了对链表的增删查改!
这里要注意的是,OS内部硬件设备的结构并不是和我们这里一样的,是通过一系列设计实现的结构,可以区分不同设备的!
但是这里为什么要提OS管理硬件设备的建模呢? (35分~45分)
操作系统内部不仅仅有调度队列,还会一个设备队列,调度队列的节点为task_struct,而设备队列的节点为device,接下来讲解为什么提硬件设备
我们之前讲解的例子,当我们在读设备读网卡读键盘的时候,设备根本没有就绪,那么我们进程就开始阻塞等待了!那么操作系统要如何理解这个阻塞等待呢?在设备结构体内会有一个指针
struct task_struct * wait_queue,device结构体是内存数据结构,所以内部也可以有一个指向其他数据结构的指针的!所以在device结构体内部有一个指向等待队列的指针,所以每个设备都会有一个等待队列!而当一个运行状态的进程正在被CPU调度,进程需要去读设备,从而系统找到在设队列中对应的设备节点,再通过设备节点访问底层硬件,但发现设备并不活跃,没有获取相应的数据,就是没有就绪,这个时候这个进程就运行不了了,所以系统会将这个进程的PCB从调度队列中转移走!转移到哪里呢?把PCB链入到对应未就绪设备节点的等待队列(即转移到对应未就绪设备节点中的等待队列中),所以这个进程选择已经不在调度队列中,会永远不会被调度了,永远不被调度所以这个进程就处于阻塞状态,所以从运行到阻塞的过程是把PCB从链入到不同的队列结构当中!
现在这个进程在等待这个设备,当这个设备活跃就绪!这个进程还没有拿到这个设备中的数据,为什么?现在是设备活跃就绪属于硬件就绪,而操作系统作为硬件的管理者,硬件状态发生了变化,操作系统必须第一个知道,而操作系统知道了硬件就绪,就会找到就绪的硬件节点并且修改其状态为active,并且检查等待队列,发现等待队列指针不为空!就把这个队列中的第一个节点先将其状态设置为允许状态,在将其链入调度队列,并从等待中移除,直到这个进程再次被CPU调度,会从之前阻塞的程序语句开始允许,把硬件数据读到进程中(在说了和进程也没关系,进程拿数据这个工作是CPU来做的,此时进程还在等待队列中,也不可能获取数据)
所以从阻塞到允许的过程本质上是找到目标PCB,并把PCB重新链入到调度队列
获得结论1 : 进程状态的变化。表现之一,就是要在不用的队列中进行流动。本质都是数据结构的增删查改!
挂起状态
挂起在操作系统里面一个比较极端的情况
我们直到进程=PCB+代码和数据,一个进程数据结构的内存大小可能有几百字节!而等待队列中的进程都会有其PCB和数据代码,并且这些进程在设备没有就绪之前是绝对不会被调用的!
当计算机内存严重不足的情况下,计算机自己不可能摆烂不管,它要自己想办法腾出内存空间!操作系统会做一件事情!
这个时候有些数据当前并不会立即被访问,但这些数据还占着内存!而阻塞状态的进程的数据与代码!阻塞进程并不会被 CPU调度,也就是一时半会并不会有上面作用,PCB、数据和代码都是闲置的!而Linux在设计的时候考虑到内存严重不足的情况,就在磁盘设计的时候,会有一块特定的分区,划分为swap交换分区!大小往往是等于内存或是内存的几倍!我们可以把每个设备的等待队列中阻塞进程的代码和数据唤出到磁盘swap分区,只留下进程的PCB来确认进程相关信息,也方便之后找回!等之后这些进程要被运行再把这些数据代码还回来!而被这样做的进程的状态为阻塞挂起状态
阻塞挂起:当系统的内存比较吃紧时,OS会对一些内存页面置换的算法,把不会被调度的进程的内存块交换到磁盘swap分区,所以像这样进程的代码和数据都被唤出到磁盘,只剩下PCB的进程称为阻塞挂起
那这个时候,硬件设备就绪了会怎么做?硬件就绪,系统第一个知道,之后OS会将对应进程曾经被唤出到磁盘的数据和代码重新加载到内存,重新构建指针映射!让进程找到对应的代码和数据!即将历史上换出到磁盘的数据唤入到内存中,形成完整进程,再放到进程调度队列里!
以上是对应swap分区的唤出和唤入操作,一个进程的数据代码放到swap交换分区就称为阻塞挂起状态,而swap分区唤入后,进程就会进入调度队列,为运行状态!
挂起是把该进程的代码和数据挂起到外设上(磁盘)!
并且当内存相当吃紧,即将设备等待队列中的所有进程都阻塞挂起后依旧不够使用,系统甚至会打调度队列的注意!OS会把调度队列末尾一半的进程也挂起到磁盘,当要调度的时候再将数据代码换入到内存,这样的状态称为运行挂起状态!无论是扫码状态,挂起都是将数据代码唤出唤入到swap分区的操作!
经过上面的学习,我们现在看上面的图片就一目了然了!
理解内核链表话题(偏移量相关知识)
理解内核链表的话题
我们通过Linux源码来理解一下这个知识点!
我们在看源码的时候,看到双链表结构是用list_head,而进一步了解list_head我们知道,这个类型里面的属性为两个指向list_head的指针,这和我们之前学习双指针表现形式不一样啊!
之前是直接让指针指向一整个结构体对象,而Linux内表现的形式不一样!这里是将处理节点指向的部分封装为一个结构体,在其他结构中嵌套使用,相当于链表节点内只有指针,没有其他属性了!而这样的用法指向节点也不用直接指向整个节点,而是指向下个节点内部的list_head的成员对象,所有我们可以认识到两种表示的区别!
现在有个问题!我们的双链表表示,指针是指向整个对象的,所有我们能够轻易访问到对象内部的属性,但是Linux的双链表表示,指针是指向下一个对象内部的list_head成员对象的!那要如何访问链表节点的成员属性呢?访问数据最重要呀!✨在解决问题之前我们要知道结构体的地址和结构体内第一个属性的地址一样,并且以此递增!
链表是通过list_head指针链接起来的,所以是通过list_head * list 指针指向链表表头做为链表接口,而list指针只能获得第一个节点task_struct内部的成员对象list_head中的指针!所以要如何访问task_struct内部其他的属性呢?
我们可以将0地址处强转为(
struct task_struct *)即struct task_struct * 0!(地址强转[1])
地址强转简单理解就对该地址的看法要根据需求转换,比如一个东西是用小盒子装的,现在我要用你有个大盒子或小盒子装它的意思!
所以此时相当于0地址处有一个struct task_struct对象在这里!
而我们现在只知道这个对象内部list_head成员对象links的地址/list_head links->next的地址!而现在用地址强转后的0地址去访问link即((struct task_struct * 0)->links),然后再对这个links取地址!即&((struct task_struct * 0)->links),我们要知道此时结构体地址为0,所以访问到的links的地址就是等于从结构体开始位置到links成员对象开始的位置,也就是获得了links相较于结构体开始位置的偏移量!(这里强转地址只是用来解读,并不会实际的使用,不然会报错,也就是理解或计算)
而我们知道当前对象的links成员对象的地址,所以我们一定会使用这个偏移量来获得这个结构体的地址,用list指针减去偏移量就可以获得这个结构对象开始的位置即
((this->links->next)/list) - &((struct task_struct * 0)->links),再通过(struct task_struct )`((this->links->next)/list) - &((struct task_struct 0)->links)`就可以访问task_struct内部全部的属性都可以访问了!这个求偏移量可以用一个宏offset来求!
所以不用担心task_struct内部嵌套一个链表,会不会影响我们访问节点!通过偏移量和这个链表,我们会知道这个节点对象的地址的,通过上面的方法,也可以是宏来获得地址!
✨所以现在在来说一个节点属于多个数据结构的问题就非常好理解了!居然我们可以通过这一个链表,以偏移量的方式可以访问到这个节点,这不说明了一个结构体内可以放多个不同的数据结构,都以偏移量的方式来访问这个节点,这样完全可行啊!
我们可以把任何对应的task_struct对象,既可以放在进程全局链表也可以放在调度队列,甚至可以放到二叉树、图这样的节点类型数据结构!所以我们的PCB就可以随意链接,不会有影响!一个正在被CPU调度的进程,其PCB即在调度队列里也在全局进程链表中!
讲这么多,其实想告诉到大家,进程PCB在Linux内核中是只有一份的!一个PCB可以隶属于多种数据结构!所以学习到现在,我们的思维要进化了,不能再用以往的简单的思想来考虑这些问题,要进步进化!
我们也有知道Linux内的数据结构往往是网状的!
我们之前讲的还是没有那么的具体,要学习具体的系统内部的进程状态才能更好的理解,因为是实际的!
Linux 进程状态 ✨
Linux内核进程状态表现形式

Linux进程状态是维护在一个task_state_array的数组里!以数组维护则可以说明进程状态是整数!即数组下标可以是状态的整数!
R(running) 0

上图为检测进程myprocess,myprocess进程在做一个循环输出字符的操作,我们在另一个XShell上while :; do .....;done和 ps axj |head -1 ;ps axj | grep myprocess来检测进程,然后我们查看STAT这栏,这栏为状态列表,我们可以发现一个现象,就是进程myprocess大部分情况下都是S(sleeping)状态,极小部分为R(running)状态!这是为什么呢?
这是因为我们的代码里面有printf,printf涉及IO,我们可以认为在1秒中,进程跑printf代码只用了1纳秒,而在这1秒除了跑代码的其他时间,都在监测进程所等待的IO设备,即在等待队列中等待IO设备就绪!所以大部分都在S状态检测中 ,所以这个进程在进程调度队列和等待队列来回切换!
那要如何做才能看到进程R状态呢?简单,不用让进程调用IO相关语句,这样也不会有IO设备未就绪,导致进入等待队列中的S状态!即不做IO就不会设备,只做计算即可!

现在进程的状态就一直是R运行状态了!
在Linux系统中,R进程状态表示着进程在运行队列里!
有个疑问,就是状态里,R是运行状态我知道是,但是R+的+号是干啥的?
我们这次运行进程myprocess用点不一样的!
./myprocess &,这里的&符,是表示进程后台运行的意思,后台运行这个不着急,要到网络再一起讲!此时我们再看myprocess的进程状态
此时状态为R而不是R+,所以R+的+号表示的是进程在前台启动的!而没有+号就表示着进程后台启动的!
注意后代运行后,我们可以继续使用指令,记得通过
kill -9 PID指令来杀掉这个后台进程!
S(sleeping) 1
我们之前讲解阻塞状态,那么阻塞状态在Linux中是如何表现的?在这里S状态就阻塞状态在Linux中的表现之一!我们要如何证明这个S状态是阻塞状态呢?
我们写一个程序,这个进程可以进入阻塞状态,那这个进程肯定要调用IO操作,做一个scanf是输入操作,只要运行到了输入语句scanf并且不在键盘上输入信息,这样进程肯定是阻塞状态并且进程在键盘device中的等待队列中!此时我们在去观察这个进程的状态就可以知道了!
所以,我们可以确定阻塞状态在Linux中用S来表示!而Linux中S状态被称为
休眠状态
而进程状态在Linux中,肯定是用数字来表示的,我们看这部分最上面的图片,数字0:表示运行状态,数字1:表示阻塞状态,把对应的数字赋值到task_struct中的state变量即可确定进程的状态

T(stopped) 4 & t(tracing stop) 8
T状态表示为暂停,可是上面讲操作系统进程状态理论部分并没有讲这个呀? 接下来就来展示!
我们现在对myprocess进行gdb调试!此时创建了一个新进程,为gdb,因为正在的myprocess进程没有run呢!而gdb进程已经启动了,此时在对代码进行打断点处理,打完断点过后,再run运行进程,此时就会有一该路径下的myprocess进程启动了,而断点的目的就是让程序停在目标位置,所以此时myprocess进程的状态为t (tracing stop)
所以t为追踪状态! 一个进程要被debug,程序运行到断点会停下来,凭什么?因为进程被暂停了 ,即断点:进程被暂停
当程序再做死循环时,通过Ctrl + Z按键能够让当前进程暂停!,此时进程暂停并不是gdb追踪暂停的,而是用户暂停的!所以这和上面的追踪状态是完全不同的,再通过ps检测进程可以看到此时进程的状态为T状态

上面讲理论两种暂停,一个是用户暂停,另一个是debug暂停,所以到底什么是暂停状态?
暂停和阻塞不一样!阻塞(sleeping)是一个进程再等待某种资源,而暂停往往是某种条件不具备或者进程做了非法操作,所以操作系统就把进程暂停了!这是Linux特有的状态,而什么是会有T状态呢?有一些进程不允许往显示器上打印,只允许往文件里写入,如果这个时候非要往显示器上打印,OS就会认为进程可能会出现错误,使用OS把这个进程暂停了,那为什么要暂停,而不是直接删除掉?因为这个打印显示器的操作是用户要求的!而OS人会这样做可能会出错,所以暂停,当用户发现进程暂停肯定会来查看,而最终程序是否继续运行,有用户决定!T状态往往是用来止损的! OS不想让你继续做了!
当进程因为Ctrl + Z而暂停,要如何接触暂停呢?这里暂停后还可以通过 kill -19 PID的方式让进程暂停,通过klii -l指令可以看信号标识,第19是sigstop,暂停信号
通过上面的图表,我们可以知道使用kill -18 PID可以取消暂停启动进程,sigcont表示继续,continue是继续的意思!这里有提到的和信号有关之后再讲!
那么在OS学科中的状态中,T和t到底算那种状态,应该为阻塞状态
T(Stopped) 和t(Tracing stop) 状态在广义上属于 “非运行” 状态,且在某些分类中被归类为阻塞状态的一种特殊形式,但在严格的操作系统内核调度理论中,它们与传统的 I/O 阻塞(Interruptible/Uninterruptible Sleep)有本质区别
- 从 CPU 占用角度:
T和t算 阻塞状态(它们不跑代码)。- 从排查问题角度:T 和 t不是正常的 I/O 阻塞。
- 如果看到大量
D,说明磁盘 I/O 瓶颈或驱动卡死。- 如果看到大量
T,说明有人把进程挂起了,或者程序写得有问题收到了停止信号。- 如果看到
t,说明你(或其他人)正在调试这个程序。一句话总结: 它们是非运行状态,被内核调度器忽略,但它们是由于控制信号而非资源等待导致的暂停
D(disk sleep) 2
上面学习的S(sleeping)在Linux中被称为休眠状态,又称为可中断休眠!也称为浅度睡眠
即一个休眠状态S的进程,我们是可以将这个进程杀掉的,会响应我们杀进程的动作!比如下面的又scanf语句的进程,运行到scanf语句时,进入S状态,此时我们Ctrl + C可以杀掉进程!

所以在Linux系统下有浅睡眠,那操作系统理论有讲吗?并没有!所以操作系统学科只能是给进程状态一个统称,而Linux在满足操作系统学科的要求的基础上会有自己的个性开发!
所以现在要讲解的D(disk sleep)为深度睡眠!也叫不可中断休眠,而D状态叫disk sleep,disk为磁盘的意思,所以也叫磁盘休眠,但这个是在描述进程状态,所以这一定和磁盘有关系的!
内存中的OS内有一个进程,这个进程打算把100MB的数据写入到磁盘,而进程在磁盘传输数据的时候,为了防止传输失败(磁盘空间不足等原因!),以及接收传输结果,并将结果通过进程告诉用户,所以进程在磁盘传输数据的时候必须等待,即此时进程状态为阻塞状态,所以现在进程状态为
S浅度睡眠状态!而这个时候内存资源严重不足,OS以及把能挂起的进程都挂起了,还是内存不足,而在内存极度不足的情况下,OS会进行杀进程一获取内存空间!(所以我们平时用的一些软件和网站会闪退,就是因为内存压力太大,所以把一些进程杀掉的结果!),而这个时候,OS开始杀进程,而上面的进程为可中断休眠,刚刚好凑巧把传输数据的这个进程给杀掉了,此时传输数据的结果无人可知,没有进程接收结果,如果此时磁盘刚刚好传输失败了,向找进程程序传输,或者告诉用户传输失败的结果,这些都做不到,相当于这100MB的数据丢失了,没有备份!如果这100MB的数据是某银行一天的交易记录(重要数据)!这个时候,银行行长来问责!把OS、进程、磁盘领出来问责!
磁盘:我就是一个打工的,让我干啥我干啥!我说了让进程等我等我,可是他没等我啊 磁盘只是在做进程安排的任务,并实时汇报结果进程:我也没办法啊!我也要等磁盘啊!可是是OS直接让我滚的! 进程是会一直保持S浅度睡眠状态!可是被高权限的OS杀掉了!
OS:行长,你赋予我高权限,我不能辜负你啊,我今天如果不让进程离开,有可能让整个OS都崩溃,这就不是丢失这100MB的数据了,而可能是好几个G的数据了 OS为了保护自己不崩溃,减小损失,必须要删进程啊!
这么一看,好像三者谁都没错!所以为了防止这样的事情在发生,以后规定一个新的进程状态,D状态,以后向磁盘传输数据的进程,为D状态,OS无权删除这个进程,即不会对OS的指令去做响应!
所以以后OS内,凡是涉及到向磁盘这一类关键存储设备进行访问、进程进行高IO时 ,这些进程状态不设为S,设为D!主要是为了防止进程丢失而导致数据丢失的问题
D状态也是阻塞的一种,D状态的进程不可被杀掉!除非断电才能杀掉D状态的进程!
所以在Linux中阻塞状态有两种,一给是S状态,一个是D状态!
那D状态什么是才会解除?一旦进程为D状态,只能等进程自己醒来!或者断电才能杀掉D状态的进程
X(dead) 16 & Z(zombie) 32
X(dead)状态为死亡状态!相当于操作系统进程状态的结束状态!
Z(zombie)
zombie是僵尸的意思,以一个例子来理解
小明一天在晨跑,跑着跑着有一个人从你身边经过,过来不久发现那个人倒下了,下面过去一看,发现那个人没有呼吸了,小明这个时候立马报警和叫救护车,过来一会,警察带着法医来到了现场,警察立马封锁现场,法医收集尸体信息,以确定是否被谋杀,以及死亡时间等等!所以警察并不会立马让救护人员把尸体搬走,而是会等法医鉴定完尸体信息后才会搬走并通知家属处理后事,而倒下的这个人倒下的时候就已经死亡了,这个人在死亡之后被抬走之前的这段时间,这个人一直躺在地上,这个人的状态为僵尸状态!
为什么要让这个人处于僵尸状态呢? 就是为了获取这个退出时的信息!当进程让人将尸体抬走时这个人才是X死亡状态!
为什么要有Z(zombie)进程!
我们创建了一个子进程,在Linux中,所有的进程都是某个进程的子进程!那我们为什么要创建一个子进程呢?我们创建这个子进程的目的是为了让这个子进程完成某种事情的!(比如通过fork创建子进程,用if,else来让子进程完成父进程代码块的一部分,这个子进程就是完成父进程一部分工作的!)而子进程既然要完成某些事,那必须知道子进程把事情完的怎么样!即结果相关信息,父进程得要知道!一个子进程退出了,并不是立马把PCB和代码数据给释放掉的!而是在子进程退出时,计算机运行进程的代码和数据释放掉,但是进程的PCB不能释放!我们需要在子进程退出之时(即躺在地上的那个人!)需要让父进程从退出进程的PCB里获得退出的结果!所以子进程执行完,最后执行的结果,我们要让父进程知道!所以一个子进程退出的时候不能立马释放掉所有资源,子进程必须保留自己退出时的所有信息暂时维持住,让父进程读取!所有在子进程退出之后父进程读取退出信息之前,必须要有一个状态,这个状态就是Z(zombie)状态!
在Z状态,进程确实已经死亡了,所有进程也不会被调度了,所有代码和数据就已经没有意义了即进程的代码和数据可以被释放掉,但是必须要维持进程退出的基本信息!让其父进程知道子进程的退出情况,获得信息后才可以宣布进程死亡状态X状态!
1、那父进程获取子进程哪些退出信息呢?
一般获取进程的退出信息,比如return 0;或者是通过kill的退出信号等!
2、信息存在哪里?
退出时,进程的代码和数据已经被释放掉了!所以只剩下PCB了,而PCB是储存在task_struct中的!所以进程退出信息是存储在PCB中的!,以后父进程可以通过系统调用来找到这个PCB中的退出信息!
如何模拟验证Z状态呢?
模拟出Z状态,就需要有父子进程,并且子进程要退出!此时父进程不处理子进程,此时子进程就要一直保持Z状态!
上图就是实验代码!通过
fork创建子进程,并返回不同的返回值给父子进程并赋值到id变量,之后通过对id的判断进行if...else...给父子进程执行不同的命令,让子进程循环5此后就结束任务,退出进程,而父进程则一直在做死循环输出字符串!此时通过ps axj 来检测父子进程的状态,就可以模拟出Z状态了!
我们在检测表中,看到退出状态的子进程状态为Z状态,而后面有一串字符
defunct!这个是什么意思呢?这个意思为失效的、无用的
如果父进程一直不管子进程,不回收,不获取子进程的退出信息,那么Z会一直存在,则当前子进程的PCB会一直存在,怎么证明?因为子进程PID还存在!
但这又有一个问题!就是PCB还在!如父进程一直不管这个PCB,这个僵尸状态一直保持,一直维护着这个PCB,那岂不是这个块PCB的内存一直会占用在内存中!这样会造成内存泄漏[2]问题 ,如何解决这个问题?——通过waitpid系统调用来回收僵尸进程!
所以父进程在获取子进程退出信息的同时还会去释放子进程PCB,解决这个内存泄漏的问题!
当父进程回收子进程PCB的同时,进程状态也会从Z状态到X状态,会一瞬间把PCB释放掉,所以X状态是看不到的!
我们通过上面的学习,在OS学科中的运行(R)、阻塞(S/D、T/t)状态我们在Linux进程状态里都有找到,但是我们并没有看到和挂起相关的状态呀,为什么?挂起主要是OS面对内存资源不足时,将阻塞状态的进程的数据和代码唤入唤出的过程,以此获得需要的内存,所以这个唤入唤出这样的动作我们用户需要关心吗?并不需要,操作系统对这样的操作是完全隐藏起来的!我们用户只关心进程有没有运行起来!所以挂起状态并没有在Linux中体现出来!
所以操作系统学科的理论放在Linux中,在概念上是一直的,但是实践上就不一定了,所以操作系统学科进程状态理论放到Windows中是可以说的通的,但是上面学习的Linux进程状态放到windows中就不一定能说的通的!所以操作系统这门学科是把所以操作系统的功能、属性、方法的共性抽取出来并提炼了一套方法论!
孤儿进程
我们上面讲到僵尸进程,僵尸进程是父子进程中,子进程退出且父进程不向子进程获取退出信息时的状态,这样的状态叫做僵尸状态;那当父子进程中,父进程先退出!这个时候子进程是什么呢?——孤儿进程
我们在实例中讲解孤儿进程:
实例为,在父子进程中,父进程只做5s就退出,而子进程继续做,我们来检测一下此时进程们的信息!
我们发现5s过后 ,父进程退出之后,此时子进程的ppid为1!所以,在父子进程关系中,如果父进程先退出,子进程要被1号进程领养,被领养的进程(子进程),叫做孤儿进程
通过上面的案例,我们要解决几个问题!
1、1号进程是什么?
1号进程就是我们上面实例中领养孤儿进程的进程,那么如果一个父进程有100个子进程,父进程先退出,那不相当于要领养100个儿子,所以1号进程到底是什么?我们来好好看看!
通过
top指令来了解1号进程!可以看到1号进程是systemd(老系统为init),我们用ps来查看一下systemd进程
好,我们之前讲过,我们通过Xshell远程登录Linux,是会有人创建这个bash,那谁帮忙创建bash呢?我们认为是系统,那么系统是谁呀?我们今天认为这个系统为一号进程!我们可以认为systemd就是操作系统或者是操作系统的一部分!而这个1号进程systemd可以处理很多事情,比如登录和设备等!
那有0号进程吗?有!但是0号进程在开机的时候被1号进程替换了!
2、为什么要领养!
为什么要领养?假如不领养?孤儿进程退出时会变为僵尸进程,此时并没有父进程接收孤儿进程的退出信息,会造成内存泄漏问题!而对于子进程,能管理的只有父进程和系统!(即只有父母和政府会管你)
这就是为什么要领养!领养过后,子进程有了新的父进程,新的父进程可以对未来的子进程进行统一的管理回收
知识点补充
父进程怎么没有孤儿进程?
我们要直到我们的父进程的父进程是bash,父进程退出会由bash来接收父进程的僵尸信息!使用父进程一般没有孤儿进程
一个子进程一旦成为孤儿进程!父进程由命令行调用,此时Ctrl+C并不能结束进程!这是因为子进程一旦被系统领养,一般会自动变为后台进程(
./cmd &),所以需要使用kill -9 PID的方式将孤儿进程杀掉!
-

-
内存泄漏
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



















所有评论(0)