一、有名信号量的核心本质
POSIX 有名信号量是 Linux 内核提供的跨进程同步 / 互斥机制,核心特征:
- 系统级可见 :通过文件系统风格的 "名称"(如
/my_sem)标识,不同进程可通过该名称访问同一个信号量; - 内核级计数器 :本质是一个受内核保护的整数计数器,所有操作(加 1 / 减 1)都是原子操作,无竞态风险;
- 阻塞唤醒机制:计数器值决定进程是否阻塞,实现 "一个进程等另一个进程完成后再执行"(同步)或 "同一时间仅一个进程访问资源"(互斥)。
⚠️ 关键区分:与 System V 信号量相比,有名信号量无需手动管理 "信号量集",API 更简洁,是进程间同步的首选。
二、核心函数详解
1. sem_open () - 创建 / 打开有名信号量
函数原型
#include <semaphore.h>
// 打开已存在的信号量
sem_t *sem_open(const char *name, int oflag);
// 创建新的信号量(需补充权限和初始值)
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
核心参数解析
| 参数 | 你的代码取值 | 详细含义 |
|---|---|---|
name |
"/my_sem" | 信号量名称(必须以 / 开头,如 /my_sem,系统全局唯一;不能包含多个 /);对应内核中的信号量对象,所有进程通过该名称访问同一个信号量。 |
oflag |
O_CREAT | O_RDWR | 打开 / 创建标志:- O_RDWR:以可读可写方式打开;- O_CREAT:若信号量不存在则创建(需配合后两个参数);- O_EXCL:与 O_CREAT 配合,确保创建新信号量(已存在则报错)。 |
mode |
0666 | 信号量的访问权限(八进制),0666 表示所有用户可读可写;仅 O_CREAT 时需要,否则忽略。 |
value |
0 | 信号量初始值(核心!决定同步 / 互斥):- 同步场景(如你的代码):设 0,确保子进程先阻塞;- 互斥场景:设 1,确保同一时间仅一个进程访问资源。 |
返回值
- 成功:返回指向
sem_t的信号量句柄(进程内唯一,可直接操作); - 失败:返回
SEM_FAILED(宏定义,本质是(sem_t*)-1),需检查errno(如EEXIST表示信号量已存在,EINVAL表示名称格式错误)。
注意点
✅ 注1:名称不以 / 开头 → 报错 EINVAL,必须以 / 开头(如 /my_sem);
✅ 注 2:编译报错 undefined reference to sem_open → 需链接 pthread 和 rt 库(CMake:target_link_libraries(xxx PRIVATE pthread rt));
✅ 注 3:重复创建已存在的信号量(不加 O_EXCL)→ 不会报错,直接打开已有信号量(按需选择)。
2. sem_wait () - 阻塞式 P 操作(申请资源)
函数原型
int sem_wait(sem_t *sem);
核心原理
P 操作(Proberen,荷兰语 "尝试"):
- 内核原子性地将信号量计数器 减 1;
- 若减 1 后计数器 ≥ 0 → 函数立即返回,进程继续执行;
- 若减 1 后计数器 < 0 → 进程立即阻塞(放弃 CPU 使用权),被放入该信号量的等待队列;
- 直到其他进程执行
sem_post()(计数器加 1),内核从等待队列中唤醒一个进程,该进程的sem_wait()才会返回。
返回值
- 成功:返回 0;
- 失败:返回 -1,设置
errno(常见:EINTR表示被信号中断,EINVAL表示信号量句柄无效)。
注意点
✅ 注 1:sem_wait() 被信号(如 SIGINT)中断 → 返回 -1(errno=EINTR),子进程会直接退出,读不到数据;解决:封装健壮版 sem_wait:
✅ 注 2:初始值设为 1(互斥值)→ 子进程 sem_wait() 直接减为 0,不会阻塞,可能读到空数据(失去同步意义)。
3. sem_trywait () - 非阻塞式 P 操作
函数原型
int sem_trywait(sem_t *sem);
核心原理
非阻塞版 P 操作:
- 内核原子性检查信号量计数器:
- 若计数器 > 0 → 减 1,返回 0(获取资源成功);
- 若计数器 = 0 → 不阻塞 ,直接返回 -1,设置
errno=EAGAIN(资源被占用)。
- 适用于 "尝试获取资源,获取不到就做其他事" 的场景(如轮询、降级处理)。
注意点
✅ 注:混淆 sem_wait 和 sem_trywait → 同步场景用 sem_wait(必须等),非阻塞需求用 sem_trywait。
4. sem_post () - V 操作(释放资源 / 唤醒进程)
函数原型
int sem_post(sem_t *sem);
核心原理
V 操作(Verhogen,荷兰语 "增加"):
- 内核原子性地将信号量计数器 加 1;
- 若加 1 后计数器 ≤ 0 → 说明有进程在该信号量的等待队列中,内核唤醒一个阻塞的进程(按优先级 / 先进先出);
- 函数立即返回,无需等待被唤醒的进程执行。
返回值
- 成功:返回 0;
- 失败:返回 -1(如
EINVAL表示信号量句柄无效,EOVERFLOW表示计数器溢出)。
关键特性
- 原子性 :即使多个进程同时调用
sem_post(),计数器也不会出错(内核保证); - 唤醒策略:内核默认采用 "先进先出(FIFO)",先阻塞的进程先被唤醒。
5. sem_close () - 关闭信号量句柄
函数原型
int sem_close(sem_t *sem);
核心原理
- 关闭当前进程的信号量句柄,释放进程内与该信号量相关的资源;
- 不会删除内核中的信号量对象,仅表示当前进程不再使用该信号量;
- 进程退出时,内核会自动调用
sem_close(),但建议手动调用(规范)。
返回值
- 成功:返回 0;
- 失败:返回 -1(如
EINVAL表示句柄无效)。
6. sem_unlink () - 删除有名信号量
函数原型
int sem_unlink(const char *name);
核心原理
- 删除信号量的 "名称",使其无法被新进程打开;
- 内核不会立即销毁信号量对象,需等待所有进程都调用
sem_close()后,才释放信号量占用的内核资源; - 是 "彻底清理" 信号量的关键步骤,避免系统资源泄漏。
注意点
✅ 注:未调用 sem_unlink() → 信号量残留(可通过 ipcs -S 查看,sem_unlink("/my_sem") 手动删除)。
三、核心使用规则
1. 初始值选择
| 场景 | 初始值 | 核心逻辑 |
|---|---|---|
| 同步 | 0 | 进程 A 先阻塞(sem_wait),进程 B 执行完后 sem_post 唤醒 A(你的代码场景); |
| 互斥 | 1 | 同一时间仅一个进程能通过 sem_wait(计数器 1→0),其他进程阻塞,直到该进程 sem_post(0→1)。 |
2. 编译链接要求
-
必须包含头文件:
#include <semaphore.h>; -
编译时必须链接
pthread和rt库:bash# 命令行编译 gcc xxx.c -o xxx -pthread -lrt # CMake 编译 target_link_libraries(xxx PRIVATE pthread rt)
3. 资源清理规范
// 完整清理流程(父进程最后执行)
sem_close(retsem); // 关闭句柄
sem_unlink(semName); // 删除名称,触发内核销毁
4. 常见错误码及解决
| errno | 含义 | 解决方法 |
|---|---|---|
| EINVAL | 信号量名称格式错误 | 名称必须以 / 开头(如 /my_sem); |
| EEXIST | 信号量已存在 | 要么加 O_EXCL 确保新建,要么直接打开; |
| EAGAIN | sem_trywait 失败 | 资源被占用,重试或做降级处理; |
| EINTR | sem_wait 被中断 | 封装 safe_sem_wait,EINTR 时重试; |
| undefined reference | 链接失败 | 编译加 -pthread -lrt; |
四、总结
- 核心定位:有名信号量是跨进程同步 / 互斥的轻量级方案,通过 "名称" 实现内核级共享;
- 核心操作 :
sem_open(创建 / 打开)→sem_wait(阻塞申请)/sem_trywait(非阻塞申请)→sem_post(释放唤醒)→sem_close+sem_unlink(清理); - 关键参数 :初始值 0(同步)/1(互斥),名称以
/开头; - 避坑核心 :编译链接
pthread+rt,处理sem_wait的信号中断,手动清理信号量避免残留。