一个进程在 系统当中有很多的状态,比如:一个进程正在被cpu执行,那这个进程就是一个 r 状态;一个进程已经准备好了,但是其中的运行这个进程需要的资源没有准备好,那么这个进程一人不能运行。
比如 在一个程序当中有一个 scanf()函数,当你 执行这个程序之后,那么这个程序就变成一个 进程了,但是,在当 scanf()函数执行开始时候,你一直没有输入数据,那么这个进程就会一直等待你输入的数据,那么此时这个进程有没有被 cpu 执行呢?肯定是没有的,因为这个进程一直等不到 你输入的数据。
操作系统当中的各个 进程都是用 某一种数据结构来把 各个进程的 PCB 链接在一起,从而,只要找到 这个数据结构的 head头,那么就可以找到 当前需要运行的进程 PCB,从而调用到这个进程带 cpu的当中进行执行。
但是,因为 系统当中的 想运行的进程有很多,而 cpu 又只能运行一个进程,,cpu 又是少数的资源。所以,这些像运行的进程就会去竞争这个cpu资源,所以,就有了调度器的存在来帮助 cpu 更好的,合理的 运行进程,而不是一个进程一直在运行,其他的进程一个在等待,这些我们后面再说。
cpu 当中为了能管理好 运行的进程,就自己维护了一个数据结构来用于管理当前正在运行的程序。这个数据结构就是 --- 运行队列(runqueue)。
在这个队列当中包含了很多属性,但是这些属性都不重要,最重要的是这个维护这个队列的 头尾指针:
struct runqueue
{
//运行队列
struct task_struct *head;
struct task_struct *tail;
```````````
`````````
}
那么,当调度器告诉cpu 现在运行哪一个进程,cpu 就可以直接从这个队列当中,找到这个进程的 PCB ,放到cpu 当中就可以调用这个 进程了。这过程就是 进程在cpu 上排队。
调度器,说白就是一个函数接口,他可以把 把cpu 当中运行队列作为参数传入到函数体内,这样在函数体当中就可以找到这个 ,或者是利用算法,计算出 当前应该要执行的进程。
而,我们把处于 运行队列当中的 进程,这些进程的状态就是 运行状态(运行态 R)。
在这个运行队列当中进程,就代表的是:这个进程已经准备好了,随时可以被调度。
在将来,如果某一个进程准备好了,他想被cpu所执行,那么他只需要 入队到 运行队列即可。
只需要等待操作系统的算法计算哪一个进程应该被调度,当计算到 新入队的进程,这个新进程就可以被执行了。
当然,肯定会出现进程占用cpu,不退的情况,比如你写了一个死循环的程序,那么,当这个程序死循环之后,就会一直占用这个 cpu,cpu 肯定不会一直让这个 进程占用这个cpu。所以,给每一个进程都有一个时间片。
简单来说就是,这个进程在cpu上执行的之间只能是 一(或多个)个时间片的时间。如果过了这个时间片,这个进程还没有执行完毕的话,就要在 入运行队列,然后等待操作系统调度cpu执行。
所以,在上述简单操作系统调度进程 在 cpu 上运行的描述之下,在一段时间之内,所有的在运行队列之上的代码都会被执行。这种情况,我们称之为 --- 多个进程的并发执行。
当然,在这段时间之内,肯定不只是执行刚开始的那些进程,在此之中就会,大量的把进程从cpu 上拿下来,再从输入设备当中拿上去cpu 的过程 ,这个过程称之为 --- 进程切换。
因为 cpu 处理速度很快,所以在我们看来,根本感受不到 进程 在切换的过程,这些进程都像是在同时被执行一样,所以看起来像是 这些个进程都是在一起运行一样。
进程就是 我们写出来的程序软件,被执行时,就变成了进程,那么程序怎么多,功能也是千变万化,那么这个程序所需要的 数据也是不同的。那么,很多程序就是需要 某些数据才能运行,数据也是 进程的组成的一部分,所以,数据是不可或缺的!!
那么,进程也不是想拿到什么数据就可以拿到什么数据的,就跟最开始我们举的例子一样,程序当中有 scanf()函数,我们不在键盘上输入数据,那么这个进程就一直不会就收到数据,那这个进程是不能执行的。那这个进程就是没有就绪,也就不能放在 运行队列当中。
我们把上述这种没有 就绪的进程,这种进程的状态就是 阻塞状态。
当然在一个 操作系统当中,他要管理软硬件资源,就是使用 先描述在组织的方式来管理的,比如他要管理键盘这个硬件,那么就是把管理键盘需要的 属性值,都构造成一个对象,这个对象当中就存储了管理 键盘需要的 属性值。
在这些属性值当中,也有一个队列,这个队列也是像 cpu 当中的 运行队列一样,是用来给要使用这个硬件的 进程来进行排队的。
比如,当前某一个进程需要使用到 键盘,那么好,就去键盘的 队列当中排队去吧,如下图所示:
这些队列,在 每一个可能被进程使用的硬件当中都存在,作用也是一样的。
我们把这些队列,称之为 --- 等待队列。
像上述 scanf()函数进程的例子,如果键盘硬件当中轮到 这个进程来执行了,键盘当中有数据了,那么这个进程就可以读取键盘当中数据,然后,如果这个进程没有数据需要接受了,这个进程也处于就绪状态了,那么这个进程就可以入到 cpu 的运行队列当中去,等待 操作系统调度器执行就行了。
所以,阻塞队列有 N 多个!!! 有非常多个,想象我们硬件有这么多,而且不止 硬件有阻塞队列。
那么,这么多的,进程在 怎么多的 阻塞队列当中排队,那么操作系统所管理的内存资源总是会有不足之时。那么如果内存被占满了,不就意味着,系统要进行卡顿吗?
所以,操作系统在此处进行了优化(本来是不优化的,因为此时迫不得已,进行了优化)。
之前我们说过,在队列当中排队是 PCB 在进行排队,此处的优化就在此:
操作系统管理的内存资源已经快满了,就把 一些 进程的 数据和代码 这些 换出 到外设(比如是磁盘)当中,当 排队轮到这个进程执行之时,才把 外设 当中的 代码 和数据 换入 到 内存当中。
那么,当某一个进程的代码和文件已经被 换出了,那么这个进程此时就是 挂起状态。
所以说,一个进程只要是 PCB 在内存当中被操作系统所管理,那么我们就认为是当前进程正在被运行,而 这个进程的 代码和数据 在不在 只是这个进程的不同 状态,代码和数据在不在 不是这个进程在不在的判断条件,PCB 在不在才是。
使用 top 可以查看到 当前实时状态下,所以的正在运行的进程的很多信息,包括下述要说明的 Linux 当中 进程的各种状态:
我们上述所描述的状态只是一个框架,对于 操作系统实现的细节,不同的操作系统所实现的细节是不一样的,比如在 Linux 当中就实现了更多的 进程状态,用于更好的管理 进程,这些进程的状态是保存在一个 const 字符数组当中的:
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 */
};
这就是课本上的操作系统 和 具体实现的操作系统之间差别。
接下来我们来一个一个描述这些个状态。
只要进程的PCB是在 cpu 的运行队列当中,那么就说明,这个当前这个进程的状态就是 运行状态。
同样,在等待外设,在外设的等待队列当中时候,我们称之为 阻塞状态。
我们不能拿我们之间的认知去 理解操作系统 在运行时给进程赋予的状态,因为现实在使用 操作系统时,所涉及的很多情况。比如如下情况:
此时我们编写下面这个死循环代码:
此时,我们在运行 有上述 源文件生成的 text 可执行程序时候,我们来查看这个 text 程序运行状态:
发现,此时的 text 进程是 S 阻塞状态,而不是 进程状态。
我们还发现,上述在描述状态不只是描述 R 或者是 S,而是 R+ , S+ 来描述的;那么此时的 “+”是什么意思呢?
这里的 "+" 其实代表的是,这个进程是在 前台运行的。
什么是前台呢?在上述我们运行程序之后,我们再输入执行,bash 就不会再解析这个指令了。
如果想让一个程序在后台运行的话,需要在 运行之时,加上一个 "&" 符号。这个符号表示的是 这个程序在后台运行。
当一个程序在后台一直运行话,我们此时使用 ctrl + C 是不能把这个进程给关闭的,因为这个进程现在不受 前台管理了,所以,此时我们要是用 kiil -9 PID 来杀掉程序:
如上述所示,ctrl + C 是不能结束进程的。
所以,其实 bash 命令行解释器在我们输入命令,回车之前,都是阻塞状态(sleeping),在命令行当中,等到我们输入命令,确认输入,然后 bash在进行解析,创建子进程,等等的操作。
如下所示,bash 都是在 S 状态:
像上述的 S 状态也是一种阻塞状态,S 的阻塞状态是 浅度睡眠,处于 浅度睡眠的 进程可以被随时唤醒,可以随时响应外部的变化,而唤醒的人,可能是 用户,也可能是 操作系统。
而 D 状态是 深度睡眠,或者称之为 -- 磁盘睡眠。
要了解 深度睡眠,我们先来了解一个场景:
我们之前谈到过挂起状态,挂起状态就是 把 进程的 数据和代码部分 放到外设当中进程暂时的存储,但是 PCB 还在内存当中进程排队,这样的可以减轻 内存的压力。那么这种情况,比如是把 数据和代码 放在 磁盘当中存储的,那么此时进程就肯定要拜托 磁盘,让磁盘帮忙把 数据写入磁盘当中。
或者说,此时某一个进程需要对磁盘当中写入数据,那么这两种情况都导致一个结果,就是 进程需要等到 磁盘写入数据。
如果,磁盘写入数据要考虑很多的情况,比如:找到一个合适空间来存储,判断是否能存储等等情况,所以,磁盘写入是需要耗费时间的。
那么,进程在等待的途中,就没事做,优哉游哉的等待 磁盘写好数据给他回应,进程才好把数据是否成功写入的结果返回给上层。
如果,在磁盘等待的途中,操作系统所管理的 内存资源已经严重不足了,那么操作系统就会去想办法优化内存资源,比如:该挂起的挂起,该释放掉释放等等操作。如果是是在没有办法的情况下,操作系统为了保证自己的程序,和 内存资源不会挂掉,那么他就是会去杀后台(也就是杀进程)。
如果操作系统选择杀进程的话,就是以当前能力的操作系统实在没办法优化了,只能杀掉他认为不重要的进程了。
如果此时,因为 上述在等待 磁盘回应的 进程,在操作系统看来就非常的“闲”,那么他有可能就会直接杀掉这个进程。
那么问题就来了,假设,这个进程是在写入一些非常重要的数据,而又恰好,磁盘在写入过程当中,出现了问题,此时,磁盘就要给 进程报错,但是又发现进程已经不在了。
那么这个重要数据就已经丢失了,如果为这个事情开庭的话,你是不是会认为是操作系统的问题?觉得因为是操作系统“杀的”啊,就是他的错。
但是实际不然,可能 操作系统 磁盘 进程,三者都没错。
操作系统:只是在完整自己的本职工作,他的本职就是要让用户一个稳定的,不会时不时就卡顿的平台,供用户使用,如果到了上述那种非常极端的情况,由不让它杀掉这个进程的话,那么可能就会导致整个操作系统的瘫痪,那么此时出问题的就不会这一个进程了,而且所以的进程都会崩掉。
磁盘:当然也不是他,因为它就只是一个跑腿的,只是帮助 进程来在外设当中写入数据,而且,事先 磁盘就会告诉进程,此次写入数据操作是会写入失败的。
进程就更不是了,进程本来就是受害者。
所以,在进程等待磁盘写入数据这么关键的事情之上(在磁盘写入数据完毕之前),我们要让进程不被任何“人“ ”“杀掉”。
我们把上述 这个进程 所处的状态 称之为 --- D(disk sleep)。
D 状态的进程不响应 操作系统的任何请求,所以才不会被操作系统杀掉。在这个状态的
进程通常会等待IO的结束。
当进程 从 磁盘上得到的了 数据被写入成功或者失败的 信息之后,进程才会从 D 状态解除到其他状态。
如果,此时在内存当中出现了很多的 D 状态的进程,操作系统是没有资格杀掉这些进程的,而且,只要是 磁盘没有给 进程返回写入信息,那么就算是把 操作系统重启都是没有办法解决的。
所以,此时要么是 直接重启(断电),要么是等待磁盘 返回写入信息。
因为,cpu 处理都是毫秒级别的速度,所以用户是感受不到 进程切换 ,挂起等等操作的过程的,所以,如果当用户已经查找了 哪怕是 一个 D 状态的进程了,说明这个进程已经存在很长时间了,那么就说明操作系统已经在奔溃的边缘了。
一般情况下,D 状态的进程 非常少,而且持续时间很短。
因为 D 状态的进程不好演示,演示的话有可能会弄坏操作系统,所以不建议演示,但是可以使用 "dd" 命令来操作,具体请看下述:
Linux dd 命令 | 菜鸟教程 (runoob.com)
这指的停止状态不是 这个进程直接被杀掉了,而是 这个进程暂停了 ,所以暂停代表的就是,只是现在停止了,但是后序可以继续执行。
Linux 当中,我们可以使用 kill -l 来查看 kill 指令 能给进程发出的各种信号的信息:
在这么多信息当中,有 18 和 19 这两个信号,18 信号是 SIGCONT 就是继续执行进程,19 信号 是 SIGSTOP 就是暂停执行进程。
但我们使用 19 信号暂停某一个 进程之后,这个进程当前就处于 T (stopped) 停止状态。
像上述例子当中的 text 就被暂停掉了,当前 text 进程所处的状态就是 暂停状态。
此时,可以使用 kill -18 PID 来恢复上述 text 进程,但是注意,此时恢复的进程是在后台运行的,在 不受前台的控制。如果此时向停止这个 进程,那么就要使用 kill -9 PID 给这个进程发出9号停止命令。
S 状态是阻塞状态,进程的PCB 在各个硬件或者其他外设等等设备的 阻塞队列当中排队的进程,此时的状态就是 阻塞状态。也就是说,处于阻塞状态的进程,是在等待某一个设备给予数据,或者是给出信号,才能继续运行。
而,T状态是 不一定需要等待 某一个设备的 数据 或者 信号,就像上述的 text 进程这个例子,我在停止之后,我想什么时候继续执行这个进程,就 使用命令 让他继续执行即可。不一定需要某些硬件的数据才能执行。
也就是说,处于 T 状态的 进程,不一定是在排队等待,而是只是单纯的暂停而已。只是目前想要 这个进程等待一下,暂停一下,可能是等待其他的事件发生。那么我们就可以把这个进程 设置为 T 状态。
当进程 处于 T 状态之后,T 进程的代码就完全的暂停执行了,一般而言,进程就不再接受各种信号或数据了。而 S 状态就是要等待某种资源,信号。
其实你也可以把 T 理解成一种阻塞状态,但是不同的是,T 状态的 进程不一定是在 等待某些资源,可能就只是停止了,可以被用户所控制。但是 S 状态是一定要等待某种资源的,这点你要清楚。
当我们在使用 gdb 调试某一个程序之时,我们可能会 逐步调试,可能会 逐块调试,或者是 直接运行到 断点处,运行到条件断点处等等调试操作。
那么这些操作在执行 的之间时刻,我们是要分析这个代码,在当前状态下的各个信息是否正确。以此来逐步推断出 可能的错误所在。
那么,其中判断,推断的过程肯定是需要时间的,在此期间,代码是停止运行的,那么如何让正在运行的进程停止下来,那么其实也是一种阻塞,但是其中不会只有S状态的阻塞,还有T状态的阻塞。
如下所示,当我们开始执行 text 的gdb 调试时,就是 gdb 是处于 S 阻塞状态下的,但是 text 是处于 T 停止运行状态下的:
所以,T 状态有自己的运用场景,和 S 状态是有区分的。
这个状态就顾名思义了,一个进程运行完毕了,那么这个进程就是退出内存,释放资源,那么这个进程所处的状态就是死亡状态了。
Linux 当中的 X 状态就和 很多教科书当中所说的 终止态是一样的。
但是,你会很好奇,为什么一个进程已经死亡了,那么为什么要有这种状态呢?
其实你想得没错,X 状态只是一个返回状态,你不会在任务列表里看到这个状态。
所以,在程序死亡之前(处于 X 状态之前),还有一个状态,叫做 Z 僵尸状态。操作系统要在 处于僵尸状态的 进程给 做检查,此时,操作系统不是直接就把这个进程的 资源等等信息直接释放清除了,而是还有维持一段时间,当都检查确认之后再进行释放。
那么,这个信息维持给谁看呢?就是这个僵尸进程的父进程,因为此时最关心当前这个僵尸进程的就是父进程,因为父进程 创造这个 子进程是需要耗费资源的,同时也可能是 互相之间是有 交互的。
当父进程拿到 这个 僵尸状态的子进程的信息之时,才会把这个子进程的状态 变成 死亡状态,释放空间 。
把这种 已经死亡的课程,但是需要有父进程来收集子进程的信息,操作系统为这个子进程维持的状态,就叫做 Z (zombie) 僵尸 状态。
进程一般在推出时,如果父进程没有主动回收子进程的信息,子进程就会一直处在Z的状态。而进程相关的资源,尤其像是 task_struct 结构体对象不能被释放。
僵尸进程在死亡时候 ,如果父类没有做出回应(接收信息),那么这个僵尸进程就会一直占用 内存资源,我们把这种由于僵尸进程占用内存资源的情况,称之为 --- 因为僵尸进程而引发的 内存泄漏 问题。
当然,如果你想看某一个进程的 僵尸状态话,可以使用一下代码:
#include
#include
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;
}
因为,cpu 处理的速度很快,假设你是使用使用 "./" 来运行的某个程序的话,是查看不了 这个进程的 僵尸状态的。因为 此时这个进程是 bash 这个命令行解释器进程的 子进程,当 这个子进程结束运行时,就会立马被 bash 父进程接受信息,从而转变为 死亡状态,释放资源。
如果,在一对父子进程当中,父进程先死亡了,但是此时子进程还在运行,没有挂掉,在这种情况下,这个子进程的 PPID 会变为1;也就是,此时这个子进程的父进程变为了 PID 为1 的这个进程。
使用 top 命令 来查看 PID 为1 的进程是什么:
发现是 systemd 这个进程。
我们来查看 systemd 这个进程的详细信息:
其实 1 号进程,也就是这个 systemd 进程,其实就是操作系统本身。(有些操作系统当中和可能不是 systemd ,也可能是 inti,都是代表的是 操作系统进程)
我们把上述的,父进程先退出,子进程 的 父进程就是 1 ,操作系统进程了,这个子进程 就称之为 -- “孤儿进程”。这个子进程就被 操作系统给领养了。
为什么要被领养呢?原因很简单,当一个进程在结束之时,要先变成僵尸状态,就要被父进程回收信息,总是要有一个为这个子进程回收信息的,那么就让操作系统帮忙了。
其实,如果这个子进程是在父进程 运行之时才创建的(就是我们上述写的代码,查看僵尸进程的例子),那么,不是 bash 不想管这个子进程,是因为 这个子进程不是 bash 进程所创建的,是有 bash 创建的这个子进程的 父进程 所创建的。而 "爷爷不管孙子"
而。操作系统不一样,不需要使用一些接口来转移关系,直接在内核层面就可以直接 修改父子关系,做上“收尸者”。
其实关于僵尸进程的危害,我们在上述已经阐述过一次了,就是当父进程如果一直没有接收 子进程僵尸状态的信息的话,那么子进程就会一直处于僵尸状态,操作系统就会一直帮助 子进程来维持在内存当中,等待父进程来接收信息;
那么,就会造成因为 僵尸进程 带来的 内存泄漏问题。
父进程之所以要接收子进程返回的信息,是因为:父进程花了资源和时间去创建了子进程,目的就是为了让子进程帮忙父进程来办事,那么子进程现在要 死亡了,称为僵尸进程了,那么子进程现在把这个事情办得怎么样了,父进程是需要知道的。
像操作系统要维护 僵尸状态的子进程的话,就要维护这个子进程的 task_struct 结构体对象;而且,如果父进程创建了多个子进程的话,那么如果此时 变成僵尸状态的不止 一个 子进程的话,那么所造成的内存泄漏问题就更严重了。