仅使用信号实现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进程顺序同步,需要做以下调整:
- 修正子进程判断条件,确保子进程执行完自身逻辑后立刻退出,不会跑父进程代码
- 用
sigaction()替代旧的signal()注册信号处理函数,保证处理函数不会被自动重置 - 用
sigsuspend()替代pause(),通过信号掩码实现原子等待,解决信号提前到达的竞态问题 - 调整唤醒逻辑,父进程确认所有子进程都进入等待状态后,再从最后一个子进程开始逆序发唤醒信号,每发送一个就等待对应子进程退出,避免越界访问。
修复后的可运行代码如下:
#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
相关产品推荐
相关产品推荐

