从 signal(SIGCHLD, SIG_IGN) 说起:默认忽略为什么不等于自动回收子进程?
很多人第一次看到这行代码都会疑惑:
c
signal(SIGCHLD, SIG_IGN);
然后问:
SIGCHLD的默认动作不就是忽略吗?那这行是不是脱裤子放屁?
答案很关键:
SIGCHLD默认动作是"忽略信号",但默认忽略并不会自动回收子进程。显式设置SIG_IGN或SA_NOCLDWAIT,才会让内核自动回收子进程,避免僵尸。
一、前置知识:子进程退出后发生了什么?
在 Linux 中,父进程 fork() 出子进程。子进程退出后,并不是立刻从系统里彻底消失。
子进程退出时,内核会:
- 释放子进程的大部分资源,比如内存、文件描述符等;
- 保留一小部分信息,比如退出状态、PID、资源使用统计;
- 等待父进程调用
wait()或waitpid()来读取这些信息。
在父进程调用 wait 之前,这个已经退出但还没被回收的子进程,就是僵尸进程。
用 ps 看,它的状态通常是 Z:
text
PID PPID STAT CMD
1234 1000 Z [child] <defunct>
僵尸进程不占内存,但占 PID 和内核进程表项。如果大量产生又不回收,可能耗尽系统资源。
如果父进程先退出,子进程会被 init 或 systemd 收养,之后由它们负责回收。
二、SIGCHLD 是什么?
SIGCHLD 是父进程收到的一种信号,用来通知:
你的某个子进程终止了、停止了,或者继续运行了。
它的默认动作是 Ign,也就是忽略。
但这里的"忽略"很容易被误解。
默认忽略的意思是:
如果没有特别设置,内核不会因为子进程退出而给父进程执行什么默认处理动作。
它不表示:
子进程退出后内核自动回收,不产生僵尸。
这是核心区别。
三、关键区别:默认 Ign 与显式 SIG_IGN
对比一下:
| 行为 | SIGCHLD 默认动作 |
显式 signal(SIGCHLD, SIG_IGN) 或 SA_NOCLDWAIT |
|---|---|---|
| 子进程退出后 | 变成僵尸,等待父进程 wait |
内核自动回收,不产生僵尸 |
父进程 wait |
可以拿到退出状态 | 通常返回 -1,errno = ECHILD |
| 主要用途 | 默认行为 | 明确表示:我不关心子进程退出状态 |
所以:
c
signal(SIGCHLD, SIG_IGN);
不是简单地"忽略一个本来就忽略的信号",而是告诉内核:
这些子进程退出后不用保留状态了,直接回收,别留僵尸。
POSIX 对这一点有规定:如果 SIGCHLD 被设置为 SIG_IGN,或者通过 sigaction 设置了 SA_NOCLDWAIT,子进程退出时不会变成僵尸,wait 会失败并返回 ECHILD。
四、实验:不回收 和 显式忽略
1. 不调用 wait,产生僵尸
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void) {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
exit(1);
}
if (pid == 0) {
puts("child exiting");
_exit(0);
}
printf("parent sleeping, child pid = %d\n", pid);
sleep(30);
return 0;
}
运行后,另开终端查看:
bash
ps -o pid,ppid,stat,cmd
你会看到子进程状态是 Z,也就是僵尸。
2. 显式设置 SIG_IGN,自动回收
c
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void) {
signal(SIGCHLD, SIG_IGN);
pid_t pid = fork();
if (pid < 0) {
perror("fork");
exit(1);
}
if (pid == 0) {
_exit(0);
}
printf("parent sleeping, child pid = %d\n", pid);
sleep(30);
return 0;
}
这次子进程退出后不会留下僵尸。
五、那如果我关心子进程退出状态怎么办
如果只是不想要僵尸,SIG_IGN 很省事。
但如果你需要知道:
- 子进程是正常退出还是被信号杀死;
- 退出码是多少;
- 哪个子进程退出了;
那就不能简单 SIG_IGN,因为退出状态会被丢弃。
正确做法通常是:注册 SIGCHLD 处理函数,在 handler 里循环调用 waitpid 回收。
c
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
static void sigchld_handler(int sig) {
(void)sig;
int saved_errno = errno;
pid_t pid;
int status;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
if (WIFEXITED(status)) {
printf("child %d exited, code = %d\n",
(int)pid, WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("child %d killed by signal %d\n",
(int)pid, WTERMSIG(status));
}
}
errno = saved_errno;
}
int main(void) {
struct sigaction sa;
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
if (sigaction(SIGCHLD, &sa, NULL) == -1) {
perror("sigaction");
exit(1);
}
for (int i = 0; i < 3; i++) {
pid_t pid = fork();
if (pid == 0) {
sleep(i + 1);
_exit(i);
}
}
sleep(10);
return 0;
}