前
咱们今天来聊聊 Linux 进程的状态 —— 这是理解系统运行的基础,但其实这些状态就像进程的 “工作模式”:有的在全力运行,有的在等待资源,有的暂时 “待命”。
本文会从实际场景出发,拆解 Linux 中常见的进程状态(比如 R、S、D 等),以及它们背后的内核逻辑,帮你搞懂进程在系统里到底是怎么 “干活” 的。
📚 Linux 入门篇
🔧 Linux 工具篇
⚙️ Linux 进程篇
进程状态是操作系统内核对进程当前活动与执行状态的描述,是内核调度资源的管理依据。
(注:不同操作系统的实际实现会有差异,以下是通用理论状态)

【状态转换逻辑】:
【说明】:
上述是操作系统理论中的全量进程状态,而在 Linux 系统中,_运行、阻塞、挂起_是最 核心的三种状态(实际会通过更具体的状态标识,如 R、S、D 等,来对应这些逻辑状态)。接下来,我们就基于进程状态的通用理论模型,来详细解析这三种状态的具体定义与特点
进程的运行状态,是衡量它在 CPU 调度环节活跃程度的核心状态。 当进程处于这一状态时,会呈现两种具体情形:
运行状态(CPU 执行中)是进程 “正在使用 CPU 资源” 的状态,而就绪态(队列待命)是进程 “具备执行条件、等待获取 CPU 资源” 的状态,二者共同构成了进程活跃性的核心体现。
进程被调度到 CPU 上运行后,不会一直执行到完毕(比如
while(1);这类无限循环代码,也不会独占 CPU),核心原因是 “时间片” 机制:
阻塞状态:进程因等待某类资源 / 事件(如 I/O 操作、信号、锁)而无法继续执行的状态:
简言之,阻塞状态是进程 “不具备执行条件(缺资源)、无法使用 CPU” 的状态,是进程非活跃性的典型体现。

阻塞挂起是挂起状态的核心子状态之一,指进程因等待资源 / 事件处于阻塞态时,又因系统内存不足,其代码和数据被换出到外存(如磁盘)的状态:

swap 分区是磁盘上的存储空间,在阻塞挂起场景中,它会暂存进程的代码和数据(此时进程 PCB 仍留在阻塞队列),是内存不足时实现进程挂起与恢复的关键载体。
就绪挂起是挂起状态的子状态,指进程本身已具备执行条件(符合就绪态要求),但因系统内存不足(且阻塞进程已被优先挂起),其代码和数据被换出到外存(如 swap 分区),PCB 仍保留在就绪队列的状态:
下面的状态是在Linux内核源代码中定义的:
/*
* 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:僵尸状态 */
};
定义:进程已具备执行条件,正在等待 CPU 调度,或正在 CPU 上执行
【测试1】:


启动这个程序后,发现它的进程状态一直是阻塞类状态(对应 Linux 中的 S 态)。这是为什么呢? 因为我们一直在用
printf函数(向显示器输出内容),进程会进入显示器的等待队列,等待 IO 资源就绪。这个操作会让进程处于 “等待 IO 资源” 的阻塞状态,而 CPU 处理速度极快,进程实际占用 CPU 的时间极短,所以大多数时间都处于阻塞状态,呈现出持续阻塞的状态。
sleep定时器到期),这个状态下不会占用 CPU 资源。Ctrl+C发送中断信号)都会直接作用于它,能直接中断进程;Ctrl+Z将其暂停并切换到后台(切换后进程状态会变成S且不带+),才能在同一个终端里执行其他命令。Ctrl+C)不会影响它,无法直接用Ctrl+C中断;fg命令将其切回前台,或用kill命令发送信号中断;&(例如./test &),让进程一开始就在后台运行。【测试2】:


去掉
printf后,进程状态始终是R+,这是因为代码里没有了等待 IO 的操作,进程会一直占用 CPU 执行无限循环,所以持续处于运行态(R);同时它是前台进程(+),所以状态显示为R+。
定义: 意味着进程在 等待事件完成 (这里的睡眠有时候也叫做 可中断睡眠 (interruptible sleep ))


运行上述程序,你会发现该进程几乎一直处于休眠状态 —— 这是因为代码里循环执行的
printf和sleep操作,都会让进程进入阻塞(休眠)状态:
printf需要向终端输出内容,进程会阻塞等待 IO 资源就绪;sleep会让进程主动休眠指定时长,期间同样处于阻塞状态。这两种操作都会让进程暂时让出 CPU,所以它大部分时间都处于休眠(阻塞)状态。

bash 始终处于 S 状态,是因为它在等待用户输入命令,此时处于 IO 阻塞的休眠状态。
D 状态(磁盘休眠状态,disk sleep)是 Linux 进程的一种阻塞状态,指进程因等待磁盘 IO 操作完成而进入的休眠状态。
Ctrl+C),只能等磁盘操作完成后才会被内核唤醒。进程凑到磁盘跟前,递过一摞数据:“劳烦帮存 1000M,存好存坏都跟我说声哈!” 磁盘擦了擦磁头应下,进程闲着没事,往旁边一躺就睡了 —— 这是能被叫醒的 “可中断休眠”(S 态)。 存到一半,操作系统攥着内存统计单急慌慌跑过来,瞅见睡着的进程就皱了眉:“内存都快挤爆了,腾地方!” 话没说完,直接把进程 “清” 没了。 这边磁盘吭哧吭哧存到 900M,突然弹出 “空间不足” 的提示 —— 存失败了。它扭头就喊进程报信,可喊破喉咙,进程早没了踪影。1000M 数据没处交代,只能跟着 “蒸发” 了。
这数据要是银行一天的转账记录,损失可就大了!行长把进程、磁盘、系统都叫到办公室:磁盘先摆手:“我按进程说的存了,最后找不着它,我有啥办法?”进程委屈巴巴:“我睡得好好的,莫名其妙就没了,我才是冤大头!”系统也喊冤:“内存都不够撑到下一秒了,不杀它系统崩了,损失更大!”
行长听完叹了口气,拍板定了规矩:“以后进程等磁盘干活时,就睡‘叫不醒的觉’—— 叫‘不可中断休眠’(D 态)。这时候谁都不能动它,必须等磁盘把活儿干完、把信儿传给它,才能接着来!”
打这儿起,进程再等磁盘这类关键设备干活时,就会钻到 D 态的 “保护壳” 里 —— 既能安心等结果,也能护着数据不丢啦。
不可中断睡眠(D 态)是系统里比较 “特殊” 的进程状态:一旦进程进入这个状态,通常只能等它完成磁盘 IO 后自行唤醒,哪怕重启系统都未必能终止它,极端情况下甚至得靠断电才能强制结束。 而且这个状态在正常系统里很少见 —— 要是你的进程长时间(比如持续几秒)卡在 D 态,基本是计算机出问题的信号,大概率是磁盘老化(比如用了 5 年以上)导致 IO 操作异常,才让进程陷在这个 “醒不来” 的状态里。
维度 | 浅睡眠(S 态) | 深度睡眠(D 态) |
|---|---|---|
触发场景 | 等待 IO、定时器等(非关键操作) | 等待磁盘等关键 IO 操作 |
信号响应 | 能被普通信号(如 Ctrl+C)唤醒 | 不响应普通信号,只能等 IO 完成 |
系统操作 | 可被系统杀死(如内存不足时) | 系统无法杀死,必须等 IO 结束 |
作用 | 灵活等待,可中断 | 保护关键数据,避免操作中断 |
T状态(停止状态,stopped)是 Linux 进程的一种暂停状态,指进程因接收到暂停信号(如 Ctrl+Z)** 而暂时停止执行,但并未终止。 核心特点:
场景 1:gdb 调试暂停
在 gdb 调试中,通过设置断点让进程进入 T 态:
break 行号/函数名(比如break main.c:10)设置断点,当程序运行到断点处时,会自动暂停执行,进入 T 态;continue命令,进程会从断点处恢复运行。场景 2:信号暂停(如 Ctrl+Z)
用户可通过kill命令向进程发送暂停信号(SIGSTOP),让进程进入 T 态:
kill -SIGSTOP 进程ID,进程会暂停运行,内存中的代码、数据等资源保持不释放;kill -SIGCONT 进程ID(发送继续信号),进程会从暂停处恢复执行简单说:T 状态是进程 “被暂停但没结束” 的状态,随时可以恢复运行。

此时进程处于S 态(可中断睡眠),因为它在持续等待 IO 设备输出。

我用
kill -19(对应SIGSTOP信号)暂停了进程,此时进程进入T 态(停止状态)。

使用
kill -18(对应SIGCONT信号)让进程恢复运行,它回到了之前的S 态(可中断睡眠),同时不再保持前台运行状态。

此时 gdb 进程处于S 态(可中断睡眠),因为它在等待用户输入调试指令。

通过
b main打主函数断点、r运行程序后,程序会在断点处暂停 —— 此时被调试的./test 进程进入 t 态(跟踪停止状态),gdb 进程仍保持 S 态(等待后续调试操作)。 这里的t态是停止状态的细分场景,它专门对应 “进程被调试器(如 gdb)跟踪暂停” 的情况,和普通的 T 态(比如用 Ctrl+Z 挂起的停止状态)在触发场景上有所区别。
gdb)跟踪而暂停执行的状态。1. 程序运行到**断点**处(`break` 命令)。
2. 使用**单步执行**命令(`next`, `step`)后。
3. 在程序运行时按下 `Ctrl + C` 主动中断。- 在调试器中使用 `continue`, `next`, `step` 等命令让程序继续执行。kill -STOP)或调试工具(gdb)主动暂停,需要人为执行恢复操作(如kill -CONT)才会继续。X(dead)是进程彻底终止前的瞬时状态,你不会在
ps/top里看到它。
下午三点,你坐在咖啡馆靠窗的位置,刚喝完最后一口拿铁,起身拿起包准备离开。 就在你推门的瞬间,邻桌的客人突然捂住胸口,趴在桌上没了动静。你吓了一跳,立刻喊来店员,同时拨了急救电话。 店员的第一反应不是直接把人抬走 —— 而是先守在桌边,不让其他客人碰桌上的物品(手机、药瓶),同时等医生和警察到现场。毕竟得先搞清楚:是突发疾病?还是有其他情况?这些 “现场信息” 得留着等专业人员确认。
这时候,这位客人虽然已经没了意识,但还在原地等着 “后续处理”—— 就像进程的僵尸状态:自身运行已经停止,但 “父进程(系统)” 还没回收它的状态信息,不能直接清理。
等医生做完初步检查、警察记录完现场信息,工作人员才把人抬上救护车 —— 这个 “信息交接完、彻底离场” 的瞬间,就是进程的死亡状态(X):所有收尾工作完成,彻底从系统里消失。
总而言之:
核心原因是父进程需要获取子进程的 “退出状态”: 进程终止时,会先将自身的代码、数据、堆栈等内存资源彻底清理,但会把 “运行结果”(正常结束 / 异常崩溃)存在 PCB(进程控制块)中;而父进程需要通过
wait()/waitpid()等系统调用读取这个状态,才能判断子进程的工作是否完成。
这就像 “人意外去世后,身体(对应进程的代码 / 数据)会被处理,但现场的证据(对应 PCB 里的退出状态)会被保留”—— 要等警方(对应父进程)来调查取证(读取退出状态),确认死亡原因后,才会彻底收尾(清理 PCB)。
若内核直接清理 PCB,父进程就会丢失子进程的退出信息,相当于 “子进程的工作结果没完成交接”。因此僵尸态是子进程留着 PCB 等父进程 “查收结果” 的临时状态—— 父进程读取状态后,内核才会彻底清理 PCB。


进程退出时,若父进程未主动回收子进程信息(未调用
wait()/waitpid()),子进程会持续处于僵尸态(Z 状态),其对应的task_struct结构体(进程控制块)无法被内核释放,仅保留退出状态等必要信息,直到父进程处理或自身终止后由 1 号进程接管清理。
父进程的父进程通常是终端 Shell(比如 bash)。当我们运行程序时,父进程由 Shell 创建并作为其子进程存在。一旦父进程执行结束或被终止,Shell 会主动调用
wait()类函数回收父进程的退出状态和 PCB 资源,不会让父进程残留为僵尸进程。此外,即使父进程意外退出,系统的 1 号进程(init/systemd)也会接管并清理其资源,因此父进程不会像未被回收的子进程那样成为僵尸。
僵尸进程的定义 处于僵尸状态(Z 状态)的子进程,即子进程已退出,但父进程未调用
wait()/waitpid()读取其退出状态时的进程。
僵尸进程的 “内存泄漏” 本质
僵尸进程不会造成常规程序的内存泄漏(如动态分配内存未释放导致堆内存占用),但从系统资源管理角度,会引发类似内存泄漏的不良影响:
task_struct**结构体中)会被保留,用于存储子进程的退出状态(如正常结束、信号终止等);不同进程的影响差异
系统不主动回收 PCB 的原因
task_struct中保存了子进程的退出状态关键信息,操作系统需要将这些信息完整交付给父进程;因此必须维护PCB直到父进程主动调用wait()获取信息 —— 回收僵尸进程的责任由开发者承担。
当父进程先于子进程退出时,子进程会变成孤儿进程(原父进程已消失,子进程 “无父”)。 此时,操作系统会将这个孤儿进程的父进程重新设置为 1 号进程(
init/systemd,系统初始化进程),由 1 号进程 “领养” 它。 当孤儿进程后续退出并进入僵尸状态(Z 状态)时,1 号进程会主动调用wait()类函数回收它的退出状态和 PCB 资源,避免其残留为僵尸进程。 简单说:父进程提前退 → 子进程变孤儿 → 1 号进程领养 → 子进程退出后由 1 号进程回收,不会成为僵尸进程。


从图中可以看出父进程结束后,子进程的PPID就变成1了,这也刚好验证了我们上面说的没问题