You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Shell开发:实现子进程非SIGINT信号终止及核心转储上报

实现Shell中子进程终止上报与核心转储提示的方案

首先,你提到的SIGCHLD信号确实是处理这类场景的核心——当子进程终止(不管是正常退出、被信号杀死还是触发核心转储),内核都会给父进程发送SIGCHLD信号。我们需要通过注册这个信号的处理函数,来捕获子进程的退出状态并完成你的需求。

我会一步步拆解实现逻辑,结合代码示例帮你理解:

1. 核心思路概述

  • 注册SIGCHLD信号的处理函数,确保父进程能收到子进程终止的通知
  • 在处理函数中使用waitpid()获取子进程的退出状态(避免僵尸进程)
  • 解析退出状态,区分「正常退出」「被信号终止」「核心转储」三种情况
  • 针对你的需求:排除SIGINT信号的终止,对其他信号终止的情况上报;若有核心转储,追加类似Bash的提示

2. 具体代码实现

先上完整的可运行示例,再逐一解释关键点:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

// SIGCHLD信号处理函数
void handle_sigchld(int sig) {
    int status;
    pid_t pid;

    // 循环处理所有已终止的子进程(WNOHANG非阻塞,避免卡住)
    while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
        if (WIFEXITED(status)) {
            // 子进程正常退出(比如调用exit()或return)
            printf("[%d] 正常退出,退出码:%d\n", pid, WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            // 子进程被信号终止
            int sig_num = WTERMSIG(status);
            // 排除SIGINT(信号2),只处理其他信号
            if (sig_num != SIGINT) {
                // 获取信号的友好名称(比如SIGTERM对应"Terminated")
                const char* sig_name = strsignal(sig_num);
                if (sig_name == NULL) sig_name = "unknown";

                // 判断是否产生核心转储
                if (WCOREDUMP(status)) {
                    printf("[%d] 被信号 %d (%s) 终止 (核心已转储)\n", pid, sig_num, sig_name);
                } else {
                    printf("[%d] 被信号 %d (%s) 终止\n", pid, sig_num, sig_name);
                }

                // 如果你需要输出128+信号号的格式(比如Bash的退出状态):
                // printf("child terminated (%d)\n", 128 + sig_num);
            }
        }
    }

    // 处理waitpid的错误(除了没有子进程的情况)
    if (pid == -1 && errno != ECHILD) {
        perror("waitpid error");
    }
}

int main() {
    // 注册SIGCHLD信号处理函数(用sigaction比signal更可靠)
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = handle_sigchld;
    // SA_RESTART:让被信号打断的系统调用自动重启(比如read、pause等)
    sa.sa_flags = SA_RESTART;

    if (sigaction(SIGCHLD, &sa, NULL) == -1) {
        perror("sigaction failed");
        exit(EXIT_FAILURE);
    }

    // 示例:fork一个子进程,模拟不同终止场景
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork failed");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // 子进程:可以替换成你需要测试的逻辑
        // 比如触发核心转储:raise(SIGSEGV);
        // 比如被SIGTERM终止:sleep(20);
        execlp("sleep", "sleep", "20", NULL);
        perror("execlp failed");
        exit(EXIT_FAILURE);
    } else {
        printf("子进程已启动,PID:%d\n", pid);
        // 父进程暂停,等待信号(实际Shell中这里会是命令行循环)
        pause();
    }

    return 0;
}

3. 关键细节解释

为什么用waitpid(-1, &status, WNOHANG)?

  • -1:表示等待任意子进程终止,适合Shell同时管理多个子进程的场景
  • WNOHANG:非阻塞模式,处理函数不会一直卡住,能一次性处理所有已终止的子进程,避免僵尸进程残留
  • 循环调用:确保不会漏掉任何一个子进程的终止通知(比如同时有多个子进程终止时)

解析退出状态的宏

  • WIFEXITED(status):判断子进程是否正常退出
  • WEXITSTATUS(status):获取正常退出的退出码
  • WIFSIGNALED(status):判断子进程是否被信号终止
  • WTERMSIG(status):获取终止子进程的信号编号
  • WCOREDUMP(status):判断子进程是否产生了核心转储(注意:部分系统需要定义_GNU_SOURCE才能使用)

信号名称的友好输出

strsignal()函数可以把信号编号转换成对应的名称(比如SIGTERM→"Terminated"),让输出和Bash的风格更一致。如果系统不支持这个函数,你也可以自己定义一个信号名称数组,比如:

const char* sig_names[] = {
    "", "Hangup", "Interrupt", "Quit", "Illegal instruction",
    // ... 按信号编号顺序补充
};

异步信号安全的注意事项

printf()并不是异步信号安全的函数(信号处理函数执行时,可能打断主进程的printf操作,导致输出错乱)。如果你的Shell需要更高的稳定性,可以改用write()函数来输出:

char buf[256];
int len = snprintf(buf, sizeof(buf), "[%d] 被信号 %d (%s) 终止 (核心已转储)\n", pid, sig_num, sig_name);
write(STDOUT_FILENO, buf, len);

不过snprintf()同样不是异步安全的,极端场景下可以手动拼接字符串,或者预先定义好所有信号的提示文本。

4. 后续推进建议

  • 把这个信号处理逻辑集成到你的Shell主循环中,替换示例中的pause()为命令行输入循环
  • 测试不同的终止场景:比如用kill -15 <子进程PID>发送SIGTERM,用kill -SEGV <子进程PID>触发核心转储,用Ctrl+C发送SIGINT(此时不会上报)
  • 如果需要支持后台进程,记得在fork后设置子进程为进程组组长,避免SIGINT被传递到后台进程

内容的提问来源于stack exchange,提问作者Lazarus

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 07:30:08