第 53 章 POSIX 信号量 (POSIX Semaphores)
核心结论
-
两种 POSIX 信号量:命名(sem_open,有名字跨进程共享)和无名(sem_init,放在共享内存或全局变量)——前者无关进程可用,后者适合线程/父子进程。
-
信号量本质:内核维护的整数,永不降到 0 以下——
sem_wait()减 1(值为 0 时阻塞或失败);sem_post()加 1(唤醒一个等待者)。 -
核心 API:sem_open/close/unlink、sem_init/destroy、sem_wait/trywait/timedwait、sem_post、sem_getvalue。
-
POSIX vs SysV 对比:POSIX 简单(一次操作一个 sem,±1);SysV 复杂(semop 多 sem 任意值);POSIX 低竞争性能优 10×(用 futex,无需 syscall);POSIX 缺点:无 undo。
-
pshared 标志:sem_init(sem, pshared, value) 中 pshared=0 为线程共享(全局/堆),非 0 为进程共享(必须在共享内存中)。
-
sem_post 异步信号安全:sem_post 是少数 async-signal-safe 的同步原语——可在 signal handler 内调用唤醒线程;pthread mutex 不行。
-
Linux 实现:2.6+ 完整支持;用 futex(kernel 2.6 + NPTL);命名 sem 在
/dev/shm/sem.<name>;同步语义「不是排队机制」。 -
限制:SEM_NSEMS_MAX(Linux 仅受内存限制);SEM_VALUE_MAX = INT_MAX。
|
本章主旨
POSIX 信号量是 SysV 信号量的现代简化版——核心改进是「单 sem 操作」「futex 实现」「引用计数」。读者需要建立三组对比:(1) 命名 vs 无名——按使用场景选择;(2) POSIX sem vs pthread mutex——线程内首选 mutex,跨进程才用 sem;(3) POSIX sem vs SysV sem——POSIX 更简单更快,SysV 更可移植有 undo。本章是第 47 章 SysV 信号量、第 30 章 pthread 同步的衔接点。 |
一、核心概念
本章围绕 6 个核心概念展开:命名 vs 无名、sem_wait/post 语义、pshared 标志、引用计数、futex 实现、与 pthread mutex 对比。
| 概念 | 定义 + 重要性 | 实现提示 |
|---|---|---|
命名信号量 |
|
§53.2;Linux 命名 sem 在 |
无名信号量 |
|
§53.4;pshared=0 线程共享;非 0 必须放在共享内存;典型嵌入动态数据结构 |
sem_wait / sem_trywait / sem_timedwait |
sem_wait 减 1(值 0 时阻塞);sem_trywait 立即失败 EAGAIN;sem_timedwait 阻塞到 abs_timeout(ETIMEDOUT) |
§53.3.1;信号 handler 打断 sem_wait 必返回 EINTR(即使 SA_RESTART);SUSv3 规定不可 restart |
sem_post |
加 1;若值从 0 升 1,唤醒一个等待者;唤醒哪个由调度策略决定(默认 round-robin 不确定) |
§53.3.2;唯一 async-signal-safe 的同步原语之一;可在 signal handler 内调用;real-time 调度下唤醒最高优先最长等待 |
pshared 标志 |
sem_init 第二个参数;0=线程共享;非 0=进程共享(sem 必须在共享内存);NPTL 忽略 pshared(旧 LinuxThreads 报 ENOSYS) |
§53.4.1;Linux 2.6+ 才支持 pshared≠0;可移植代码仍应正确指定 |
POSIX vs SysV vs pthread mutex |
POSIX 简单(一次 1 个,±1);SysV 复杂(多 sem 任意值,有 undo);pthread mutex 有 ownership 适合线程内 |
§53.5;POSIX 低竞争下性能优 10×(futex 无 syscall);线程内首选 mutex;跨进程用 POSIX sem |
二、详细笔记
53.1 命名信号量生命周期
What:sem_open / sem_close / sem_unlink——POSIX 命名 sem 的三件套。
Why:与 POSIX IPC 三件套同款 API——引用计数删除;创建+初始化原子。
How:
// 摘自《The Linux Programming Interface》 第 53 章
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
sem_t *sem_open(const char *name, int oflag, ...);
int sem_close(sem_t *sem);
int sem_unlink(const char *name);
/* 创建(创建+初始化原子) */
sem_t *sem = sem_open("/demo", O_CREAT|O_EXCL, 0660, 0);
if (sem == SEM_FAILED) errExit("sem_open");
/* 打开已存在 */
sem_t *sem = sem_open("/demo", 0);
/* 用完 */
sem_close(sem);
sem_unlink("/demo"); /* 引用归零销毁 */
Linux 命名 sem 位于 /dev/shm/sem.<name>(tmpfs 文件系统);创建需 root 或目录有写权限。
When:(1) 无关进程间同步;(2) 守护进程创建并 unlink(避免名字残留);(3) 创建时务必给足够权限(很多应用 O_RDWR)——sem 实际无 O_RDONLY 等。
Example:第 53 章 psem_create -cx /demo 666 0 创建权限 666、初始值 0 的 sem;ls /dev/shm/sem.* 看到 sem.demo。
53.2 sem_wait / sem_post 操作语义
What:sem_wait 减 1(值 0 阻塞);sem_post 加 1(唤醒等待者)。
Why:核心同步原语——「P/V」操作的现代化实现。
How:
// 摘自《The Linux Programming Interface》 第 53 章
#include <semaphore.h>
int sem_wait(sem_t *sem); /* 阻塞减 1 */
int sem_trywait(sem_t *sem); /* 非阻塞,失败 EAGAIN */
int sem_timedwait(sem_t *sem,
const struct timespec *abs_timeout); /* 超时减 1,ETIMEDOUT */
int sem_post(sem_t *sem); /* 加 1,唤醒等待者 */
/* 经典生产者-消费者 */
sem_t empty, full;
void *producer(void *arg) {
/* produce item */
sem_wait(&empty); /* 等待空槽 */
/* put item in buffer */
sem_post(&full); /* 通知消费者 */
}
void *consumer(void *arg) {
sem_wait(&full); /* 等待数据 */
/* get item from buffer */
sem_post(&empty); /* 通知生产者 */
}
When:(1) 互斥——初始值 1 的 sem 当 mutex 用(但首选 pthread mutex);(2) 资源计数——初始值 N 表示 N 个可用资源;(3) 信号——sem_post 在 handler 内通知工作线程。
Example:第 53 章示例——psem_create -c /demo 600 0 创建初始 0;后台 psem_wait /demo 阻塞;前台 psem_post /demo 唤醒。
53.3 无名信号量与 pshared
What:sem_init(sem, pshared, value) 初始化内存中的 sem_t;sem_destroy 销毁。
Why:(1) 线程间共享——全局变量或堆 sem 即可,无需名字;(2) 父子进程共享——sem 放在 mmap SHARED|ANON 或 POSIX shm。
How:
// 摘自《The Linux Programming Interface》 第 53 章
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
/* 线程共享(pshared=0) */
sem_t sem;
sem_init(&sem, 0, 1); /* 当 mutex 用 */
pthread_mutex_lock(&sem_to_mutex(&sem)); /* 推荐用 mutex */
/* 进程共享(pshared!=0,sem 必须在共享内存) */
struct shm {
sem_t sem;
/* 共享数据 */
};
int fd = shm_open("/myshm", O_CREAT|O_RDWR, 0660);
ftruncate(fd, sizeof(struct shm));
struct shm *p = mmap(NULL, sizeof(struct shm),
PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
sem_init(&p->sem, 1, 1); /* 跨进程 mutex */
pshared 行为:
-
pshared = 0— 线程共享(sem 在全局/堆);NPTL 忽略但建议正确指定。 -
pshared != 0— 进程共享(sem 必须在共享内存中);Linux 2.6+ 支持;旧 LinuxThreads 报 ENOSYS。
When:(1) 线程间同步——首选 pthread mutex,sem 仅作互斥不推荐(无 ownership);(2) 父子进程同步——sem 在 mmap SHARED|ANON;(3) 无关进程同步——用命名 sem。
Example:第 53 章 thread_incr_psem.c 用 sem_init(&sem, 0, 1) 实现两线程对全局变量互斥递增。
53.4 引用计数删除 vs SysV 显式删除
What:POSIX sem_unlink 立即删名字,引用归零才真销毁;SysV semctl IPC_RMID 需显式删除。
Why:POSIX 避免「最后一个用户忘记删」导致的资源泄漏。
How:
| 场景 | POSIX sem | SysV sem |
|---|---|---|
创建+初始化 |
sem_open 原子(避免竞争) |
semget 后 semctl SETVAL(非原子) |
删除 |
sem_unlink 立即删名字;引用归零销毁 |
semctl IPC_RMID 显式删除;最后用户负责 |
进程退出 |
自动 close(减引用) |
不自动删除;内核持久 |
资源计数 |
自动 |
需应用管理 |
When:(1) 守护进程——创建后立即 unlink(避免名字残留,引用归零自动销毁);(2) SysV 移植——确保退出前 IPC_RMID;(3) 大量动态 sem——无名 sem 嵌入数据结构,进程退出自动消失。
Example:POSIX 守护进程模板:sem_open("/x", O_CREAT, …) → sem_unlink("/x") → 用 sem;进程退出后 sem 自动消失。
53.5 POSIX vs SysV 信号量
What:POSIX 简单(一次 1 个,±1);SysV 复杂(semop 多 sem 任意值,有 undo)。
Why:理解两者取舍——决定项目用哪套。
How:
| 维度 | POSIX sem | SysV sem |
|---|---|---|
操作粒度 |
一次 1 个;±1 |
semop 多 sem 任意值 |
wait-for-zero |
无 |
有(sem_op=0) |
undo |
无 |
SEM_UNDO(进程退出回滚) |
性能(低竞争) |
快 10×(futex 无 syscall) |
每次都 syscall |
命名 |
名字 + sem_open |
key + semget |
可移植性 |
Linux 2.6+;SUSv3 可选 |
SUSv3 强制;几乎所有 UNIX |
创建初始化 |
原子 |
非原子(需技巧避免竞争) |
When:(1) 新项目 Linux 为主——POSIX 更简单更快;(2) 多 UNIX 平台——SysV 更安全;(3) 需要 undo(防死锁)——SysV;(4) 需要 wait-for-zero——SysV;(5) 高频 sem 操作——POSIX(futex 优势)。
Example:第 47 章 SysV sem 实现 svsem_xfr vs 第 53 章 POSIX sem 实现 thread_incr_psem——POSIX 代码明显简洁。
53.6 POSIX sem vs pthread mutex
What:POSIX sem 和 pthread mutex 都可同步线程——但 mutex 有 ownership,sem 没有。
Why:线程内首选 mutex(ownership 强制代码结构);sem 仅在跨进程或 async-signal 场景用。
How:
| 维度 | pthread mutex | POSIX sem |
|---|---|---|
ownership |
✓(只有 locker 可 unlock) |
✗(线程 A wait,线程 B post) |
性能 |
相当(都用 futex) |
相当(低竞争) |
异步信号安全 |
✗(不可在 handler 内用) |
✓(sem_post 可在 handler 内) |
跨进程 |
默认不行(需放共享内存且用 PTHREAD_PROCESS_SHARED 属性) |
✓(命名 sem 或共享内存无名 sem) |
代码结构 |
强制清晰 |
易混乱(称为「goto of concurrent programming」) |
When:(1) 线程内互斥——首选 pthread_mutex;(2) 跨进程同步——POSIX sem;(3) signal handler 唤醒工作线程——sem_post;(4) 高频 sem 操作——POSIX 比 SysV 快。
Example:典型 pthread mutex 用法——pthread_mutex_lock / pthread_mutex_unlock;POSIX sem 互斥——sem_wait / sem_post,但任何线程可 post,需更谨慎。
三、关键图表
|
非可视化条目(API / 比较)
|
四、思维导图
mindmap
root((第 53 章 POSIX 信号量))
两种类型
命名 sem_open
无名 sem_init
命名 跨进程
无名 线程或父子
sem_wait post
sem_wait 减1
sem trywait EAGAIN
sem timedwait ETIMEDOUT
sem_post 加1
sem_post 唤醒等待者
sem_post 异步信号安全
pshared
0 线程共享
非0 进程共享
必须共享内存
Linux 2.6+ 支持
引用计数
sem_unlink 删名字
引用归零销毁
避免最后用户负责
SysV 显式 IPC_RMID
futex 实现
低竞争无 syscall
性能 优 10 倍
NPTL 线程库
kernel 2.6
vs SysV pthread
SysV 任意值 undo
SysV 可移植性强
pthread mutex ownership
mutex 线程内首选
sem 跨进程首选
== 五、重点与易错点
. *POSIX sem 必须 `-lrt` 链接*——glibc 部分版本需要;现在很多发行版已集成。
. *Linux 完整支持需 2.6+*——LinuxThreads(glibc 2.3 之前)只支持线程共享;NPTL(2.6+)支持进程共享。
. *sem_open 失败返回 SEM_FAILED*——是 `((sem_t *) 0)`(Linux)或 `((sem_t *) -1)`;不要用 NULL 判断。
. *sem_post 唯一 async-signal-safe 同步原语*——可在 signal handler 内调用唤醒工作线程;pthread_mutex_lock 不行。
. *sem_wait 被信号打断返回 EINTR*——即使 SA_RESTART 也不自动重启;必须手动循环或用 sem_timedwait。
. *POSIX sem 不是排队机制*——多个等待者唤醒哪个由调度策略决定;不要依赖 FIFO。
. *POSIX 无 undo*——进程退出时不会回滚 sem 状态;SysV 有 SEM_UNDO;小心死锁。
. *pshared 非 0 时 sem 必须在共享内存*——否则 fork 后子进程看不到 sem;用 mmap SHARED|ANON 或 POSIX shm。
. *sem_init 仅能调一次*——重复初始化未定义行为;设计时确保单一进程/线程初始化。
. *sem_destroy 之前必须无等待者*——否则未定义行为;用 sem_getvalue + 循环等待。
. *不要对 sem_t 副本操作*——只能对原 sem 操作;`sem2 = *sp; sem_wait(&sem2)` 是 UB。
. *命名 sem 创建需足够权限*——Linux 用 `/dev/shm` 下文件,目录需有写权限;普通用户一般可创建。
. *POSIX vs SysV 性能*——低竞争 POSIX 优 10×(futex);高竞争相当;POSIX 缺点是无法在 handler 内用 SysV semctl。
. *POSIX sem 与 pthread mutex 选择*——线程内 mutex 首选(ownership);跨进程 POSIX sem;async-signal 用 sem_post。
. *跨章衔接*:第 30 章 pthread mutex/cond(线程内同步);第 47 章 SysV sem(可移植 + undo);第 49 章 mmap SHARED|ANON(无名 sem 的进程共享载体);第 54 章 POSIX shm(无名 sem 也可放 POSIX shm)。