linux系统复习:僵尸进程与孤儿进程

一、先看内核源码怎么写的

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 */
};

📌 注意注释里的两个细节,很多人会忽略:

  1. 这是一张 "位图" ,值是 0/1/2/4/8/16/32 ------ 是 2 的幂,说明状态可以按位组合测试;
  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 回收。
相关推荐
专注仿真1 小时前
ThinkPHP6 API接口开发实例:从零搭建RESTful风格接口
后端·php·restful
殷忆枫1 小时前
告别依赖地狱!在 Ubuntu 上用 Docker 极速部署 Home Assistant 与 MQTT,并接入 Siri 与嵌入式 C 终端
linux·ubuntu·docker
第404号工位1 小时前
手机到底有没有偷听我们?我抓包了 72 小时,抓到了真凶
linux·人工智能·架构
老李IT笔记2 小时前
连不上不一定是网不好:新系统不认的服务端配置卡在哪一档
运维·服务器·网络
字节跳动开源2 小时前
ByteDance 2026 上半年 Linux 内核贡献回顾
linux·开源
小此方2 小时前
Linux网络(二十八):从NAT转换到正反向代理:图解内网通信、NAPT端口映射
linux·运维·网络
刚入门的大一新生2 小时前
Linux-进程池
linux·运维·服务器
数据库小学妹2 小时前
数据库运维五级成熟度:监控、备份、高可用怎么串起来
运维·数据库·监控
AliCloudROS3 小时前
从 1 万到 20 万:OOS Loop 任务规模提升 20 倍的攻坚记录
运维·性能优化