目录
一、背景知识回顾
在进程管理一章中,我们学习过使用 wait 和 waitpid 函数来清理僵尸进程。父进程有两种方式来处理子进程的终止:
-
阻塞等待:父进程调用
wait或waitpid并阻塞,直到有子进程结束。这种方式下,父进程在等待子进程期间无法处理自己的其他工作。 -
非阻塞查询(轮询):父进程可以非阻塞地调用
waitpid,通过设置WNOHANG选项,检查是否有子进程结束等待清理。但这种方式需要父进程在处理自己工作的同时,时不时地进行轮询,增加了程序实现的复杂性。
二、SIGCHLD 信号的作用
-
其实,子进程在终止时会给父进程发送
SIGCHLD信号。该信号的默认处理动作是忽略。 -
父进程可以自定义
SIGCHLD信号的处理函数,这样父进程就可以专心处理自己的工作,而不必时刻关心子进程的状态。 -
当子进程终止时,会通知父进程,父进程在信号处理函数中调用
wait或waitpid来清理子进程,获取子进程的退出状态等信息。
三、自定义 SIGCHLD 信号处理函数的程序示例
1、处理一个子进程的退出
以下是一个 C 程序示例,父进程 fork 出子进程,子进程调用 exit(2) 终止,父进程自定义 SIGCHLD 信号的处理函数,在其中调用 waitpid 获得子进程的退出状态并打印。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
void handler(int sig) {
pid_t id;
int status;
// 使用 while 循环确保清理所有已终止的子进程
while ((id = waitpid(-1, &status, WNOHANG)) > 0) {
if (WIFEXITED(status)) {
printf("Child process %d exited with status %d\n", id, WEXITSTATUS(status));
}
}
printf("SIGCHLD signal handler executed in process %d\n", getpid());
}
int main() {
// 注册 SIGCHLD 信号的处理函数
signal(SIGCHLD, handler);
pid_t cid;
if ((cid = fork()) == 0) { // 子进程
printf("Child process: %d\n", getpid());
sleep(3);
exit(2);
} else if (cid > 0) { // 父进程
while (1) {
printf("Father process is doing some work!\n");
sleep(1);
}
} else {
perror("fork");
return 1;
}
return 0;
}
代码说明:
-
handler函数是SIGCHLD信号的处理函数。在函数中,使用waitpid函数并设置WNOHANG选项,以非阻塞的方式清理已终止的子进程。通过WIFEXITED和WEXITSTATUS宏来判断子进程是否正常退出,并获取其退出状态。 -
在
main函数中,首先使用signal函数注册SIGCHLD信号的处理函数handler。然后通过fork创建子进程,子进程睡眠 3 秒后调用exit(2)退出,父进程则进入一个无限循环,模拟处理自己的工作。

注意:
-
SIGCHLD是普通信号,其pending位只有一个。当多个子进程同时退出时,handler函数实际上只能处理其中一个子进程。因此,在使用waitpid清理子进程时,需要通过while循环持续清理。
-
调用waitpid函数时应设置WNOHANG选项,实现非阻塞等待。若不设置该选项,当所有子进程都已清理完毕时,循环会再次调用waitpid并导致阻塞。
-
采用这种处理方式后,父进程可以专注于自身工作,而无需主动关注子进程状态。子进程终止时,父进程会自动收到SIGCHLD信号并执行预设的handler函数,完成子进程的清理工作。
2、处理多个子进程同时退出
场景:如果有多个子进程几乎同时退出,会发送多个 SIGCHLD 信号,但由于信号不排队,可能只触发一次 handler。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
int handled_count = 0;
void handler(int sig) {
pid_t id;
int status;
printf("=== 第%d次执行handler ===\n", ++handled_count);
// 关键:使用while + WNOHANG清理所有已退出的子进程
while ((id = waitpid(-1, &status, WNOHANG)) > 0) {
printf("清理子进程: %d (退出码: %d)\n", id, WEXITSTATUS(status));
}
printf("handler执行完成\n\n");
}
int main() {
printf("父进程PID: %d\n", getpid());
// 注册SIGCHLD信号处理
signal(SIGCHLD, handler);
// 创建3个子进程,几乎同时退出
for (int i = 0; i < 3; i++) {
if (fork() == 0) {
// 子进程
printf("子进程%d创建 (PID: %d)\n", i, getpid());
sleep(1); // 所有子进程都在1秒后退出
exit(i + 1);
}
}
// 父进程继续工作
for (int i = 0; i < 5; i++) {
printf("父进程工作中... %d/5\n", i + 1);
sleep(1);
}
return 0;
}
运行结果分析:

1. 信号不排队问题
-
可能3个子进程几乎同时退出
-
每个退出都会发送SIGCHLD信号
-
但信号不排队,可能只触发1次handler调用
2. WNOHANG的作用
第一次 waitpid:
id = waitpid(-1, &status, WNOHANG) // 返回 651445
-
找到子进程 651445(已退出)
-
返回值:651445 (>0),进入循环体
-
打印:"清理子进程: 651445"
第二次 waitpid:
id = waitpid(-1, &status, WNOHANG) // 返回 651446
-
找到子进程 651446(已退出)
-
返回值:651446 (>0),进入循环体
-
打印:"清理子进程: 651446"
第三次 waitpid:
id = waitpid(-1, &status, WNOHANG) // 返回 0
-
此时子进程 651447 还没有退出(或者刚退出但内核还没处理完)
-
返回值:0,表示没有更多已退出的子进程
-
因为循环条件
> 0,所以此时不成立,退出while循环
3. waitpid 返回值说明
| 返回值 | 含义 |
|---|---|
> 0 | 成功回收了一个子进程,返回值是子进程PID |
= 0 | 使用WNOHANG时,没有已退出的子进程 |
-1 | 错误(如没有子进程,或被信号中断) |
4. 如果没有WNOHANG会怎样?
// 错误写法:会阻塞
id = waitpid(-1, &status, 0); // 阻塞等待,只处理一个子进程
结果:另外两个子进程会成为僵尸进程!因为:
-
handler可能被调用多次
-
但每次只处理一个子进程
-
如果信号丢失(同时到达,但是只处理一次),就可能有子进程无法被清理
5. 总结
WNOHANG 在多个子进程同时退出时的作用:
-
批量处理:一次handler调用清理所有已退出的子进程
-
避免阻塞:不会挂起父进程的执行
-
防止僵尸:确保所有子进程都被正确回收
这就是为什么在SIGCHLD处理函数中要使用 while + WNOHANG 的标准模式!
四、避免产生僵尸进程的另一种方法(重点!!!)
-
由于 UNIX 的历史原因,还有一种避免产生僵尸进程的方法:父进程调用
sigaction将SIGCHLD的处理动作置为SIG_IGN。 -
这样,
fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。 -
一般系统默认的忽略动作和用户用
sigaction函数自定义的忽略通常没有区别,但这是一个特例。(重点要注意的!!!) -
此方法对于 Linux 可用,但不保证在其它 UNIX 系统上都可用。
在 UNIX - like 系统中,信号处理机制通常遵循一些通用的规则,但 SIGCHLD 信号在某些方面表现出特殊性,尤其是在默认忽略和用户自定义忽略的处理上,以下是详细解释:
1、常规信号处理中默认忽略与用户自定义忽略的一致性
在大多数信号的情况下,系统默认的忽略动作和用户使用 sigaction 函数自定义的忽略动作在行为上是相似的。
-
系统默认忽略:当系统为某个信号设置了默认的忽略处理方式时,意味着当该信号产生时,系统不会执行任何特定的操作,信号会被简单地丢弃,不会对进程的执行流程产生任何影响。例如,对于一些不太关键或不需要特殊处理的信号,系统默认采用忽略的方式。
-
用户自定义忽略:用户可以通过
sigaction函数显式地将某个信号的处理方式设置为忽略。从行为上看,这与系统默认的忽略效果是一样的,即信号产生时不会触发任何额外的处理逻辑,进程继续正常执行。
2、SIGCHLD 信号的特殊性
man 7 signal 的作用是查看 Linux 信号概述和详细说明:
man 7 signal
我们下翻可以查找 SIGCHLD 信号的默认处理动作是忽略:

SIGCHLD 信号在子进程终止、暂停或继续时发送给父进程。其特殊性主要体现在以下方面:
(1)默认行为与预期不符
-
系统默认对
SIGCHLD信号的处理动作是忽略。 -
然而,这种默认的忽略行为会导致子进程在终止后变成僵尸进程。
-
僵尸进程是指子进程已经结束运行,但其在进程表中的条目仍然保留,因为它需要等待父进程获取其退出状态等信息。
-
如果父进程一直不处理
SIGCHLD信号(默认忽略),这些僵尸进程就会一直占用进程表的空间,直到父进程终止。
(2)用户自定义忽略的特殊效果
-
当用户使用
sigaction函数将SIGCHLD信号的处理动作显式设置为SIG_IGN时,情况与系统默认的忽略有所不同。 -
在这种情况下,子进程在终止时会自动被清理,不会产生僵尸进程。
-
这是因为内核在检测到父进程将
SIGCHLD信号的处理方式设置为忽略时,会采取一种特殊的处理机制,直接回收子进程的资源,而不需要父进程显式地调用wait或waitpid函数。
(3)历史原因与系统差异
-
这种特殊性源于 UNIX 系统的历史发展。
-
不同的 UNIX 系统在实现上可能存在一定的差异,虽然这种将
SIGCHLD设置为SIG_IGN来自动清理子进程的方法在 Linux 系统中是有效的,但不能保证在所有的 UNIX 系统上都能正常工作。 -
这是因为不同的系统对信号处理和进程管理的实现细节可能有所不同。
3、验证该方法的程序示例
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/signal.h> // 部分系统可能需要这个头文件,用于 sigaction 结构体等
#include <unistd.h>
int main() {
struct sigaction act;
act.sa_handler = SIG_IGN; // 设置处理动作为忽略
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
// 使用 sigaction 函数设置 SIGCHLD 信号的处理方式
if (sigaction(SIGCHLD, &act, NULL) == -1) {
perror("sigaction");
return 1;
}
pid_t cid;
if ((cid = fork()) == 0) { // 子进程
printf("Child process: %d\n", getpid());
sleep(3);
exit(2);
} else if (cid > 0) { // 父进程
while (1) {
printf("Father process is doing some work!\n");
sleep(1);
}
} else {
perror("fork");
return 1;
}
return 0;
}
代码说明:
-
使用
sigaction函数来设置SIGCHLD信号的处理方式。将act.sa_handler设置为SIG_IGN,表示忽略该信号。 -
同样通过
fork创建子进程,子进程睡眠 3 秒后退出,父进程进入无限循环模拟处理自己的工作。通过这种方式验证子进程终止时不会产生僵尸进程。
此时子进程在终止时会自动被清理掉,不会产生僵尸进程,也不会通知父进程。

1. 代码功能概述
这段代码演示了如何让父进程忽略子进程的退出信号,从而避免产生僵尸进程。
2. 逐行讲解
1. 设置信号忽略
struct sigaction act;
act.sa_handler = SIG_IGN; // 关键:设置处理动作为忽略
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
-
sa_handler = SIG_IGN:告诉内核忽略这个信号 -
sigemptyset(&act.sa_mask):执行handler时不额外屏蔽其他信号 -
sa_flags = 0:使用默认标志
2. 注册信号处理
sigaction(SIGCHLD, &act, NULL)
-
将
SIGCHLD信号的处理方式设置为"忽略" -
SIGCHLD是子进程状态改变时(退出、停止等)发送给父进程的信号
3. 创建子进程
if ((cid = fork()) == 0) { // 子进程
printf("Child process: %d\n", getpid());
sleep(3);
exit(2);
}
-
子进程打印自己的PID,睡眠3秒后退出
4. 父进程工作
else if (cid > 0) { // 父进程
while (1) {
printf("Father process is doing some work!\n");
sleep(1);
}
}
-
父进程进入无限循环,持续工作
3. 关键机制讲解
为什么要忽略 SIGCHLD?
默认情况:
-
子进程退出时,会向父进程发送
SIGCHLD信号 -
如果父进程不处理,子进程会成为"僵尸进程"
-
僵尸进程会占用系统资源
忽略 SIGCHLD 的效果:
act.sa_handler = SIG_IGN; // 魔法就在这里!
-
当父进程忽略
SIGCHLD信号时,内核会自动回收子进程资源 -
子进程不会成为僵尸进程
-
父进程不需要调用
wait()或waitpid()
4. 执行流程分析
时间线:
-
0秒:程序启动,设置忽略SIGCHLD
-
0秒:创建子进程
-
0-3秒:
-
父进程:持续打印工作信息
-
子进程:睡眠中
-
-
3秒:子进程退出
-
3秒后:内核自动回收子进程,没有僵尸进程
-
持续:父进程继续工作
5. 验证方法
你可以打开另一个终端验证:
# 查看进程状态
ps -ef | grep 你的程序名
# 或者查看所有进程状态
ps aux | grep Z # 查看僵尸进程
我们跑起来这个程序后,等待3秒,也就是子进程退出后,验证你会发现:没有僵尸进程产生!!!符合预期和结论!!!

6. 与默认行为的对比
| 处理方式 | 结果 |
|---|---|
| 默认处理 | 子进程成为僵尸进程,需要手动wait |
| 自定义handler | 需要编写清理代码,使用waitpid |
| 忽略SIGCHLD | 内核自动回收,无僵尸进程 |
7. 实际应用场景
这种技术在以下场景很有用:
-
服务器程序:不需要关心子进程退出状态
-
守护进程:创建临时工作进程
-
快速原型:简化进程管理
8. 注意事项
⚠️ 重要:如果你需要知道子进程的退出状态,不能使用这种方法,因为退出信息会被丢弃。
9. 总结
这段代码的核心价值在于:
-
使用
SIG_IGN忽略SIGCHLD信号 -
让内核自动处理子进程资源回收
-
父进程可以专注于自己的工作,不需要关心子进程清理
这是一种优雅的避免僵尸进程的方法!
通过以上两个程序示例,我们可以更好地理解和应用 SIGCHLD 信号来处理子进程的终止和僵尸进程的清理问题。
2092

被折叠的 条评论
为什么被折叠?



