C语言fork()与kill()同时调用失效问题的解决方案
这个问题的核心原因很直白:父进程在fork出子进程后立刻发送SIGINT,但此时子进程可能还在完成execv的程序加载,或者刚启动还没执行到注册信号处理函数的代码。这时候SIGINT会触发默认的进程终止行为,子进程直接退出,根本没机会执行你的handler写日志,主进程的wait也会直接回收已经终止的子进程,不会等待那2秒的sleep。
sleep(1)能临时凑效,只是因为它给了子进程足够的时间完成信号处理函数的注册,但这完全不可靠——在资源紧张的系统上,1秒可能不够;在性能好的系统上,又是没必要的资源浪费。下面给你两个不需要依赖sleep的可靠解决方案:
方案1:信号阻塞同步(推荐,简洁高效)
思路是:父进程在fork前先阻塞SIGINT信号,子进程会继承这个阻塞状态。这样哪怕父进程立刻发送SIGINT,子进程也不会立刻处理这个信号,直到子进程完成信号处理函数的注册,主动解除SIGINT的阻塞,这时信号才会被触发执行你的handler。
主进程代码修改
在fork循环前添加SIGINT阻塞逻辑:
int main() { pid_t childs[CHILDS]; char *execv_argv[3]; int n = CHILDS; execv_argv[0] = "./debugging_procs/wait_time_at_interrupt"; execv_argv[1] = "2"; execv_argv[2] = NULL; // 阻塞SIGINT,子进程会继承这个阻塞状态 sigset_t sigint_mask; sigemptyset(&sigint_mask); sigaddset(&sigint_mask, SIGINT); if (sigprocmask(SIG_BLOCK, &sigint_mask, NULL) == -1) { perror("sigprocmask block failed"); exit(1); } for (int i = 0; i < n; i++) { childs[i] = fork(); if (childs[i] == 0) { execv(execv_argv[0], execv_argv); perror("execv failed"); _exit(1); } else if (childs[i] == -1) { perror("fork failed"); exit(1); } } // 直接发送SIGINT,子进程此时处于阻塞状态,不会立刻处理 for (int i = 0; i < n; i++) { if (kill(childs[i], SIGINT) == -1) { perror("kill failed"); } } // 等待所有子进程退出 while (wait(NULL) > 0); return 0; }
子进程代码修改
在注册完信号处理函数后,解除SIGINT的阻塞:
#include <signal.h> #include <stdlib.h> #include <unistd.h> #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> void sigint_handler(int signum) { // 加上O_CREAT和权限,避免日志文件不存在时打开失败 int fd = open("./aux/log1", O_WRONLY | O_APPEND | O_CREAT, 0644); if (fd == -1) { perror("open log1 failed"); return; } char buf[124]; (void)signum; sprintf(buf, "SIGINT %d\n", getpid()); write(fd, buf, strlen(buf)); close(fd); } int main(int argc, char **argv) { int wait_time = (argv[1]) ? atoi(argv[1]) : 5; // 用sigaction代替signal,行为更稳定可移植(signal的跨系统行为不一致) struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = sigint_handler; if (sigaction(SIGINT, &sa, NULL) == -1) { perror("sigaction failed"); exit(1); } // 解除SIGINT阻塞,此时之前收到的SIGINT会被触发处理 sigset_t sigint_mask; sigemptyset(&sigint_mask); sigaddset(&sigint_mask, SIGINT); if (sigprocmask(SIG_UNBLOCK, &sigint_mask, NULL) == -1) { perror("sigprocmask unblock failed"); exit(1); } pause(); sleep(wait_time); return 0; }
这里还做了两个实用优化:
- 子进程的
open调用新增了O_CREAT和权限参数,防止日志文件不存在时打开失败; - 用
sigaction替代了signal函数,因为signal的行为在不同UNIX系统上存在差异,sigaction是POSIX标准接口,行为更一致可靠。
方案2:管道同步(适合复杂初始化场景)
如果需要更明确的"子进程完全准备好"的通知,可以用管道实现父子进程的同步:
- 父进程创建一个匿名管道;
- fork子进程后,父进程等待从管道读取数据;
- 子进程完成信号处理函数注册等初始化工作后,向管道写入一个字节;
- 父进程收到数据后,再发送SIGINT信号。
这种方法逻辑更直观,但代码相对繁琐,适合子进程需要完成多步初始化后才能处理信号的场景。
适配你的任务控制程序场景
不管是执行一次还是多次restart my_program命令,方案1的信号阻塞方法都能稳定工作:每次创建子进程时,子进程都会继承SIGINT的阻塞状态,直到它注册好信号处理函数后才解除阻塞,确保SIGINT只会在子进程准备就绪的时候被处理,彻底解决行为不稳定的问题。
内容的提问来源于stack exchange,提问作者Emil Terman

