用信号量实现进程互斥,进程同步,进程前驱关系(操作系统os)
用一个大家都很熟悉的场景来贯穿这三种应用:准备一场团队聚餐。
- 进程:团队里的每个成员(小明、小红、小刚等)。
- 任务:买菜、洗菜、切菜、炒菜、摆碗筷等。
1. 用信号量实现进程互斥 - “厨房只有一个灶台,大家轮流用”
在我们的聚餐准备中,灶台就是那个最关键的临界资源,一次只能有一个人炒菜。如果小明和小红同时去炒菜,那锅就得飞起来了。
实现步骤:
- 划定临界区:找到那段必须互斥执行的代码,也就是“炒菜”这个动作。
- 设定互斥信号量:我们引入一个专门用于“灶台使用权”的信号量,叫
mutex_stove
,它的初始值设为 1。这个1
就代表“当前有1个可用的灶台名额”。 - 在临界区前后加上P、V操作:
- 任何一个人想炒菜(进入临界区)之前,必须先执行
P(mutex_stove)
。这个P操作的意思是“申请一个灶台名额”。如果名额有(value
从1变0),就进去炒。如果名额没了(value
从0变-1),就去旁边等着(阻塞)。 - 炒完菜(退出临界区)之后,必须立刻执行
V(mutex_stove)
。这个V操作的意思是“归还灶台名额”。这会让value
加1,如果有人在等,就能把他唤醒。
- 任何一个人想炒菜(进入临界区)之前,必须先执行
代码框架:
semaphore mutex_stove = 1; // 互斥信号量,初值为1Process_小明() {...P(mutex_stove); // 申请使用灶台// --- 临界区开始 ---炒菜();// --- 临界区结束 ---V(mutex_stove); // 释放灶台... }Process_小红() {...P(mutex_stove); // 申请使用灶台// --- 临界区开始 ---炒菜();// --- 临界区结束 ---V(mutex_stove); // 释放灶台... }
关键点:
- 互斥信号量的初始值永远是1。
- P、V操作必须成对出现。忘了
P
,大家就一起冲进去炒菜了;忘了V
,第一个用完灶台的人就把门锁死,谁也别想再用了。
2. 用信号量实现进程同步 - “必须先洗完菜,才能切菜”
现在,我们有了更复杂的协作要求。团队里,小明负责洗菜,小红负责切菜。这个顺序是绝对不能乱的,你不能切一堆带着泥的土豆。
实现步骤:
- 分析制约关系:明确哪个动作是“前操作”(洗菜),哪个是“后操作”(切菜)。
- 设定同步信号量:我们引入一个专门用于“洗菜完成”这个信号的信号量,叫
vegetable_washed
,它的初始值设为 0。这个0
代表“默认情况下,菜还没洗好”。 - 遵循“前V后P”原则:
- 前操作之后,执行V操作:小明洗完菜后,他要“吼一嗓子”,告诉大家菜洗好了。这个动作就是
V(vegetable_washed)
。它会把信号量value
从0变成1,表示“有1份洗好的菜可以用了”。 - 后操作之前,执行P操作:小红在准备切菜之前,她必须先等待“菜洗好了”这个信号。这个动作就是
P(vegetable_washed)
。如果信号量是0(菜没洗好),她就会在这里被阻塞。直到小明执行了V操作,信号量变成1,她才能通过P操作,开始切菜。
- 前操作之后,执行V操作:小明洗完菜后,他要“吼一嗓子”,告诉大家菜洗好了。这个动作就是
代码框架:
semaphore vegetable_washed = 0; // 同步信号量,初值为0Process_小明() { // 前驱进程...洗菜();V(vegetable_washed); // 发出“菜已洗好”的信号... }Process_小红() { // 后继进程...P(vegetable_washed); // 等待“菜已洗好”的信号切菜();... }
关键点:
- 同步信号量的初始值通常是0。
- 记住这个口诀:“前V后P”。前驱任务做完后V,后继任务开始前P。
3. 用信号量实现前驱关系 - “复杂的流水线作业”
现在,我们的聚餐准备工作形成了一个复杂的流程图(前驱图):
- S1: 买菜
- S2: 洗菜 (必须等S1完成)
- S3: 洗锅 (必须等S1完成)
- S4: 切菜 (必须等S2完成)
- S5: 准备调料 (必须等S3完成)
- S6: 炒菜 (必须等S4和S5都完成)
这本质上就是一连串的同步问题。
实现步骤:
- 为每一对直接制约关系设立一个同步信号量。
S1 -> S2
:需要一个信号量s12
S1 -> S3
:需要一个信号量s13
S2 -> S4
:需要一个信号量s24
S3 -> S5
:需要一个信号量s35
S4 -> S6
:需要一个信号量s46
S5 -> S6
:需要一个信号量s56
- 所有这些同步信号量的初始值都设为 0。
- 对每一条“弧线”,都应用“前V后P”原则。
- 为每一对直接制约关系设立一个同步信号量。
代码框架(重点看S1, S6):
// 声明所有需要的同步信号量,初值都为0 semaphore s12=0, s13=0, s24=0, s35=0, s46=0, s56=0;S1_买菜() {买菜();V(s12); // 通知S2可以开始了V(s13); // 通知S3可以开始了 }// ... S2, S3, S4, S5 的代码省略,都是标准的前V后P ...S6_炒菜() {P(s46); // 等待S4(切菜)完成P(s56); // 等待S5(备调料)完成炒菜(); }
关键点:
- 一个任务有多少个直接后继,它后面就要跟多少个V操作。
- 一个任务有多少个直接前驱,它前面就要有多少个P操作。
必会题与详解
题目一:在使用信号量实现进程互斥时,为什么互斥信号量mutex
的初始值必须是1?如果设为0或2会发生什么?
答案详解:
互斥信号量mutex
的值代表“允许进入临界区的名额数量”。
初始值必须是1:因为我们希望在任何时候,最多只允许一个进程进入临界区。初始值为1,表示一开始有1个名额。第一个进程执行P操作后,
mutex
变为0,名额用完。其他进程再执行P操作就会被阻塞,从而保证了互斥。如果设为0:表示一开始就没有任何名额。那么第一个到达的进程执行
P(mutex)
时,mutex
会从0变为-1,该进程会被立即阻塞。结果就是没有任何进程能够进入临界区,系统陷入死锁。如果设为2:表示一开始有2个名额。第一个进程执行
P(mutex)
后,mutex
变为1。第二个进程执行P(mutex)
后,mutex
变为0。这意味着系统将允许两个进程同时进入临界区,这完全违背了“互斥”的初衷,会导致对临界资源的访问发生混乱。
题目二:在实现“进程A执行完代码a1后,进程B才能执行代码b1”这一同步关系时,同步信号量S的初始值应该是什么?P(S)和V(S)应该分别放在哪个位置?请解释原因。
答案详解:
初始值:同步信号量S的初始值应该设为 0。这代表一个“消息”或“事件”的初始状态是“未发生”。在这里,它表示“进程A的代码a1尚未完成”。
P、V操作的位置:
- V(S) 应该放在进程A的代码a1之后。
- P(S) 应该放在进程B的代码b1之前。
原因(遵循“前V后P”原则):
- 进程B是后继进程,它依赖于进程A的完成。因此,B在执行b1前必须“等待”一个信号,这个等待动作就是
P(S)
。由于S初始为0,进程B会在此处阻塞,直到S的值变为正数。 - 进程A是前驱进程,它在完成a1后,有责任“发送”一个信号来通知B可以继续了。这个发送信号的动作就是
V(S)
。它会使S的值从0变为1,从而唤醒正在等待P(S)
的进程B,使得B可以越过P操作,继续执行b1。这个机制保证了正确的执行顺序。
- 进程B是后继进程,它依赖于进程A的完成。因此,B在执行b1前必须“等待”一个信号,这个等待动作就是