王道操作系统笔记,视频链接:2.3.2.1 进程互斥的软件实现方法
知识总览
- 进程互斥的软件实现方法:
- 单标志法
- 双标志先检查
- 双标志后检查
- Peterson算法
- PS:该小节考察频率较高,经常出现在选择题或大题中考察。
- 学习提示:
- 理解各个算法的思想、原理
- 结合上小节学习的"实现互斥的四个逻辑部分",重点理解个算法在进入区、退出区都做了什么
- 分析各算法存在的缺陷(结合"实现互斥要遵循的四个原则'进行分析)
没有进程互斥的情况
- 例子:比如进程A、B在系统中并发地运行,都是使用打印机打印内容,如果没有进程互斥,可能会出现A在使用打印机的过程中,时间片耗尽,操作系统调度B,B也使用打印机打印,结果就是A、B的打印内容混在一起了。
单标志法
- 算法思想:两个进程在访问完临界区 后,会把使用临界区的权限转交给另一个进程。也就是说每个进程进入临界区的权限只能被另一个进程赋予。
- 代码举例:
cpp
int turn = 0; //turn 表示当前允许进入临界区的进程号
P0 进程:
while (turn != 0); ① //进入区
critical section; ② //临界区
turn = 1; ③ //退出区
remainder section; ④ //剩余区
P1 进程:
while (turn != 1); ⑤ //进入区
critical section; ⑥ //临界区
turn = 0; ⑦ //退出区
remainder section; ⑧ //剩余区
// turn的初值为0,即刚开始只允许0好进程进入临界区。
// 若P1线上处理机运行,则会一直卡在⑤,直到P1的时间片用完,发生调度,切换P0上处理机运行。
// 代码①则不会卡住P0,P0可以正常访问临界区,在P0访问临界区期间即时切换回P1,P1依然会卡在⑤
// 只有P0在退出区将turn改为1后,P1才能进入临界区
因此,该算法可以实现"同一时刻最多只允许一个进程访问临界区"。
- 方法存在的问题:
- 按照上面的例子,临界资源只能按P0 → \to →P1 → \to →P0 → \to →P1 → \to →......这样轮流访问。
- 这种必须"轮流访问"带来的问题是,如果此时允许进入临界区的进程是P0,而P0一直不访问临界区,那么虽然此时临界区空闲,但是并不允许P1访问。
- 因此,单标志法 存在的主要问题 是:违背"空闲让进"原则。
双标志先检查法
- 算法思想:设置一个布尔型数组
flag[],数组中各个元素用来标记各进程想进入临界区的意愿,比如"flag[0]=true",意味着0号进程P0现在想要进入临界区。每个进程在进入临界区之前先检查当前有没有另一个进程想进入临界区,如果没有,则把自身对应的标志flag[i]设为true,之后开始访问临界区。 - 代码举例:
cpp
bool flag[2]; //表示进入临界区意愿的数组
flag[0] = false;
flag[1] = false; //刚开始设置为两个进程都不想进入临界区
PO 进程:
while (flag[1]); ①
flag[0] = true; ②
critical section; ③
flag[0] = false; ④
remainder section;
P1 进程:
while (flag[0]); ⑤ //如果此时 P0 想进入临界区,P1 就一直循环等待
flag[1] = true; ⑥ //标记为 P1 进程想要进入临界区
critical section; ⑦ //访问临界区
flag[1] = false; ⑧ //访问完临界区,修改标记为 P1 不想使用临界区
remainder section;
// 每个进程在进入区(对应①②和⑤⑥)做的两个事情:
// 第一,检查其他进程是否想要进入临界区,
// 第二,如果其他进程不想进入,就表达自己想要进入(将flag设置为ture,视为上锁)
// 使用完临界资源后,将flag设置为false,也就是解锁。
- 方法存在的问题:
- 按照上述例子,如果按照①⑤②⑥③⑦...的顺序执行,P0和P1将会同时访问临界区。
- 也就是假如P0进程和P1进程并行运行,P0运行完①后,还没运行②,P1进程刚好运行运行了⑤,
- 此时会造成:P0和P1都认为临界区没有其他进程使用,于是都会标记,同时使用临界区。
- 因此,双标志先检查法 的主要问题 是:违反"忙则等待"原则。
- 原因在于,进入区 的"检查"和"上锁"两个处理不是一气呵成的。"检查"后,"上锁"前可能发生进程切换。
- PS:和单标志法区别?
- 看起来好像都是要看有没有标志,但是单标志法的标志会一直存在(比如例子中的默认
turn = 0,运行后修改为turn = 1),所以不会出现某进程认为临界区没有其他进程使用的情况(哪怕turn对应的进程真的没在使用) - 而双标志先检查法会出现一段时间所有的flag均为0,此时若多个进程在其被修改前进行检查,就可能出现都认为临界区没有其他进程使用的情况。
- 看起来好像都是要看有没有标志,但是单标志法的标志会一直存在(比如例子中的默认
双标志后检查法
- 算法思想:双标志先检查法的改版。前一个算法的问题是先"检查"后"上锁",但是这两个操作又无法一气呵成,因此导致了两个进程同时进入临界区的问题。因此,人们又想到**先"上锁"后"检查"**的方法,来避免上述问题。
- 代码举例:
c++
bool flag[2]; //表示进入临界区意愿的数组
flag[0] = false;
flag[1] = false; //刚开始设置为两个进程都不想进入临界区
P0 进程:
flag[0] = true; ①
while (flag[1]); ②
critical section; ③
flag[0] = false; ④
remainder section;
P1 进程:
flag[1] = true; ⑤ //标记为 P1 进程想要进入临界区
while (flag[0]); ⑥ //如果 P0 也想进入临界区,则 P1 循环等待
critical section; ⑦ //访问临界区
flag[1] = false; ⑧ //访问完临界区,修改标记为 P1 不想使用临界区
remainder section;
- 方法存在的问题:
- 若按照①⑤②⑥...的顺序执行,P0和P1将都无法进入临界区
- 因此,双标志后检查法虽然解决了"忙则等待"的问题,但是又违背了"空闲让进"和"有限等待"原则,会因各进程都长期无法访问临界资源而**产生"饥饿"**现象。
- 两个进程都争着想进入临界区,但是谁也不让谁,最后谁都无法进入临界区。
Peterson算法
- 算法思想:结合双标志法、单标志法的思想。如果双方都想进入临界区,那可以让进程尝试"孔融让梨"(谦让),做一个有礼貌的进程。
- 代码示例:
c++
bool flag[2]; //表示进入临界区意愿的数组,初始值都是false
int turn = 0; //turn表示优先让哪个进程进入临界区
P0 进程:
flag[0] = true; ①
turn = 1; ②
while (flag[1] && turn==1); ③
critical section; ④
flag[0] = false; ⑤
remainder section;
P1 进程:
flag[1] = true; ⑥ //表示自己想进入临界区
turn = 0; ⑦ //可以优先让对方进入临界区
while (flag[0] && turn==0); ⑧ //对方想进,且最后一次是自己"让梨",那自己就循环等待
critical section; ⑨
flag[1] = false; ⑩ //访问完临界区,表示自己已经不想访问临界区了
remainder section;
// 代码中的进入区对应①②③和⑥⑦⑧,分别对应三个动作:
// 1. 主动争取
// 2. 主动谦让
// 3. 检查对方是否也想使用,且最后依次是不是自己说了"客气话"
// 谁最后说了"客气话",谁就失去了行动的优先权。
// 动手推导:
// 动手推导:
// 按不同的顺序穿插执行会发生什么?
// ①②③⑥⑦⑧...
// ①⑥②③...
// ①③⑥⑦⑧...
// ①⑥②⑦⑧...
- 结论:
- 优点:
- Peterson算法用软件方法解决了进程互斥问题。
- 遵循了空闲让进、忙则等待、有限等待三个原则。
- 缺点:
- 但是依然未遵循让权等待的原则。(即P0进程会在无法进入临界区时卡在while循环的部分)
- 总结:Peterson算法相较于之前三种软件解决方案来说,是最好的,但依然不够好。
- 优点:
知识回顾与重要考点

- 总结:
- 单标志法------谦让
- 双标志法------进入意愿
- 双标志先检查思路没问题,问题在于检查和上锁无法一气呵成,如果可以实现一气呵成(比如利用硬件实现),这个方法就没问题。
- Peterson算法------兼顾两者