多子进程SIGUSR1信号处理异常及printf替代方案咨询
嘿,这个问题我之前踩过坑!咱们一步步拆解解决:
为什么信号会丢失?
首先得搞清楚核心原因:SIGUSR1是不可靠信号,这类信号的特性是不会排队——如果多个同类型信号在父进程处理前扎堆到达,内核只会保留一个,剩下的直接丢弃。另外,父进程在wait()这类系统调用时被信号打断,也可能导致后续逻辑跳错,漏处理信号。
修复信号丢失的方案
我们需要用更可靠的信号处理机制,同时确保父进程能正确回收所有子进程、处理每一个信号:
1. 用sigaction()替代signal()设置信号处理
signal()的行为在不同系统里有差异,而sigaction()能明确控制信号处理的属性,比如开启SA_RESTART让被信号打断的系统调用自动重启,或者手动处理EINTR错误。另外,我们需要一个原子性的计数器来记录收到的信号数——因为信号处理和主进程是异步执行的,必须用volatile sig_atomic_t类型,防止编译器优化导致变量更新不及时。
2. 父进程循环等待所有子进程,处理信号中断
当父进程在waitpid()时收到信号,waitpid()会返回-1并把errno设为EINTR,这时候我们不能直接退出,得重新调用waitpid(),直到所有子进程都被回收。
3. 可选:改用实时信号彻底避免丢失
如果你的系统支持实时信号(大部分现代Unix/Linux都支持),可以把SIGUSR1换成SIGRTMIN到SIGRTMAX之间的信号——这类信号是排队的,每个发送的信号都会被内核保存并逐个传递给父进程,从根源上解决信号丢失问题。
信号处理函数里替代printf的方法
printf是不可重入函数(内部维护了全局缓冲区等状态),在信号处理中调用可能导致数据损坏甚至死锁。正确的做法是用write()系统调用,它是可重入的,直接向文件描述符写入数据(stdout的文件描述符是1)。
修复后的完整代码(main.c)
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> #include <sys/wait.h> #include <errno.h> #include <string.h> #include <time.h> // 原子计数器,记录收到的信号数 volatile sig_atomic_t signal_count = 0; // 信号处理函数:用write替代printf void sig_handler(int sig) { signal_count++; const char msg[] = "Got signal from child!\n"; // write是可重入的,直接输出到stdout write(1, msg, strlen(msg)); } int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "Usage: %s <number_of_children>\n", argv[0]); exit(EXIT_FAILURE); } int N = atoi(argv[1]); struct sigaction sa; // 初始化随机数种子,让子进程的等待时间更随机 srand((unsigned int)time(NULL)); // 设置信号处理(这里用SIGRTMIN做示例,也可以换回SIGUSR1) #define USE_RELIABLE_SIGNAL 1 #ifdef USE_RELIABLE_SIGNAL int target_sig = SIGRTMIN; #else int target_sig = SIGUSR1; #endif memset(&sa, 0, sizeof(sa)); sa.sa_handler = sig_handler; // SA_RESTART让被信号打断的系统调用自动重启(可选,也可以手动处理EINTR) sa.sa_flags = SA_RESTART; if (sigaction(target_sig, &sa, NULL) == -1) { perror("Failed to set signal handler"); exit(EXIT_FAILURE); } // 创建N个子进程 for (int i = 0; i < N; i++) { pid_t pid = fork(); if (pid == -1) { perror("Fork failed"); exit(EXIT_FAILURE); } else if (pid == 0) { // 子进程:随机等待0-3秒 sleep(rand() % 4); // 向父进程发送信号 kill(getppid(), target_sig); // 子进程必须退出,避免执行父进程后续代码 exit(EXIT_SUCCESS); } } // 父进程循环等待所有子进程结束 int remaining_children = N; while (remaining_children > 0) { pid_t pid = waitpid(-1, NULL, 0); if (pid == -1) { if (errno == EINTR) { // 被信号打断,重新等待 continue; } perror("Waitpid failed"); exit(EXIT_FAILURE); } remaining_children--; } // 输出最终统计结果(这里用snprintf拼接字符串,然后write输出) char final_msg[64]; snprintf(final_msg, sizeof(final_msg), "Total signals received: %d\n", signal_count); write(1, final_msg, strlen(final_msg)); return EXIT_SUCCESS; }
关键细节提醒
volatile sig_atomic_t:这个类型是C标准专门为信号处理场景定义的,确保变量在异步环境下能被安全读写,不会被编译器优化掉。- 实时信号的选择:如果用SIGRTMIN,记得不要和系统其他服务使用的实时信号冲突,大部分场景下SIGRTMIN是安全的。
- 子进程必须exit:fork之后子进程会从fork的位置继续执行,所以子进程发送信号后一定要调用
exit(),否则会跑到父进程的等待逻辑里,导致错误。
内容的提问来源于stack exchange,提问作者grzegorzs

