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

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:管道同步(适合复杂初始化场景)

如果需要更明确的"子进程完全准备好"的通知,可以用管道实现父子进程的同步:

  1. 父进程创建一个匿名管道;
  2. fork子进程后,父进程等待从管道读取数据;
  3. 子进程完成信号处理函数注册等初始化工作后,向管道写入一个字节;
  4. 父进程收到数据后,再发送SIGINT信号。

这种方法逻辑更直观,但代码相对繁琐,适合子进程需要完成多步初始化后才能处理信号的场景。


适配你的任务控制程序场景

不管是执行一次还是多次restart my_program命令,方案1的信号阻塞方法都能稳定工作:每次创建子进程时,子进程都会继承SIGINT的阻塞状态,直到它注册好信号处理函数后才解除阻塞,确保SIGINT只会在子进程准备就绪的时候被处理,彻底解决行为不稳定的问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 08:48:52