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
相关产品推荐
相关产品推荐

