承接上篇,本文聚焦殭屍進程:子進程結束但父進程未回收,導致 PCB 滯留進程表。大量殭屍進程會耗盡 PID,癱瘓系統建立新進程能力。
父進程建立子進程後睡眠 3 秒,期間不調用 wait,並用 system("ps") 觀察進程狀態:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
pid_t pid;
if ((pid = fork()) == -1) {
perror("fork");
return 1;
} else if (pid == 0) { // 子進程
printf("child_pid pid=%d\n", getpid());
exit(0); // 立即結束,進入殭屍狀態
}
// 父進程
sleep(3); // 3秒內不回收,子進程成為殭屍
system("ps"); // 觀察進程列表
exit(0);
}
child_pid pid=1108
PID TTY TIME CMD
847 pts/1 00:00:00 bash
1107 pts/1 00:00:00 a.out // 父進程
1108 pts/1 00:00:00 a.out <defunct> // 殭屍進程!標記 <defunct>
1109 pts/1 00:00:00 ps
PID 1108 顯示 <defunct> 標記,確認為殭屍進程。父進程 PID 1107 仍在運行,但未回收子進程。
| 策略 | 適用場景 | 優點 | 缺點 |
|---|---|---|---|
| 同步阻塞 wait() | 簡單父子協作 | 代碼簡單 | 父進程阻塞,無法並行處理 |
| 非阻塞輪詢 waitpid(-1, &status, WNOHANG) | 事件循環/遊戲主循環 | 不阻塞主流程 | 輪詢頻率難把控,CPU 浪費或延遲 |
| SIGCHLD 信號處理 + waitpid | 高併發網路服務、Daemon | 事件驅動、即時回收、不阻塞 | 信號處理函數限制多,需重入安全 |
| 雙重 fork 技巧 | 不關心子進程退出碼的場景 | 完全避免殭屍,無需父進程處理 | 增加一次 fork 開銷,孫進程由 init 收養 |
void sigchld_handler(int sig) {
int saved_errno = errno;
while (waitpid(-1, NULL, WNOHANG) > 0); // 循環回收所有已終止子進程
errno = saved_errno;
}
int main() {
struct sigaction sa;
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP; // 重啟被中斷系統調用、不因子進程停止產生信號
sigaction(SIGCHLD, &sa, NULL);
// 主業務邏輯...
while (1) { /* 服務循環 */ }
}
關鍵點:
sigaction 而非 signal (可移植、可靠)SA_RESTART:被信號中斷的系統調用自動重啟SA_NOCLDSTOP:僅子進程終止產生信號,停止/繼續不產生waitpid(WNOHANG) 回收所有殭屍,避免信號合併導致遺漏errno,維護線程安全if (fork() == 0) { // 子進程
if (fork() == 0) { // 孫進程
// 執行實際任務
exit(0);
}
exit(0); // 子進程立即結束
}
waitpid(pid, NULL, 0); // 父進程僅等待子進程,孫進程成孤兒由 init 收養
孫進程父進程 (子進程) 先結束 → 孫進程成孤兒 → init 收養 → 孫進程結束由 init 回收 → 零殭屍。
wait 即可掌握進程回收機制,是編寫生產級 Linux 系統程式的門檻技能。