Linux:对有关 SIGCHLD 的深层理解

从 signal(SIGCHLD, SIG_IGN) 说起:默认忽略为什么不等于自动回收子进程?

很多人第一次看到这行代码都会疑惑:

c 复制代码
signal(SIGCHLD, SIG_IGN);

然后问:

SIGCHLD 的默认动作不就是忽略吗?那这行是不是脱裤子放屁?

答案很关键:

SIGCHLD 默认动作是"忽略信号",但默认忽略并不会自动回收子进程。显式设置 SIG_IGN 或 SA_NOCLDWAIT,才会让内核自动回收子进程,避免僵尸。


一、前置知识:子进程退出后发生了什么?

在 Linux 中,父进程 fork() 出子进程。子进程退出后,并不是立刻从系统里彻底消失。

子进程退出时,内核会:

  1. 释放子进程的大部分资源,比如内存、文件描述符等;
  2. 保留一小部分信息,比如退出状态、PID、资源使用统计;
  3. 等待父进程调用 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;
}
相关推荐
布裘16 分钟前
【银河麒麟】重启后时区丢失?排查发现 localtime 被业务程序替换成硬链接
linux·运维·网络
zhikouai1 小时前
把外部助手接进聊天软件的架构分层与参数配置
服务器·网络·php
huainingning1 小时前
锐捷10.X路由器版本升级不生效问题排查
运维·网络·智能路由器
荣合技术服务1 小时前
HPC 超算集群故障维修实录:找荣合科算维护后,模拟仿真计算又能跑了
运维·服务器·超算服务器·荣合科算
GeW3 小时前
高端化转型进入“补底层”阶段:Linux与数据库的再定位
linux
silent3 小时前
龙芯3B6000M运行Yolov8+FastApi
linux·loongarch·loongnix·龙芯
西西弗Sisyphus4 小时前
理解 Linux 动态库加载:LD_LIBRARY_PATH
linux
AR-26710-4 小时前
Linux Day31——实操启停服务改端口重启
linux·运维
百度Geek说5 小时前
QCon上海2026:百度 SRE Agent 规模化落地实践解读
运维·自动化运维