第 26 章 监控子进程 (Monitoring Child Processes)

      +

      核心结论

      • wait/waitpid 是父回收子状态的系统调用:返回子 PID + 终止状态;wait 等任意子,waitpid 可指定 PID + flags(WUNTRACED/WCONTINUED/WNOHANG)。

      • wait status 4 种情形:正常 exit、被信号杀、被信号 stop(WUNTRACED)、被 SIGCONT 恢复(WCONTINUED,2.6.10+);用 WIFEXITED/WIFSIGNALED/WIFSTOPPED/WIFCONTINUED + WEXITSTATUS/WTERMSIG/WSTOPSIG 宏提取。

      • 僵尸进程:子终止但父未 wait——内核保留进程表项记录 PID + status;不可被信号杀(包括 SIGKILL);父 wait 后或父终止(init 收养)后清除。

      • SIGCHLD 是子状态变化的默认通知:默认 ignore;handler 中必须用 while(waitpid(-1, NULL, WNOHANG) > 0) 循环回收——因为标准信号不排队,多次 SIGCHLD 只投递一次。

      • SIGCHLD = SIG_IGN 自动回收:Linux 上让子终止时立即清除,不变僵尸(不需 handler);SA_NOCLDWAIT 标志等效。

        • 孤儿进程:父先终止 → 子被 init(PID 1)收养;getppid() 返回 1。

      本章主旨

      本章是「进程生命周期」四章的第三章——父进程如何知道子进程的状态变化。读者应掌握:wait/waitpid/waitid 系统调用族、wait status 的位布局与 W* 宏提取、僵尸进程的成因与清理、SIGCHLD handler 的标准循环模式、SIGCHLD = SIG_IGN 与 SA_NOCLDWAIT 自动清理、孤儿进程被 init 收养。理解这些是写「长生命周期父进程」(daemon、shell、服务器)的关键——避免僵尸耗尽进程表、SIGCHLD handler 正确回收所有子进程。

      一、核心概念

      本章围绕 7 个核心概念展开:从 wait 基础入手,到 waitpid 选项、wait status 解析、waitid/wait3/wait4 变体、僵尸与孤儿、SIGCHLD handler 设计、SIGCHLD 处置的特殊性。

      概念 定义 + 重要性 实现提示

      wait/waitpid 系统调用

      wait(&status) 阻塞等任意子终止;waitpid(pid, &status, options) 可指定 PID、进程组、flags;返回子 PID,写 status。

      §26.1.1-§26.1.2;WNOHANG 非阻塞;WUNTRACED 包括 stop;WCONTINUED 包括 SIGCONT 恢复;wait 等价 waitpid(-1, &status, 0)。

      wait status 4 情形 + W 宏*

      正常 exit、被信号杀、被信号 stop、被 SIGCONT 恢复;WIFEXITED + WEXITSTATUS;WIFSIGNALED + WTERMSIG + WCOREDUMP;WIFSTOPPED + WSTOPSIG;WIFCONTINUED。

      §26.1.3;W* 宏是跨 UNIX 可移植方式;不要直接位操作 status;WCOREDUMP 非 SUSv3 但 Linux 有;WIFCONTINUED 自 Linux 2.6.10。

      waitid/wait3/wait4 变体

      waitid(idtype, id, infop, options) 更精细的 id 类型(P_ALL/P_PID/P_PGID)与 infop 填 siginfo_t;wait3/wait4 还返回 rusage。

      §26.1.5/§26.1.6;waitid 自 2.6.9 完整支持;wait3/wait4 非 SUSv3,不推荐用于可移植代码。

      僵尸进程 (Zombie)

      子终止但父未 wait;内核保留进程表项记录 PID + status + rusage;不可被信号杀;父 wait 后或父终止(init 收养 wait)后清除。

      §26.2;僵尸耗尽进程表 → fork 失败 EAGAIN;ps 显示 <defunct>;必须 wait 回收;kill -9 pid 杀不掉。

      孤儿进程 (Orphan)

      父先终止 → 子被 init(PID 1)收养;子 getppid 返回 1;init 自动 wait 子。

      §26.2;孤儿进程组还会收到 SIGHUP + SIGCONT(详见第 34 章);prctl(PR_SET_PDEATHSIG, sig) 让进程在父死时收到信号。

      SIGCHLD handler 标准模式

      子终止时内核发 SIGCHLD(默认 ignore);handler 中循环 while (waitpid(-1, NULL, WNOHANG) > 0)——标准信号不排队,必须循环回收所有僵尸。

      §26.3.1;handler 进入时保存 errno 防止破坏主程序 errno;用 SA_NOCLDSTOP 屏蔽 stop 通知;现代代码常用 signalfd 替代。

      SIGCHLD = SIG_IGN 自动回收

      设置 SIGCHLD 为 SIG_IGN(不是默认 ignore!默认 ignore 不变僵尸)——子终止时立即清除,不变僵尸;父 wait 返回 ECHILD。

      §26.3.3;SA_NOCLDWAIT 等效;Linux 2.6+;最简单防止僵尸的方法——但不保留子状态信息。

      二、详细笔记

      26.1 wait/waitpid 基础

      What:父调用 wait/waitpid 阻塞等待子进程终止,并获取子进程状态。

      Why:防止僵尸积累;让父知道子是否成功、为什么终止。

      How:核心 API:

      #include <sys/wait.h>
      
      pid_t wait(int *status);                                    /* 等任意子 */
      pid_t waitpid(pid_t pid, int *status, int options);         /* 等指定子 */
      pid_t waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);

      wait 语义(§26.1.1):

      • 若所有子都已 wait 过且没有 unwaited 子 → 返回 -1,errno = ECHILD。

      • 若无子终止 → 阻塞。

        • status 非 NULL 时填入子进程状态;返回子 PID。

      waitpid 的 pid 取值(§26.1.2):

      • > 0:等指定 PID。

      • == 0:等同进程组的任意子。

      • < -1:等进程组 ID = |pid| 的任意子。

      • == -1:等任意子(等价 wait)。

      options 标志(§26.1.2):

      • WNOHANG——无子终止立即返回 0(不阻塞)。

      • WUNTRACED——包括被 stop 的子。

      • WCONTINUED——包括被 SIGCONT 恢复的子(Linux 2.6.10+)。

      waitid 比 waitpid 更灵活(§26.1.5):

      • idtype = P_ALL / P_PID / P_PGID。

      • options 独立标志 WEXITED / WSTOPPED / WCONTINUED + WNOHANG + WNOWAIT。

        • 填 siginfo_t,含 si_pid、si_code(CLD_EXITED/KILLED/STOPPED/CONTINUED)、si_status、si_signo = SIGCHLD、si_uid。

      wait3/wait4 比 wait/waitpid 多返回 rusage(§26.1.6);非 SUSv3——避免。

      When

      • wait(&status)——简单 shell 模式,阻塞等所有子。

      • waitpid(pid, &status, WNOHANG)——非阻塞查询特定子。

        • waitpid(-1, &status, WNOHANG)——SIGCHLD handler 中循环回收所有僵尸。

        • waitid——需要 siginfo_t 详细信息的场景。

      Example:第 26 章 Listing 26-1 multi_wait.c——创建多子并 wait:

      for (j = 1; j < argc; j++) {
          switch (fork()) {
          case 0:
              sleep(getInt(argv[j], GN_NONNEG, "sleep-time"));
              _exit(EXIT_SUCCESS);
          default: break;
          }
      }
      while ((childPid = wait(NULL)) != -1) continue;  /* 回收所有子 */
      /* wait 返回 -1 + errno = ECHILD 表示没有未回收的子 */

      26.1.3 wait status 解析

      What:status 是 int,只有低 16 位有意义;按 4 种情形组织(§26.1.3 + Figure 26-1)。

      Why:用 W* 宏提取比直接位操作可移植——SUSv3 不规定位布局。

      How:status 布局(Linux/x86-32,布局因实现而异):

      情形 status 高 8 位 status 低 8 位

      正常 exit

      0

      exit status (0-255)

      被信号杀

      信号号

      0;若 core 则 0x80

      被 SIGSTOP 等

      0x7F

      stop 信号号

      被 SIGCONT 恢复

      0xFFFF

      0xFF

      提取宏:

      含义 配套宏

      WIFEXITED(status)

      true if 正常 exit

      WEXITSTATUS → exit status

      WIFSIGNALED(status)

      true if 被信号杀

      WTERMSIG → 信号号;WCOREDUMP → 是否 core

      WIFSTOPPED(status)

      true if 被 stop

      WSTOPSIG → 信号号

      WIFCONTINUED(status)

      true if 被 SIGCONT 恢复

      (无)

      When

      • W* 宏是必须——不要直接位操作。

      • WCOREDUMP 不是 SUSv3 但 Linux/BSD/Solaris 都有——用 #ifdef WCOREDUMP 保护。

      Example:第 26 章 Listing 26-2 printWaitStatus()

      if (WIFEXITED(status)) {
          printf("child exited, status=%d\n", WEXITSTATUS(status));
      } else if (WIFSIGNALED(status)) {
          printf("child killed by signal %d (%s)", WTERMSIG(status), strsignal(WTERMSIG(status)));
      #ifdef WCOREDUMP
          if (WCOREDUMP(status)) printf(" (core dumped)");
      #endif
          printf("\n");
      }

      信号 handler 中终止子(§26.1.4):

      • 在 SIGTERM 等信号的 handler 中清理后调 exit——父 wait 看到正常退出。

      • 想让父知道是因信号终止——handler 中 signal(sig, SIG_DFL); raise(sig);——让默认动作再杀一次。

      26.2 孤儿与僵尸

      What:僵尸 = 子终止但父未 wait;孤儿 = 父先终止子还在跑。

      Why:理解这两种「特殊状态」才能设计不会内存泄漏的父进程。

      How:僵尸(§26.2):

      • 子终止时内核保留进程表项(PID + status + rusage)——其他资源释放。

      • 父调用 wait 后清除该表项。

      • 不可被信号杀(包括 SIGKILL)——确保父总能 wait。

        • 父未 wait 就终止 → 子被 init 收养 → init 自动 wait → 僵尸清除。

        • 大量僵尸会塞满进程表 → fork 失败 EAGAIN。

      孤儿(§26.2):

      • 父终止后子仍在跑 → 子被 init(PID 1)收养。

      • 子 getppid() 返回 1。

        • 若进程组变孤儿且有 stopped 进程 → 内核给该组所有进程发 SIGHUP + SIGCONT(第 34 章)。

      When

      • 长生命周期父(daemon、shell、服务器)——必须回收所有子,避免僵尸。

      • prctl(PR_SET_PDEATHSIG, sig) 让子进程在父死时收到信号——docker/k8s 用此检测父死。

      Example:第 26 章 Listing 26-4 make_zombie.c——演示僵尸:

      $ ./make_zombie
      Child (PID=1014) exiting
        1014 pts/4    00:00:00 make_zombie <defunct>      # <defunct> = 僵尸
      $ kill -9 1014                                      # 杀不掉
        1014 pts/4    00:00:00 make_zombie <defunct>
      # 父退出后 init 收养 + wait → 僵尸清除

      26.3 SIGCHLD handler 设计

      What:子终止/停止/恢复时内核发 SIGCHLD 给父(默认 ignore);handler 中回收僵尸。

      Why:避免「wait 阻塞主程序」与「waitpid 轮询浪费 CPU」——SIGCHLD 是「事件驱动」的理想模式。

      How:标准 SIGCHLD handler(§26.3.1):

      static void sigchldHandler(int sig) {
          int savedErrno = errno;                       /* handler 可能改 errno */
          while (waitpid(-1, NULL, WNOHANG) > 0)        /* 循环回收所有僵尸 */
              continue;
          errno = savedErrno;                           /* 恢复 errno */
      }
      
      int main(void) {
          struct sigaction sa = {0};
          sa.sa_handler = sigchldHandler;
          sigemptyset(&sa.sa_mask);
          sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;      /* 不发 stop 通知 */
          sigaction(SIGCHLD, &sa, NULL);
          /* ... */
      }
      ```
      
      关键设计点:
      
      * **`while` 循环**——标准信号不排队,多个子并发终止只发一次 SIGCHLD;handler 调用一次只能 wait 一次会漏回收。
      * **`WNOHANG`**——非阻塞,没子可回收立即返回 0。
      * **保存 errno**——waitpid 可能改 errno,主程序可能正在检查 errno。
      * **`SA_NOCLDSTOP`**——不接收子 stop 通知(除非需要 job control)。
      * **建立 handler 在 fork 之前**——Linux 不为已终止的子回送 SIGCHLD;建立 handler 后再 fork 才完整。
      
      *When*:
      
      * 长生命周期父——SIGCHLD handler 是「事件驱动 + 不阻塞」的标准模式。
      * shell job control——SIGCHLD + SA_NOCLDSTOP 不设置 + WUNTRACED/WCONTINUED 检测子状态变化。
      
      *Example*:第 26 章 Listing 26-5 `multi_SIGCHLD.c`——3 个子不同时间退出,handler 循环回收:
      
      [source,bash]

      $ ./multi_SIGCHLD 1 2 4 Child 1 exiting # 1 秒后 handler: Caught SIGCHLD # 第 1 次 handler: Reaped child 17767 Child 2 exiting # 2 秒后(在 handler sleep 中) Child 3 exiting # 4 秒后(在 handler sleep 中) handler: returning # 5 秒后 handler 返回 handler: Caught SIGCHLD # 第 2 次——之前 SIGCHLD 不排队 handler: Reaped child 17768 handler: Reaped child 17769 # 一次 handler 回收 2 个僵尸

      *SIGCHLD 与 stopped 子*(§26.3.2)
      
      * `SA_NOCLDSTOP` 不设置 → 子 stop 时父也收 SIGCHLD(job control 用)。
      * `SA_NOCLDSTOP` 设置 → 只在子 terminate 时收 SIGCHLD。
      * Linux 2.6.10+ 还发 SIGCHLD 给子 resume(SIGCONT)。
      
      *SIGCHLD = SIG_IGN 自动回收*(§26.3.3):
      
      * 设置 `signal(SIGCHLD, SIG_IGN)` 或 `sigaction(... SIG_IGN)` → 子终止时立即清除,**不变僵尸**。
      * 父 wait 返回 ECHILD。
      * 这是「防止僵尸」的最简单方法——不需 handler。
      * 但**默认 ignore**(SIGCHLD 默认动作)**不变僵尸**——必须显式 `SIG_IGN` 才生效。
      * `SA_NOCLDWAIT` 标志等效(Linux 2.6+)。
      
      *When*:
      
      * 不关心子退出状态——用 SIG_IGN 或 SA_NOCLDWAIT 简化代码。
      * daemon fork worker 后不想 wait——SIG_IGN 自动清理。
      
      == 三、关键图表
      
      (本章无独立编号图表)
      
      [NOTE]
      .wait status 4 情形与提取宏
      ====
      [cols="1,2,3,2", options="header"]
      |===
      | 情形 | 判定宏 | 提取宏 | 状态字段含义
      
      | 正常 exit | WIFEXITED | WEXITSTATUS | 0-255 的 exit status
      | 被信号杀 | WIFSIGNALED | WTERMSIG / WCOREDUMP | 信号号 / 是否 core
      | 被 stop | WIFSTOPPED | WSTOPSIG | 信号号(SIGSTOP/SIGTSTP 等)
      | 被 SIGCONT 恢复 | WIFCONTINUED | - | -
      |===
      ====
      
      [NOTE]
      .防止僵尸的三种方法
      ====
      [cols="1,2,3", options="header"]
      |===
      | 方法 | 代码 | 优点 | 缺点
      
      | wait/waitpid | 显式 wait 或 SIGCHLD handler 循环 | 保留子状态;可移植 | 需写 handler 或 wait
      | SIGCHLD = SIG_IGN | signal(SIGCHLD, SIG_IGN) | 简单;不需 handler | 丢失子状态
      | SA_NOCLDWAIT | sigaction flag | 等效 SIG_IGN 但可保留 handler | Linux 2.6+ 才完整
      |===
      ====
      
      == 四、思维导图
      
      [source,mermaid]

      mindmap root第 26 章 监控子进程 wait waitpid wait 等任意子 waitpid 指定 pid options flags waitid 更精细 wait3 wait4 rusage wait status 4 种情形 WIF EXITED SIGNALED WIF STOPPED CONTINUED WEXIT WTERM WSTOP 宏 16 位布局 僵尸进程 子终止未 wait 内核保留表项 不可被信号杀 ps defunct 耗尽进程表 孤儿进程 父先死 init PID 1 收养 getppid 返回 1 SIGHUP SIGCONT SIGCHLD handler while waitpid NOHANG 循环回收 标准信号不排队 保存恢复 errno SA NOCLDSTOP SIGCHLD SIG IGN 自动清理 不变僵尸 默认 ignore 无效 SA NOCLDWAIT 等效 设计要点 handler 在 fork 前 WNOHANG 非阻塞 SA RESTART 防止中断 短任务 SA NOCLDWAIT 长任务 handler

      五、重点与易错点

      1. wait 返回 -1 + ECHILD = 没有未回收的子——可用此判断「所有子已 wait」;不能用返回值判断「等待超时」(wait 默认阻塞)。

      2. waitpid 的 WNOHANG 让 wait 非阻塞——用于 SIGCHLD handler 中循环回收;无子可回收返回 0。

      3. W 宏是必须*——不要直接位操作 status;SUSv3 不规定位布局;WCOREDUMP 不是 SUSv3 但 Linux 有。

      4. WIFCONTINUED 自 Linux 2.6.10——之前内核不支持;WAITPID 选项 WCONTINUED 也是 2.6.10+。

      5. 僵尸进程不可被信号杀——包括 SIGKILL;只能父 wait 或父终止(init 收养 wait)清除。

      6. SIGCHLD 默认 ignore 不变僵尸——必须显式 signal(SIGCHLD, SIG_IGN) 才让子终止时立即清除;这是「SIGCHLD 默认动作」与「显式 SIG_IGN」的关键区别。

      7. SIGCHLD handler 必须用 while 循环 waitpid——标准信号不排队,handler 调用一次可能漏回收多个僵尸。

      8. 建立 SIGCHLD handler 必须在 fork 之前——Linux 不为已终止的子回送 SIGCHLD。

      9. SIGCHLD handler 中保存/恢复 errno——waitpid 可能改 errno;主程序可能正在检查 errno。

      10. SA_NOCLDSTOP 控制 stop 通知——不设 → 子 stop 时父收 SIGCHLD;设 → 只 terminate 通知;job control 通常不设。

      11. 孤儿进程被 init (PID 1) 收养——getppid() 返回 1;孤儿进程组中 stopped 子收到 SIGHUP+SIGCONT。

      12. SA_NOCLDWAIT 等效 SIG_IGN——Linux 2.6+;handler 仍可能被调用但 wait 返回 ECHILD。

      13. wait/waitpid 不会回收 init 的子——只回收调用进程直接 fork 的子。

        • kill -9 杀僵尸无效——必须杀父或重启。

        • 跨章衔接:第 20-22 章信号基础与 SIGCHLD 投递;第 24 章 fork 创建子;第 25 章 _exit 终止;第 34 章 shell job control 用 WUNTRACED/WCONTINUED;第 36 章 rusage 与 getrusage。