信号量
1.1信号量的描述
信号量是一个特殊变量,一般取正数值,用于控制对共享资源的访问。
| 概念 | 说明 |
|---|---|
| 信号量的值 | 代表允许访问的资源数目 |
P操作(sem_wait) | 获取资源:信号量值原子减1;若值为0,则阻塞等待 |
V操作(sem_post) | 释放资源:信号量值原子加1;唤醒等待的线程/进程 |
| 二值信号量 | 值只取0 或 1(相当于互斥锁) |
| 计数信号量 | 值> 1(控制多个资源,如连接池) |
信号量本质是一个"计数器":
>0→ 有可用资源,拿一个走(值减1)==0→ 没资源了,要排队等(阻塞)用完归还 → 值加1,唤醒排队的人
临界资源:同一时刻,只允许被一个进程或线程访问的资源(如共享变量、打印机)
临界区:访问临界资源的代码段(需要用信号量/锁保护)
1.2信号量使用
头文件sem.h
union semun{ int val; }; void sem_init(); // 初始化信号量 void sem_p(); // P操作(申请资源) void sem_v(); // V操作(释放资源) void sem_destory(); // 销毁信号量(拼写应为 destroy)sem.c实现
信号量核心函数与结构体
1.semget—— 创建或获取信号量集
#include <sys/sem.h> int semget(key_t key, int nsems, int semflg);| 参数 | 说明 |
|---|---|
key | 全局唯一标识符(ftok生成,或IPC_PRIVATE) |
nsems | 信号量个数(通常为 1) |
semflg | 权限标志(如IPC_CREAT | 0666) |
| 返回值 | 说明 |
|---|---|
>= 0 | 成功,返回信号量集 ID(semid) |
-1 | 失败 |
2.semctl—— 控制信号量集
int semctl(int semid, int semnum, int cmd, ...);| 参数 | 说明 |
|---|---|
semid | semget返回的信号量集 ID |
semnum | 操作第几个信号量(从 0 开始) |
cmd | 控制命令 |
常用cmd | 作用 |
|---|---|
SETVAL | 设置信号量的值(通过第四个参数) |
GETVAL | 获取信号量的值(返回值) |
IPC_RMID | 删除信号量集 |
// 设置信号量值为 1 semctl(semid, 0, SETVAL, 1); // 获取信号量当前值 int val = semctl(semid, 0, GETVAL); // 删除信号量集 semctl(semid, 0, IPC_RMID);3.结构体sembuf—— 定义信号量操作
struct sembuf { unsigned short sem_num; // 操作第几个信号量(从0开始) short sem_op; // 操作数(正/负/零) short sem_flg; // 操作标志(阻塞/非阻塞) };sem_flg常用值 | 说明 |
|---|---|
0 | 默认:阻塞等待 |
IPC_NOWAIT | 非阻塞:不等待,立即返回错误 |
SEM_UNDO | 进程退出时自动撤销对该信号量的操作(防止死锁) |
sem_op值 | 效果 |
|---|---|
| 负数(-1) | P操作:获取资源(信号量值减少),若值不够则阻塞 |
| 正数(+1) | V操作:释放资源(信号量值增加),唤醒等待者 |
| 0 | 等待信号量值为 0 |
执行过程:
通过
semid在内核中找到对应的信号量对象从联合体
a中取出val的值(1)把这个值赋给找到的信号量
- 信号量的状态从原来的值(可能是0)变成了1
- P = 获取资源,把信号量从"未占用"改为"占用"
V = 释放资源,把信号量从"占用"改为"未占用" struct sembuf完整结构,用于描述对信号量的具体操作
面试题
例题:进程a和进程b模拟访问打印机,进程a输出第一个字符‘a’表示开始使用打印
机,输出第二个字符‘a’表示结束使用,b进程操作与进程相同。由于打印机同一时刻
只能被一个进程使用,所以输出结果不应该出现abab),
c#a.c
C#b.c代码实现:
生产者-消费者 vs 读者-写者
一、核心关系对比
| 维度 | 生产者-消费者 | 读者-写者 |
|---|---|---|
| 核心冲突 | 速率不匹配(生产太快/消费太慢) | 读写冲突(数据一致性) |
| 角色 | 生产数据 vs 消费数据 | 读数据 vs 写数据 |
| 并发允许 | 生产和消费不能同时操作同一个槽位 | 读-读可并发,读-写互斥,写-写互斥 |
二、同步条件
| 生产者-消费者 | 读者-写者 |
|---|---|
| 缓冲区不满 → 才能生产 | 无写者 → 读者可读 |
| 缓冲区不空 → 才能消费 | 无读者/写者 → 写者可写 |
| 互斥访问缓冲池 | 写者优先级通常高于读者(防止写饿死) |
三、信号量设计
| 生产者-消费者 | 读者-写者 |
|---|---|
empty:空位数(生产者关心) | wrt:写互斥(读写/写写互斥) |
full:数据数(消费者关心) | mutex:保护读者计数 |
mutex:保护缓冲池 | readcount:当前读者数 |
四、典型应用
| 生产者-消费者 | 读者-写者 |
|---|---|
| 任务队列、日志缓冲、管道 | 数据库读写锁、文件系统缓存 |
总结
生产者-消费者管的是速率匹配(队列空/满),用
empty/full两个信号量;读者-写者管的是访问权限(读读共享,读写互斥),用
wrt保证写独占。
Reactor 模型
| 生产者-消费者角色 | Reactor 中的对应组件 | 说明 |
|---|---|---|
| 生产者 | 主线程(Reactor 线程) | 通过epoll_wait监听事件,一旦有 I/O 就绪,就生产一个“任务” |
| 产品/任务 | 就绪事件(fd 可读/可写) | 一个“连接可读”或“连接可写”的事件 |
| 缓冲队列 | 请求队列 | 主线程将事件放入队列,工作线程从队列取 |
| 消费者 | 工作线程(Worker 线程) | 从队列中取事件,执行 Read、Process、Write |
映射关系
主线程(生产者) │ ├── epoll_wait() → 监听事件(原材料) ├── 插入就绪的事件 → 生产任务(放入队列) └── 请求队列 → 缓冲区(任务队列) │ ▼ 工作线程(消费者) ├── Read → 读数据 ├── Process → 处理业务 └── Write → 写回客户端
Reactor 的变体分类
| 变体 | 说明 | 生产者-消费者体现 |
|---|---|---|
| 单 Reactor 单线程 | 一个线程既负责监听,又负责处理 | 生产者和消费者是同一个线程(串行) |
| 单 Reactor 多线程 | 主线程负责监听,工作线程负责处理 | 主线程是生产者,工作线程是消费者 |
| 多 Reactor 多线程 | 多个 Reactor 线程各自监听和处理 | 多个生产者 + 多个消费者 |
Reactor 模式 = 主线程(生产者)用 epoll 生产事件 + 请求队列(缓冲区)+ 工作线程(消费者)处理事件。
这就是生产者-消费者模型在网络编程中的具体应用。