一、先看内核源码怎么写的
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 */
};
📌 注意注释里的两个细节,很多人会忽略:
- 这是一张 "位图" ,值是 0/1/2/4/8/16/32 ------ 是 2 的幂,说明状态可以按位组合测试;
- "running" 是 0。这意味着"正在运行"是默认值,其他状态都是"因为要等待某事而额外加上去的位"。
二、七个状态逐个讲
R ------ running(运行)
并非意味着进程一定正在 CPU 上跑,它表明进程要么正在运行,要么在运行队列里排队。
这是最容易误解的一个:ps 里看到 R,不代表它此刻真的在占 CPU,也可能只是"ready",等着被调度。
📌 反过来更值得警惕:一个 CPU 密集的程序,如果你很少看到 R,说明它在排队 (系统负载高)。ps aux 里的 %CPU 也印证这一点。
S ------ sleeping(可中断睡眠)
进程在等待某个事件完成(比如等
sleep时间到、等用户输入、等网络数据)。
叫可中断睡眠(interruptible sleep) ,意思是它虽然睡着,但信号可以把它唤醒 (比如你 Ctrl+C 或 kill 它)。
📌 绝大多数"正常待着"的进程都在 S。上一篇那个 sleep(1) 死循环程序就是 S:一个周期里 99.99% 的时间在睡。
D ------ disk sleep(不可中断睡眠)
也叫不可中断睡眠(uninterruptible sleep) ,通常是在等待 IO 结束。
关键差别:它不响应信号 (包括 kill -9)。你只能用 kill -9 试,但通常无效------因为进程卡在内核里等硬件返回,只有等 IO 真的完成或超时,它才会醒。
📌 运维里经典的"D 状态进程杀不掉 "就是这么来的。常见于 NFS 挂死、磁盘故障。这时候的解法不是杀进程,而是修存储。
T ------ stopped(停止)
可以通过发送
SIGSTOP信号让进程进入T;再发SIGCONT让它继续运行。
典型场景:
kill -STOP <pid> # 暂停
kill -CONT <pid> # 继续
你在终端里按 Ctrl+Z 是把前台任务挂起(本质也是 SIGTSTP → 停止),然后用 fg / bg 恢复或转后台。
t ------ tracing stop(被调试器追踪而停止)
被
ptrace之类追踪时停住(比如你在 gdb 里打断点,进程就停在这里)。
小写 t 和大写 T 的区别就在"是不是被追踪导致的"。
X ------ dead(死亡)
这是一个返回状态,你不会在任务列表里看到它。
进程彻底销毁的瞬间状态,转瞬即逝,ps 抓不到。
Z ------ zombie(僵尸)
子进程退出,但父进程没有读取它的退出状态。
这就是本篇的主角,下面细讲。
三、怎么查看状态
ps aux # 常用快照
ps axj # 带进程组、会话、父进程信息
参数含义(课件原话):
| 参数 | 含义 |
|---|---|
a |
显示一个终端所有的进程,包括其他用户的进程 |
x |
显示没有控制终端的进程,例如后台运行的守护进程 |
j |
显示进程归属的进程组 ID、会话 ID、父进程 ID,以及与作业控制相关的信息 |
u |
以用户为中心的格式显示,提供用户、CPU 和内存使用情况等详细信息 |
另外:
top # 动态看
kill -9 <pid> # 强杀(对 D 和 Z 通常无效)
四、僵尸进程(Z)
4.1 它是怎么产生的
规则一句话:
只要子进程退出了,父进程还在运行,但父进程没有读取子进程的退出状态,子进程就进入 Z 状态。
为什么内核不直接把它清掉?因为子进程的退出状态必须被维持下去------它要告诉父进程:"你交给我的任务,我办得怎么样了(成功/失败/为什么失败)。"
这个退出状态就保存在子进程的 task_struct 里。父进程不来读,内核就不能删。
4.2 复现一个僵尸
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if (id < 0) {
perror("fork");
return 1;
} else if (id > 0) { // 父进程
printf("parent[%d] is sleeping...\n", getpid());
sleep(30); // 关键:父进程不 wait,直接睡 30 秒
} else { // 子进程
printf("child[%d] is begin Z...\n", getpid());
sleep(5);
exit(EXIT_SUCCESS); // 5 秒后子进程退出,父进程却没读它的状态
}
return 0;
}
跑起来之后,在另一个终端:
ps aux | grep -w Z # 5 秒后能看到一条 <defunct> 的记录
输出里的 Z 和 <defunct> 就是僵尸的标记。
4.3 僵尸为什么"杀不死"
进程一旦变成僵尸状态,就刀枪不入,
kill -9也无能为力------因为你没有办法杀死一个已经死去的进程。
它已经退出了,只是档案还没被销 。真正能清掉它的,只有父进程调用 wait/waitpid 把状态读走,或者父进程自己死掉(然后由 init 接手回收)。
4.4 危害
沿着课件那串追问走一遍:
- 退出状态要用数据维护,属于进程基本信息,保存在
task_struct(PCB)里; - 所以 Z 状态不退,PCB 就要一直维护;
- 一个父进程创建了很多子进程,就是不回收 → 一堆
task_struct永远占着内存; - 数据结构对象本身就要占内存(想想 C 里定义一个结构体变量,内核里也是同样的道理);
- 结果就是------内存泄漏。
📌 危害不在"CPU 占用"(僵尸不耗 CPU),而在"PID 和内核内存被占死 "。进程数有上限,僵尸攒满了就 fork 失败。
4.5 怎么避免
一句话:父进程必须回收子进程 。手段就是 wait / waitpid(第 7 篇详讲),或者让父进程先退出(那样僵尸就变成孤儿,被 init 收走)。
五、孤儿进程
5.1 定义
父进程先退出,子进程后退出,这个子进程就成了"孤儿进程"。
5.2 谁管它
孤儿进程会被 1 号进程(init / systemd) 领养,由它负责回收。
所以孤儿进程不会变成僵尸挂着没人管------它被"过继"了,退出时 init 会替它收尸。
5.3 复现
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if (id < 0) {
perror("fork");
return 1;
} else if (id == 0) { // 子进程
printf("I am child, pid : %d\n", getpid());
sleep(10); // 长命的孩子
} else { // 父进程
printf("I am parent, pid: %d\n", getpid());
sleep(3);
exit(0); // 3 秒后爸爸先走
}
return 0;
}
3 秒后父进程退出,子进程(10 秒后才退)就成了孤儿:
ps -o pid,ppid,stat,cmd -p <child_pid>
# PPID 会变成 1
📌 这也是"守护进程(daemon)要 double fork"的由来:故意让父进程退出,让子进程被 init 领养,从而脱离终端控制(这样关掉终端它也不受影响)。
六、整张状态转换图
fork 创建
│
▼
┌───────┐ 被调度上 CPU ┌───────┐
│ R │ ◄────────────► │ R(run)│
└───┬───┘ └───────┘
等事件/等 IO │
┌──────────────┼──────────────┐
▼ ▼ ▼
┌───────┐ ┌───────┐ ┌───────┐
│ S │ │ D │ │ T │ ← SIGSTOP / Ctrl+Z
│可中断 │ │不可中断│ └───┬───┘
└───┬───┘ └───┬───┘ │ SIGCONT
│ 事件就绪 │ IO 完成 │
└──────┬───────┘ │
▼ │
回到 R ◄────────────────────┘
退出(return / exit / _exit / 信号)
│
▼
┌──────────┐ 父进程还没 wait
│ Z │ ────────────────┐
│ 僵尸 │ │
└────┬─────┘ ▼
│ 父进程 wait 父进程先走了
▼ │
┌──────────┐ ▼
│ X │ 被 init 领养(孤儿)
│ 彻底销毁 │ 最终由 init wait
└──────────┘
七、易错点速记
| 误解 | 正确理解 |
|---|---|
ps 里是 R 就一定在跑 CPU |
也可能在就绪队列里排队 |
| 僵尸进程占用 CPU | 不占 CPU,占的是 PID + 内核内存 |
kill -9 能杀掉一切 |
杀不掉 D(不可中断睡眠)和 Z(已死) |
| 孤儿进程会变成僵尸没人管 | 会被 1 号 init/systemd 领养并回收 |
| 子进程先退出 = 父进程先退出 | 父子谁先退都行,顺序由调度和代码决定 |
本篇小结
- 状态是位图:R=0,其他都是"因为等待而加上去的位"。
R= 运行或 就绪;S= 可中断睡眠;D= 不可中断睡眠(等 IO,杀不掉);T/t= 停止/被追踪停止;X= 转瞬即逝的死亡;Z= 僵尸。- 僵尸 :子进程退出 + 父进程没
wait→ 状态和 PCB 留在进程表里 → 内存/PID 泄漏;kill -9无效。 - 孤儿:父进程先退出 → 被 init/systemd 领养并回收,所以不会泄漏。
- 避免僵尸的唯一正道:父进程调用
wait/waitpid回收。