System V 信号量
概述
System V 信号量是内核提供的同步原语, 用于跨进程的互斥与资源计数。信号量本质上是一个计数器, 支持 P 操作 (wait, 减 1) 和 V 操作 (signal, 加 1)。信号量通常用于控制对共享资源的访问, 实现进程间的同步。
典型用途:
- 互斥锁: 初值 1, 保证同一时刻只有一个进程进入临界区
- 计数信号量: 初值为可用资源数, 控制并发访问额度
- 进程间同步: 作为事件/条件的等待与通知
信号量由内核维护, 通过键值 key 标识, 支持集合 (多个 sem 组成的数组)。
通信原理
基本概念
信号量的特点:
- 计数器: 本质上是一个非负整数计数器
- 原子操作: P 和 V 操作是原子的, 不会被中断
- 阻塞机制: 当信号量为 0 时, P 操作会阻塞
- 同步原语: 用于实现互斥和同步
- 集合概念: System V 信号量以集合形式存在
实现机制
-
创建/获取信号量集:
- 使用
semget()系统调用创建或获取信号量集 - 通过唯一的键值 (key) 标识信号量集
- 指定信号量集中信号量的数量
- 返回信号量集标识符 (semid)
- 使用
-
信号量操作:
- 使用
semop()对信号量进行 P/V 操作 - P 操作 (wait): 信号量减 1, 如果为 0 则阻塞
- V 操作 (signal): 信号量加 1, 唤醒等待的进程
- 使用
-
信号量控制:
- 使用
semctl()设置信号量的初始值 - 可以获取信号量的值
- 可以删除信号量集
- 使用
-
同步模式:
进程A: P操作(等待) ──┐ ├──> [信号量] ──> 控制共享资源访问 进程B: P操作(等待) ──┘ 进程C: V操作(释放) ──> 信号量+1,唤醒等待进程 -
互斥模式:
- 信号量初始值为 1
- 进程访问共享资源前执行 P 操作
- 访问完成后执行 V 操作
- 保证同一时刻只有一个进程访问资源
使用流程
semget创建/获取信号量集 (指定 key、数量、权限, 可带 IPC_CREAT/IPC_EXCL)semctl初始化值 (SETVAL/SETALL), 可查询/删除 (GETVAL/IPC_RMID)semop执行 P/V 操作 (原子减/加), 支持阻塞或非阻塞 (SEM_UNDO/IPC_NOWAIT)- 使用完后可由负责的进程调用
semctl(..., IPC_RMID, ...)删除信号量集
API 说明
semget()
c
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
- 功能: 创建或获取信号量集
- 参数 :
key: 信号量集的键值 (可以用ftok()生成或使用 IPC_PRIVATE)nsems: 信号量集中信号量的数量semflg: 标志位 (IPC_CREAT, IPC_EXCL, 权限等)
- 返回值: 成功返回信号量集标识符, 失败返回 -1
- 常见错误: EACCES, EEXIST, ENOENT, ENOMEM, ENOSPC
semop()
c
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);
- 功能: 原子执行一组 P/V 操作
- 参数 :
semid: 信号量集标识符sops: 操作数组指针nsops: 操作数量
- 返回值: 成功返回 0, 失败返回 -1
- 常见错误: EACCES, EIDRM, EINVAL, EAGAIN (非阻塞且资源不足), EINTR (被信号中断)
sembuf 结构
c
struct sembuf {
unsigned short sem_num; // 信号量索引/编号
short sem_op; // 操作值: 负数=等待并减 (P), 正数=加 (V), 0=等待为 0
short sem_flg; // 标志位: SEM_UNDO (进程退出自动回滚), IPC_NOWAIT (非阻塞)
};
semctl()
c
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
- 功能: 控制/查询信号量
- 参数 :
semid: 信号量集标识符semnum: 信号量编号cmd: 命令 (SETVAL 设置值, GETVAL 获取值, IPC_RMID 删除等)...: 可变参数 (根据 cmd 不同而不同, 需要 union semun 作为第四参数)
- 常用 cmd :
SETVAL/GETVAL: 设定/获取单个信号量值SETALL/GETALL: 设定/获取信号量数组IPC_RMID: 删除信号量集IPC_STAT/IPC_SET: 查询/设置权限与属性
- 返回值: 根据 cmd 不同而不同, 失败返回 -1
- 注意: 需要 union semun 作为第四参数 (用户需自行声明)
示例代码
示例 1: 互斥锁风格的 P/V
c
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
// P 操作: 获取锁 (阻塞等待)
int sem_lock(int semid, int idx) {
struct sembuf op = { .sem_num = idx, .sem_op = -1, .sem_flg = SEM_UNDO };
return semop(semid, &op, 1);
}
// V 操作: 释放锁
int sem_unlock(int semid, int idx) {
struct sembuf op = { .sem_num = idx, .sem_op = 1, .sem_flg = SEM_UNDO };
return semop(semid, &op, 1);
}
int main(void) {
key_t key = ftok(".", 's');
int semid = semget(key, 1, IPC_CREAT | 0666);
if (semid == -1) {
perror("semget");
return 1;
}
// 初始化为 1 (互斥锁)
union semun arg;
arg.val = 1;
if (semctl(semid, 0, SETVAL, arg) == -1) {
perror("semctl");
return 1;
}
if (sem_lock(semid, 0) == -1) {
perror("lock");
return 1;
}
printf("PID %d in critical section\n", getpid());
sleep(1); // 模拟工作
sem_unlock(semid, 0);
// 由负责的进程删除信号量 (可选)
semctl(semid, 0, IPC_RMID);
return 0;
}
说明:
- 互斥锁语义: 初值设为 1, P=-1 获取, V=+1 释放
- 阻塞行为: 未持有锁时 P 会睡眠; 使用 IPC_NOWAIT 可改为非阻塞
- SEM_UNDO: 进程异常退出时内核回滚 sem 值, 降低死锁风险
示例 2: 计数信号量 (资源池)
c
// 初始化为资源数 N; P 获取一个资源, V 归还
// 将 SETVAL 初值设为可用资源总数 (例如连接数、缓冲块数)
// 当 sem_op=-1 时, 若值为 0 则阻塞或 EAGAIN (IPC_NOWAIT)
参考 examples/05-semaphore/ 目录下的更多示例代码。
性能评价
优点
- 同步机制: 提供强大的进程同步能力
- 原子操作: P/V 操作是原子的, 保证正确性
- 阻塞机制: 自动处理进程阻塞和唤醒
- 灵活性: 可以用于互斥、同步、资源计数等多种场景
- 持久性: 信号量集在系统中持久存在
缺点
- 复杂性: API 相对复杂, 需要理解信号量概念
- 系统限制: 系统对信号量数量有限制
- 死锁风险: 使用不当可能导致死锁
- 系统资源: 占用系统资源, 需要显式删除
- 性能开销: 需要系统调用, 有一定开销
性能特点
- 延迟: 中等 (需要系统调用)
- 吞吐量: 中等 (受系统限制影响)
- CPU 占用: 中等
- 内存占用: 小 (每个信号量占用少量内存)
适用场景
- ✅ 进程同步和互斥
- ✅ 控制共享资源访问
- ✅ 生产者-消费者问题
- ✅ 读者-写者问题
- ✅ 需要复杂同步机制的场景
- ❌ 简单的单向通信 (管道更合适)
- ❌ 不需要同步的场景
注意事项
- 键值管理 : 使用
ftok()生成键值或使用 IPC_PRIVATE; key 相同会共享同一信号量集, 部署时需管理好 key 分配 - 初始值设置 : 使用
semctl(SETVAL)设置信号量初始值 - 资源清理 : 使用完毕后应该删除信号量集 (
semctl(IPC_RMID)); 约定哪个进程初始化/删除信号量, 避免误删 - 死锁避免: 注意避免死锁, 合理设计 P/V 操作顺序; 保持一致的加锁顺序, 避免循环等待; 对长时间持锁的操作谨慎设计
- 错误处理: 注意处理信号量操作失败的情况
- 系统限制 : 注意系统的信号量数量限制 (
ipcs -l查看) - SEM_UNDO 标志: 建议在 P/V 中使用, 防止进程崩溃后信号量泄漏, 降低死锁风险; 但大量使用可能有内核开销
- 权限与安全: 合理设置权限位, 避免被非预期进程操作
- 非阻塞模式: IPC_NOWAIT 可避免阻塞, 需处理 EAGAIN 重试或回退
- 删除时机: 删除会使阻塞的 semop 返回 EIDRM; 先确保使用方已退出或能正确处理
- 多 sem 场景 :
semop支持一次操作多个信号量, 可用于原子更新多个资源状态
常见错误码提示
EACCES: 权限不足EEXIST: IPC_CREAT|IPC_EXCL 且已存在ENOENT: 未指定 IPC_CREAT 且不存在ENOMEM/ENOSPC: 资源不足或达到系统上限EAGAIN: 非阻塞且当前不可用EIDRM: 信号量集已被删除EINTR: 被信号中断, 可按需重试EINVAL: 参数无效
扩展阅读
man 2 semgetman 2 semopman 2 semctlman 7 sem_overview