认识信号量机制、以及用信号量来实现进程互斥于进程同步
一、信号量机制
信号量机制是操作系统提供的一种高效同步工具,用户进程可以通过使用操作系统提供一对原语来对信号量进行操作,从而很方便的视线了进程互斥、进程同步。
1. 信号量
信号量本质是一个表示系统资源数量的变量,可以是整型或结构体(记录型)。它的核心作用是:
- 当表示 “资源” 时:值为当前可用资源的数量;
- 当用于 “同步” 时:值可表示进程间的执行顺序关系(如 “等待 - 唤醒” 信号)。
2. 一对原语---P、V 原语
对信号量的操作只有三种:初始化、P 操作(申请资源)、V 操作(释放资源),且 P、V 操作是原子操作(执行过程不可中断,避免并发漏洞)。
- P 操作:对应wait(S)原语,意为 “申请一个单位资源”;
- V 操作:对应signal(S)原语,意为 “释放一个单位资源”;
- 其中S是信号量变量,是操作的核心参数。
二、整型信号量
用一个整数型的变量作为信号量,用来表示系统中 某种资源的量
1. 定义与操作逻辑
以 “计算机系统有 1 台打印机” 为例,整型信号量的实现如下:
// 初始化:S=1,表示1台可用打印机
int S = 1;// P操作(申请资源):进入区
void wait(int S) {while (S <= 0); // 资源不够时,循环等待(忙等)S = S - 1; // 资源足够,占用1个
}// V操作(释放资源):退出区
void signal(int S) {S = S + 1; // 释放1个资源,归还系统
}
2. 如何实现进程互斥?
以上述打印机为例,两个进程使用打印机的逻辑:
// 进程P0
wait(S); // 申请打印机(P操作)
使用打印机... // 临界区:唯一进程可进入
signal(S); // 释放打印机(V操作)// 进程P1
wait(S); // 若P0未释放,P1会卡在while循环
使用打印机...
signal(S);
核心逻辑:通过 P 操作 “检查并占用资源”,V 操作 “释放资源”,确保同一时间只有一个进程进入临界区,实现互斥。
3. 致命缺陷:不满足 “让权等待”
整型信号量的最大问题是忙等(Busy Waiting):当资源不足时,进程会在while (S <= 0)循环中持续检查,占用 CPU 却不做有用功,违背 “让权等待” 原则(进程无法获取资源时,应主动让出 CPU,避免资源浪费)。
⚠️ 易错点:整型信号量的 “忙等” 与自旋锁类似,但自旋锁是 “短时间等待” 优化,而整型信号量的忙等无时间限制,效率极低,实际系统中很少使用。
三、记录型信号量:解决整型信号量的 “忙等”问题
为了满足 “让权等待” 原则,操作系统引入记录型信号量—— 在整型基础上增加 “等待队列”,资源不足时让进程阻塞而非忙等,是实际系统中常用的信号量类型。
1. 定义:结构体存储资源与等待队列
记录型信号量是一个结构体,包含两个核心字段:
typedef struct {int value; // 剩余资源数量(核心)struct process *L; // 等待该资源的进程队列(解决忙等的关键)
} semaphore;
2. P、V 操作逻辑
以 “系统有 2 台打印机” 为例(初始化S.value=2,S.L=NULL),操作逻辑如下:
(1)P 操作(申请资源):先减后判断
void wait(semaphore S) {S.value--; // 先申请:资源数减1if (S.value < 0) { // 资源不足(负数绝对值=等待进程数)block(S.L); // 进程自我阻塞:从运行态→阻塞态,插入等待队列S.L}// 资源足够:直接进入临界区
}
- S.value--是 “先占坑”,避免多个进程同时申请导致资源计数错误;
- 当S.value < 0:说明当前资源已分配完,当前进程需阻塞,主动让出 CPU(满足 “让权等待”);
- 例如:2 台打印机被 P0、P1 占用(S.value=0),P2 申请时S.value=-1,P2 阻塞,等待队列S.L=[P2]。
(2)V 操作(释放资源):先加后判断
void signal(semaphore S) {S.value++; // 先释放:资源数加1if (S.value <= 0) { // 仍有进程在等待(负数→有等待,0→刚唤醒最后一个)wakeup(S.L); // 唤醒等待队列首进程:从阻塞态→就绪态}// 无进程等待:直接结束
}
- S.value++是 “先归还”,更新资源计数;
- 当S.value <= 0:说明释放资源后仍有进程在等待(如S.value=-1→释放后S.value=0,需唤醒 1 个进程);
- 例如:P0 释放打印机(S.value=-1+1=0),此时S.value<=0,唤醒等待队列的 P2,P2 进入就绪态。
3. 记录型信号量的优势
- 解决 “忙等”:资源不足时进程阻塞,让出 CPU,提高资源利用率;
- 支持多资源管理:可通过多个信号量分别管理不同类型资源(如打印机、内存、磁盘);
- 灵活支持同步与互斥:既能实现 “同一资源互斥访问”,也能实现 “多进程按顺序执行”。
四、信号量的两大核心应用
信号量机制的核心价值,在于能灵活实现进程互斥和进程同步。
1. 应用 1:实现进程互斥
设置一个 “互斥信号量”mutex,初始化mutex=1(表示 “临界区可用”),在所有进程的 “临界区前后” 分别执行 P、V 操作。
示例:两个进程共享一个缓冲区
// 初始化互斥信号量:mutex=1
semaphore mutex = {1, NULL};// 进程A
P(mutex); // 申请进入临界区(互斥)
写入数据到缓冲区... // 临界区
V(mutex); // 释放临界区// 进程B
P(mutex); // 若A未释放,B会阻塞
从缓冲区读取数据... // 临界区
V(mutex);
关键点:
- 互斥信号量mutex的初值必须为 1;
- 每个进程的临界区必须被 “P (mutex)” 和 “V (mutex)” 包围,且不能遗漏(否则会导致互斥失效)。
2. 应用 2:实现进程同步
设置 “同步信号量”S,初始化S=0(表示 “初始时需等待”),在 “需要等待的进程” 后执行 P 操作,在 “触发等待进程的进程” 后执行 V 操作,强制进程按顺序执行。
示例:进程 A 必须在进程 B 之后执行(B 先输出 “Hello”,A 再输出 “World”)
// 初始化同步信号量:S=0(A需等待B的信号)
semaphore S = {0, NULL};// 进程B
printf("Hello "); // B的任务
V(S); // 释放信号,通知A“可以执行”// 进程A
P(S); // 等待B的信号(S初始为0,A会阻塞,直到B执行V(S))
printf("World"); // A的任务,确保在B之后执行
关键点:
- 同步信号量S的初值根据执行顺序设定(通常为 0,表示 “先执行的进程需触发后执行的进程”);
- P 操作放在 “需要等待的进程” 的关键节点前,V 操作放在 “触发等待的进程” 的关键节点后。
五、核心总结
知识点 | 关键内容 |
信号量核心操作 | P(S):申请资源(S--,<0 则阻塞);V(S):释放资源(S++,<=0 则唤醒) |
整型信号量缺陷 | 不满足 “让权等待”,存在忙等 |
记录型信号量优势 | 有等待队列,资源不足时进程阻塞,满足让权等待 |
原子操作的意义 | 避免 P/V 操作被中断,防止信号量值混乱(如多个进程同时执行 S-- 导致计数错误) |
六、与互斥锁的对比:何时选信号量?
上一篇的互斥锁和本篇的信号量都能实现进程互斥,但适用场景不同:
维度 | 互斥锁 | 信号量 |
核心功能 | 仅实现临界区互斥 | 可实现互斥 + 同步 + 多资源管理 |
资源类型 | 单一种类资源(如一把锁) | 多种资源(多个信号量) |
等待机制 | 阻塞等待(传统互斥锁) | 阻塞等待(记录型) |
适用场景 | 简单共享资源竞争(如单缓冲区) | 复杂进程协作(如生产者 - 消费者、读者 - 写者) |