父子进程的不同信号处理程序:fork后信号处理时机疑问
首先,先明确fork()调用时发生的信号相关继承行为:
当你调用fork()创建子进程时,子进程会完全继承父进程的信号处理配置——包括已设置的信号处理函数、信号掩码(阻塞的信号集合),以及当前的未决信号集(已经收到但还没处理的信号)。所以在fork()完成后,子进程一开始的SIGINT处理函数就是父进程设置的signalhandler,直到子进程调用signal(SIGINT, signalhandler_for_child)覆盖它为止。
1. 如果在子进程设置新处理函数前收到SIGINT会发生什么?
如果此时子进程收到SIGINT,内核会调用父进程的signalhandler函数来处理这个信号——因为这时候子进程还没替换掉继承来的处理函数。
举个简单的验证示例:
#include <stdio.h> #include <signal.h> #include <unistd.h> void signalhandler(int sig) { printf("Parent's handler called (PID: %d)\n", getpid()); } void signalhandler_for_child(int sig) { printf("Child's handler called (PID: %d)\n", getpid()); } int main() { signal(SIGINT, signalhandler); pid_t pid = fork(); if (pid == 0) { // 故意让子进程暂停,模拟未设置新handler的时间窗口 sleep(2); signal(SIGINT, signalhandler_for_child); printf("Child has set its own handler\n"); while(1); } else { printf("Parent PID: %d, Child PID: %d\n", getpid(), pid); while(1); } return 0; }
运行程序后,在2秒内用kill -INT <子进程PID>发送信号,你会看到输出父进程的handler信息,而非子进程的。
2. 这种情况是否可能出现?
完全可能。进程的调度由操作系统内核决定:fork()完成后,内核可能先调度父进程,也可能先调度子进程,但子进程从fork()返回后,需要执行到signal()调用才能替换处理函数——这中间的时间窗口里,内核随时可能把CPU时间片分给子进程,同时如果SIGINT信号到达,就会触发父进程的handler。
哪怕你的代码里fork()之后紧接着就是子进程的signal()调用,也无法完全避免这种情况——内核可能在fork()返回后、子进程执行signal()前插入其他调度事件,比如信号投递。
3. 如何对信号进行排队处理?
首先要明确:普通信号(比如SIGINT)是不支持内核级排队的。如果在这段时间内多次收到同一个普通信号,内核只会保留一个实例,子进程最终只会处理一次。
如果需要真正的信号排队,你需要使用实时信号(范围是SIGRTMIN到SIGRTMAX,具体数量取决于系统,一般至少有32个),同时配合sigaction()而非signal()来设置处理函数,并且要指定SA_SIGINFO标志——这样内核会为实时信号维护队列(最多排队SIGQUEUE_MAX个,不同系统有不同限制),且能传递额外信息。
示例代码片段:
// 父进程设置实时信号处理(可选,子进程会继承) struct sigaction sa; sa.sa_sigaction = parent_rt_handler; sa.sa_flags = SA_SIGINFO; sigemptyset(&sa.sa_mask); sigaction(SIGRTMIN, &sa, NULL); pid_t pid = fork(); if (pid == 0) { // 子进程设置自己的实时信号处理 struct sigaction child_sa; child_sa.sa_sigaction = child_rt_handler; child_sa.sa_flags = SA_SIGINFO; sigemptyset(&child_sa.sa_mask); sigaction(SIGRTMIN, &child_sa, NULL); }
发送实时信号时要用sigqueue()而非kill(),以保证信号排队并传递自定义数据:
union sigval val; val.sival_int = 123; // 自定义数据 sigqueue(pid, SIGRTMIN, val);
如果一定要针对SIGINT这类普通信号实现类似"排队"的效果,你需要在用户态自己维护计数器——每次收到信号时递增计数器,然后在程序主循环里定期检查计数器,处理积压的信号事件。但这是模拟排队,并非内核级的原生支持。
内容的提问来源于stack exchange,提问作者Daniel Ortega

