Linux System V 信号量详解与进程同步实战
一、前言在 Linux 多进程编程中当多个进程需要访问临界资源同一时刻只能被一个进程访问的资源时就必须引入同步机制来避免竞争条件race condition。信号量Semaphore是经典的进程同步工具之一它可以实现进程间的互斥Mutex和同步Synchronization。本文将详细讲解System V 信号量的核心概念、相关 API以及一个简单却实用的示例两个进程通过信号量互斥地向屏幕输出字符“A”和“B”保证输出不会交错混乱。二、信号量基础概念信号量Semaphore本质上是一个整数计数器用于控制对共享资源的访问。P 操作wait / down申请资源信号量值减 1。若值变为负数或 0则进程阻塞等待。V 操作signal / up释放资源信号量值加 1。若有进程在等待则唤醒其中一个。临界资源同一时刻只允许一个进程访问的资源如共享内存、文件、终端输出等。临界区访问临界资源的代码段。本示例中我们使用二元信号量初始值为 1实现互斥访问终端输出确保每个进程的 “打印-睡眠-打印” 操作是原子性的。三、System V 信号量核心函数主要涉及三个系统调用头文件 sys/sem.hsemget()创建或获取信号量集。semctl()控制信号量如初始化值、删除信号量。semop()执行 P/V 操作。此外还需要联合体 union semun 来传递参数。四、代码实现1. 信号量封装头文件 sem.hC// sem.h #ifndef SEM_H #define SEM_H #include sys/sem.h union semun { // 用于 semctl 的联合体 int val; }; void sem_init(); // 初始化信号量 void sem_p(); // P 操作申请资源 void sem_v(); // V 操作释放资源 void sem_destroy(); // 删除信号量 #endif2. 信号量实现 sem.c或直接放在主文件中C// sem.c 或直接包含在 main 前 #include sem.h #include stdio.h #include sys/sem.h static int semid -1; void sem_init() { semid semget((key_t)1234, 1, IPC_CREAT | IPC_EXCL | 0600); // 尝试全新创建 if (semid -1) { // 已存在则直接获取 semid semget((key_t)1234, 1, 0600); if (semid -1) { perror(semget err); return; } } else { // 首次创建初始化为 1二元信号量实现互斥 union semun a; a.val 1; if (semctl(semid, 0, SETVAL, a) -1) { perror(semctl init err); } } } void sem_p() { struct sembuf buf {0, -1, SEM_UNDO}; // sem_num0, sem_op-1 (P), SEM_UNDO 保证进程异常退出时自动释放 if (semop(semid, buf, 1) -1) { perror(semop P err); } } void sem_v() { struct sembuf buf {0, 1, SEM_UNDO}; // sem_op1 (V) if (semop(semid, buf, 1) -1) { perror(semop V err); } } void sem_destroy() { if (semctl(semid, 0, IPC_RMID) -1) { perror(semctl destroy err); } }3. 进程 A输出 AC// processA.c #include sem.h #include stdio.h #include stdlib.h #include unistd.h int main() { sem_init(); srand(getpid()); // 随机种子更好 for (int i 0; i 5; i) { sem_p(); // 进入临界区 printf(A); fflush(stdout); int n rand() % 3; sleep(n); printf(A); fflush(stdout); sem_v(); // 离开临界区 n rand() % 3; sleep(n); // 非临界区睡眠模拟其他工作 } return 0; }4. 进程 B输出 B最后销毁信号量C// processB.c #include sem.h #include stdio.h #include stdlib.h #include unistd.h int main() { sem_init(); srand(getpid()); for (int i 0; i 5; i) { sem_p(); printf(B); fflush(stdout); int n rand() % 3; sleep(n); printf(B); fflush(stdout); sem_v(); n rand() % 3; sleep(n); } sleep(10); // 等待足够时间让 A 进程完成 sem_destroy(); // 销毁信号量 return 0; }五、编译与运行Bashgcc -o sem.o sem.c -c # 如果分开编译 gcc -o A processA.c sem.o gcc -o B processB.c sem.o ./A ./B 运行效果 你会看到类似 AABB AABB AA BB ... 的交替输出而不会出现 ABAB 这样的字符交错因为 printf(A); sleep(); printf(A); 被信号量保护成了原子操作。六、注意事项与扩展key 值使用固定 key如 1234便于两个进程共享。实际项目中推荐使用 ftok() 生成唯一 key。SEM_UNDO标志防止进程异常退出导致信号量死锁。二元信号量 vs 计数信号量本例初始值为 1实现互斥若初始值为 N则允许多个进程同时进入临界区。局限性System V 信号量是内核对象需要手动删除IPC_RMID否则会残留。可用 ipcs -s 查看ipcrm -s semid 删除。现代替代POSIX 信号量sem_open / sem_init更简单但 System V 信号量在老项目和共享内存同步中仍很常见。七、总结通过这个简单示例我们可以看到信号量如何优雅地解决进程互斥问题。掌握 semget、semop、semctl 三个函数是深入理解 Linux 进程间通信IPC的重要一步。