1. OS教材中的进程状态
首先我们要知道,系统中的进程有三个最基本也是最重要的状态:运行、阻塞、挂起。
然后要介绍一个概念,调度队列。
1.1 调度队列和等待队列
操作系统会为 CPU 提供一个调度队列,它是操作系统内核用于管理进程调度的核心队列结构,1里面存放的是所有已经具备运行条件、等待 CPU 分配时间片的进程。

大概逻辑就是这样。
在我们学校当中的教材里面,绝大部分会将就绪态 和运行态 严格区分开来,教材中认为,就绪状态是指进程已经获得除 CPU 以外的所有所需资源,完成了运行前的全部准备工作,只等待操作系统调度器分配 CPU 时间片即可立即执行的状态。
处于就绪状态的进程会被操作系统放入调度队列中,由调度器按照调度策略依次选择上 CPU 运行。它与正在 CPU 上执行的运行状态的唯一区别,就是当前是否占用 CPU;一旦调度器切换 CPU 使用权给该进程,它就从就绪状态变为运行状态;如果时间片用完被换下 CPU,它又回到就绪状态继续排队。
1.2 阻塞和运行状态
但是在实际的 Linux 操作系统中, 只要进程位于 CPU 的调度队列中,内核就统一将其标记为 TASK_RUNNING 状态,也就是我们常说的运行状态。这里的 "运行状态" 包含两层含义:一种是进程正在 CPU 上真实执行指令,另一种是进程已准备就绪、在调度队列中等待被调度器选中执行。操作系统并不单独区分 "就绪队列" 和 "运行队列",凡是在调度队列中、具备执行条件的进程,都属于运行状态,调度器会按照调度算法从队列中依次挑选进程上 CPU 运行。
所以,我们就记住:进程在调度队列里,就可以认定它处于运行状态。
就绪状态和运行状态大概了解了,接下来我们讲一讲阻塞状态。为了能更好的让大家理解,我们先看一看进程处于阻塞状态的现象:

对于上图中这个代码,我们因为写了一个scanf,所以需要我们先从键盘上写入内容,然后再读取才能继续运行。同时我们查看进程,可以看到这个myprocess可执行程序是一个正在运行的进程,所以这种一个进程卡住不动的现象,就叫做阻塞。
这是从看到的现象中去理解阻塞的意义,那么接下来我们从操作系统的角度再去理解。
首先,我们写下scanf函数并执行之后,我们计算机中的操作系统就在等待键盘上能输入数据。而这个键盘属于是硬件设备 ,一旦键盘输入数据,就表示键盘数据就绪 ,即硬件就绪。
操作系统作为硬件的管理者,管理的方式还是我们之前讲过的:先描述,再组织。所以对于硬件来说,它也有对应的类似于 struct task_struct 一样的属性信息结构体。并且因为硬件不止一个,所以有 struct device *next 给链接起来,和我们在上篇文章中提到的操作系统底层的信息管理逻辑一样。同时还存在一个指向进程描述符类型的指针 :struct task_struct *wait_queue。
当我们要运行这个代码的时候:

一遇到scanf,操作系统就会去向键盘要数据,但是因为用户还没有通过键盘输入数据,所以就表示硬盘未就绪,此时就相当于该进程正在等待,那么此时,操作系统就会把调度队列中的我们本来要运行的这个进程拿出来,将这个进程的PCB中的运行状态改为阻塞状态,再放到键盘设备对应的等待队列里。此时因为进程现在已经不在调度队列里了,那就不再参与 CPU 调度,所以才会出现"卡住"的样子,直到所等待的事件发生,内核才会将进程从等待队列中取下,重新放回调度队列,使其恢复运行状态:

因此我们可以做一个总结:阻塞和运行的本质区别,实际上就是看你这个进程的task_struct在谁提供的队列当中。
1.3 挂起状态
接着我们来谈一谈什么叫做挂起。

在操作系统同时运行大量进程的场景下,若系统检测到物理内存资源不足,为了优化内存使用、保证关键进程正常运行,操作系统会选择一部分优先级较低、暂时不急需执行的进程,将其数据与代码迁移到其他存储空间,再释放该进程占用的内存。在硬件层面,磁盘上专门用于此类临时存储的区域称为 swap 交换分区,它的作用就是存放这些暂时不紧急的进程数据。
那么如何判断哪些进程属于 "不紧急、可迁移" 的对象呢?操作系统通常优先选择处于阻塞状态的进程。因为阻塞进程正在等待某一事件(如 I/O 完成、键盘输入、资源释放等),短期内无法投入运行。系统会将这类进程的代码、数据等从内存换出,保存到 swap 分区,然后释放内存空间,供其他更活跃、更急需的进程使用。
当阻塞进程等待的事件就绪、需要继续执行时,操作系统会将其数据从 swap 分区重新加载回内存,使进程恢复可调度状态,再将其放入调度队列,等待 CPU 调度执行。
因此,挂起状态 可以这样定义:当 "进程的代码、数据等内容被暂时换出内存、保存到磁盘的 swap 分区,仅在内存中保留进程控制块 task_struct 以标识进程存在" 时,该进程所处的状态即为挂起状态。处于挂起状态的进程既不占用实际内存空间,也不参与 CPU 调度,必须先被重新调入内存,才能继续等待事件或参与调度。
其中,如果进程原本处于阻塞状态,在内存不足时被操作系统换出到 swap 分区,进入挂起,这叫做阻塞挂起;如果进程原本已经具备运行条件,在调度队列当中,处于运行状态,只是因为内存紧张而被换出到 swap 分区(这种是因为已经将阻塞状态的进程的数据放到swap分区后,内存空间依然不足),那就叫做运行挂起。
关于 swap 交换分区我们还需要再详细介绍一下:
它是操作系统在磁盘上划分的一块专用存储空间,主要用作虚拟内存 ,用来缓解物理内存不足的问题,其空间大小一般为 内存空间大小的 1~2 倍。当系统同时运行大量进程,导致物理内存资源紧张时,内核会将部分暂时不活跃的进程数据从内存转移到 Swap 分区,从而腾出宝贵的物理内存供当前急需运行的程序使用。因此,Swap 本质上是用磁盘空间扩展内存容量的一种机制。
将数据从内存写入 Swap 分区 的过程,称为 swap out 。当挂起进程等待的事件就绪、需要继续执行时,操作系统会将之前存放在 Swap 分区中的代码和数据重新读回物理内存,并恢复其运行环境。这个从 Swap 分区读回内存 的过程,称为 swap in。
简单来说,Swap 分区相当于内存的 "后备仓库",swap out 负责腾出内存,swap in 负责恢复进程运行。借助这一机制,操作系统既可以在内存紧张时保证系统稳定运行,又能实现进程挂起与唤醒,提升整体资源利用率。不过,swap交换分区也有一个缺点,因为 Swap 在磁盘上,速度远慢于内存,频繁 swap 会导致系统卡顿。
2. 具体OS中的状态
我们上面提到的阻塞、运行、挂起,都只是在简单介绍它们的概念,仅仅停留在基础知识,属于是通用操作系统理论。但是在实际的Linux操作系统当中,这三种状态都是怎么被标识的,到底长什么样子,我们还不了解。
cpp
/*
*The task state array is a strange "bitmap" of
*reasons to sleep. Thus "running" is zero, and
*you can test for combinations of others with
*simple bit tests.
*/
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 */
};
这是Linux操作系统中关于进程状态的数组,里面存储了进程的不同状态。
- R 运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
- S 睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
- D 磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待 IO 的结束。
- T 停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
- X 死亡状态(dead): 这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
- t 跟踪停止状态(tracing stop)对应内核的
TASK_TRACED状态。它是 T 停止状态的特殊变体,专门用于调试场景。 - Z 僵尸状态(zombie)对应内核的
EXIT_ZOMBIE状态。当子进程已经退出,但父进程尚未通过wait()/waitpid()回收其退出状态时,子进程就会进入僵尸状态。
并且每一个状态都有自己对应的标识符,标识符以数字的形式表示。比如R状态就用 0 代表,S 状态就用 1 代表。
3. 查看状态
3.1 S 可中断睡眠状态(sleeping)
我们首先创建一个文件myprocess,然后写入代码:

编译运行之后,我们来查看它的状态:

会发现这个进程的状态是 S+ ,+ 到底是什么意思我们先不管,我们先看 S ,这代表这个进程目前处于睡眠状态。但是我们的程序一直在运行啊?一直在执行while循环里面的printf语句,持续的在屏幕中打印我们所定的内容,可为什么这个进程的状态还是休眠呢,它不应该是运行吗?
我们再将 myprocess.c 文件中的代码修改一下:


我们发现当程序持续执行while空语句的时候,这个进程的状态从原来的睡眠状态变成了运行状态。
这是因为,在 Linux 操作系统中,进程状态会根据自身执行行为发生明确变化。保留 printf 与注释 printf 会让进程呈现完全不同的状态,其核心原因在于进程是否需要等待事件、是否主动放弃 CPU。
当程序中保留 printf 与 sleep 语句时,进程会在循环中执行输出操作并主动等待。printf 属于典型的输出 I/O 操作 ,即进程通过系统调用向终端设备写入数据,由设备驱动最终刷新显示到屏幕;而 sleep 则让进程主动等待指定时长。
由于 I/O 设备的处理速度远慢于 CPU,进程在执行 printf 时无法立即完成操作,必须等待设备响应。此时内核会将进程从运行队列移出,挂入对应的等待队列,使其主动让出 CPU,进入 Linux 下的可中断睡眠状态(S) ,对应操作系统理论中的阻塞状态 。直到 I/O 完成或定时时间到达,进程才会被唤醒并重新放回运行队列继续执行。因此在未注释 printf 时,进程绝大多数时间都处于等待状态,状态标识为 S。
当程序注释掉 printf 与 sleep 语句 后,进程仅执行一个无限空循环。该循环不涉及任何 I/O 操作,不等待任何外部事件,也不会主动放弃 CPU。此时进程始终具备执行条件,要么正在 CPU 上运行,要么时间片用完后回到运行队列等待再次调度。这两种情况在 Linux 中统一表示为运行 / 就绪状态(R) 。因此注释 printf 后,进程会持续处于 R 状态,不会进入阻塞。
简单来说,是否出现等待行为,是决定进程状态为 R 还是 S 的根本原因:有等待则进入阻塞(S),无等待则保持运行 / 就绪(R)。
那既然进程处于 S 可中断睡眠状态,我们之前讲过可以使用 Ctrl + C 的键盘操作去中断这个进程,现在,我们还可以使用 kill 命令:

使用 kill -9 PID 就可以强制杀掉这个正在运行或者卡住的进程,其中 -9 是 Linux 系统中的信号编号 ,代表系统预定义的 SIGKILL(强制终止信号)。
Linux 操作系统通过信号来控制进程的行为,不同的数字对应不同的指令:

-9 是最强制、最高优先级的终止信号 ,内核会立即杀死指定进程,不允许进程忽略、阻塞或处理,进程无法做任何清理操作,直接被系统强制结束。
普通的 kill PID(不带 -9)默认使用 -15(SIGTERM) ,属于温和的退出信号,允许进程保存数据、释放资源后正常关闭。
接着我们来讲一下S+的 + 的含义:

首先我们写出这样一段代码,执行一个死循环,一直睡眠 1 秒,然后去查看进程:

大家会发现,此时我的进程处于 S+ 状态,当我再次输入各种指令的时候,我的Shell不做任何反应,输入什么内容都没有用。其实这是因为 + 作为状态修饰符,表明该进程是前台进程 组的成员,Linux 系统默认就是让进程在前台运行 ,这是系统的标准行为。也就是说,./myprocess 这一进程是一个前台进程,前台进程会占用当前终端,可直接与用户交互,且能接收 Ctrl+C、Ctrl+Z 等键盘信号,但是终端会被其占用,无法在同一窗口输入新命令。
如果我不想让我的进程处于前台进程,那么可以手动的加上这一符号:

此时的状态变成了 S :

所以,在命令末尾添加符号 & ,表示将进程放到后台运行 。此时进程状态从 S+ 变为 S,+ 符号消失 ,意味着进程不再占用当前终端,用户可以继续在终端输入其他指令,且无法通过 Ctrl + C 直接终止该进程。只能通过kill指令去删除进程。
3.2 D 不可中断睡眠状态(disk sleep)
D状态也被称为磁盘睡眠(Disk Sleep),是 Linux 中一种特殊的阻塞状态。
当进程需要与硬件设备进行关键、不可打断的数据交互时,就会进入 D 状态。最典型的场景是进程访问磁盘、读写文件,此时进程会向磁盘设备发起 I/O 请求。由于磁盘 I/O 速度远慢于 CPU,进程必须等待硬件完成操作,不能继续执行。
为了保证数据安全与设备状态稳定,内核不允许这种等待被信号打断。如果进程在磁盘读写中途被强制终止,可能导致数据不完整、文件系统损坏或设备状态异常。因此,内核会将进程标记为不可中断睡眠,让它安静等待 I/O 结束,期间不响应任何普通信号。
处于D状态的进程其实会有一定的风险。与常见的 S 状态(可中断睡眠)不同,处于 D 状态的进程无法被 Ctrl+C 终止,也无法被 kill 命令杀死,即使是 kill -9 也无效。它只能等待 I/O 完成、硬件返回结果后,由内核自动唤醒,回到 R 就绪状态继续执行。
所以,大量长期处于 D 状态的进程会占用系统资源与进程描述符 ,导致系统可用进程数量减少,严重时会影响新程序启动,甚至引发系统服务异常。更关键的是,D 状态通常意味着 I/O 瓶颈。持续出现 D 状态说明磁盘或硬件设备响应缓慢,也就是磁盘或者硬件设备老化了。会导致整个系统 I/O 性能下降,文件读写卡顿、系统负载升高,用户会明显感觉到操作延迟、程序响应变慢。
在极端情况下,如果磁盘故障或设备断开导致 I/O 永远无法完成,相关进程会永久卡在 D 状态,形成不可杀死的 "僵尸化" 进程,只有重启系统才能彻底清除,严重影响系统稳定性与可用性。
不过一般正常情况下,磁盘 I/O 很快,D 状态只会短暂出现,用户几乎感知不到。但如果磁盘繁忙、I/O 阻塞或设备异常,进程会长时间停留在 D 状态,表现为系统卡顿、进程无法杀死,这也是判断 I/O 瓶颈的重要标志。
由于D状态一般不会出现或者说很难捕捉到D状态,在这就不做实际演示。感兴趣的可以去大模型搜索一下如何在命令行输入指令模拟D状态进程的出现,友情提示:这个实验可能会导致你的操作系统崩溃,需要重装。
3.3 T 停止状态(stopped)
T 状态即停止状态,表示进程被暂停执行,不再参与 CPU 调度,但进程的内存、打开的文件等资源依然保留,随时可以恢复运行。
进程进入 T 状态通常是收到了暂停类信号,最常见的是用户在终端按下 Ctrl+Z,或是系统调用 kill -19 PID 发送 SIGSTOP 信号。与 S 状态、D 状态等待 I/O 或事件不同,T 状态下的进程并非在等待外部条件,而是被强制冻结,内核不会再为其分配时间片。
比如我们还是使用这段代码:

运行之后,我们对其使用 kill -19 PID 的指令:

就会发现原来的进程从S+的状态变为了T状态。
处于 T 状态的进程既不运行也不退出,只是暂时 "挂起"。它可以被 kill -18 PID(对应 SIGCONT 信号)唤醒,恢复到之前的 R 或 S 状态继续执行。这种状态常用于调试程序、临时暂停任务,是操作系统对进程精细控制的一种体现。

在前面讲解可中断睡眠状态的时候,我们提到了前台进程和后台进程,经过讲解我们可以总结成一句话:前台进程和后台进程最大的区别就是,是否能接收键盘输入。前台进程占用终端,能够获取用户的键盘操作与信号;后台进程脱离终端控制,无法接收键盘输入,用户的按键操作不会传递给该进程。
如果现在有一个进程就是后台进程,但是这个进程还需要从键盘中获取输入,操作系统就会把它设置为T停止状态,就像这种场景:

这段代码里调用了scanf函数,所以需要从键盘中获取输入。

我们手动设置进程后台运行,此时该进程的状态就被设置成了 T 。
3.4 t 跟踪停止状态(tracing stop)
这个状态对应内核的 TASK_TRACED 状态。它是 T 停止状态的特殊变体,专门用于调试场景 。当进程被调试器(如 gdb) attach 或设置断点时,会收到跟踪信号并进入此状态。此时进程处于暂停执行,等待调试器指令(继续、单步、断点恢复等)。它不仅能被 SIGCONT (kill -18)恢复,还能由调试器主动控制,是系统调试机制的核心状态之一。
我们刚开始调试这段代码的时候,先给代码打上一个断点,然后执行运行指令。在我们执行运行指令之前这段程序是没有被运行的,所以我们看到进程里面,只有GDP调试器调用的一个myprocess的进程。当我们运行之后由调试器创建了一个子进程,让这个程序得以运行。
这个得以运行的程序的状态就是 t 跟踪停止状态。这就相当于父进程GDP先创建了一个子进程,遇到断点的时候,向子进程发起了kill 19号信号。
3.5 X 死亡状态 和 Z 僵尸状态
为了更好的让大家能理解,我们来讲个故事:
你叫小明,是一个朝气蓬勃的大学生,非常喜欢跑步。有一天早上六点钟,你正沿着路上在慢跑,突然从你的后面窜出来一个八十岁的老大爷,跑的飞快宛如风驰电掣。当他跑到你前面一段距离的时候,突然嘎嘣一下倒在地上站不起来了。作为一个有社会道德的有为青年,你当机立断打了急救电话和报警电话,老大爷一个人直的不能再直的躺在地上等着,过了一段时间后警察和急救人员都赶到了。
当赶到的时候,警察和急救人员并不是马上就把老大爷给抬走,而是先就地检查老大爷的状况,医护人员判断老大爷是否已经死亡,警察人员判断老大爷的死亡原因到底是什么。等检查和判断全部完毕之后,医护人员才会把老大爷拉走,警察人员才会疏散现场。
在上面这段虚构的故事当中的老大爷就相当于是一个进程,在他躺在地上的时候,就相当于这个进程已经结束。但是老大爷到底有没有死亡,还不得而知,需要医护人员来判断。对于进程而言,当这个进程已经结束,但是还没有人来检查进程的信息的时候,在这段期间,该进程的状态就叫做:僵尸状态 。当信息检查完毕,确认该进程确实真的已经结束了,才会宣判这个进程的状态为:死亡状态。
不过要注意的是,死亡状态是一个瞬时状态,至少以我们当前的知识储备,是没有直接观察到进程处于死亡状态的时候的。所以大家只要先了解死亡状态的概念:死亡状态是进程彻底退出、所有资源被内核完全回收后的最终状态,进程会从系统中彻底消失,不再留下任何信息。
我们主要要讲解的是:僵尸状态。
是 Linux 操作系统中进程生命周期结束后的一种特殊中间状态。当一个子进程执行完毕、通过exit()系统调用正常退出,或因信号异常终止时,其用户态内存、打开的文件描述符、堆栈等运行时资源会被内核立即释放,但为了向父进程提供该进程的退出状态码、终止信号等退出信息,内核会保留该进程的进程控制块(PCB/task_struct)与进程号(PID),使其暂时留在进程列表中。此时进程已不再执行任何代码、不占用 CPU 与内存资源,却仍以进程形式存在,这种已终止运行但退出信息未被父进程回收的状态,即为僵尸状态,处于该状态的进程称为僵尸进程。
用一句话简单理解就是:当子进程已经执行完毕退出或被意外终止,而父进程仍在运行且未及时回收子进程的退出信息时,子进程就会处于僵尸状态。
比如说对于这段代码:


我们创建一个子进程之后,让父子进程单独执行自己的语句,子进程只执行一次printf,但父进程要持续执行printf。当子进程的语句已经执行完毕的时候,父进程依然在执行语句。此时子进程虽然已经结束退出了,但退出信息没有被父进程回收。那么子进程的状态就会变成僵尸状态。
到这里,对于僵尸进程我们只要有四个问题:
- 进程退出了,退出信息是什么?
- 进程退出了,退出信息保存在哪里?
- 检测 Z 状态进程,回收 Z 状态进程,本质是在做什么?
- 具体怎么回收?谁来回收?
其中第四个问题因为涉及到进程控制的问题,所以我们只是简单提一下,后续文章中会讲解。
首先第一个和第二个问题:
进程的退出信息(Exit Status)是一个整数数值,通常包含两部分核心内容:
1. 退出状态码(Exit Code) :进程正常结束时返回的值(如 main 函数的返回值,通常 0 表示成功,非 0 表示错误类型)。
2. 终止原因(Termination Reason):进程是如何结束的。包括:
正常退出 (Normal Exit):如 return 0、exit(1)。
异常退出 (Abnormal Exit):如被信号杀死(Segmentation Fault 段错误、SIGKILL 强制终止等),此时会记录导致退出的信号编号
退出信息是被存储在 当前子进程自己的 task_struct 结构体里,也就是说子进程虽然结束了,但是它的 task_struct 是一直保留的,这也是我们能在状态查看是看到这个子进程的PID的原因。之所以存储 task_struct,这是为了**:** 父进程未来通过 wait() 或 waitpid() 系统调用时,内核会从这里读取数据并复制给父进程。

exit_state就代表退出时的状态,exit_code 就是退出编码。
第三个问题:
本质是在 "读取数据" 与 "释放占位"。
读取数据(读操作) :父进程通过调用 wait()/waitpid(),主动去读取子进程 task_struct 中遗留的退出信息(退出码 / 信号)。这是父子进程间最后的通信仪式,父进程必须确认子进程是 "成功了" 还是 "出错了"。
释放资源(清理操作) :当读取完成后,内核确认不再需要该进程的任何信息,才会彻底释放子进程占用的 PID(进程号) 和 task_struct 结构。
第四个问题:
谁来回收?
主动回收:由父进程 主动调用 wait() 或 waitpid() 函数。
被动 / 继承回收:如果父进程先于子进程退出,子进程会被托管给 init 进程(PID 1) 或系统守护进程,由它们自动循环调用 wait() 来回收所有孤儿进程的僵尸状态。
具体怎么回收?
阻塞等待:父进程调用 wait(),如果子进程还在运行,父进程会进入阻塞状态(S),直到子进程退出。
获取状态:子进程退出后,内核将退出信息填入 wait() 的参数中,父进程读取。
彻底销毁:内核检测到所有进程引用都已解除,释放僵尸进程的资源,使其变为 X 状态并消失。
以上问题回答中的 wait () ,实际上是一个系统调用,叫做等待函数,后续我们会提到,大家暂时只要知道有这么个东西就行。
最后还有一个问题需要大家理解:如果说父进程一直不回收子进程会怎么样?
首先一定会出现的情况就是:子进程的PID也没有被回收,如果说创建的进程太多,PID不够用了,就会导致进程创建出现问题;另外:子进程的task_struct一直没有被释放,会占用内存空间资源,这就是内存泄漏的问题。
那么就有一个衍生的问题:如果一个对应的进程已经结束了,内存泄露的问题还会存在吗?为了让大家能更好的理解,我说一个场景:当你打开一个游戏正在愉快的玩耍的时候,同时你的后台又在挂着网易云音乐听歌,同时你的后台还在下载电影,对于部分手机或者电脑来说,你就会发现你在玩游戏的过程当中非常的卡顿,这就是内存空间不足导致的问题。当你把后台下载电影的软件关掉,音乐软件也关掉,就会发现打游戏会变得无比丝滑顺畅。这就相当于释放内存,清理内存空间的操作。
所以我们可以得到结论:如果一个进程已经结束了,那么该进程导致的内存泄漏问题就会消失。
所以对于我们平时使用的绝大部分软件,除非当我们点击退出,否则这个软件程序就会一直运行,其本质上就是这个软件在进行死循环,属于内存泄漏问题。像这种一直运行的进程,就叫做常驻进程。
3.6 孤儿进程
我们前面提到了,如果子进程运行已经结束或被信号等其他指令杀死,且父进程还没有回收子进程的退出信息,那么子进程的状态就会被判定为僵尸状态。就相当于,父进程还在运行,但子进程先退出了。那如果是父进程先退出了,子进程还在运行,那么会是什么情况呢?
这就涉及到一个进程概念:孤儿进程。
孤儿进程是指在程序运行过程中,父进程先于子进程退出,而子进程仍然继续执行所形成的特殊进程。当父进程正常结束或异常终止后,原本由其管理的子进程失去了原有的父进程,便成为孤儿进程。
我们以这段代码为例:

我们使用这一行指令来控制持续查看进程:


大家会发现,当子进程一直在进行的时候父进程已经结束了,于是在下一次执行程序的时候,子进程的PPID就由原来的5568变成了 1 ,而 1 就代表操作系统的初始进程,即操作系统内核 PID=1的进程。所以此时该子进程就变成了一个孤儿进程,并且交给了 PID = 1 的进程,即操作系统内核托管。
之所以会被回收托管,是 为了保证系统资源能够正常回收,Linux 内核会自动将这类孤儿进程 "过继" 给系统中的 init 进程(PID 为 1)或 systemd 进程托管,由其负责后续的资源回收工作,因此孤儿进程在退出时会被及时清理,不会像僵尸进程那样长期占用系统资源。
同时,我们发现子进程的进程状态由原来的 S+ 变成了 S ,也就是从前台进程变成了后台进程,所以当我们想使用 Ctrl+C 的操作去关闭进程的时候,是关闭不了的,只有通过kill指令才能关闭。这是因为:S+ 中的 + 代表进程属于前台终端进程组,与控制终端关联。当父进程(终端 Shell)退出后,孤儿进程被内核重新托管给 init 进程,并脱离原终端,不再属于前台进程组,因此 + 符号消失,状态变为 S。进程本身仍处于可中断睡眠状态,并未发生本质改变。
4. 进程优先级基本概念
进程优先级是操作系统用于决定多个进程谁先获得 CPU 资源、谁后执行的调度依据,它决定了进程在竞争处理器时的 "重要程度" 和 "排队顺序"。优先级越高的进程,在系统资源紧张时越容易被调度器优先选中运行;优先级越低,则越容易被高优先级进程 "抢占",等待时间更长。
5.查看系统进程
我们前面学习过 ps axj 指令,可以查看到进程的PID和其他的详细信息,现在我们来学习ps -l 指令:

我们先执行了一个程序 peocessTest,然后查看它的进程信息。首先我们来看UID,这代表启动这个程序的用户的名称,因为对于操作系统来说,它会把我们的用户名转化成一个编号以用来区分是谁运行了哪些程序。比如我的用户名 chen ,在操作系统中的编号就是 1001 :

其次,进程信息中的 PRI 和 NI 就代表优先级信息。
6. PRI 和 NI
PRI 是进程的实际优先级(Priority),由操作系统内核动态计算和调度,数值越小表示进程优先级越高,越容易被 CPU 优先执行。
NI 即 nice 值,是用户可以手动设置的优先级修正值,用于影响最终的 PRI。之所以叫 nice ,是因为全称是 nice value,其中的nice的中文意思是:(调度器)偏好 / 友好程度。
nice 越低(越严格):进程的优先级越高,系统会优先照顾它(比如实时任务、关键服务)。
nice 越高(越客气):进程的优先级越低,系统会尽量少占用它的资源(比如后台任务、非关键进程)。
在 Linux 中,nice 值范围通常为 -20 ~ 19,NI 越小,优先级越高;NI 越大,优先级越低。普通进程的 PRI 一般会在默认基础上加上 nice 值得到最终优先级,用户通过调整 NI 可以间接控制进程优先级,普通用户只能调高 NI(降低优先级),只有 root 权限才能降低 NI(提升优先级)。
系统调度器依据 PRI 分配 CPU 时间片,从而保证重要进程更及时地运行。
手动修改 NI 值的方法就是调用 top 命令:

当我们输入top指令的时候,会弹出来当前操作系统内所有正在运行的进程。然后我们再按一下R键,系统会提醒我们要把哪个进程的 NI 值进行修改,接着输入进程的 PID ,然后再输入你想把该进程的优先级改成多少:

比如我们把 NI 改成 10 ,那PRI就会变成 90 。
所以这里优先级的改变有一个公式:PRI(new) = PRI(old) + NI ;其中这里的 PRI(old)指的是进程的初始的PRI。
本文到此结束,感谢各位读者的阅读,如果有讲解的不到位或者错误的地方,欢迎各位读者批评或指正。