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

仅使用信号实现N个进程同步的代码问题排查

问题根因

你的代码存在4个核心问题,其中一个是低级逻辑错误,剩下三个是信号机制的典型使用误区:

  • 子进程判断逻辑完全写错:你写的判断条件是if (pid == 0),其中pid是进程ID数组的首地址,永远不可能等于0,导致所有fork出来的子进程、孙进程根本不会进入你写的子进程分支,不会执行exit(0),全部会继续执行父进程的后续逻辑。当N=2时,fork链路总共会生成4个独立进程(初始父进程、第一次fork的子进程、该子进程fork的孙进程、第二次fork的子进程),每个进程都会走到最后打印All fine,和你观察到的现象完全一致。正确的判断条件应该是if (pid[i] == 0),判断当前次fork返回的ID值。
  • pause()存在天然竞态:你期望最后一个子进程给父进程发SIGUSR2唤醒父进程,但如果信号在父进程执行到pause()之前就送达,信号处理函数执行完后父进程才走到pause(),就会永久阻塞等待一个已经收到过的信号,直接死锁。
  • 旧版signal()语义不可靠:多数Unix/Linux系统中,通过signal()注册的处理函数在信号触发一次后,会自动把信号处置重置为默认行为(SIGUSR1/SIGUSR2默认是直接终止进程),你代码里还是发完信号才重新注册handler,时序完全错误,很容易导致进程被信号直接杀死。
  • 信号发送逻辑越界:你在wait()循环里无条件执行i--再发信号,当所有子进程都回收完wait()返回-1时,i已经被减到-1,会执行kill(pid[-1], SIGUSR1)触发内存越界的未定义行为;同时你没有等待上一个子进程真正完成退出就发下一个信号,时序完全不可控。
修复方案

要实现纯信号的N进程顺序同步,需要做以下调整:

  1. 修正子进程判断条件,确保子进程执行完自身逻辑后立刻退出,不会跑父进程代码
  2. 用sigaction()替代旧的signal()注册信号处理函数,保证处理函数不会被自动重置
  3. 用sigsuspend()替代pause(),通过信号掩码实现原子等待,解决信号提前到达的竞态问题
  4. 调整唤醒逻辑,父进程确认所有子进程都进入等待状态后,再从最后一个子进程开始逆序发唤醒信号,每发送一个就等待对应子进程退出,避免越界访问。

修复后的可运行代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

// 信号处理函数中唯一安全的全局标志类型
volatile sig_atomic_t sig_arrived = 0;

void sigHandler(int signum)
{    
    sig_arrived = 1;
}

void sigHandler2(int signum)
{
    sig_arrived = 1;
}

int main(int argc, char *argv[])
{
    int N = 2;
    pid_t pid[N];
    int i;

    // 用sigaction可靠注册信号处理,避免旧signal的语义问题
    struct sigaction sa;
    sa.sa_handler = sigHandler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGUSR1, &sa, NULL);

    sa.sa_handler = sigHandler2;
    sigaction(SIGUSR2, &sa, NULL);

    // 初始化阶段先阻塞两个用户信号,避免信号提前到达导致丢唤醒
    sigset_t block_mask, old_mask;
    sigemptyset(&block_mask);
    sigaddset(&block_mask, SIGUSR1);
    sigaddset(&block_mask, SIGUSR2);
    sigprocmask(SIG_BLOCK, &block_mask, &old_mask);

    for (i = 0; i < N; i++)
    {
        pid[i] = fork();
        // 修正子进程判断条件
        if (pid[i] == 0)
        {
            sig_arrived = 0;
            // 最后一个子进程通知父进程:所有子进程已进入等待状态
            if (i == N - 1)
            {
                kill(getppid(), SIGUSR2);
            }
            // 原子等待信号:临时解除信号阻塞,直到收到信号唤醒
            while (!sig_arrived) {
                sigsuspend(&old_mask);
            }
            printf("%i completed\n", getpid());
            exit(0);
        }
    }

    // 父进程等待所有子进程就绪的通知信号
    sig_arrived = 0;
    while (!sig_arrived) {
        sigsuspend(&old_mask);
    }

    // 从最后一个子进程开始逆序唤醒,等当前子进程退出再唤醒下一个
    for (i = N-1; i >=0; i--) {
        kill(pid[i], SIGUSR1);
        waitpid(pid[i], NULL, 0);
    }

    printf("All fine\n");
    exit(0);
}
补充说明
  • 不要在信号处理函数里直接调用printf这类非异步信号安全的函数,否则可能触发死锁或输出错乱,信号处理函数里只做最简单的标志位设置即可。
  • 标准信号(SIGUSR1/SIGUSR2属于这类)默认不排队,同个信号多次发送只会保留一个,因此用标志位+循环等待的方式,比单纯调用一次pause()可靠得多。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 01:01:40