﻿# waitpid 内核内部实现：从系统调用到 task_struct 释放

## 概述

`waitpid()` 是 POSIX 进程管理中最重要的系统调用之一。用户态的 `waitpid()` / `wait()` / `waitid()` 最终都在内核汇聚为同一条路径。本文逐层拆解这个系统调用在内核中的完整处理逻辑——从系统调用入口一直到 `task_struct` 被 `free_task()` 彻底释放。

> 前置阅读：[task-struct.md](/concepts/process/task-struct.md)（`task_struct` 结构）→ [process-group-and-child-reap.md](/concepts/process/process-group-and-child-reap.md)（进程组概念与六种回收策略）

---

## 零、先建立全局认识

`waitpid()` 的本质：父进程从内核的"死亡名单"中**认领**一个已退出的子进程，读取退出码，然后让内核释放 `task_struct`。如果子进程还没死，父进程就**睡在等待队列上**，等子进程死后被唤醒。

### 关键角色全景图

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<parent>> #C8E6C9
  BorderColor<<parent>>   #388E3C
  BackgroundColor<<child>> #FFCCBC
  BorderColor<<child>>   #E64A19
  BackgroundColor<<zombie>> #E1BEE7
  BorderColor<<zombie>>  #7B1FA2
  BackgroundColor<<kern>> #BBDEFB
  BorderColor<<kern>>    #1976D2
  BackgroundColor<<queue>> #FFF9C4
  BorderColor<<queue>>   #F9A825
}
' 父进程
rectangle "父进程\nint status;\nwaitpid(child_pid, &status, 0)" <<parent>> as PARENT
' 内核核心
rectangle "kernel_wait4()\n→ do_wait() ★ 核心循环\n  1. add_wait_queue(等待队列)\n  2. do_wait_thread() 扫描 children\n  3. 没僵尸 → schedule() 睡眠\n  4. 被唤醒 → goto 重扫\n  5. 有僵尸 → wait_task_zombie()" <<kern>> as DO_WAIT
rectangle "wait_task_zombie()\n  • 提取 exit_code\n  • 提取 rusage (CPU/内存统计)\n  • 调用 release_task()" <<kern>> as WTZ
rectangle "release_task()\n  • __exit_signal()  清理信号\n  • __unhash_process() 从 PID 表中移除\n  • put_task_struct() → free_task()\n  ★ task_struct 彻底释放" <<kern>> as RELEASE
' 等待队列
rectangle "wait_chldexit\n等待队列\n(task_struct 内嵌)\n父进程睡眠在这里" <<queue>> as WQ
' 子进程路径
rectangle "子进程 A\ndo_exit()\n  → exit_state = EXIT_ZOMBIE\n  → do_notify_parent(SIGCHLD)\n  → 加入父亲的 children 链表" <<child>> as CHILD_A
rectangle "子进程 B\n(僵尸)\n只有 task_struct 残留\n等待父亲回收" <<zombie>> as CHILD_B_Z
' children 链表
rectangle "父亲的 children 链表\n(task_struct.children)\n遍历查找僵尸子进程" <<queue>> as CHILDREN
' 关系
PARENT -down-> DO_WAIT : waitpid()
DO_WAIT -down-> CHILDREN : do_wait_thread()\n遍历 children 链表
DO_WAIT -left-> WQ : 没有僵尸 →\nadd_wait_queue() 入队\nschedule() 睡眠
CHILD_A -down-> CHILDREN : do_notify_parent()\n子进程切入僵尸态\n唤醒父进程的 wait_chldexit
CHILDREN -up-> WQ : __wake_up_parent()\n唤醒睡眠的父进程
DO_WAIT -down-> WTZ : 找到僵尸\nwait_task_zombie()
WTZ -down-> RELEASE : 释放 task_struct
note right of RELEASE : task_struct 占 ≈5-10KB\n释放后 PID 可复用
@enduml
```

**一句话**：父进程扫描 `children` 链表找僵尸 → 没有就睡在 `wait_chldexit` 上 → 子进程退出时唤醒父进程 → 父进程提取退出码后释放 `task_struct`。

### 核心问题速览

| 问题 | 回答 | 详见 |
|------|------|------|
| 父进程睡在哪里？ | `task_struct` 内嵌的 `wait_chldexit` 等待队列 | §三 |
| 谁唤醒它？ | 子进程 `do_exit() → do_notify_parent() → __wake_up_parent()` | §八 |
| 僵尸进程残留什么？ | `task_struct`(≈5-10KB) + `exit_code` + `rusage`，PID 不可复用 | §四/§五 |
| WNOHANG 和非阻塞有什么区别？ | 无僵尸 → 立即返回 0（不睡眠）；阻塞 → 睡眠直到有僵尸 | §三 |
| 为什么必须循环 wait？ | 标准信号不排队：多个子进程退出只发一个 SIGCHLD | §九 |
| `release_task` 做什么？ | `__exit_signal → __unhash_process → put_task_struct → free_task` | §五 |

---

## 一、调用链总览

```bash
用户态:  waitpid(pid, &status, options)

         ├─ glibc: syscall(SYS_waitpid, ...)    [或 wait4]

         ▼
内核态:  SYSCALL_DEFINE3(waitpid, pid_t, pid, int __user *, stat_addr, int, options)
         │                       [kernel/exit.c]
         ├─ SYSCALL_DEFINE4(wait4, ...)   // 兼容入口

         └─ kernel_wait4(pid, stat_addr, options, NULL)
              │                  [kernel/exit.c]
              ├─ 构造 struct wait_opts (wnotify=NULL, wo_type=P_PID, ...)

              └─ do_wait(wo)
                   │              [kernel/exit.c]
                   ├─ do_wait_thread(current, wo)
                   │   │          [kernel/exit.c]
                   │   ├─ 遍历 current 的 children/sibling 链表
                   │   │
                   │   └─ wait_consider_task(current, 0, child, wo)
                   │        │     [kernel/exit.c]
                   │        ├─ wait_task_zombie(wo, p)
                   │        ├─ wait_task_stopped(wo, ptrace, p)
                   │        └─ wait_task_continued(wo, p)

                   └─ [无匹配子进程]
                        └─ schedule()  // 阻塞在 wait_chldexit 等待队列
                             ▲
                             │ 被 do_notify_parent() 唤醒

子进程退出:  do_exit() → exit_notify() → do_notify_parent()

                                              ├─ __wake_up_parent(tsk, tsk->parent)
                                              │   → 唤醒父进程 do_wait 中的 schedule()

                                              └─ 父进程从 schedule() 返回，重新扫描 children
                                                  → wait_consider_task() 命中了！
                                                  → wait_task_zombie() → release_task()
```

---

## 二、系统调用入口：从用户态到内核态

### 2.1 三个用户态入口

| glibc 函数 | 系统调用号 | 内核入口 | 差异 |
|------------|-----------|---------|------|
| `wait(&status)` | `SYS_wait4` | `SYSCALL_DEFINE4(wait4, WAIT_ANY, stat_addr, 0, NULL)` | 等待任意子进程，阻塞 |
| `waitpid(pid, &status, options)` | `SYS_waitpid` | `SYSCALL_DEFINE3(waitpid, pid, stat_addr, options)` | 可指定 pid 和选项 |
| `waitid(idtype, id, &info, options)` | `SYS_waitid` | `SYSCALL_DEFINE5(waitid, ...)` | 更精细的控制 |

`waitpid()` 是 `wait4()` 的薄包装，而 `wait4()` 最终调用 `kernel_wait4()`：

```c
// kernel/exit.c
SYSCALL_DEFINE3(waitpid, pid_t, pid, int __user *, stat_addr, int, options)
{
    return kernel_wait4(pid, stat_addr, options, NULL);
}
// kernel/exit.c
SYSCALL_DEFINE4(wait4, pid_t, pid, int __user *, stat_addr,
                int, options, struct rusage __user *, ru)
{
    return kernel_wait4(pid, stat_addr, options, ru);
}
```

### 2.2 pid 参数的语义映射

`kernel_wait4()` 首先把用户态的 `pid` 参数转换为内部查找结构 `wait_opts`：

```c
// kernel/exit.c: kernel_wait4()
struct wait_opts wo = {
    .wo_type = P_PID,         // 默认按 PID 匹配
    .wo_pid  = pid,
    .wo_flags = options,
    .wo_stat  = stat_addr,
    .wo_rusage = ru,
};
```

`do_wait()` 中通过 `eligible_child()` 进行匹配判断：

| 用户传的 pid | 匹配逻辑 | 说明 |
|-------------|---------|------|
| `pid > 0` | 精确匹配 PID == pid 的子进程 | 等待指定的那个孩子 |
| `pid == 0` | 匹配与调用者同进程组（PGID）的任意子进程 | 等待同组的任意孩子 |
| `pid == -1` | 匹配任意子进程 | 相当于 `wait()`，等待任意孩子 |
| `pid < -1` | 匹配进程组 ID == \|pid\| 的任意子进程 | 等待指定进程组的任意孩子 |

---

## 三、核心调度循环：`do_wait()` 的内部实现

`do_wait()` 是 wait 族的真正核心。它是一个 **阻塞循环**：扫描子进程链表 → 找到就回收 → 找不到就睡眠等待。

### 3.1 伪代码结构

```c
// kernel/exit.c: do_wait()
static long do_wait(struct wait_opts *wo)
{
    struct task_struct *tsk = current;
    DECLARE_WAITQUEUE(wait, tsk);  // 准备一个等待队列项
    add_wait_queue(&tsk->signal->wait_chldexit, &wait);
    //     ↑ 把自己挂到 wait_chldexit 等待队列上
    //       所有调用 wait 的线程都挂在这里
repeat:
    // ===== 第一轮扫描 =====
    // 扫描方式取决于 options 的 __WNOTHREAD 标志：
    //   - 默认（未设置）：do_wait_thread(current, wo)
    //       遍历 current 所在的**线程组**的所有子进程
    //   - 设置了 __WNOTHREAD：do_wait_thread(tsk, wo)
    //       只遍历**当前线程**的子进程（clone 出的孩子）
    if (wo->wo_flags & __WNOTHREAD) {
        retval = do_wait_thread(tsk, wo);    // 仅本线程的孩子
    } else {
        thread_group_cpuset_allow = false;
        retval = do_wait_thread(current, wo); // 线程组的孩子
    }
    if (retval)
        goto end;  // 找到了！回收完成，退出
    // ===== 第一轮没找到 =====
    // 检查是否有活着的子进程（存在但状态未合适），决定是否继续等
    retval = ptrace_do_wait(...);  // ptrace 的特殊处理
    if (retval)
        goto end;
    // ===== 阻塞等待 =====
    if (wo->wo_flags & WNOHANG) {
        retval = 0;         // ← WNOHANG：不阻塞，立即返回 0
        goto end;
    }
    // 设置当前状态为 TASK_INTERRUPTIBLE，然后让出 CPU
    schedule();  // ← 父进程在这里睡眠
    //         ↑
    //   子进程退出时会调用 do_notify_parent()
    //     → __wake_up_parent() 唤醒这里
    goto repeat;  // 被唤醒后重新扫描
end:
    remove_wait_queue(&tsk->signal->wait_chldexit, &wait);
    return retval;
}
```

### 3.2 `wait_chldexit` 等待队列

这是连接父进程 wait 和子进程 exit 的**枢纽**：

```bash
父进程 (do_wait)                   子进程 (do_exit)
     │                                  │
     ├─ add_wait_queue(                │
     │   &current->signal->            │
     │   wait_chldexit, &wait)         │
     │                                  │
     ├─ retval = do_wait_thread()      │ 子进程还在跑...
     │   → 返回 0 (没找到)              │
     │                                  │
     ├─ schedule()  ← 睡眠在此          │
     │   State: TASK_INTERRUPTIBLE     │
     │                                  ├─ do_exit()
     │                                  │   └─ exit_notify()
     │                                  │       └─ do_notify_parent()
     │                                  │           └─ __wake_up_parent(
     │  ←── 被唤醒！── ←── ←── ←── ←── │               tsk, tsk->parent)
     │                                  │
     ├─ goto repeat                     │
     ├─ wait_consider_task() 命中！     │
     └─ wait_task_zombie() → 回收       │
```

**关键数据结构**：

```c
struct signal_struct {
    // ...
    wait_queue_head_t   wait_chldexit;   // ← 等待子进程退出的线程排队这里
    // ...
};
// wait_queue_head_t 底层是一个自旋锁保护的双向链表
// 每个调用 waitpid 并睡眠的线程都是一个 wait_queue_entry
```

---

## 四、逐子进程扫描：`do_wait_thread()` → `wait_consider_task()`

### 4.1 `do_wait_thread()` —— 遍历子进程链表

```c
// kernel/exit.c: do_wait_thread()
static int do_wait_thread(struct task_struct *tsk, struct wait_opts *wo)
{
    struct task_struct *p;
    // 遍历 tsk 的直接 children
    // children 是双向链表，通过 sibling 串起所有兄弟
    list_for_each_entry(p, &tsk->children, sibling) {
        int ret = wait_consider_task(wo, 0, p);
        if (ret)
            return ret;
    }
    return 0;  // 没有找到可回收的子进程
}
```

**`children` 链表结构**：

```bash
父进程 task_struct
├── struct list_head children
│   │
│   ├── child1 task_struct
│   │   └── sibling → child2
│   │
│   ├── child2 task_struct
│   │   └── sibling → child3
│   │
│   └── child3 task_struct (尾)

└── (每个 child 的 real_parent 指向父进程)
```

### 4.2 `wait_consider_task()` —— 逐个检查孩子

```c
// kernel/exit.c: wait_consider_task()
static int wait_consider_task(struct wait_opts *wo, int ptrace,
                              struct task_struct *p)
{
    // 1. 先调用 eligible_child() 检查是否匹配
    int ret = eligible_child(wo, ptrace, p);
    if (!ret)
        return 0;  // 不匹配，跳过
    // 2. 根据子进程的 exit_state 走不同分支
    if (unlikely(p->exit_state == EXIT_DEAD))
        return 0;  // 已经被回收了，跳过
    if (p->exit_state == EXIT_ZOMBIE)
        return wait_task_zombie(wo, p);    // ← 僵尸：回收！
    if (p->exit_state == EXIT_TRACE)
        ;  // ptrace 场景...
    // 3. 子进程还活着（TASK_RUNNING / TASK_STOPPED）
    //    根据 options 决定是否报告停止/继续状态
    if (p->state == TASK_STOPPED && (wo->wo_flags & WUNTRACED))
        return wait_task_stopped(wo, ptrace, p);   // 报告停止
    if (p->state == TASK_STOPPED && ...)
        // 子进程停止但没设置 WUNTRACED，视为尚未退出
        return 0;
    // 子进程还活着且不满足任何报告条件 → 不回收，不阻塞
    return 0;
}
```

**判断流程**：

```bash
对每个 child:

  ├─ eligible_child() 匹配合适？
  │   └─ NO  → skip

  ├─ exit_state == EXIT_DEAD?
  │   └─ YES → skip (已经释放了)

  ├─ exit_state == EXIT_ZOMBIE?
  │   └─ YES → wait_task_zombie() ← 回收！

  ├─ WUNTRACED && stopped?
  │   └─ YES → wait_task_stopped() ← 报告暂停

  ├─ WCONTINUED && 刚从 stop 恢复?
  │   └─ YES → wait_task_continued() ← 报告恢复

  └─ 其他: return 0 (子进程还活着，不处理)
```

---

## 五、回收僵尸：`wait_task_zombie()` → `release_task()`

这是回收的核心——从僵尸进程的 `task_struct` 中提取退出信息，然后彻底释放。

### 5.1 `wait_task_zombie()` 关键步骤

```c
// kernel/exit.c: wait_task_zombie()
static int wait_task_zombie(struct wait_opts *wo, struct task_struct *p)
{
    int retval, status;
    pid_t pid = task_pid_vnr(p);
    // ===== 步骤 1: 提取退出状态 =====
    if (likely(p->ptrace == 0)) {
        // 正常子进程：从 task_struct 中获取 exit_code
        status = (p->signal->group_exit_code & 0x7f)  // 低 7 位 = 信号
               | ((p->exit_code & 0xff) << 8);        // 高 8 位 = 退出码
    } else {
        // ptrace 跟踪的子进程：信号编号放在高 8 位
        status = (p->signal->group_exit_code & 0x7f) << 8
               | (p->exit_code & 0xff);
    }
    // ===== 步骤 2: 获取资源使用统计 =====
    if (wo->wo_rusage) {
        // 累积的 CPU 时间、缺页次数、上下文切换次数等
        getrusage(p, RUSAGE_BOTH, rusage);
        copy_to_user(wo->wo_rusage, rusage, ...);
    }
    // ===== 步骤 3: 写入用户态的 status 地址 =====
    if (wo->wo_stat) {
        put_user(status, wo->wo_stat);  // copy_to_user
    }
    // ===== 步骤 4: 释放 task_struct =====
    release_task(p);  // ← 最终释放！
                       //   子进程的 task_struct 从此不复存在
    return pid;  // 返回回收的 PID
}
```

### 5.2 `release_task()` —— 最终释放

```c
// kernel/exit.c: release_task()
void release_task(struct task_struct *p)
{
    struct task_struct *leader;
    int zap_leader;
    // ① 需要 tasklist_lock 写锁
    write_lock_irq(&tasklist_lock);
    // ② ptrace 的解绑处理
    ptrace_release_task(p);
    // ③ 从父进程的 children 链表移除
    __ptrace_unlink(p);
    // ④ 如果 p 是线程组 leader 且还有线程活着，先 zap
    //    (让其他线程知道 leader 死亡)
    leader = p->group_leader;
    if (leader != p) {
        // p 是线程组成员，但不是 leader
        // 这发生在只有部分线程被 wait 回收的情况
    }
    // ⑤ 释放信号相关资源
    write_unlock_irq(&tasklist_lock);
    __exit_signal(p);
    //   ├─ 从 PID namespace 的 hash 表移除
    //   ├─ 释放 p->signal (如果这是最后一个引用)
    //   ├─ 释放 p->sighand (信号处理描述符)
    //   └─ 清理 timer 等资源
    // ⑥ 从进程树和 PID 表中移除
    write_lock_irq(&tasklist_lock);
    __unhash_process(p, thread_group_leader(p));
    //   ├─ detach_pid(p, PIDTYPE_PID)  → PID 释放，可复用
    //   ├─ detach_pid(p, PIDTYPE_TGID)
    //   ├─ detach_pid(p, PIDTYPE_PGID)
    //   └─ detach_pid(p, PIDTYPE_SID)
    write_unlock_irq(&tasklist_lock);
    // ⑦ 调度器注销
    sched_exit(p);
    // ⑧ 释放 task_struct 内存本身
    free_task(p);
    //   ├─ kmem_cache_free(task_struct_cachep, p)
    //   └─ 如果配置了延迟释放 (task_struct 的 RCU 保护)，则 call_rcu
}
```

**资源释放顺序**：

```bash
release_task(p)

  ├─ ptrace_release_task(p)         // 调试器解绑
  ├─ __ptrace_unlink(p)             // 从父进程 children 链表摘除
  ├─ __exit_signal(p)               // 信号/定时器/sighand 释放
  ├─ __unhash_process(p)            // 从 PID hash/进程树摘除
  │   ├─ PID 从此可复用
  │   └─ /proc/<pid>/ 目录消失
  ├─ sched_exit(p)                  // 调度器清理
  └─ free_task(p)                   // 释放 task_struct slab 内存
```

---

## 六、完整时序图

### 6.1 阻塞 wait —— 父进程阻塞直到子进程退出

```plantuml
@startuml
skinparam backgroundColor #FFFFFF
title 阻塞 waitpid() 完整内核流程
participant "父进程\n用户态" as PUSER
participant "父进程\n内核态" as PKERNEL
participant "do_wait\n循环" as DOWAIT
participant "子进程\n内核态" as CKERNEL
database "子进程\ntask_struct" as TASK
== 父进程调用 waitpid(-1, &status, 0) ==
PUSER -> PKERNEL : waitpid(-1, &status, 0)\n(SYSCALL_DEFINE3)
PKERNEL -> PKERNEL : kernel_wait4()
note right: wo.wo_type = P_PID\nwo.wo_pid  = -1\nwo.wo_flags = 0
PKERNEL -> DOWAIT : do_wait(wo)
== 第一轮扫描 ==
DOWAIT -> DOWAIT : add_wait_queue()\n挂到 current->signal->wait_chldexit
DOWAIT -> DOWAIT : do_wait_thread(current, wo)
note right: 遍历 children 链表...
DOWAIT -> TASK : child1: running 状态\neligible_child() → skip
DOWAIT -> TASK : child2: running 状态\neligible_child() → skip
DOWAIT -> DOWAIT : retval = 0\n没有可回收的僵尸
DOWAIT -> DOWAIT : WNOHANG 未设置 → 阻塞\nset_current_state(TASK_INTERRUPTIBLE)
DOWAIT -> DOWAIT : schedule()
note right #lightyellow: **父进程在此睡眠**\n等待 wait_chldexit 队列
== 子进程退出，唤醒父进程 ==
CKERNEL -> CKERNEL : do_exit()
CKERNEL -> CKERNEL : exit_notify()
CKERNEL -> CKERNEL : do_notify_parent(tsk)
note right: 向父进程发 SIGCHLD
CKERNEL -> CKERNEL : __wake_up_parent(tsk, parent)
TASK -> TASK : exit_state = EXIT_ZOMBIE
CKERNEL -> DOWAIT : **唤醒 !**\nwake_up(&parent->signal->wait_chldexit)
== 第二轮扫描 ==
DOWAIT -> DOWAIT : schedule() 返回\ngoto repeat
DOWAIT -> DOWAIT : do_wait_thread(current, wo)
DOWAIT -> TASK : child2: exit_state == EXIT_ZOMBIE!\neligible_child() → 命中!
DOWAIT -> DOWAIT : wait_task_zombie(wo, child2)
note right: 提取 status (退出码 8 位 + 信号 7 位)\n写回用户态 stat_addr
DOWAIT -> DOWAIT : release_task(child2)
note right: __unhash_process (释放 PID)\nfree_task (释放 task_struct slab)
TASK -> TASK : **[X] task_struct 彻底释放**
DOWAIT -> DOWAIT : remove_wait_queue()
DOWAIT --> PKERNEL : return pid (子进程 PID)
PKERNEL --> PUSER : waitpid() 返回 child2_pid
@enduml
```

### 6.2 WNOHANG 非阻塞路径

```plantuml
@startuml
skinparam backgroundColor #FFFFFF
title waitpid(-1, &status, WNOHANG) —— 非阻塞，没有僵尸立即返回 0
participant "用户态" as USER
participant "内核态" as KERNEL
database "children\n链表" as CHILDREN
USER -> KERNEL : waitpid(-1, &status, WNOHANG)
KERNEL -> KERNEL : kernel_wait4(-1, stat_addr, WNOHANG, NULL)
KERNEL -> KERNEL : do_wait(wo)\nwo.wo_flags = WNOHANG
KERNEL -> KERNEL : add_wait_queue()
KERNEL -> KERNEL : do_wait_thread(current, wo)
KERNEL -> CHILDREN : 遍历 children...
loop 每个 child
    KERNEL -> CHILDREN : 检查 task_state
    alt exit_state == EXIT_ZOMBIE
        KERNEL -> KERNEL : wait_task_zombie()\n→ release_task()
        KERNEL --> USER : **返回 PID** (有僵尸可回收)
    else 还活着 (running/stopped)
        KERNEL -> CHILDREN : 跳过
    end
end
KERNEL -> KERNEL : retval = 0\n(没有任何可回收的僵尸)
KERNEL -> KERNEL : if (WNOHANG)
note right #lightyellow: retval = 0\n**不阻塞，直接返回！**
KERNEL -> KERNEL : remove_wait_queue()
KERNEL --> USER : **返回 0**\n(没有已退出的子进程)
@enduml
```

### 6.3 子进程退出唤醒父进程的完整路径

```plantuml
@startuml
skinparam backgroundColor #FFFFFF
title 子进程 do_exit → 父进程 do_wait 被唤醒的完整信号链
participant "子进程" as CHILD
participant "do_exit" as EXIT
participant "do_notify_parent" as NOTIFY
participant "等待队列\nwait_chldexit" as WQ
participant "父进程\ndo_wait" as PARENT
== 子进程退出流程 ==
CHILD -> EXIT : do_exit(exit_code)
EXIT -> EXIT : exit_mm()\n释放 mm_struct
EXIT -> EXIT : exit_files()\n释放 files_struct
EXIT -> EXIT : exit_fs()\n释放 fs_struct
EXIT -> EXIT : exit_signals()
note right: 所有 pending 信号被清空
EXIT -> EXIT : cgroup_exit()
== 进程树处理 ==
EXIT -> EXIT : exit_notify(EXIT_ZOMBIE)
EXIT -> EXIT : forget_original_parent(tsk)
note right: ts->children 中的所有子进程\n被 reparent 给最近 subreaper 或 init
EXIT -> EXIT : exit_task_work()
note right: 执行 task_work 回调\n(如 fput, 关闭文件)
EXIT -> EXIT : tsk->exit_state = EXIT_ZOMBIE
note right #lightyellow: **此时成为僵尸！**\n用户态内存已释放\n但 task_struct 仍存在
== 通知父进程 ==
EXIT -> NOTIFY : do_notify_parent(tsk, SIGCHLD)
NOTIFY -> NOTIFY : __send_signal(parent->signal)
note right: 向父进程投递 SIGCHLD
alt 父进程的信号处理中
    note right: 如果父进程 SA_NOCLDWAIT\n跳过通知
end
NOTIFY -> WQ : __wake_up_parent(tsk, tsk->parent)
note right #palegreen: wake_up(&parent->signal->\n           wait_chldexit)
== 父进程被唤醒 ==
WQ -> PARENT : **wake_up !**
note left #lightyellow: parent 从 schedule() 返回
PARENT -> PARENT : do_wait_thread() 重新扫描
PARENT -> PARENT : wait_consider_task()
note left: child->exit_state == EXIT_ZOMBIE
PARENT -> PARENT : wait_task_zombie()
PARENT -> PARENT : 读取 exit_code + rusage
PARENT -> PARENT : release_task(child)
note left: __unhash_process\nfree_task
== 子进程最终状态 ==
EXIT -> EXIT : do_task_dead()
note right #lightgray: schedule() 不会再被调度\n因为 exit_state != 0
@enduml
```

---

## 七、选项位（options）的完整语义

`waitpid()` 的 `options` 参数是位掩码，直接影响 `do_wait()` 和 `wait_consider_task()` 的行为：

| 标志位 | 十六进制 | 内核含义 | 用户态行为 |
|--------|---------|---------|-----------|
| `WNOHANG` | `0x00000001` | 设置后 `do_wait()` 不调用 `schedule()`，立即返回 0 | 非阻塞查询：有僵尸就收，没有就返回 0 |
| `WUNTRACED` | `0x00000002` | 子进程因 `SIGSTOP`/`SIGTSTP`/`SIGTTIN`/`SIGTTOU` 停止时也报告 | 可用于 shell 作业控制的 `fg`/`bg` 命令 |
| `WCONTINUED` | `0x00000008` | 子进程被 `SIGCONT` 恢复时报告（Linux 2.6.10+） | 跟踪子进程的暂停/恢复生命周期 |
| `__WNOTHREAD` | `0x20000000` | 只扫描**当前线程**的子进程（线程组中其他线程的孩子不管） | 仅 `waitid()` 可用 |
| `__WALL` | `0x40000000` | 不区分 clone 标志，扫描**所有**子进程（含 `CLONE_THREAD` 创建的不算） | 仅 `waitid()` 可用 |
| `__WCLONE` | `0x80000000` | 只扫描通过 `clone()` 创建的"线程子进程" | 仅 `waitid()` 可用，与 `__WALL` 互斥 |

### 7.1 `WNOHANG` 的内核路径

```c
// do_wait() 中的关键分支
notask:
    retval = wo->wo_notask_ret;  // 默认是 0
    if (!retval && !(wo->wo_flags & WNOHANG)) {
        // WNOHANG 未设置 → 阻塞
        schedule();
        goto repeat;
    }
    // WNOHANG 设置 → 直接跳到 end，返回 0
end:
    __set_current_state(TASK_RUNNING);
    remove_wait_queue(...);
    return retval;
```

### 7.2 `WUNTRACED` 与 `WCONTINUED` 的交错

| 场景 | 未设 WUNTRACED | 设置 WUNTRACED | 设置 WCONTINUED |
|------|---------------|----------------|-----------------|
| 子进程 running | wait 阻塞/返回0 | 同左 | 同左 |
| 子进程被 SIGSTOP 停止 | **wait 继续阻塞** | **wait 返回，WIFSTOPPED 为真** | WIFSTOPPED 为真 |
| 子进程被 SIGCONT 恢复 | **wait 继续阻塞** | **wait 返回**（需要 WCONTINUED） | **wait 返回，WIFCONTINUED 为真** |

---

## 八、子进程退出链接：`do_notify_parent()` → `__wake_up_parent()`

### 8.1 发信号 + 唤醒父进程

```c
// kernel/signal.c: do_notify_parent()
bool do_notify_parent(struct task_struct *tsk, int sig)
{
    struct task_struct *parent = tsk->parent;
    // ① 发送 SIGCHLD 信号给父进程
    __group_send_sig_info(sig, &info, parent);  // sig = SIGCHLD
    // ② 唤醒父进程中阻塞在 waitpid 的线程
    //    关键：直接操作 wait_chldexit 等待队列
    __wake_up_parent(tsk, parent);
    return ...;
}
// kernel/exit.c
void __wake_up_parent(struct task_struct *p, struct task_struct *parent)
{
    wake_up(&parent->signal->wait_chldexit);
    //       ↑
    //   这会唤醒所有阻塞在 parent 的 do_wait() 上的线程
    //   每个被唤醒的线程会重新扫描 children 链表
}
```

### 8.2 SA_NOCLDWAIT 的影响

当父进程设置了 `SA_NOCLDWAIT`，内核在 `do_notify_parent()` 中走快捷路径：

```c
// kernel/exit.c: do_notify_parent()
if (tsk->exit_signal == -1) {
    // SA_NOCLDWAIT 效果：子进程的 exit_signal 被设为 -1
    // → 不通知父进程，直接 autoreap
    wake_up_parent = 0;   // 不唤醒父进程
    autoreap = true;      // 直接自动回收！
}
if (autoreap) {
    release_task(tsk);    // 立即回收，不产生僵尸
    return;
}
```

---

## 九、时序说明：为什么 "标准信号不排队" 是 waitpid 必须循环的原因

```bash
父进程的 signal_struct 中 SIGCHLD pending 位:
初始: [pending: 0]

  ├─ child1 退出 → __send_signal(SIGCHLD)
  │   pending |= (1 << SIGCHLD)   → [pending: SIGCHLD]

  ├─ child2 退出 → __send_signal(SIGCHLD)  // 几乎同时
  │   pending 已经是 SIGCHLD → **不会排队第二个!**
  │   [pending: SIGCHLD] (仍然是 1 个信号)

  └─ SIGCHLD handler 触发
      ├─ waitpid(-1, &status, WNOHANG)
      │   → child1: 回收!

      │   如果不循环而直接用 if:
      │     → child2 漏掉了!! → 永久僵尸

      └─ 正确做法 while(waitpid(...) > 0):
          → child1 回收, child2 回收
          → waitpid 返回 -1, errno=ECHILD
```

**结论**：因为标准信号不排队，`while (waitpid(-1, ..., WNOHANG) > 0);` 循环是**必须的**——一个 `SIGCHLD` 可能承载多个子进程退出事件。

---

## 十、与低延时实践的关系

| 场景 | waitpid 的影响 | 建议 |
|------|--------------|------|
| 核心延时线程调用 `waitpid(0)` | 阻塞在 `schedule()`，延迟不可控 | 绝对不要在延时路径上调用 |
| `waitpid(WNOHANG)` 在延时线程 | 仅链表遍历 + 锁获取，~100ns-1μs | 可接受，但应在非关键窗口 |
| `signalfd + epoll` 替代 | 转换为 fd 事件，统一事件循环 | 推荐用于低延时服务 |
| `SA_NOCLDWAIT` 完全避过 | 内核自动回收，零 `waitpid` 开销 | 适合不需要退出状态的场景 |

---

## 十一、参考资料

- Linux kernel: `kernel/exit.c` — `do_wait()`, `do_wait_thread()`, `wait_consider_task()`, `wait_task_zombie()`, `release_task()`
- Linux kernel: `kernel/exit.c` — `do_notify_parent()`, `__wake_up_parent()`, `forget_original_parent()`
- Linux kernel: `kernel/signal.c` — `do_notify_parent()` (信号发送部分)
- Linux kernel: `include/linux/wait.h` — wait_queue 机制
- `man 2 waitpid` / `man 2 waitid` / `man 2 wait`
- 《The Linux Programming Interface》第 26 章（进程回收）
- 《Linux Kernel Development》第 3 版，第 3 章（进程管理）

> **一句话**：`waitpid()` 在内核中是一条 `waitpid → kernel_wait4 → do_wait → do_wait_thread → wait_task_zombie → release_task` 的完整链路。父进程阻塞在 `wait_chldexit` 等待队列上，子进程 `do_exit` 时通过 `do_notify_parent() → __wake_up_parent()` 唤醒。被唤醒后重新扫描 `children` 链表命中僵尸，提取退出码和资源统计，最后 `release_task()` 释放 PID 和 `task_struct`。

