3、深度睡眠状态D
除了【浅度睡眠】之外呢,还有一种叫做【深度睡眠】,它们俩呢,都是 ==阻塞状态==
- 对于浅度睡眠来说,之所以称为 “浅度”,是有原因的:也就是处于这种状态的进程容易被唤醒。例如说我们在上面所讲到的这个处于阻塞状态的进程,我们使用
kill -9 8664
向这个进程发送【9号信号】,那么这个进程就被杀死了,你也可以认为被唤醒了
好,接下去呢我就通过一个故事:book:来描述一下这个【深度睡眠】到底是怎样一种状态
- 现在在操作系统里有一个进程,它呢想要把数据写到磁盘上去,磁盘这个时候听到了它的请求,就开始写数据,不过我们知道外设尤其是像磁盘这种设备的速度都是比较慢的,所以呢这个时候进程就会一直地等待下去
- 但是呢,光这么无休止地等待下去可不行。因为当前这个时候操作系统中的已经有非常多的进程了,那么OS它看不下去了,路过的时候看到当前的这个进程正处于等待状态,也不运行,于是二话不说把它终止了
💬 所以呢,同学们,我们要明白这么一个道理:操作系统认为当前系统能行的话就直接让用户去用就可以了,如果扛不住了就置换页表,实在不行了就去会杀进程🔪
- 那么当这个进程被杀掉的时候,磁盘说:我在写数据的时候出了问题。它这个时候就回去找那个调度它的进程,但是发现这个进程没了???所以磁盘就特别尴尬:我要怎么办呢?写好的这个数据该不该还放在这里【一般的设备都会选择丢失】
- 如果这份数据是普通的那还好,但如果其为银行当中那种非常重要的数据呢?该怎么办?
一场有趣的官司
那因为随着这份重要数据的丢失呢,就引发了一场官司🔨
- 到了法庭上,法官开始问话,审讯【操作系统】、【进程】、【磁盘】这三个嫌疑人,读者认为是谁的问题的?锅在谁呢?
以下呢,是三个人的辩词
💬 【操作系统】:
- 请问有没有赋予我管理它软硬件资源的权利?请问我杀掉进程是不是在特别极端的情况下杀掉的?请问我有没有履行我操作系统的职责?我的职责是保证系统不挂?数据丢失和我有什么关系?
- 我就是在做我操作系统该做的事,如果你判我有罪了,那么请问我下次再遇到类似的情况时,到底我还做不做,我如果不杀的话,最终导致操作系统挂掉:① 数据该丢还是得丢;② 可能还会影响其他进程。这些责任谁来承担呢?
法官👨⚖️听了这番说辞后心里想了想,确实是。于是呢这个时候便把茅头指向了丢掉数据的磁盘✒ 发问道:你为什么要丢数据呢?
💬 【磁盘】:
- 法官大人啊,这你不能怪我╮(╯▽╰)╭ 在这件事情上我就是个跑腿的,人家让我干啥就干啥,我在写入的时候就已经告诉了对方我会失败,我让他去等的,我要给它结果的,我的工作模式向来都是这样,其他磁盘也是这样
- 如果你认为我有罪的话,是不是其他的磁盘也有问题呢?我丢失也是有原因的,其他进程也要叫我写入数据,那我也去写了,并且汇报回去了,那其他磁盘怎么没出问题呢?
操作系统一听:诶,这货说的好像确实没什么问题。那就就把视角转向了受害人即【进程】这一方。反正操作系统没错、磁盘没错,那就是你的问题喽!
💬 【进程】:
- 法官,我可是受害人啊/(ㄒoㄒ)/~~ 我就是静静地坐在那里,“人在家中坐,锅从天上来呀”。我是被杀掉的,我怎么能有错呢?
那这个时候法官一想,它们三个说的似乎都挺有道理,难道是我错了吗?
- 其实上面三者都没错,操作系统履行了它的职责,在关键时刻杀掉了进程;而磁盘呢则是起到了它本能的义务,丢失了没人要的数据;进程呢则因为在等候期间无意中被操作系统给杀掉了
- 当这个进程在等磁盘写入数据这么关键的时候,应该要让其受到保护,不可以被任何人给杀掉才对
那我们在上面有提到过处于【浅度睡眠】的进程,是可以被
kill
掉的,那么我们就要让这个进程的状态变为【深度睡眠】才对,即[D]
- 那既然这个进程的状态被设置为
D
后,当它在等待这个磁盘写入的时候,操作系统路过,看到这个进程没有跑起来空等着,于是在想把他杀掉的时候就被这块 《秒死金牌》给吓到了😮
- 那么这个进程就不会被杀死了,当磁盘写完数据后告知进程,那么它就可以将自己放入【运行队列】里去运行,此时它的状态就变成
R
了
💬 那这个时候就又有同学问了:D状态这么强大吗,那如果一个操作系统里有很多的D状态,这怎么办呢?
- 这个同学你问得很好,确实这个D状态的话操作系统是没有办法将其杀掉的,而是要等到磁盘写入完毕或者什么事情执行完毕后其才会去自动结束这个进程,或者是在外部断电、直接拔掉电源即可
- 一般一个系统里面如果存在D状态的话,那这个系统中的磁盘压力已经非常大了;如果存在多个D状态的话,则表示这个系统已经是非常危险了,最好马上重装一下系统💻
不过呢这个[D]
就没办法在这里给读者演示了,因为D状态的进程只有处于高IO的情况才可以演示。有兴趣的可以去研究一下Linux下的命令dd
4、停止状态T
好,接下去呢我们来讲讲【停止状态T】
- 首先我们要通过下面这句命令来查看一下对应的进程信号
kill -l
- 此处我们要使用到的是18、19号信号
- 接下去我们就来试一试如何让这个进程暂停之后又重新启动会是怎样的
- 所以我们来总结一下
- 暂停进程
kill -19 PID
- 启动进程
kill -18 PID
💬 所以呢,如果我们要将一个进程给终止的话,发送19号信号即可,要让其继续启动起来,则发起18号信号
那我现在要问了,这个T停止状态和S睡眠状态有什么不同呢?
- stopped状态进程 完全暂停了, 其不会再接收任何信号了
- 一个进程通过stopped状态可以控制另一个
- S和D一定是在等待某种资源,而T状态可能在等待某种资源,也可能被其他进程控制
5、进程跟踪状态t
接下去呢,我们再来说说进程的跟踪状态t,还记得我们在基础篇中所学习的 GDB调试 吗?
- 首先我们在正常状态下查看这个进程的状态,其为
S
睡眠状态。接下去呢我进行了【gdb】调试,在行内打上断点后,输入r
运行起来后,我们再去查看这个进程的状态时,就发现其状态变成了t
,原因就在于这个进程在调试的时候停了下来
6、死亡状态X
对于【死亡状态X】来说呢,这个状态只是一个返回状态,你不会在任务列表里看到这个状态🙅
- 第一种方法就是向这个进程发送9号信号,就可以杀掉这个进程
kill -9 PID
- 第二种方法就是通过这个进程的名称来杀掉它
killall 进程名
7、僵死状态Z —— 两个特殊进程
接下去我们来介绍一种状态叫做【僵死状态Z】,对于这个状态,我们要涉及到两个特殊的进程叫做 僵尸进程 与 孤儿进程
💬 不过在讲这个僵死状态前,我们先来看看return 0
这个东西
- 对于这个相信读者在学习C语言的时候写
main
函数一定写过,但是呢为什么要返回这个0
呢,你可能还不是很清楚,那现在我们学习到了Linux下的进程状态,就可以给大家来讲讲了,这个0
就叫做【进程退出码】
这里我们写几句代码来测试一下
36 int ret = 20; 37 if(ret == 20) return 0; 38 else return 3;
- 我们让这个进程运行起来然后通过
echo $?
去查看一下当前进程的退出码,发现没问题是0
- 但此时若是我修改了初始值为
30
后再去运行的话,就发现这个进程退出码发生了变化
① 僵尸进程
💬 首先我们要来介绍的是僵尸进程,这里呢通过一个故事来进行引入
- 你呢,很喜欢晨跑🏃,这一天早晨6点又起来跑步了,当你路过一个公园的时候,遇到了一个晨练的【程序员】,边跑边掉头发😀 但是呢,跑着跑着,此时突然他就
“啪叽”
倒了下来。那你此时就非常担惊受怕了,马上拨打了110
和120
的电话,然后守在他的身边等待救援来到,周边的人看到也都纷纷围了过来,其中不免有一些人会紧急救援。不过等了一会这个人就没了呼吸🖤
- 过了十几分钟后,救护车和警车都来了,警察先封锁了,让法医过来检验一下其状况,就说:“这个人已经没救了,赶紧通知家属准备后事吧~。”
好,我们回归正题,来说说这个【僵尸进程】
- 因为在救护车来之前这个人其实就已经死亡了,但是其状态还没有被检测,当前并不知道它的死因,所以我们操作系统就可能会维护当前的这个状态,这个状态即为
Z状态
,即[僵死状态]
💬 那我现在想问了:有一个进程暂时退出了,它要将它的状态暂时维持一段时间,问题是它维持给谁看呢?
- 答:父进程!当一个进程退出的时候,那最关心它的便是【父进程】。因为这个父进程费了很大的劲才将这个子进程
fork
出来,此时呢它突然挂掉了,那么此时父进程就必须去关心一下对应的子进程退出时的原因 - 不过光凭我们现在所学习的知识还无法去查看这个,等我们后面讲到
wait()
系统调用的时候再去做一定的介绍
就上面这样生冷的文字来叙述还不太行,接下去我们通过实际的案例来观察一下:mag:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 5 int main(void) 6 { 7 pid_t id = fork(); 8 if(id == 0) 9 { 10 // child 11 int cnt = 5; 12 while(cnt) 13 { 14 printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt); 15 sleep(1); 16 17 cnt--; 18 } 19 exit(0); 20 } 21 else 22 { 23 // father 24 while(1) 25 { 26 printf("I am father, pid: %d, ppid: %d\n", getpid(), getppid()); 27 sleep(1); 28 } 29 } 30 return 0; 31 }
- 运行起来可以看到,在一开始的 5s 内,父子进程还是同时在跑的,但是当子进程在循环结束后退出了。不过此时呢父进程还在运行并没有退出。所以我们在右侧查看这两个进程的状态时间就发生了一定的变化
- 仔细地来看一下这个状态的变化,其中【PID】为
5486
,其父进程为5485
,一开始它们均为S睡眠状态
,但是呢到了后面子进程的状态就变成了Z僵死状态
。也就意味着此时子进程已经死亡了,但是呢父进程还不知道 - 这里还可以通过右侧的这个
这个来看,它表示【失效的、不再存在的】
所以我们总结一下:
💬 进程一般退出的时候,一般其不会立即彻底退出。如果父进程没有主动回收子进程信息,子进程会一直让自己处于Z状态,这也是为了方便后续父进程读取子进程的相关退出结果。
那对于上面的这种子进程,我们就将其称作为是【僵尸进程】,不过呢这种进程是存在一定危害的!
- 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间
- 那么对于上面的这种危害我们就称作为是【内存泄漏】,要如何去进行避免呢,之后的文章会做讲解~
② 孤儿进程
- 上面我们讲到,当一个子进程突然退出但是父进程并没有去主动回收的话,那么此时这个子进程就会变成【僵尸进程】
- 那看到下面我们将这个进程突然终止之后,父子进程都不见了
💬 那此时我想问:这个父进程突然之间退出了,但是呢它的父进程并不知晓,那为何这个父进程没有处于【僵尸状态Z
】呢?
- 因为当前这个进程是
bash
的子进程,当其一退出之后,bash
就将其回收了。可是这个子进程为什么也没了呢?这个的话我们就要聊聊【孤儿进程】了
接下去我们来将上面测试僵尸进程的代码做个修改,让父进程先于子进程退出
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 5 int main(void) 6 { 7 pid_t id = fork(); 8 if(id == 0) 9 { 10 // child 11 int cnt = 500; 12 while(cnt) 13 { 14 printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt); 15 sleep(1); 16 17 cnt--; 18 } 19 exit(0); 20 } 21 else 22 { 23 int cnt = 5; 24 // father 25 while(cnt--) 26 { 27 printf("I am father, pid: %d, ppid: %d\n", getpid(), getppid()); 28 sleep(1); 29 } 30 } 31 return 0; 32 }
- 然后通过进程状态追踪来看看,我们观察到一开始两个父进程和子进程是同步在走的,但是呢过了一会父进程终止了,只有子进程还在跑,此时我们需要留意的不是其进程状态,而是这个子进程的
PPID
即父进程的PID
,它由原先的【19956】变成了【1】,这是为什么呢?
- 这里的话就要给读者普及一下了,对于
PID
的值为1的进程,我们一般将其称作为是【系统进程】。我们可以使用下面这句指令去查找一下
ps ajx | head -1 && ps ajx | grep systemd
- 然后我们看到这个【系统进程】的
PID
即为1
那为何会出现上面这种现象呢?是这个子进程突然换父进程了吗?
- 对于父子进程来说,如果父进程先于子进程结束的话,那么这个子进程就会被称作为是【孤儿进程】,对于孤儿进程来说呢,它会有1号进程即【系统进程】所领养,因为此时这个进程没有了父进程了,所以需要有人去带领它
💬 但是这个孤儿进程为什么要被领养呢?
- 其实很简单,既然它是一个进程的话,最终都是要退出的,但是没了父亲的话就只能由系统进程来进行维护了
那其实我们就可以解释上面的事情了
- 当这个父进程退出的时候,它是被
bash
给回收了,那么对于父进程的子进程来说,我们刚才谈到了,如果一个进程的父进程先于子进程结束的话,那么在这一刻这个子进程立马变成了【孤儿进程】,被系统领养并回收了,所以我们就看不到了
四、总结与提炼
最后来总结一下本文所学习的内容:book:
- 在本文中,我们主要讲解了两个大点,一个是在操作系统中所常见的一些进程状态,它们分别为:运行状态、阻塞状态、挂起状态。
- 所被调度的、处于运行队列里的这些进程所处的状态我们称之为
运行状态R
- 处于等待队列中,同时在等待外设相应的进程所处的状态我们称之为
阻塞状态R
- 当把一个进程的 数据和代码都重新交换到外设当中,进程所处的状态我们称之为
挂起状态R
- 接下去我们又介绍了在Linux系统下的七种进程状态,分别是:运行状态R、浅度睡眠状态S、深度睡眠状态D、停止状态T、进程跟踪状态t、死亡状态X、僵死状态Z
- 当一个进程没有在等待IO流的时候,其就会处于
运行状态R
- 使用
sleep()
函数可以很好地使一个进程处于浅度睡眠状态S
- 如果不想让一个进程在等待磁盘操作的时候被操作系统杀掉,则可让其处于
深度睡眠状态D
- 可以向一个进程发送【19】号信号使其暂停,处于
停止状态T
继续发送【18】号信号的话则可以使其重新启动 - 在【gdb】的环境下去运行一个断点的话则可以使其处于
进程跟踪状态t
- 使用
kill -9 PID
就可以杀掉一个进程,使其处于死亡状态X
- 如果让一个子进程在父进程不知晓的时候退出,那么其就会处于
僵死状态Z
,变为【僵尸进程】;若是在父子进程中父进程先于子进程退出的话,那么这个子进程就会变成【孤儿进程】
以上就是本文要介绍的所有内容,感谢您的阅读:rose: