第 47 章 System V 信号量 (System V Semaphores)
核心结论
-
信号量本质:内核维护的非负整数;P 减(值 < 0 阻塞)、V 加(值=0 时唤醒等待者);常用于「资源计数」(如 N 个共享资源)或「互斥」(二值 semaphore)。SysV 分配在*集合*中(一个 semget 可创建多个),单次 semop 可原子操作多个。
-
semget 创建/打开:同 SysV IPC 模式——
semget(key, nsems, mode[, IPC_CREAT][, IPC_EXCL]);返回 set id(不是单个 sem);nsems是 set 中 sem 数。 -
semop 原子操作:
struct sembuf { sem_num, sem_op, sem_flg };sem_op > 0加、sem_op == 0等 0(否则阻塞)、sem_op < 0减(值够才立即执行,否则阻塞);一次可执行 N 个操作(原子)。 -
semctl 控制:GETVAL/SETVAL/GETALL/SETALL/GETPID/GETNCNT/GETZCNT/IPC_RMID/IPC_STAT/IPC_SET;UNION
semun必须程序自己定义。 -
初始化竞态:get 与 setval 是两次调用,peer 进程要「先创建者用 sem_otime 标志初始化完成」——多进程协作的「谁的 init」问题。
-
SEM_UNDO 限制:进程退出时自动撤销 semop,但「撤销值过大」会丢;Linux 限制 semadj ≤ SEMVMX (32767);
SETVAL/SETALL会清零所有进程的 semadj。
|
本章主旨
SysV 信号量是「内核维护的整数 + 原子操作」——专门为*同步*而非数据传输设计。本章覆盖 (1) 信号量的本质(P/V 操作 + 集合概念);(2) semget 创建;(3) semop 原子操作(三类操作:加、等、减);(4) semctl 控制(含 semun union);(5) 初始化竞态与 sem_otime 协议;(6) SEM_UNDO 风险;(7) Limits 与替代品。读者应理解「binary sem」是更易用的常见协议——SysV 提供的 semop 集合操作虽灵活但 API 复杂;写库用「1 个 sem 表示 0/1」+ |
一、核心概念
本章围绕 6 个核心概念展开:从信号量本质、semget/semop/semctl、init 竞态、SEM_UNDO、二值信号量、limits。
| 概念 | 定义 + 重要性 | 实现提示 |
|---|---|---|
信号量本质(集合概念) |
内核维护非负整数;P 减、V 加;SysV 一次 get 可创建 N 个(集合),单次 semop 可原子操作 N 个——比 POSIX sem 灵活但 API 复杂 |
§47.0-1; |
semop 三类操作 |
|
§47.6; |
semctl 控制 |
GETVAL/SETVAL/GETALL/SETALL 取设值;GETPID/GETNCNT/GETZCNT 取 wait 信息;IPC_RMID 删;IPC_STAT/SET 改关联数据 |
§47.3;UNION |
初始化竞态(sem_otime 协议) |
多 peer 进程同时 init 时——「谁的 init 是合法的」用 |
§47.5;BSD 一些版本不更新 sem_otime,可移植代码慎用 |
SEM_UNDO 风险 |
进程退出时自动撤销 semop——per-process semadj 总和;不能超过 SEMVMX (32767);SETVAL/SETALL 清零所有进程的 semadj; |
§47.8;不能完全代替「进程退出释放资源」——资源本身可能还泄漏;通常仍要 atexit/exit handler 清理 |
二进制信号量协议 |
1 表示可用、0 表示占用;reserve = sem_op=-1(可能阻塞);release = sem_op=+1; |
§47.9;用于「单一共享资源互斥」——比 full semop 简单;常作为更上层 API |
二、详细笔记
47.1 概述 + 信号量集
What:SysV 信号量分配在「集合」中——semget(key, nsems, …) 一创建就是 N 个;操作时指定 sem_num 索引。
Why:让「多 sem 原子操作」成为可能(一个 semop 可同时操作 N 个——保证 N 个资源的「全部可用」才进入临界区)。
How——四步使用流程(来自 §47.1):
-
semget()创建/打开 set。 -
semctl(… SETVAL/SETALL)初始化——只一个进程做。 -
semop()实际操作。 -
semctl(… IPC_RMID)删——只一个进程做。
When:所有 SysV sem 程序都按此流程。
47.2 semget 创建/打开
What:semget(key, nsems, mode[, IPC_CREAT][, IPC_EXCL]) 创建或打开 N 个 sem 的集合。
Why:与 msgget/shmget 一致的「key → id」入口。
How:
// 摘自《The Linux Programming Interface》 第 47 章
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
/* 创建一个 sem 的集合 */
int semid = semget(IPC_PRIVATE, 1, 0660); /* IPC_PRIVATE 一定新建 */
int semid = semget(key, 5, IPC_CREAT | 0660); /* 找或建 5 个 sem 的集 */
int semid = semget(key, 0, 0); /* 找(取已存在 id) */
nsems: . 创建时 = set 中 sem 数(> 0)。 . 打开时 ≤ set 实际大小(否则 EINVAL)。 . 改不了大小——一创建定终身。
When:写 sem 程序几乎都用此入口。
47.3 semctl 控制
What:semctl(semid, semnum, cmd, …) 控制单个 sem 或 set。
Why:初始化值、查状态、删除、改权限。
How——UNION semun 必定义(SUSv3 强制):
// 摘自《The Linux Programming Interface》 第 47 章 Listing 47-2
union semun {
int val; /* SETVAL */
struct semid_ds * buf; /* IPC_STAT, IPC_SET */
unsigned short * array; /* SETALL, GETALL */
#if defined(__linux__)
struct seminfo * __buf; /* IPC_INFO, SEM_INFO */
#endif
};
常见 cmd:
/* 初始化 set 中所有 sem */
unsigned short vals[5] = {1, 0, 1, 0, 1};
union semun arg;
arg.array = vals;
semctl(semid, 0, SETALL, arg);
/* 取第 3 个 sem 的值 */
int v = semctl(semid, 2, GETVAL, dummy);
/* 删 set */
semctl(semid, 0, IPC_RMID, dummy);
注意:SETVAL 会清除所有进程的 semadj 记录(SETALL 同)——见 §47.8。
When:server 启动时 SETALL 一次;监控用 GETVAL/GETNCNT/GETZCNT。
47.4 semid_ds 关联数据
What:struct semid_ds 4 字段:sem_perm/sem_otime(最后 semop 时间)/sem_ctime(最后变化时间)/sem_nsems(set 中 sem 数)。
Why:记录 set 状态;监控用。
How:
// 摘自《The Linux Programming Interface》 第 47 章
struct semid_ds ds;
union semun arg = {.buf = &ds};
semctl(semid, 0, IPC_STAT, arg);
printf("nsems: %lu, last semop: %s\n", ds.sem_nsems, ctime(&ds.sem_otime));
When:写监控/管理工具时读。
47.5 初始化竞态
What:semget 和 semctl SETVAL 是两次 syscall;多 peer 进程「都想 init 时」——可能出现「A 创建 + 还没 SETVAL 时 B 已用」。
Why:用未初始化的 sem 是灾难性的——值任意。
How——sem_otime 协议(来自 §47.5 Listing 47-6):
// 摘自《The Linux Programming Interface》 第 47 章 Listing 47-6
semid = semget(key, 1, IPC_CREAT | IPC_EXCL | perms);
if (semid != -1) { /* 我们是创建者 */
union semun arg = {.val = 0};
semctl(semid, 0, SETVAL, arg); /* init */
struct sembuf sop = {.sem_num = 0, .sem_op = 0}; /* wait for 0 */
semop(semid, &sop, 1); /* 触发 sem_otime 变更 */
} else { /* 已存在 */
int tries = 10;
while (tries-- > 0) {
struct semid_ds ds;
union semun arg = {.buf = &ds};
semctl(semid, 0, IPC_STAT, arg);
if (ds.sem_otime != 0) break; /* 已被 init */
sleep(1);
}
if (tries == 0) fatal("Existing semaphore not initialized");
}
Why this works:sem_otime 仅在 semop 成功后变更;创建者用 sem_op=0(等 0)后立即通过;后续「打开者」看到 sem_otime != 0 即可安心使用。
When:多个 peer 进程可创建/初始化同一 sem 时必用——单 server 创建可省略。
Limitation:BSD 一些版本 semop 不更新 sem_otime;可移植代码要小心。
47.6 semop 原子操作
What:semop(semid, sops, nsops) 原子执行 1..N 个 sembuf 操作。
Why:让「多 sem 协调」成为可能——不会被打断。
How——三类操作(来自 §47.6):
// 摘自《The Linux Programming Interface》 第 47 章
struct sembuf sops[3] = {
{.sem_num = 0, .sem_op = -1, .sem_flg = 0}, /* 减 1 */
{.sem_num = 1, .sem_op = +2, .sem_flg = 0}, /* 加 2 */
{.sem_num = 2, .sem_op = 0, .sem_flg = IPC_NOWAIT}, /* 等 0 (非阻塞) */
};
semop(semid, sops, 3); /* 全部按序执行,原子 */
操作语义:
. sem_op > 0:value += sem_op;唤醒等待者;需 alter (write) 权限。
. sem_op == 0:value == 0 才立即通过;否则阻塞;需 read 权限。
. sem_op < 0:value ≥ |sem_op| 才立即通过;否则阻塞;需 alter 权限。
atomicity:所有 N 个 op 要么全做要么全不做;中间状态对其他进程不可见。
flags:
. IPC_NOWAIT:不阻塞;条件不满足立即 EAGAIN。
. SEM_UNDO:记录 undo——进程退出时反向操作。
EINTR/EIDRM:信号打断 → EINTR(不自动重启);set 被 IPC_RMID → EIDRM。
When:所有 sem 程序都依赖 semop 的「阻塞」+「原子性」。
47.7 多个阻塞 op 的处理顺序
What:当多个进程同时等待 sem 时——减量相同时顺序未定义;减量不同时按「能完成」顺序;可能*饿死*(starvation)。
Why:不正确的 sem 设计会导致「某些进程永远等不到」。
How——starvation 例子(来自 §47.7):
-
sem 初始值 0;A 想减 2;B 想减 1。
-
第三方加 1。
-
B 先得(因为 B 要 1 即可,A 要 2)。
-
如果系统不断「+1 → B 走 → +1 → B 走」,A 永远等不到 2。
When:设计多 sem 协议时务必想 starvation 场景——用「所有 sem 同号」或「有序等待」避免。
47.8 SEM_UNDO 风险
What:SEM_UNDO 让进程退出时自动撤销之前 semop 的影响——per-process semadj 总和。
Why:进程异常退出时不留「资源被占用」状态。
How:
struct sembuf sop = {.sem_num = 0, .sem_op = -1, .sem_flg = SEM_UNDO};
semop(semid, &sop, 1); /* value--; 进程退出时 value++ */
但有限制(§47.8):
-
不能完全恢复资源——只恢复 sem,没恢复资源本身(如未释放的文件锁、未关闭的 fd)。
-
Linux 处理「调整时 value 不足」——减到 0 后跳过;SUSv3 留空。
-
跨 SEMVMX (32767) 上限——undo 时 value > SEMVMX 也强加,导致 sem 值越界。
-
SETVAL/SETALL——会清所有进程的 semadj(破坏性)。
-
fork — 不继承;exec — 保留。
-
Linux 2.6 + NPTL:threads 共用 semadj(CLONE_SYSVSEM)。
When:仅在「进程退出时确实想撤销 sem 影响」的场景使用——常见「signal handler 跳过退出」的进程不必用。
47.9 二值信号量协议
What:binary semaphore——只用 0/1 两个值表示「可用/占用」;reserve=减、release=加;比 full semop 简单。
Why:大多数应用只要互斥——用 binary 协议简单。
How——binary_sems.c(Listing 47-10):
// 摘自《The Linux Programming Interface》 第 47 章
int initSemAvailable(int semId, int semNum) {
union semun arg = {.val = 1};
return semctl(semId, semNum, SETVAL, arg);
}
int initSemInUse(int semId, int semNum) {
union semun arg = {.val = 0};
return semctl(semId, semNum, SETVAL, arg);
}
int reserveSem(int semId, int semNum) {
struct sembuf sops = {.sem_num = semNum, .sem_op = -1,
.sem_flg = bsUseSemUndo ? SEM_UNDO : 0};
while (semop(semId, &sops, 1) == -1)
if (errno != EINTR || !bsRetryOnEintr) return -1;
return 0;
}
int releaseSem(int semId, int semNum) {
struct sembuf sops = {.sem_num = semNum, .sem_op = +1,
.sem_flg = bsUseSemUndo ? SEM_UNDO : 0};
return semop(semId, &sops, 1);
}
全局开关:bsUseSemUndo(是否带 SEM_UNDO)、bsRetryOnEintr(EINTR 是否 retry)。
When:写 SysV sem 库时标准实现;用于共享内存互斥(见第 48 章 svshm_xfr)。
47.10 SysV Sem Limits
What:kernel 通过 /proc/sys/kernel/sem(4 个值)限制 sem。
Why:防资源耗尽。
How——Limits(来自 §47.10 Table 47-1):
| 限制 | 含义 | Linux 调整 |
|---|---|---|
SEMMNI |
最大 set 数 |
≤ 32768 (IPCMNI); |
SEMMSL |
set 最大 sem 数 |
≤ 65536;实际推荐 ≤ 8000; |
SEMMNS |
系统级总 sem 数 |
≤ INT_MAX; |
SEMOPM |
单次 semop 最大操作数 |
推荐 ≤ 1000; |
SEMVMX |
sem 最大值 |
固定 32767 |
SEMAEM |
semadj 最大值 |
固定 32767 |
查看:
$ cat /proc/sys/kernel/sem
250 32000 32 128 # SEMMSL SEMMNS SEMOPM SEMMNI
$ sudo sh -c "echo '500 64000 64 256' > /proc/sys/kernel/sem"
When:装高并发 sem 程序时调 SEMMNS。
47.11 SysV Sem 缺点
What:SysV sem 与 SysV msg 共享许多设计缺陷。
Why:新代码用 POSIX sem 或其他机制更优。
How——7 个主要缺点(来自 §47.11):
-
int id 非 fd:不能 select/poll/epoll。
-
key 而非 pathname:定位比 fd 麻烦。
-
get/setval 分开:要小心 init 竞态。
-
无引用计数:何时删需要协调。
-
API 复杂:集合概念、超集 op 罕见用。
-
limits 复杂:要在装服务器时调。
-
替代品丰富:POSIX sem(53 章)、file lock(55 章)。
When:避免新代码用 SysV sem;维护老代码理解即可。
三、关键图表
|
非可视化条目(semop 操作与 semctl 命令)
|
|
semctl 命令一览
|
四、思维导图
mindmap
root((第 47 章 SysV 信号量))
集合概念
semget N 个 sem
semop 原子多 op
semop 三类
加 sem_op 大于 0
等 0 sem_op 等 0
减 sem_op 小于 0
semctl
GETVAL SETVAL
GETALL SETALL
IPC RMID STAT SET
UNION semun 自定义
init 竞态
sem_otime 协议
创建者先 SETVAL
sem_op 0 触发 otime
SEM_UNDO
semadj 记录
进程退出撤销
SEMVMX 32767 限制
二值 sem
0 占用 1 可用
reserve release
binary_sems.c
limits
SEMMNI SEMMSL SEMMNS
SEMOPM SEMVMX
proc sys kernel sem
五、重点与易错点
-
信号量是非负整数——所有操作保证不降到 0 以下;阻塞语义是「值不够时挂起」。
-
集合(set)是 SysV 特点——一个 semget 创建 N 个;一次 semop 可操作 N 个(原子);比 POSIX sem 复杂但灵活。
-
semop 多操作原子——3.4 章 N 个 sops 全部要能完成才做;有一个不能就全不做并阻塞。
-
sem_op == 0 用来「等 0」——常用于「全部释放」后才让一个进程继续;也可用于
init 竞态协议。 -
UNION semun 必须程序自己定义(SUSv3 强制)——
#include "semun.h";glibc < 2.1 提供,新 glibc 不再提供(_SEM_SEMUN_UNDEFINEDmacro)。 -
init 竞态用 sem_otime 协议——「创建者用 sem_op=0 触发 otime 变化」+「非创建者轮询 otime != 0」;不依赖 fork 顺序。
-
SEM_UNDO 谨慎使用——只能回滚 sem 影响,不能释放资源;过 SEMVMX 越界;SETVAL/SETALL 会清所有进程 semadj。
-
SEM_UNDO 不 fork 继承——子进程不继承父的 semadj;exec 保留。
-
阻塞 op 的顺序:相同减量顺序不定;不同减量按「能完成」顺序;可能 starvation——设计要避免「某些 op 永远完不成」。
-
binary semaphore 是简化协议——大多数应用只要互斥;reserve/release 比 semop 全语义简单得多。
-
semget 的 nsems 不可改——创建后不能扩缩。
-
SETVAL 会唤醒等待者——如果新值满足之前阻塞的 op 条件,OS 自动唤醒。
-
Linux 的 SEMVMX 固定 32767——sem 不能涨到此值以上(除非 SEM_UNDO 越界)。
-
limits 在 /proc/sys/kernel/sem——4 个空格分隔值;可 echo 改(运行时)。
-
跨章衔接:第 38 章 set-UID 安全;第 48 章 SysV shm 用 sem 协调;第 53 章 POSIX sem;第 55 章 file lock。