# 信号量 (Semaphore) ## 概念 信号量是一个计数器,用于控制多个进程/线程对共享资源的访问。信号量支持两个原子操作: - **P操作(wait)**:计数器减1,若计数器为0则阻塞等待 - **V操作(post)**:计数器加1,唤醒等待的进程/线程 **核心特点**: - 用于同步,而非数据传输 - 支持进程间和线程间同步 - 二值信号量(0/1)实现互斥,计数信号量控制并发数量 ## 二值信号量 vs 计数信号量 | 类型 | 计数值范围 | 典型用途 | | ---------- | ---------- | ---------------------------- | | 二值信号量 | 0 或 1 | 互斥访问(类似互斥锁) | | 计数信号量 | 0 ~ N | 限制并发访问数量(如连接池) | ## System V 信号量 ### 关键API ```c #include #include #include // 创建/获取信号量集 int semget(key_t key, int nsems, int semflg); // 信号量操作 int semop(int semid, struct sembuf *sops, unsigned nsops); // 信号量控制 int semctl(int semid, int semnum, int cmd, ...); ``` ### sembuf 结构体 ```c struct sembuf { unsigned short sem_num; // 信号量编号(在信号量集中的索引) short sem_op; // 操作:-1=P操作,+1=V操作,0=等待为0 short sem_flg; // 标志:IPC_NOWAIT(非阻塞),SEM_UNDO(进程退出自动还原) }; ``` ### semctl 常用命令 | 命令 | 说明 | | ---------- | ---------------- | | `SETVAL` | 设置信号量值 | | `GETVAL` | 获取信号量值 | | `IPC_RMID` | 删除信号量集 | | `GETALL` | 获取所有信号量值 | | `SETALL` | 设置所有信号量值 | ## POSIX 信号量 ### 关键API ```c #include // 初始化信号量 int sem_init(sem_t *sem, int pshared, unsigned int value); // P操作(等待/减1) int sem_wait(sem_t *sem); // 非阻塞P操作 int sem_trywait(sem_t *sem); // V操作(通知/加1) int sem_post(sem_t *sem); // 销毁信号量 int sem_destroy(sem_t *sem); ``` **参数说明**: - `pshared`:0 表示线程间共享,非0 表示进程间共享(需放在共享内存中) - 命名信号量:`sem_open` / `sem_close` / `sem_unlink`(可用于无亲缘关系的进程) ## 信号量与共享内存配合(经典组合) 信号量最常见的用途是保护共享内存的并发访问: ``` 进程A 进程B │ │ ├── P(写锁) ────────────>│ │ 写入共享内存 │ ├── V(读锁) ────────────>│ │ P(读锁) │ 读取共享内存 │ V(写锁) ``` ## 完整代码示例:生产者-消费者 ```c // sem_prodcons.c #include #include #include #include #include #define BUFFER_SIZE 5 int buffer[BUFFER_SIZE]; int in = 0, out = 0; sem_t sem_empty; // 空闲槽位数 sem_t sem_full; // 已用槽位数 sem_t sem_mutex; // 互斥访问缓冲区 void *producer(void *arg) { int id = *(int *)arg; for (int i = 0; i < 10; i++) { int item = id * 100 + i; sem_wait(&sem_empty); // 等待空闲槽位 sem_wait(&sem_mutex); // 互斥访问 buffer[in] = item; in = (in + 1) % BUFFER_SIZE; printf("[Producer %d] Produced: %d\n", id, item); sem_post(&sem_mutex); // 释放互斥 sem_post(&sem_full); // 增加已用槽位 usleep(100000); // 模拟生产耗时 } return NULL; } void *consumer(void *arg) { int id = *(int *)arg; for (int i = 0; i < 10; i++) { sem_wait(&sem_full); // 等待有数据 sem_wait(&sem_mutex); // 互斥访问 int item = buffer[out]; out = (out + 1) % BUFFER_SIZE; printf("[Consumer %d] Consumed: %d\n", id, item); sem_post(&sem_mutex); // 释放互斥 sem_post(&sem_empty); // 增加空闲槽位 usleep(150000); // 模拟消费耗时 } return NULL; } int main(void) { pthread_t prod_tid[2], cons_tid[2]; int prod_ids[2] = {1, 2}; int cons_ids[2] = {1, 2}; // 初始化信号量 sem_init(&sem_empty, 0, BUFFER_SIZE); // 初始空闲槽位 = 缓冲区大小 sem_init(&sem_full, 0, 0); // 初始已用槽位 = 0 sem_init(&sem_mutex, 0, 1); // 互斥信号量初始值 = 1 // 创建生产者和消费者线程 for (int i = 0; i < 2; i++) { pthread_create(&prod_tid[i], NULL, producer, &prod_ids[i]); pthread_create(&cons_tid[i], NULL, consumer, &cons_ids[i]); } // 等待线程结束 for (int i = 0; i < 2; i++) { pthread_join(prod_tid[i], NULL); pthread_join(cons_tid[i], NULL); } // 销毁信号量 sem_destroy(&sem_empty); sem_destroy(&sem_full); sem_destroy(&sem_mutex); printf("All done.\n"); return 0; } ``` **编译与运行**: ```bash gcc -o sem_prodcons sem_prodcons.c -pthread ./sem_prodcons ``` ## System V 信号量示例 ```c // sem_sysv.c #include #include #include #include #include #include #include #define SEM_KEY 0x5678 union semun { int val; struct semid_ds *buf; unsigned short *array; }; void p_operation(int semid, int sem_num) { struct sembuf op = {sem_num, -1, 0}; if (semop(semid, &op, 1) == -1) { perror("P operation"); exit(EXIT_FAILURE); } } void v_operation(int semid, int sem_num) { struct sembuf op = {sem_num, 1, 0}; if (semop(semid, &op, 1) == -1) { perror("V operation"); exit(EXIT_FAILURE); } } int main(void) { // 创建信号量集(1个信号量) int semid = semget(SEM_KEY, 1, IPC_CREAT | 0666); if (semid == -1) { perror("semget"); exit(EXIT_FAILURE); } // 初始化信号量值为 1(二值信号量) union semun arg; arg.val = 1; if (semctl(semid, 0, SETVAL, arg) == -1) { perror("semctl SETVAL"); exit(EXIT_FAILURE); } pid_t pid = fork(); if (pid == -1) { perror("fork"); exit(EXIT_FAILURE); } if (pid == 0) { // 子进程:进入临界区 printf("[Child] Waiting for semaphore...\n"); p_operation(semid, 0); printf("[Child] Entered critical section\n"); sleep(2); printf("[Child] Leaving critical section\n"); v_operation(semid, 0); exit(EXIT_SUCCESS); } else { // 父进程:等待信号量 printf("[Parent] Waiting for semaphore...\n"); p_operation(semid, 0); printf("[Parent] Entered critical section\n"); sleep(2); printf("[Parent] Leaving critical section\n"); v_operation(semid, 0); wait(NULL); } // 删除信号量集 semctl(semid, 0, IPC_RMID); printf("Semaphore removed\n"); return 0; } ``` **编译与运行**: ```bash gcc -o sem_sysv sem_sysv.c ./sem_sysv ``` ## 注意事项 1. **信号量不是锁**:信号量用于同步计数,互斥锁才是真正的互斥机制 2. **死锁风险**:P操作顺序不当可能导致死锁,应按固定顺序获取信号量 3. **SEM_UNDO 标志**:使用 `SEM_UNDO` 可防止进程异常退出后信号量未释放 4. **POSIX 信号量移植性更好**:优先使用 POSIX 接口 5. **命名信号量**:POSIX 命名信号量(`sem_open`)可用于无亲缘关系的进程 6. **性能**:信号量操作是系统调用,频繁操作有开销;线程间可考虑用户态同步原语 ## 面试要点 1. **P/V 操作含义**:P = Proberen(测试/等待),V = Verhogen(增加/释放) 2. **信号量 vs 互斥锁**:信号量可以跨进程、可计数;互斥锁更简单但仅限线程 3. **经典同步问题**:生产者-消费者、读者-写者、哲学家就餐 4. **信号量与共享内存配合**:共享内存提供数据,信号量提供同步 5. **System V vs POSIX**:POSIX 更现代、可移植,推荐在新项目中使用 6. **死锁预防**:固定加锁顺序、超时机制、资源分级