多子进程共享变量更新捕获异常:仅单一子进程生效的修复方案
问题根源:无同步机制引发的竞态条件
兄弟,你这是碰到了共享内存编程里的经典坑——竞态条件(Race Condition)!为啥只有第一个子进程的修改能被父进程捕获?本质是多个进程(父+所有子进程)在毫无约束的情况下同时读写共享的标志位c,导致整个操作流程完全乱套:
- 多个子进程可能同时看到
c='n',但只有第一个完成修改的进程能触发父进程的捕获,后续进程的修改会被覆盖或者根本没机会完成; - 更糟的是,子进程的“检查
c→修改n和pid→设置c='y'”不是原子操作,父进程可能在子进程还没写完n和pid就去读取,或者其他子进程中途打断修改流程。
修复方案:用信号量给共享内存加“互斥锁”
咱们得给共享内存的访问加个“门禁”——用System V信号量(和共享内存配套使用最方便)来实现互斥同步,确保同一时间只有一个进程能进入操作共享变量的临界区。具体分三步:
1. 创建并初始化信号量
- 建一个包含1个信号量的信号集,初始值设为1(相当于一把可复用的锁,1表示锁可用);
- 用
semop实现P(抢锁,信号量减1)和V(放锁,信号量加1)操作。
2. 临界区强制加锁
- 不管是父进程还是子进程,在检查/修改共享变量之前必须先抢锁,操作完再放锁;
- 这样就能保证“检查→修改→设置标志位”的整个流程是原子的,不会被其他进程打断。
3. 清理时别忘了信号量
- 处理Ctrl-C的信号函数里,除了杀子进程、释放共享内存,还要把信号量也删掉,不然会留在系统里占资源。
完整修复后的代码示例
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/sem.h> #include <sys/wait.h> #include <signal.h> #include <time.h> #define SHM_KEY 0x1234 #define SEM_KEY 0x5678 #define K 3 // 你可以按需修改子进程数量 struct data { int pid; int n; char c; }; int shmid; int semid; struct data *shared_data; pid_t *child_pids; // 信号量操作需要的联合体 union semun { int val; struct semid_ds *buf; unsigned short *array; }; // P操作:获取锁(信号量减1) void sem_p() { struct sembuf sb = {0, -1, 0}; semop(semid, &sb, 1); } // V操作:释放锁(信号量加1) void sem_v() { struct sembuf sb = {0, 1, 0}; semop(semid, &sb, 1); } // 处理Ctrl-C的清理函数 void sigint_handler(int sig) { printf("\n收到终止信号,正在清理所有资源...\n"); // 逐个终止子进程并等待退出 for (int i = 0; i < K; i++) { kill(child_pids[i], SIGTERM); waitpid(child_pids[i], NULL, 0); } // 分离并删除共享内存 shmdt(shared_data); shmctl(shmid, IPC_RMID, NULL); // 删除信号量集 semctl(semid, 0, IPC_RMID); free(child_pids); exit(0); } int main() { srand(time(NULL)); child_pids = malloc(K * sizeof(pid_t)); if (!child_pids) { perror("malloc failed"); exit(1); } // 创建共享内存 shmid = shmget(SHM_KEY, sizeof(struct data), IPC_CREAT | 0666); if (shmid == -1) { perror("shmget failed"); exit(1); } shared_data = (struct data *)shmat(shmid, NULL, 0); if (shared_data == (void *)-1) { perror("shmat failed"); exit(1); } // 初始化共享变量 shared_data->c = 'n'; shared_data->n = 0; shared_data->pid = 0; // 创建信号量并初始化为1(互斥锁初始可用) semid = semget(SEM_KEY, 1, IPC_CREAT | 0666); if (semid == -1) { perror("semget failed"); exit(1); } union semun sem_union; sem_union.val = 1; semctl(semid, 0, SETVAL, sem_union); // 注册Ctrl-C信号处理 signal(SIGINT, sigint_handler); // 创建K个子进程 for (int i = 0; i < K; i++) { pid_t pid = fork(); if (pid == -1) { perror("fork failed"); exit(1); } else if (pid == 0) { // 子进程循环逻辑 while (1) { // 随机休眠0-2秒 sleep(rand() % 3); sem_p(); // 进入临界区前抢锁 if (shared_data->c == 'n') { shared_data->n = rand() % 100; shared_data->pid = getpid(); printf("子进程PID %d:设置n为%d\n", getpid(), shared_data->n); shared_data->c = 'y'; } sem_v(); // 退出临界区后放锁 } exit(0); } else { child_pids[i] = pid; } } // 父进程循环逻辑 while (1) { // 随机休眠0-2秒 sleep(rand() % 3); sem_p(); // 进入临界区前抢锁 if (shared_data->c == 'y') { printf("父进程捕获:PID %d 修改了n为%d\n", shared_data->pid, shared_data->n); shared_data->c = 'n'; } sem_v(); // 退出临界区后放锁 } return 0; }
核心修复点解释
- 信号量互斥保护:所有访问共享内存的代码都被
sem_p()和sem_v()包裹,彻底杜绝了多个进程同时操作共享变量的情况; - 原子性保障:子进程的修改流程和父进程的捕获流程都变成了原子操作,不会被打断或干扰;
- 完整资源清理:新增了信号量的删除操作,避免系统中残留无用的IPC资源。
内容的提问来源于stack exchange,提问作者Hamsa
相关产品推荐
相关产品推荐

