「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
#include <semaphore.h>

2.1 信号量主要函数

  1. 创建/打开命名信号量

    1
    sem_t *sem_open(const char *name, int oflag, ... /* mode_t mode, unsigned int value */);
    • name:信号量名称(用于进程间共享)
    • oflagO_CREATO_EXCL 等标志
    • mode:权限(如 0644)
    • value:初始值
  2. 初始化无名信号量(线程/进程内共享)

    1
    int sem_init(sem_t *sem, int pshared, unsigned int value);
    • sem:指向信号量变量的指针
    • pshared
      • 0:仅限当前进程内线程共享
      • 非0:可用于进程间共享(需放在共享内存中)
    • value:初始值。可用资源的数量,直接决定了 可以同时访问临界区的线程数量
  3. P 操作(等待)

    1
    2
    3
    int sem_wait(sem_t *sem);        // 阻塞等待
    int sem_trywait(sem_t *sem); // 非阻塞尝试
    int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout); // 超时等待
  4. V 操作(释放)

    1
    int sem_post(sem_t *sem);
  5. 获取当前值

    1
    int sem_getvalue(sem_t *sem, int *sval);
  6. 销毁/关闭

    1
    2
    3
    int sem_destroy(sem_t *sem);     // 销毁无名信号量
    int sem_close(sem_t *sem); // 关闭命名信号量
    int sem_unlink(const char *name); // 删除命名信号量

2.2 信号量使用示例

以下为一个简单的使用信号量实现线程同步的示例。允许同时最多 2 个线程访问临界资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <semaphore.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

sem_t sem;

void* thread_func(void* arg) {
int id = *(int*)arg;
sem_wait(&sem); // V: sem -- , sem < 0 阻塞等待
printf("线程 %d 进入临界区\n", id);
sleep(1); // 模拟工作
printf("线程 %d 离开临界区\n", id);
sem_post(&sem); // P: sem ++ , sem >= 0 唤醒一个线程
return NULL;
}

int main() {
pthread_t threads[5];
int thread_id[5];

// 设置信号量 value = 2,表示最多允许2个线程同时访问
sem_init(&sem, 0, 2);

for (int i = 0; i < 5; i++) {
thread_id[i] = i + 1;
pthread_create(&threads[i], NULL, thread_func, &thread_id[i]);
}

for (int i = 0; i < 5; i++) {
pthread_join(threads[i], NULL);
}

sem_destroy(&sem);

return 0;
}



3 信号量实现生产者-消费者模型

3.1 生产者-消费者模型需要哪些信号量

  • 生产者:生成数据并放入缓冲区。
  • 消费者:从缓冲区取出数据并消费。
  • 缓冲区:大小有限,需同步访问。

我们需要三个信号量:

信号量 作用 初始值
empty 空槽位数量 缓冲区大小 N
full 已填充槽位数量 0
mutex 互斥访问缓冲区 1
  • emptyfull 控制资源数量,避免缓冲区溢出或下溢。
  • mutex 保证 对缓冲区的互斥访问,防止数据竞争。

在信号量配合互斥量实现的生产者-消费者模型中:

  • mutex 互斥量:确保同一时刻只有一个线程能访问缓冲区(临界资源)。
  • emptyfull 信号量:控制有多少个线程可以等待在缓冲区操作上,但不控制同时访问的线程数量。

3.2 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#include <time.h>

#define BUFFER_SIZE 3
#define PRODUCER_NUM 2
#define CONSUMER_NUM 5

// 缓冲区
int buffer[BUFFER_SIZE];
int in = 0, out = 0; // 生产/消费指针

// 信号量
sem_t empty; // 空槽位
sem_t full; // 满槽位
sem_t mutex; // 互斥锁

// 生产者线程
void* producer(void* arg) {
int id = *(int*)arg;
for (int i = 0; i < 10; i++) {
int item = rand() % 100;

sem_wait(&empty); // 等待空位
sem_wait(&mutex); // 进入临界区

buffer[in] = item;
printf("生产者 %d 生产数据 %d 到位置 %d\n", id, item, in);
in = (in + 1) % BUFFER_SIZE;

sem_post(&mutex); // 离开临界区
sem_post(&full); // 增加满位

usleep(rand() % 1000 * 1000); // 模拟生产时间
}
return NULL;
}

// 消费者线程
void* consumer(void* arg) {
int id = *(int*)arg;
for (int i = 0; i < 4; i++) {
sem_wait(&full); // 等待有数据
sem_wait(&mutex); // 进入临界区

int item = buffer[out];
printf("消费者 %d 消费数据 %d 从位置 %d\n", id, item, out);
out = (out + 1) % BUFFER_SIZE;

sem_post(&mutex); // 离开临界区
sem_post(&empty); // 增加空位

usleep(rand() % 1000 * 1000); // 模拟消费时间
}
return NULL;
}

int main() {
srand(time(NULL));

// 初始化信号量
sem_init(&empty, 0, BUFFER_SIZE); // 初始空位 = 缓冲区大小
sem_init(&full, 0, 0); // 初始满位 = 0
sem_init(&mutex, 0, 1); // 互斥锁

// 创建线程
pthread_t prod_threads[PRODUCER_NUM];
pthread_t cons_threads[CONSUMER_NUM];
int ids[PRODUCER_NUM + CONSUMER_NUM];

for (int i = 0; i < PRODUCER_NUM; i++) {
ids[i] = i + 1;
pthread_create(&prod_threads[i], NULL, producer, &ids[i]);
}

for (int i = 0; i < CONSUMER_NUM; i++) {
ids[PRODUCER_NUM + i] = i + 1;
pthread_create(&cons_threads[i], NULL, consumer, &ids[PRODUCER_NUM + i]);
}

// 等待线程结束
for (int i = 0; i < PRODUCER_NUM; i++) {
pthread_join(prod_threads[i], NULL);
}
for (int i = 0; i < CONSUMER_NUM; i++) {
pthread_join(cons_threads[i], NULL);
}

// 销毁信号量
sem_destroy(&empty);
sem_destroy(&full);
sem_destroy(&mutex);

return 0;
}

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
2
3
4
5
sem_wait(&empty);   // 等待空位 —— 控制资源数量
sem_wait(&mutex); // 互斥访问缓冲区
// ... 操作缓冲区 ...
sem_post(&mutex);
sem_post(&full); // 通知消费者
  • 条件变量实现
1
2
3
4
5
6
7
8
9
pthread_mutex_lock(&mutex);
while (count == BUFFER_SIZE) { // 必须用 while,防止虚假唤醒
pthread_cond_wait(&not_full, &mutex); // 自动释放 mutex,等待
}
// ... 操作缓冲区 ...
count ++ ;
pthread_cond_signal(&not_empty); // 通知一个消费者
// pthread_cond_broadcast(&not_empty); // 通知所有消费者
pthread_mutex_unlock(&mutex);

4.3 核心机制和唤醒行为对比

  • 信号量:资源计数驱动

    • empty 信号量初始值 = 缓冲区大小 → 表示“可用空位数量”
    • 每次 sem_wait(&empty) 成功,计数器减1 → 表示占用一个空位
    • 每次 sem_post(&empty),计数器加1 → 表示释放一个空位
    • 计数器值直接反映资源可用数量 → 系统自动管理“谁该阻塞/唤醒”

    ✅ 优点:逻辑清晰,自动管理资源配额,不易出错。
    ✅ 适合:资源数量明确、需要限制并发访问数量的场景

  • 条件变量:条件判断驱动

    • 没有内置计数器,需程序员自己维护 countinout 等状态变量
    • pthread_cond_wait() 只是“睡一觉”,醒来后必须重新检查条件是否满足
    • pthread_cond_signal() 不保证“正好唤醒需要的人”,可能唤醒错误线程(需重新 sleep)

    ✅ 优点:更灵活,可表达复杂条件(如“缓冲区 > 50% 才通知生产者”)
    ⚠️ 缺点:易出错(如用 if 代替 while、忘记发 signal、signal 丢失等)


行为 信号量 条件变量
默认唤醒 唤醒一个等待者 signal 唤醒一个,broadcast 唤醒所有
是否公平 通常 FIFO(取决于实现) 不保证(可能饥饿)
是否可预测 ✅ 计数器决定,行为确定 ❌ 依赖调度,行为较难预测(需要自己避免虚假唤醒)

4.4 对比总结

  1. 信号量限制每个时间段内最多活跃的线程数量

  2. 信号量 vs 条件变量

    • 信号量更适合 资源数量明确、需要限制并发度 的场景(如连接池、线程池、缓冲区管理)。
    • 条件变量更适合 复杂条件判断、事件驱动 的场景(如“当队列长度 > N 时唤醒生产者”)。
  3. 在生产者-消费者模型中:

    • 信号量版本更简洁、安全、高效,推荐优先使用。
    • 条件变量版本更灵活,但容易出错,适合需要复杂条件的情况。

「Linux 系统编程」信号量与生产者-消费者模型
https://marisamagic.github.io/2025/09/21/20250921/
作者
MarisaMagic
发布于
2025年9月21日
许可协议