在 Linux 進程模型中,子進程終止後會進入**僵屍狀態**,佔用 PID 和內核任務結構,必須由父進程調用 wait 或 waitpid 回收,釋放資源。本文系統講解兩大回收 API 的語法、阻塞/非阻塞行為、狀態碼提取宏、以及典型編程模式。
exit() 或從 main 返回後,**不會立即銷毀**,而是轉為僵屍進程(ps 顯示 Z),保留:PID、終止狀態碼、資源使用統計(rusage)。wait/waitpid/waitid 讀取狀態碼,內核才會釋放任務結構。**忽略回收會導致 PID 洩漏、進程表填滿**。init (PID 1) 或子收割器收養,由其負責回收。SIGCHLD 信號在子進程終止/停止/繼續時發送給父進程,默認忽略。設置 SA_NOCLDWAIT 或忽略 SIGCHLD 可讓內核自動回收(但無法獲取狀態碼)。#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
errno=ECHILD)。NULL,存儲終止狀態(編碼格式見下)。等價於 waitpid(-1, status, 0)。
pid_t waitpid(pid_t pid, int *status, int options);
| 值 | 含義 |
|---|---|
> 0 | 等待**指定 PID** 的子進程 |
-1 | 等待**任意**子進程(同 wait) |
0 | 等待**同進程組**的任意子進程 |
< -1 | 等待**進程組 ID 等於 |pid|** 的任意子進程 |
| 宏 | 行為 |
|---|---|
WNOHANG | 非阻塞:無已終止子進程時立即返回 0,不掛起。輪詢/事件循環必備。 |
WUNTRACED | 子進程**停止**(收到 SIGSTOP/SIGTSTP/SIGTTIN/SIGTTOU)時也返回。配合 WIFSTOPPED 使用。 |
WCONTINUED | 子進程從停止狀態**繼續運行**(收到 SIGCONT)時返回。Linux 2.6.10+。 |
status 為編碼後的整數,**不可直接判斷**,必須用以下宏解碼:
| 宏 | 前提判斷宏 | 含義 | 典型值 |
|---|---|---|---|
WIFEXITED(status) | — | 子進程**正常終止**(調用 exit/_exit 或 return) |
真 (非 0) |
WEXITSTATUS(status) | WIFEXITED 為真 |
提取 exit() 的**低 8 位參數** (0-255) |
0 成功,非 0 失敗碼 |
WIFSIGNALED(status) | — | 子進程**被信號殺死**(未捕獲、非 SIGSTOP/SIGKILL 等) |
真 |
WTERMSIG(status) | WIFSIGNALED 為真 |
提取**致死信號編號** | SIGSEGV(11), SIGKILL(9), SIGTERM(15) |
WCOREDUMP(status) | WIFSIGNALED 為真 |
是否產生**核心轉儲文件** | 真/假 (BSD/GLibc 擴展) |
WIFSTOPPED(status) | — | 子進程**處於停止狀態** (需 WUNTRACED) |
真 |
WSTOPSIG(status) | WIFSTOPPED 為真 |
提取**導致停止的信號編號** | SIGSTOP(19), SIGTSTP(20) |
WIFCONTINUED(status) | — | 子進程**從停止繼續運行** (需 WCONTINUED) |
真 |
pid_t child = fork();
if (child == 0) { /* child */ exit(5); }
else {
int status;
pid_t ret = waitpid(child, &status, 0); // 阻塞等待指定子進程
if (ret == -1) perror("waitpid");
else if (WIFEXITED(status))
printf("Child %d exited normally, code=%d\n", ret, WEXITSTATUS(status));
else if (WIFSIGNALED(status))
printf("Child %d killed by signal %d\n", ret, WTERMSIG(status));
}
void sigchld_handler(int sig) {
int saved_errno = errno;
while (true) {
int status;
pid_t pid = waitpid(-1, &status, WNOHANG);
if (pid <= 0) break; // 0: 無僵屍; -1: 無子進程/出錯
// 處理 status...
if (WIFEXITED(status)) log_exit(pid, WEXITSTATUS(status));
else if (WIFSIGNALED(status)) log_signal(pid, WTERMSIG(status));
}
errno = saved_errno;
}
關鍵點:循環調用waitpid(-1, ..., WNOHANG)直到返回 0 或 -1,一次信號可能對應多個子進程終止。保存/恢復errno防止中斷其他系統調用。
// 父進程創建多個子進程組,僅等待組內任一終止
pid_t pgid = getpgrp();
pid_t ret = waitpid(0, &status, 0); // pid=0 等待同組任意子進程
SIGCHLD 處理器或事件循環中調用 waitpid(-1, ..., WNOHANG)。printf、malloc、lock 等**禁止**在信號處理器中使用。僅調用 waitpid、write (到管道/日志 fd)、設置標誌位,主循環再處理詳情。SIGCHLD 導致無法獲取狀態碼:signal(SIGCHLD, SIG_IGN) 讓內核自動回收,但**無法獲取退出碼/信號**。需狀態碼時**不可忽略**,必須自行回收。status 值:if (status == 0) **錯誤**!必須用 WIFEXITED + WEXITSTATUS。status 包含信號、核心轉儲、停止等信息,非簡單退出碼。waitpid 返回 0 處理不當:WNOHANG 返回 0 表示「無已終止子進程」,**不是錯誤**,循環應 break,而非報錯。wait:wait/waitpid 在多線程中**會阻塞整個進程**(所有線程)。專用回收線程 + signalfd/eventfd 通知主線程更優雅。
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
idtype 可為 P_PID/P_PGID/P_ALL;options 必須包含 WEXITED/WSTOPPED/WCONTINUED/WNOHANG/WNOWAIT。siginfo_t 結構,直接包含 si_pid、si_uid、si_signo、si_status (退出碼/信號)、si_code (CLD_EXITED/CLD_KILLED/CLD_DUMPED/CLD_STOPPED/CLD_CONTINUED)、**無需宏解碼**。WNOWAIT:保留僵屍,允許後續再次 waitid/waitpid 回收(調試器場景)。新代碼推薦使用 waitid,信息更豐富、介面更清晰、避免宏陷阱。
wait/waitpid 是父進程履行回收義務的核心工具。**阻塞模式**用於同步等待特定子任務,**非阻塞 WNOHANG + 循環**用於異步回收大量子進程。**嚴格使用狀態提取宏** 而非直接判斷 status,是編寫健壯進程管理代碼的鐵律。現代項目建議遷移至 waitid 或封裝庫(如 libuv、boost::process)屏蔽底層複雜性。