基于C POSIX IPC信号量与共享内存实现计数信号量遇阻塞问题
问题分析与修复方案
核心错误点
- LOCK信号量初始值错误:原代码中将用于挂起等待进程的LOCK信号量初始值设为1,不符合设计逻辑。该信号量初始值应为0,仅当计数从0变为1时才触发一次up操作唤醒等待进程。
- sem_up共享内存泄漏:原代码中仅在
*data ==1的分支执行了dettach_n,其余分支未释放共享内存映射,会导致引用计数异常、资源泄漏。 - sem_down逻辑漏洞:进程因计数为0阻塞在LOCK上被唤醒后,没有对共享计数器执行减1操作,直接返回导致计数逻辑完全混乱。
- 临界区保护不完整:阻塞被唤醒后的计数器操作没有纳入临界区保护,存在竞态风险。
修复后的核心代码
1. 信号量初始化修正
int sem_init(Sem *s, int n) { key_t key; Args arg; int *data; /* sem */ if ((key = ftok(PATH, SID)) == -1) { return -1; } if ((s->semid = semget(key, SEMN, 0600 | IPC_CREAT)) == -1) { return -1; } // CRIT信号量初始为1,用于保护临界区 arg.val = 1; if (semctl(s->semid, CRIT, SETVAL, arg) == -1) { return -1; } // LOCK信号量初始为0,用于挂起等待计数的进程 arg.val = 0; if (semctl(s->semid, LOCK, SETVAL, arg) == -1) { return -1; } /* 共享内存初始化部分不变 */ if ((key = ftok(PATH, SHMID)) == -1) { return -1; } if ((s->shmid = shmget(key, SHM_SIZE, 0664 | IPC_CREAT)) == -1) { return -1; } data = shmat(s->shmid, (void *)0, 0); if (data == (int *)(-1)) { return -1; } *data = n; if (shmdt(data) == -1) { return -1; } return 0; }
2. sem_down 逻辑修正
int sem_down(Sem *s) { int *data; enter_crit(s); data = get_n(s); printf("[%d] checked %d\n", getpid(), *data); if (*data == 0) { dettach_n(data); exit_crit(s); // 阻塞到LOCK上,被唤醒说明已有可用计数 acquire_lock(s); // 被唤醒后重新进入临界区操作计数器 enter_crit(s); data = get_n(s); } // 所有分支统一执行计数器减1 (*data)--; dettach_n(data); exit_crit(s); return 0; }
3. sem_up 逻辑修正
int sem_up(Sem *s) { int *data; enter_crit(s); data = get_n(s); printf("[%d] read %d\n", getpid(), *data); (*data)++; // 判断是否需要唤醒等待进程 int need_wake = (*data == 1); // 所有分支统一释放共享内存映射 dettach_n(data); exit_crit(s); if (need_wake) { release_lock(s); } return 0; }
逻辑说明
- CRIT二元信号量始终用于保护共享计数器的读写操作,保证同一时间只有一个进程访问计数器,避免竞态。
- 当计数器为0时,后续申请信号量的进程会阻塞在LOCK信号量上,直到有进程释放信号量使得计数器从0变为1,触发一次LOCK的up操作唤醒一个阻塞进程。
- 被唤醒的进程需要重新进入临界区,完成计数器减1操作,确保计数逻辑正确。
- 每次操作共享内存后都统一执行解除映射操作,避免资源泄漏。
测试时可设置SLOTS=2、NPROC=5,可以观察到同一时间最多2个进程进入working状态,所有5个进程都能正常退出,不会出现永久阻塞的问题。如果测试过程中出现异常退出导致System V IPC资源残留,可以用ipcs查看残留的信号量、共享内存,用ipcrm命令手动清理。
内容的提问来源于stack exchange,提问作者argot
相关产品推荐
相关产品推荐

