「Linux 系统编程」信号量与生产者-消费者模型
1 信号量
1.1 信号量概述
信号量(Semaphore) 是一种用于控制多个进程/线程对共享资源访问的同步机制。
信号量的本质:
信号量本质上是一个 计数器 + 等待队列。
- 计数器值 ≥ 0:表示当前可用资源的数量。
- 计数器值 = 0:表示资源已被占用,后续进程/线程需阻塞等待。
- 计数器值 < 0(某些实现):绝对值表示等待该资源的进程/线程数量。
1.2 信号量操作
信号量支持两个原子操作:
- P 操作(Proberen / wait):申请资源。若资源不足,则阻塞。
- V 操作(Verhogen / signal):释放资源。若有等待者,则唤醒一个。
1.3 信号量类型
- 二进制信号量(Binary Semaphore):值只能是 0 或 1,等价于互斥锁(Mutex)。
- 计数信号量(Counting Semaphore):值可以是任意非负整数,用于管理多个同类资源。
2 Linux C 信号量相关函数
Linux 系统提供两套信号量接口:
- System V 信号量(较老,功能强大但复杂)
- POSIX 信号量(通常使用这个,简洁易用)
以下主要介绍 POSIX 信号量。
头文件:
1 |
|
2.1 信号量主要函数
-
创建/打开命名信号量
1
sem_t *sem_open(const char *name, int oflag, ... /* mode_t mode, unsigned int value */);
name
:信号量名称(用于进程间共享)oflag
:O_CREAT
、O_EXCL
等标志mode
:权限(如 0644)value
:初始值
-
初始化无名信号量(线程/进程内共享)
1
int sem_init(sem_t *sem, int pshared, unsigned int value);
sem
:指向信号量变量的指针pshared
:0
:仅限当前进程内线程共享非0
:可用于进程间共享(需放在共享内存中)
value
:初始值。可用资源的数量,直接决定了 可以同时访问临界区的线程数量
-
P 操作(等待)
1
2
3int sem_wait(sem_t *sem); // 阻塞等待
int sem_trywait(sem_t *sem); // 非阻塞尝试
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout); // 超时等待 -
V 操作(释放)
1
int sem_post(sem_t *sem);
-
获取当前值
1
int sem_getvalue(sem_t *sem, int *sval);
-
销毁/关闭
1
2
3int sem_destroy(sem_t *sem); // 销毁无名信号量
int sem_close(sem_t *sem); // 关闭命名信号量
int sem_unlink(const char *name); // 删除命名信号量
2.2 信号量使用示例
以下为一个简单的使用信号量实现线程同步的示例。允许同时最多 2 个线程访问临界资源。
1 |
|
3 信号量实现生产者-消费者模型
3.1 生产者-消费者模型需要哪些信号量
- 生产者:生成数据并放入缓冲区。
- 消费者:从缓冲区取出数据并消费。
- 缓冲区:大小有限,需同步访问。
我们需要三个信号量:
信号量 | 作用 | 初始值 |
---|---|---|
empty |
空槽位数量 | 缓冲区大小 N |
full |
已填充槽位数量 | 0 |
mutex |
互斥访问缓冲区 | 1 |
empty
和full
控制资源数量,避免缓冲区溢出或下溢。mutex
保证 对缓冲区的互斥访问,防止数据竞争。
在信号量配合互斥量实现的生产者-消费者模型中:
mutex
互斥量:确保同一时刻只有一个线程能访问缓冲区(临界资源)。empty
和full
信号量:控制有多少个线程可以等待在缓冲区操作上,但不控制同时访问的线程数量。
3.2 代码实现
1 |
|
sem_wait(&empty)
和 sem_post(&full)
是信号量操作,它们控制的是有多少个线程可以等待在缓冲区操作上(即等待进入临界区)。
sem_wait(&mutex)
和 sem_post(&mutex)
是互斥量操作,它们控制的是 同一时刻有多少个线程能进入临界区(即同时访问缓冲区)。
4 信号量与条件变量对比
4.1 基本概念对比
特性 | 信号量 (Semaphore) | 条件变量 (Condition Variable) |
---|---|---|
本质 | 计数器 + 等待队列 | “条件等待 + 通知”机制,必须配合互斥锁使用 |
是否自带互斥 | ❌ 不自带(需额外 mutex 保护临界区) | ❌ 不自带(必须与 mutex 搭配) |
操作原语 | sem_wait() / sem_post() |
pthread_cond_wait() / pthread_cond_signal() / pthread_cond_broadcast() |
计数能力 | ✅ 支持计数(可表示多个资源) | ❌ 无计数能力(只表示“条件是否满足”) |
唤醒策略 | 自动唤醒一个等待者(计数器决定) | 需手动调用 signal(唤醒一个)或 broadcast(唤醒所有) |
“记忆性” | ✅ 有(post 多次,wait 可多次成功) | ❌ 无(signal 早于 wait 会丢失) |
4.2 实现生产者-消费者关键部分
- 信号量实现
1 |
|
- 条件变量实现
1 |
|
4.3 核心机制和唤醒行为对比
-
信号量:资源计数驱动
empty
信号量初始值 = 缓冲区大小 → 表示“可用空位数量”- 每次
sem_wait(&empty)
成功,计数器减1 → 表示占用一个空位 - 每次
sem_post(&empty)
,计数器加1 → 表示释放一个空位 - 计数器值直接反映资源可用数量 → 系统自动管理“谁该阻塞/唤醒”
✅ 优点:逻辑清晰,自动管理资源配额,不易出错。
✅ 适合:资源数量明确、需要限制并发访问数量的场景。 -
条件变量:条件判断驱动
- 没有内置计数器,需程序员自己维护
count
、in
、out
等状态变量 pthread_cond_wait()
只是“睡一觉”,醒来后必须重新检查条件是否满足pthread_cond_signal()
不保证“正好唤醒需要的人”,可能唤醒错误线程(需重新 sleep)
✅ 优点:更灵活,可表达复杂条件(如“缓冲区 > 50% 才通知生产者”)
⚠️ 缺点:易出错(如用if
代替while
、忘记发 signal、signal 丢失等) - 没有内置计数器,需程序员自己维护
行为 | 信号量 | 条件变量 |
---|---|---|
默认唤醒 | 唤醒一个等待者 | signal 唤醒一个,broadcast 唤醒所有 |
是否公平 | 通常 FIFO(取决于实现) | 不保证(可能饥饿) |
是否可预测 | ✅ 计数器决定,行为确定 | ❌ 依赖调度,行为较难预测(需要自己避免虚假唤醒) |
4.4 对比总结
-
信号量限制每个时间段内最多活跃的线程数量。
-
信号量 vs 条件变量:
- 信号量更适合 资源数量明确、需要限制并发度 的场景(如连接池、线程池、缓冲区管理)。
- 条件变量更适合 复杂条件判断、事件驱动 的场景(如“当队列长度 > N 时唤醒生产者”)。
-
在生产者-消费者模型中:
- 信号量版本更简洁、安全、高效,推荐优先使用。
- 条件变量版本更灵活,但容易出错,适合需要复杂条件的情况。
「Linux 系统编程」信号量与生产者-消费者模型
https://marisamagic.github.io/2025/09/21/20250921/