💭 前言:本章我们专门讲解进程的状态。我们先学习具体的 Linux 系统状态,再去介绍 OS 学科面对的概念如何理解 —— 运行态、终止态、阻塞态以及挂起态。
进程状态(Process Status)
什么是进程状态?
进程状态在 Linux 内核中就是个 整数,这个整数在进程的 task_stuct 中:
一个进程通常有三种状态:就绪、运行、阻塞。
- Ready(就绪):进程已经准备好运行,但由于某种原因,操作系统选择不在此时运行。
- 在就绪状态下,进程已经准备好运行,但由于某种原因,操作系统选择不在此时运行。
- Running(运行):进程正在处理器上运行
- 在运行状态下,进程正在处理器上运行(这意味着它正在执行指令)。
- Blocked(阻塞):一个进程执行了某个操作(比如I/O),因此其他进程可以使用处理器。
- 在阻塞状态下,一个进程执行了某种操作,直到发生其他事时才会准备运行。比如进程向磁盘发起 I/O 请求时,它会被阻塞。因此其他进程可以使用处理器。
将这些状态映射到图上表示:
这些操作系统教材的理论,必须保证在 Linux 下是正确的,在 Windows 下也是正确的。
操作系统中进程的运行状态
运行态:进程在运行队列中,代表我已经准备好了,随时可以调度。
进程只要在运行队列中,就叫做 运行态。
每一个 task_struct 都能找到对应的代码和数据,让进程排队。
现在我们使用的电脑,手机有四核的,有八核,十二核的,也就是有多个CPU,然而不管有多少个CPU,每一个CPU
都有一个对应的运行队列
,CPU会去此队列中寻找数据来做处理
只有在运行队列里的进程才叫做运行态,而运行态不代表我正在运行,而代表已准备就绪。
终止态
终止态:进程还在,只不过永远不会被调度器调度运行了,它随时等待被释放。
思考:进程都终止了,为什么不立马释放对应的资源,而要维护一个中止态呢?
释放要花时间吗?有没有可能,当前你的操作系统很忙呢?
释放也需要成本和时间!
所以说,你退出程序系统直接退出,只是理想状态罢了。既然操作系统不一定能立马释放,那我们就必须得维护一个终止态,来告诉操作系统自己已经退出了,等操作系统不忙的时候再来释放。
进程阻塞
阻塞:进程等待某种资源(非CPU),资源没有就绪的时候,进程需要在该资源的等待队列中进行排队,此时进程的代码并没有运行,此时进程所处的状态就叫做阻塞。
为了讲解进程阻塞,我们先了解两个知识点:
① 一个进程使用资源的时候,可不仅仅是在申请 CPU 资源
② 进程可能会申请其它资源:磁盘、网卡、显卡,显示器资源……
如果我们申请 CPU 资源无法暂时无法得到满足,这就需要排队的 "运行队列" 。那么如果我们申请其他慢设备的资源呢?也是需要排队的(task_struct 在进程排队)。
CPU 的轮转周期既然很快,CPU 得多快呢?
外设速度满,CPU 太快了,所以才会有内存这样的设备。操作系统的核心工作叫做先描述再组织,通过这样的方式来对软硬件资源作管理。
最简单的例子,当代码出现scanf或者cin时,系统会等待用户输入,只要
不输入
,要访问的资源就没有就绪
此时代码就无法运行!
此时进程就处于阻塞状态了吗?
对!但是不完全对!
struct cpu_info {
// 速度
// 帧数
// 频率
// runqueue
task_struct* queue;
};
我们所看到的软件在系统中一定会存在数据结构维护描述,每一个都有对应的资源。
假设现在有一个进程正在被 CPU 调度,它要读 1G 的数据到内存,此时 CPU 就开始执行它读数据的代码,可是磁盘当前正在忙着呢。
这种情况传统意义上就是 IO 读取时,可是磁盘数据没有就绪,进程该怎么办?
如果磁盘没就绪,我就把你这个进程丢到磁盘的等待队列当中。
在你等待的期间,我们的 CPU 正在同时处理其他任务,如果此时又有个读网卡的进程,操作系统还在忙着呢,就直接丢到网卡的等待队列当中。
所以,当访问某些资源(磁盘,网卡等),如果该资源暂时没有准备好,或者正在给其他进程提供服务,那么此时:
① 当前进程要从 runqueue 中逐出。
② 将当前进程放入对应设备的描述结构体中的等待队列。
上面这些任务都是由操作系统完成的,实际上就是对进程的管理任务,仍然是 "管理" 的本质。
如果已经可以入场了,会把该进程的 PCB从等待队列放入运行队列中。放到运行队列中,CPU 就可以去处理这个进程了。
当我们的进程此时在等待外部资源的时(处于等待队列),该进程的代码不会被执行。
当前进程去等待某种资源就绪而导致并不运行时所处的状态,就叫做 进程阻塞。
操作系统要进行管理,必须先描述再组织,所有的资源都是需要排队的,本质上都是 PCB 在排。
进程挂起
进程挂起:一个进程对应的代码和数据被操作系统因为资源不足而导致操作系统将该进程的代码和数据临时地置换到磁盘当中,此时叫做进程挂起。
挂起和阻塞很像,最终挂起也是卡住,但是挂起和阻塞在操作系统的定义上是不一样的。
我们还是带着大家理解一下挂起,挂起就要换另一种讲法了:
因为此时进程处于阻塞
状态,并且内存已严重不足,所以
OS会将PCB对应的数据和代码
弄到磁盘,为内存腾出一部分空间
此时,进程就被挂起了!
注意,将内存数据换到外设上这一操作是针对所有阻塞进程的,不用担心与外设频繁交互会降低
操作系统的效率,因为此时要关心的是操作系统还能不能存活下去!
注意,swap分区是磁盘中真实存在的,它的大小往往是很小的,这个部分专门用于内存严重不足时和内存进行交互并且当内存情况缓解后,曾经被置换出去的代码和数据又会重新加载进来!
Linux 进程状态
我们逐个来讲:
实践:
哪里可以看某个进程的状态呢?可以用 ps 看!
我们还是用上一章学的 ps 方式,当时是为了看 的。
这里我们也能拿 ps 指令来看进程的状态,我们开小窗输入:
ps axj | head -1 && ps axj | grep process | grep -v grep
这个 栏记录的就是该 process 可执行程序进程的状态了:
怎么是 ,我们的 process 不是在运行吗,怎么不是 ?
首先需要声明一点,状态后面跟加号,表示是一个 前台进程,你只需要知道的是,能够在键盘上 Ctrl+c 暂停的都可以叫前台进程。
我们 process.c 里的代码值得执行的也就一个 printf 输出语句而已,很快时间就完了。
所以大部分时间你都在 sleep(1) !
既然如此,那我们把 sleep(1) 给砍了
我们来运行一下:
woc,疯了疯了!!!
我们在用 ps 指令看看状态 栏是个什么:
还是 状态。气不气??
这个死循环在不断像显示器打印的时候,显示器本身是个外设,它非常慢,即便它闲着呢,准备好刷新它也要花时间的,所以这个进程它看起来像死循环地进行 printf 打印,实际上这个进程 90% 的情况都在等所对应的显示器就绪进行打印,因为显示器太慢了!只是因为打印的东西很快一瞬间就完成,所以我们 ps 查看到的这个进程,大部分情况在内核中都处于 S 状态。
你的 printf 代码就那么一点,CPU 一瞬间就跑完了,跑完之后发现显示器设备没有就绪,没办法给你刷新,冯诺依曼那一章我们说过,打印数据可不是直接打印到外设上的,是刷到我们对应的内存里的,你 pritnf 里面带了 \n 所以要刷盘,所以显示器不一定就绪,几遍就绪它也很慢。所以大部分情况下我们的进程都在等待显示器的资源。我们虽然还没开始介绍 S,但是我们能猜测出来了:
" S 是一个休眠状态 "
举这个例子就是为了让大家有一个深刻的印象:CPU 远比你想象中 —— 快,猛,狠 !
如果把printf也给删去呢??
这个代码也没有访问其他外设资源,页没有读文件也没有读磁盘也没打印,就纯纯的死循环。
所以这个进程不访问任何资源,只等你 CPU,只要你被运行期间不访问外设,就不会被阻塞。
不访问外设,那么死也会在等待队列里,一直在等待队列中,这就让 process 达成 状态!
S 状态(阻塞状态)
若一个进程是 状态,那么它也能称作是 阻塞状态,这也意味着它一定是在等待某种资源。
在 Linux 中它是以休眠状态的方式,让我们的进程去等待某种资源。
#include <stdio.h>
#include <unistd.h>
int main() {
while (1) {
printf("I am a process: %d\n", getpid());
// sleep(1);
}
}
因为它要等待资源不就绪。既然睡了,那谁会去叫醒它呢?
操作系统会去做叫醒它,将它的 状态设置为 状态,将其列入运行队列中。
我们一般把 状态叫作 浅度睡眠,也叫做 可中断睡眠。
- 顾名思义,当进程处于 状态,它可以随时被唤醒。
- 不仅仅是操作系统可以唤醒,你也可以唤醒,甚至你想杀掉它都行。
$ kill -9 [pid]
D 状态(阻塞状态)
下面我们来看 状态, 状态也是一种阻塞状态,它也是要我们得进程等待某种资源。
- 资源不就绪,就处于某种等待状态。那么与 状态有什么区别呢?我们细看:
- S (sleeping)
- D (disk sleep)
disk sleep?disk 表示磁盘,那应该适合磁盘有关联了。
一般而言,在 Linux 中,如果我们等待的是磁盘资源,我们进程阻塞所处的状态就是 状态。
某进程在等磁盘干完活期间挂出 D 状态,此时系统繁忙 OS 发现该进程不干活,就把它干掉了。
此时磁盘写入失败了,这些数据该如何处理?雇用磁盘的进程被杀死了,这个货该交给谁?
那就直接丢掉?如果这 些数据是转账信息呢?是银行系统呢?是重要数据呢?
我们不妨探讨一下,出这种问题,谁应该背锅?是 OS 的锅,还是进程的锅,还是磁盘的锅?
此时 OS 想了想,我作为管理者,我应该给自己立个法则,创造一个 OS 都杀不死的状态。
我们一般把 状态叫做 深度睡眠,也叫 不可中断睡眠。
深度睡眠的 状态进程,只能通过 自己醒来,OS 无权唤醒或杀死之。
此时通过 甚至 kill -9 都没有任何卵用,只有等到磁盘读写完毕将数据交付给该进程后,
此时 状态的该进程才会醒来,其状态或将变为 。
但这也并不意味着 状态进程真的天下无敌,高枕无忧了!"关机重启" 和 "拔电源" 就能干掉它。
除了这两种情况,一般没有人能将 状态进程杀死,如果你足够NB,还有一种方法:
" 直接修改操作系统源代码 "
让 OS 见到 状态也能杀掉。但这么做没有意义,既然都能杀,那 和 就没有任何区别了。
最后:深度睡眠是睡眠,它在等磁盘,由于磁盘是外设所以 D 状态也属于阻塞状态 。
这个 状态我们就不模拟了……负载太大了,我的电脑受不了
实际上,如果一个系统中存在大量的 gif.latex?D 状态进程,关机是关不掉的,要长时间关都关不掉,最后只能是强制断电拔插头,才能关掉。有时候强制断电你可能会发现有些东西坏掉了,这其实并不是硬件坏掉了,而是系统的某个软件就不能用了,可能在你强制断电的时候 操作系统中的某些数据正在让磁盘写,你强制断电就是将所有设备都断电这也当然会包括磁盘,所以导致数据丢了。
X 状态(死亡状态)
状态很简单,我们刚才也介绍过了,我们先说说 状态:
dead 代表死亡,所以 状态对应的就是 死亡状态。
这个没有什么好说的, 状态的进程就代表死亡了,可以随时等待 OS 来收尸了。
Z 状态(僵尸进程)
我们下面重点来说一下 状态, 表示 Zombie(僵尸), 状态就称之为 僵尸状态。
僵尸状态:当一个 Linux 中的进程退出的时候,一般不会直接进入 状态(死亡,资源可以立马被回收),而是进入 状态。
状态是一种已经死亡的状态,僵尸,什么是僵尸?
僵尸又称为活死人,是一种半死不活的东西。
就是一个进程死了之后,我们等一等,不要它立马把资源释放,阻止 立刻进入 状态。
为什么要先进入 ?为什么?
进程为什么被创建出来呢?一定是因为要有任务让这个进程执行,当该进程退出的时候,我们怎么知道这个进程把任务给我们完成的如何了呢?当然要了!一般需要将进程的执行结果告知给父进程或OS。
进程为 状态,就是为了维护退出信息,可以让父进程或者 OS 读取的,退出信息会写入 test_struct。
通过进程等待来进行读取的:
父进程可以通过进程的 中的 exit_state, exit_code, exit_signal 变量查看进程的退出状态。
僵尸进程存在的意义:表征进程退出时是因为什么原因而退出的。
现在我们来模拟一下僵尸进程:
如果创建子进程,子进程退出了,父进程不退出也不等待子进程(回收),此时子进程退出之后所处的状态就是 状态。
代码演示:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
pid_t id = fork();
if (id == 0) {
// child
int cnt = 5;
while (cnt) {
printf("我是子进程,我还剩下 %ds\n", cnt--);
sleep(1);
}
printf("我是子进程,我已经变僵尸了,等待被检测\n");
exit(0);
}
else {
// father
while (1) {
sleep(1);
}
}
}
用 ps 检测一下看看,我们每隔一秒检测一次,写一个监控脚本:
while :; do ps axj | head -1 && ps axj | grep process | grep -v grep; sleep 1; echo "######" ; done
效果如下所示:
运行结果:
思考:长时间僵尸,会引发什么问题?
如果没有人收尸,该状态会一直维护,该进程的相关资源 (task_struct) 不会被释放!内存泄露!一般必须要求父进程进行回收,如何回收的问题我们会在进程控制章节讲解。
顺带讲解:孤儿进程
其实听这个名字就知道,孤儿进程
大概率就是父进程挂掉了的进程
定义:
当一个子进程还没有退出,而父进程
先退出了,这个子进程被称为孤儿进程
当一个进程称为孤儿进程了,意味着没有父进程为他:“收尸"了,不回收就会占用操作系统的资源,最终使得操作系统崩溃!所以操作系统会让孤儿进程找一个"干爹"充当父进程为它"收尸”
我们来模拟一下:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
pid_t id = fork();
if (id == 0) {
// child
int cnt = 5;
while (1) { // 死循环,孩子进程就不退了
printf("我是子进程,我还剩下 %ds\n", cnt--);
sleep(1);
}
printf("我是子进程,我已经变僵尸了,等待被检测\n");
exit(0);
}
else {
// father
int cnt = 3;
while (cnt) {
printf("我是父进程,我: %d\n", cnt--);
sleep(1);
}
exit(0);
}
}
还是用我们刚才写的监控脚本,监控一下:
while :; do ps axj | head -1 && ps axj | grep process | grep -v grep; sleep 1; echo "######" ; done
运行结果:
父进程退出,为什么父进程没有变成僵尸?我们怎么没有看到父进程 为 ?
那是因为父进程的父进程是-bash,它会自动回收它的子进程,也就是这里的父进程。这里之所以没有看到父进程变成僵尸,是因为被 -bash回收了,的状态很快,所以你没看到。
那为什么刚才我自己代码中的父进程创建的子进程,父进程没有回收子进程呢?那是因为你的代码压根就没有写回收,所以你的子进程就没有回收。
上图中父进程退出后,子进程变为了 1,其实就是被 "领养" 了。
也就是说,如果父进程 提前退出,子进程还在运行,子进程会被 1 号进程领养。
1 号进程是何方神圣?其实就是操作系统!
1 号进程,即 为 1 的进程,Linux 系统启动后,第一个被创建的用户态进程。
我们把被 1 号进程领养的进程,称之为 孤儿进程 。
T 状态(暂停状态)
这两个 分别是 stopped 和 tracing stop,这两个状态可以说是一种状态:暂停
两 在内核当中实际上没有区别,给用户呈现的都是大 ,只是 tracing stop 比较特殊一些。
我们先介绍一下这个 暂停 究竟是什么,什么情况进程会被暂停呢?
进程暂停与进程休眠(阻塞) 没有关系,只是单纯不想让这个进程跑了。
比如有些进程在执行任务时,用户想让这个进程暂停一下,这其实很好理解。
比如看视频,听音乐,下载,都会有暂停。当你点击暂停的时候下载对应的代码就不跑了,此时这个进程你就可以认为是暂停状态。
再比如说我们调试程序,让程序打断点之后让程序运行起来,程序在打断点处停住的时候是将进程暂停了,所以你在 调试或在 VS 下调试时你会发现程序会停下来,这就是暂停。
所以,暂停具备很强的功能性。
为了方便讲解,我们将 stopped 记为 ,tacing stop 即为 ,我们先来讲一下 暂停。
演示: 暂停的概念
再上一章介绍 kill 命令的时候我们说后面跟 -9 就行了,具体的内容我们放到后续的信号章节去讲解,这里我们暂且再讲一下,我们可以输入 kill -l 列出所有的信号:
$ kill -l
我们可以看到 -9 实际上就是 ,我们现在需要用一下 -19 的 信号。
这就是用来暂停的!我们让一个进程跑起来,然后输入 kill -19 [pid] 看看效果如何:
此时,我们查看一下该进程的状态,发现 已经变为 状态了:
我们将该进程暂停了,现在如何将进程解除暂停呢?用 -18 信号。
CONT 就是 continue,继续的意思,输入 kill -18 [pid] 即可解除暂停:
我们可以通过信号的方式来控制进程的起和停。
讲完 (stopped) 暂停的概念,我们来讲一下 (tracing stop) ,我们还是通过一个例子去讲解。
演示: 暂停的概念
我们先修改一下我们 process.c 的 Makefile 文件,加上 以获得调试信息:
然后我们通过 调试我们的 process:
$ gdb process # 进入gdb调试
(gdb) l # 查看代码
(gdb) b 9 # 打断点
跑起来后我们输入我们启动刚才的监控脚本:
while :; do ps axj | head -1 && ps axj | grep process | grep -v grep; sleep 1; echo "######" ; done
此时我们能看到有一个 gdb process 正在被调试,这是我们的 进程:
此时 状态就出现了,它叫做 tracing stop,即 追踪暂停。
追踪暂停指的是:进程被调试的时候遇到断点时所处的状态。
- (stoped):常规暂停
- (tracing stop) :追踪暂停
总结:至此,所有的 全部讲解完毕!
- 运行状态:
- 终止状态:
- 进程阻塞:
- 进程挂起:
感谢!!!!