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

多子进程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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.22 08:54:18