☕️
文章目录
model ☁️
上一篇文章地址链接: 【操作系统⑥】——进程联系与临界区管理【同步与互斥 Dekker算法 TS指令 SWAP指令】.
下一篇文章地址链接: 【操作系统⑧】——信号量与PV操作(下)【哲学家进餐问题 AND型信号量 信号量集机制】.
期末考试总复习——地址链接:《操作系统期末总复习——绝地求生版》.
一、概述
● 在上一篇文章中提到各种管理临界区的,不管是软件方法,还是硬件的方法,都是存在一个隐藏的问题:它们都是一种平等进程间的协商机制,在协商分配资源时,可能会出现公用资源的使用问题。这时我们就需要一个地位高于进程的管理者来解决这类问题。
● 1965年,荷兰著名科学家,Dijkstra提出了一种控制进程同步、互斥的信号量与P/V操作机制。有效地解决了其问题。
▶ 这里举个栗子
来说,在百度百科上找的例子上进行了拓展:
以一个停车场的运作为例。简单起见,假设停车场只有 3 个车位,一开始 3 个车位都是空的。这时如果同时来了 5 辆车,看门人允许其中 3 辆直接进入,然后放下车拦,剩下的车则必须在入口等待。此后来的车也都不得不在入口处等待。这时,有 1 辆车离开停车场,看门人
得知后,打开车拦,放入外面的 1 辆进去,如果又离开 2 辆,则又可以放入 2 辆,如此往复。
在这个停车场系统中,车位是公共资源,每辆车好比一个进程,看门人
起的就是信号量
的作用。
那这和上一章的并发处理机制比起来,有什么不同呢?
如果说,上一章也存在这么一个 “停车场和车位”(公共资源),也有这么些 “车”(进程)的话。那么上一章就不存在这个 “看门人
”(信号量)。而当前 3 辆车进入时,第 4、5 两辆车就不能进入,他俩就在原地等待,但是他们的车 “没有熄火”,一直 “启动着车的”,因为他俩不知道多久有车能出来,所以他俩人也是醒着的。但是,当有了 “看门人
”(信号量) 的时候,他俩就可以 “熄火并安心睡觉” 了。因为当有车出来的时候,“看门人
” 会自动来叫醒他俩。另外,“看门人” 具有 “P操作
”(放车进去) 和 “V操作
”(放车出去)。
绿色字体部分是个人理解,不一定完全正确,旨在帮助理解。
● 重点掌握两个模板、几个经典例题的PV操作。然后在遇到问题时,能够回忆起这些模板和PV操作时,再结合具体情形,即可快速有效地解决问题。
二、信号量的定义
● 信号量的一个数据结构。
/* 基于 C 语言写的伪代码 */
strcut semaphore
{
int count;
queue<进程的PCB> wait_queue;
}
◆ 信号量一般由两部分组合,第一个部分是整数类型的计数器(储存了信号量的数值)。第二个部分是一个等待队列,里面装有等待进程的唯一标识符 PCB 。
▶ 信号量具有一些特性,若结合刚才停车场的例子来说的话就是:
① 信号量是一个非负整数(车位数)。
② 所有 “进或出” 它的进程(车辆)都会将该整数 “减1或加1”。(进入它就是为了获取并使用资源,离开它就是释放资源)
③ 当该整数值为 0 时,所有试图 “进入” 它的进程都将会处于等待状态。
三、P 和 V 的定义
● 信号量的数值仅能由 P、V 原语 操作改变。原语的执行必须是连续的,可以通过关中断指令来实现。
● P 原语的定义如下:
/* 基于 C 语言写的伪代码 */
void P(s) // 假设在前面已经新建了一个 信号量变量s
{
s.count = s.count - 1;
if( s.count < 0 )
{
将该进程状态设置为等待状态;
将该进程的 PCB 插入相应的等待队列(s.wait_queue)的末尾;
}
}
◆ P 原语操作主要动作的说明:
① 当一个进程调用 P 操作时,它要么得到资源然后将信号量减 1 (即“s.count = s.count - 1;”)。
② 要么一直等待下去,即放入阻塞队列。
③ 直到信号量 >= 1 时,“看门人” 唤醒 ta,ta(进程) 便再次开始执行。
● V 原语的定义如下:
/* 基于 C 语言写的伪代码 */
void V(s) // 假设在前面已经新建了一个 信号量变量s
{
s.count = s.count + 1;
if( s.count <= 0 )
{
在相应等待队列(s.wait_queue)中唤醒一个等待进程;
改变其状态为就绪态, 并将其插入就绪队列;
}
}
◆ V 原语操作的说明:当一个进程调用 V 操作时,它要么释放资源然后将信号量加1 (即“s.count = s.count + 1;”)。
◆ 补充说明一:S 大于 0 那就表示有临界资源可供使用,为什么不唤醒进程?该说明源自参考附录[3] 📚。
答:S 大于 0 的确表示有临界资源可供使用,也就是说这个时候没有进程被阻塞在这个资源上,所以不需要唤醒。
◆ 补充说明二:S 小于 0 应该是说没有临界资源可供使用,为什么还要唤醒进程?该说明源自参考附录[3] 📚。
答:V 原语操作的本质在于:一个进程使用完临界资源后,释放临界资源,使 S 加 1,以通知其它的进程。这个时候如果 S<0,表明有进程阻塞在该类资源上,因此要从阻塞队列里唤醒一个进程来 “转手” 该类资源。比如,有两个某类资源,四个进程 A、B、C、D 要用该类资源,最开始初始化 S=2,当 A 进入,S=1。当 B 进入 S=0,表明该类资源刚好用完。 当 C 再进入时 S=-1 ,表明有一个进程被阻塞了。然后D进入,S=-2,也被阻塞。当 A 用完该类资源时,进行 V 操作,S=-1,释放该类资源,因为S<0,表明有进程阻塞在该类资源上,于是唤醒一个。
◆ 补充说明三:S 的绝对值表示等待的进程数,同时又表示临界资源,这到底是怎么回事?
答:当信号量 S 小于 0 时,其绝对值表示系统中因请求该类资源而被阻塞的进程数目。S 大于 0 时表示可用的临界资源数。注意在不同情况下所表达的含义不一样。当 S 等于 0时,表示刚好用完。
四、互斥实现的模版
● 利用信号量和 PV 操作实现进程互斥的一般模型是:
"其中信号量 s 用于互斥, 初值为1。"
进程P1 进程P2
... ...
P(s); P(s);
临界区相关操作; 临界区相关操作;
V(s); V(s);
... ...
◆ 代码说明:P1 进程进入临界区之后 s 的值就变为 0 ,P2 执行 P(s) 之后进入阻塞队列从而实现了对临界区的互斥访问。
◆ 使用 PV 操作实现进程互斥时应该注意的是:
① 每个程序中用户实现互斥的 P、V 操作必须成对出现,先做 P 操作,进临界区,后做 V 操作,再出临界区。若有多个分支,要认真检查其成对性。
② P、V 操作应分别紧靠临界区的头尾部,临界区的代码应尽可能短,不能有死循环。
③ 互斥信号量 s 的初值一般为 1。
五、同步实现的模版
● 利用信号量和 PV 操作实现进程同步的一般模型是:
"其中信号量 s 用于同步, 初值为0。"
进程P1 进程P2
... ...
P(s); V(s);
... ...
◆ 代码说明:s 的初值为 0,就意味着只能先做 V 操作,也就是先执行 P2,然后才能执行P1 里的 P 操作。所以这实现了 P1 和 P2 先后次序的同步关系。
◆ 使用 PV 操作实现进程同步时应该注意的是:
① 要分析清楚进程之间的制约关系,确定信号量种类(上述模板中只有一种,也就是 s )。
② 在保持进程间有正确的同步关系情况下,还要设定好哪个进程先执行,哪些进程后执行,彼此间通过什么资源(信号量)进行协调。
③ 信号量的初值与相应资源的数量有关,也与 P、V 操作在程序代码中出现的位置有关。
④ 同一信号量的 P、V 操作要成对出现,但它们分别在不同的进程代码中。
六、生产者消费者经典问题
● 为了更好地理解问题的实质,接下来将把生产者消费者问题由简至难分为三种类型,分别做分析。
6.1 简单类型问题
● 简单类型:一个生产者,一个消费者,共用一个缓冲区。
▲ 分析一:两个进程一个生产,一个消费。每一次操作中,生产者生产的产品只能放一个进入 “公用的缓冲区”。同样地,在每一次操作中,消费者也只能从 “公用的缓冲区” 里取出一个产品。
▲ 分析二:“程序” 开始执行后。首先,两进程互斥地进入缓冲区,然后是需要同步,先生产再消费。但也需要考虑存在的另一种同步,即当缓冲区满的时候,是先消费再生产。
6.2 中等类型问题
● 中等类型:一个生产者,一个消费者,共用 n 个环形缓冲区。
▲ 分析一下和第一种问题的区别:区别就是需要设缓冲区的编号为 1~n ,定义两个指针 in 和 out ,分别是 生产者进程和消费者进程 使用来指向下一个可用缓冲区的 “指针” 。而且同步和互斥的关系和第一种问题是一样的。
6.3 复杂类型问题
● 复杂类型:a 个生产者,b 个消费者,共用 n 个环形缓冲区
▲ 互斥关系分析:
① 首先设定
P
i
P_i
Pi 为生产者,
C
j
C_j
Cj 为消费者,
i
=
1
,
2
,
.
.
.
,
a
i = 1,2,...,a
i=1,2,...,a,
j
=
1
,
2
,
.
.
.
,
b
j = 1,2,...,b
j=1,2,...,b 。
②
i
i
i 不同的
P
i
P_i
Pi 之间是互斥的。
③
j
j
j 不同的
C
j
C_j
Cj 之间是互斥的。
④ 在
i
i
i和
j
j
j 相同的情况下,
P
i
P_i
Pi和
C
j
C_j
Cj 之间是互斥的。
▲ 同步关系分析:
① 缓冲区为空时,先生产再消费。
② 缓冲区为满时,先消费再生产。
● 解决问题步骤1 —— 定义 4 个信号量:
① empty —— 表示缓冲区是否为空,初值为 n【第一类同步信号量】
② full —— 表示缓冲区中是否为满,初值为 0【第二类同步信号量】
③ mutex1 —— 生产者之间的互斥信号量,初值为 1
④ mutex2 —— 消费者之间的互斥信号量,初值为 1
● 解决问题步骤2 —— 设定操作过程
① 为了同步生产者和消费者的行为,需要记录缓冲区中物品的数量。数量可以使用信号量来进行统计,这里需要使用两个信号量:empty 记录空缓冲区的数量、full 记录满缓冲区的数量。需要注意同步信号量初值不一定为 0,只要根据同步关系保证先后执行次序就可以了。
② empty 信号量是在生产者进程中使用,当 empty 不为 0 时,生产者才可以放入物品;
③ full 信号量是在消费者进程中使用,当 full 信号量不为 0 时,消费者才可以取走物品。
④ 对同一资源使用的一组进程可以设置一个互斥信号量。互斥信号量初值一般为 1,也就是同一时刻只能一个生产者(或一个消费者)对缓冲区做相应处理。
⑤ 和中等问题类似,也要设定缓冲区的编号为 1~n,并定义两个指针 in 和 out ,分别是 生产者进程和消费者进程 使用来指向下一个可用的缓冲区。
● 同步和互斥的示意图及代码:
/* 基于 C 语言写的伪代码 */
semaphore empty = n; // 表示缓冲区是否为空,初值为 n
semaphore full = 0; // 表示缓冲区中是否为满,初值为 0
semaphore mutex1 = 1; // 表⽰生产者之间的互斥关系, 初值为 1
semaphore mutex2 = 1; // 表⽰消费者之间的互斥关系, 初值为 1
int in = 1; // 生产者进程 使用来指向下一个可用缓冲区的 “指针”
int out = 1; // 消费者进程 使用来指向下一个可用缓冲区的 “指针”
while(true)
{
"生产者进程 Pi():" // i = 1, 2, 3, ..., a
while(true)
{
生产一个产品;
P(empty); // 对第一类同步信号量 empty 做 P 操作, empty = empty - 1. 若 empty < 0, 则该(生产者)进程阻塞, 反之则不阻塞.
P(mutex1); // 对生产者的互斥信号量 mutex1 做 P 操作, mutex1 = mutex1 - 1. 若 mutex1 < 0, 则该(生产者)进程阻塞, 反之则不阻塞.
产品送往缓冲区 buffer(in);
in = (in+1) mod n;
V(mutex1); // 对 mutex1 做 V 操作, mutex1 = mutex1 + 1. 若 mutex1 <= 0, 则唤醒其他(生产者)阻塞进程, 反之则不唤醒.
V(full); // 对第二类同步信号量 full 做 V 操作, full = full + 1. 若 full <= 0, 则唤醒其他(消费者)阻塞进程, 反之则不唤醒.
}
"消费者进程 Cj():" // j = 1, 2, 3, ..., b
while(true)
{
P(full) // 对 full 做 P 操作, full = full - 1. 若 full < 0, 则该(消费者)阻塞, 反之则不阻塞.
P(mutex2); // 对消费者的互斥信号量 mutex2 做 P 操作, mutex2 = mutex2 - 1. 若 mutex2 < 0, 则该(消费者)进程阻塞, 反之则不阻塞.
从缓冲区 buffer(out) 中取出产品;
out = (out+1) mod n;
V(mutex2); // 对 mutex2 做 V 操作, mutex2 = mutex2 + 1. 若 mutex2 <= 0, 则唤醒其他(消费者)阻塞进程, 反之则不唤醒.
V(empty); // 对第一类同步信号量 empty 做 V 操作, empty = empty + 1. 若 empty <= 0, 则唤醒其他(生产者)阻塞进程, 反之则不唤醒.
消费该产品;
}
}
● 补充内容:注意题目要求。如果说对于 生产者/消费者 分别可以有一个人同一时间对缓冲区做处理,那么就同时用 mutex1 和 mutex2。如果缓冲区同一时间只能一个人处理,那就只能设一个 mutex ,代码如下:
/* 基于 C 语言写的伪代码 */
semaphore empty = n; // 表示缓冲区是否为空,初值为 n
semaphore full = 0; // 表示缓冲区中是否为满,初值为 0
semaphore mutex = 1; // 表⽰生产者/消费者之间的互斥关系, 初值为 1
int in = 1; // 生产者进程 使用来指向下一个可用缓冲区的 “指针”
int out = 1; // 消费者进程 使用来指向下一个可用缓冲区的 “指针”
while(true)
{
"生产者进程 Pi():" // i = 1, 2, 3, ..., a
while(true)
{
生产一个产品;
P(empty);
P(mutex); // 对互斥信号量 mutex 做 P 操作, mutex = mutex - 1. 若 mutex < 0, 则该进程(生产者)阻塞, 反之则不阻塞.
产品送往缓冲区 buffer(in);
in = (in+1) mod n;
V(mutex); // 对 mutex 做 V 操作, mutex1 = mutex1 + 1. 若 mutex1 <= 0, 则唤醒其他(生产者/消费者)阻塞进程, 反之则不唤醒.
V(full);
}
"消费者进程 Cj():" // j = 1, 2, 3, ..., b
while(true)
{
P(full)
P(mutex); // 对互斥信号量 mutex 做 P 操作, mutex = mutex - 1. 若 mutex < 0, 则该(消费者)进程阻塞, 反之则不阻塞.
从缓冲区 buffer(out) 中取出产品;
out = (out+1) mod n;
V(mutex); // 对 mutex 做 V 操作, mutex = mutex + 1. 若 mutex <= 0, 则唤醒其他(生产者/消费者)阻塞进程, 反之则不唤醒.
V(empty);
消费该产品;
}
}
● 扩展问题:对上面两 “页” 代码而言,分别在上述两个进程(生产者和消费者的 wihle 循环中)中,两个 P 操作能否交换顺序?两个 V 操作能否交换顺序?【比如 P(empty); 和 P(mutex);】
▲ 答:对于第 2 “页” 代码而言。不能先对缓冲区进行加锁,再做 “测试信号量” 。也就是说,不能先执行 P(mutex) 再执行 P(empty)。如果这么做了,那么可能会出现这种情况:生产者对缓冲区加锁后,执行 P(empty) 操作,如果刚好发现 empty = 0,也就是没有空的缓冲区可以放产品,此时生产者只能阻塞。但因为 mutex 的值已经为 0,所以会导致消费者不能进入临界区,消费者就永远无法执行 V(empty) 操作,empty 就永远都为 0,导致生产者永远等待下,不会释放锁,消费者因此也会永远等待下去。【这就是后面要讲的死锁现象,所以两个 P 操作不可以互换顺序,但两个 V 操作顺序任意,也就是同步 P 操作要放在互斥 P 操作之前】
但是,对于第 1 “页” 代码而言,是可以交换的。因为 mutex1 和 mutex2 相互独立,互不干扰。
这里的信号量 mutex 的实质:像是一把钥匙,进程要运行下去,需要先拿到这把钥匙,运行完后要归还钥匙。通俗点来讲就是:信号量在某种允许的范围内,进程才能够执行。
七、典型的双缓冲问题
● 有 3 个进程 PA、PB 和 PC 合作解决文件打印问题:
① PA功能:将⽂件从磁盘读⼊主存的缓冲区 1,每执⾏⼀次就读⼀个文件;
② PB功能:将缓冲区 1 的内容复制到缓冲区 2,每执⾏⼀次就复制⼀个文件;
③ PC功能:将缓冲区 2 的内容打印出来,每执⾏⼀次就打印⼀个文件。缓冲区的大小等于⼀个文件的大小。
请⽤ P、V 操作来保证⽂件的正确打印。
▲ 先分析此题和生产消费问题有什么异同?
这是典型的双缓冲区问题,两个缓冲区⼤小均为 1。其中 PA 为⽣产者,PB 既是⽣产者又是消费者,PC 为消费者. 故按照⽣产消费者问题的思路解决该题即可。
但是需要注意的问题是:如果 PA 将数据放⼊缓冲区之后,PB 没有及时取的话,如果此时 PA 进程又继续执行,那么会将之前的数据覆盖掉。缓冲区 2 也⼀样,因此这⾥还需要设置两个信号量以限制缓冲区 1 和缓冲区 2 中记录的数量。
答案:
/* 基于 C 语言写的伪代码 */
semaphore full1 = 0; // 第一类同步信号量: 缓冲区 1 是否有文件可供处理, 初值为 0
semaphore full2 = 0; // 第二类同步信号量: 缓冲区 2 是否有文件可供处理, 初值为 0
semaphore empty1 = 1; // 第三类同步信号量: 缓冲区 1 是否还有空位可放文件, 初值为 1
semaphore empty2 = 1; // 第四类同步信号量: 缓冲区 2 是否还有空位可放文件, 初值为 1
semaphore mutex1 = 1; // 第一类异步信号量: 表⽰对缓冲区 1 的访问控制, 初值为 1
semaphore mutex2 = 1; // 第二类异步信号量: 表⽰对缓冲区 2 的访问控制, 初值为 1
while(true)
{
"进程PA():"
while(true)
{
从磁盘读⼀个文件; // 读完后, 文件会 “挂” 在 PA 身上
P(empty1); // empty1 = empty1 - 1, 若 empty1 < 0, 则(PA)阻塞, 反之则不阻塞
P(mutex1); // mutex1 = mutex1 - 1, 若 mutex1 < 0, 则(PA)阻塞, 反之则准备将文件放入缓冲区 1
将文件存入缓冲区 1 ;
V(mutex1); // mutex1 = mutex1 + 1, 若 mutex1 <= 0, 则唤醒因第一类异步信号量而阻塞的某一进程, 反之则不唤醒
V(full1); // full = full1 + 1, 若 full1 <= 0, 则唤醒因第一类同步信号量而阻塞的某一进程, 反之则不唤醒
}
"进程PB():"
while(true)
{
P(full1);
P(mutex1);
从缓冲区 1 中读出一个文件记录;
V(mutex1);
V(empty1);
P(empty2);
P(mutex2); // mutex2 = mutex2 - 2, 若 mutex2 < 0, 则(PB)阻塞, 反之则准备将文件放入缓冲区 2
将一个文件读入缓冲区 2 ;
V(mutex2); // mutex2 = mutex2 + 1, 若 mutex2 <= 0, 则唤醒因第二类异步信号量而阻塞的某一进程, 反之则不唤醒
V(full2);
}
"进程PC():"
while (true)
{
P(full2);
P(mutex2);
从缓冲区 2 取⼀个文件;
V(mutex2);
V(empty2);
打印文件;
}
}
◆ 说明:这道题的核心是要弄清楚,谁是 “生产”、谁是 “消费”、“谁与谁在什么上同步”、“谁与谁在什么上互斥”。
八、四道练习题【由易到难】
8.1 火车售票问题
● 例题1、某车站售票厅,任何时刻最多可容纳 20 名购票者进入。当售票厅中少于 20 名购票者时,则厅外的购票者可立即进入,否则需在外面等待。若把一个购票者看作一个进程,请回答下列问题:
(1)用 P、V 操作管理这些并发进程时,应怎样定义信号量,写出信号量的初值以及信号量各种取值的含义。
(2)若欲购票者最多为 n 个人,写出信号量可能的变化范围最大值和最小值。
(1)答案:
定义信号量 s ,初值为 20.
当 s > 0 时, 它表示可以进入购票厅的人数;
当 s = 0 时, 表示厅内已有 20 人正在购票;
当 s < 0 时, s 的绝对值表示正等待进入购票厅的人数。
/* 基于 C 语言写的伪代码 */
semaphore s = 20;
while(true)
{
"买票进程Pi():" // Pi 表示第 i 个进购票厅买票的人(进程)
while(true)
{
P(s);
进购票厅并买票;
V(s);
}
}
(2)答案:最大值为 20, 最小值为 20 - N.
◆ 说明:这道题里面,可以把购票者理解为 “即是生产者又是消费者”,进入售票厅就生产 “票”,然后把 “票” 交给了自己,这一过程没有 “缓冲区”。
8.2 阅览室登记问题
● 例题2、有一阅览室,读者进入时必须先在一张登记表上登记。该表中每个单元格代表阅览室中的一个座位。读者离开时要注销掉其登记信息。阅览室共有 50 个座位。登记表每次仅允许一位读者进行登记或注销。某一读者登记时,若发现登记表满,则他需在阅览室外等待,直至有空位再登记进入。试用 类Pascal语言 和 P、V 操作,描述读者行为。【国防科技大学2000】(注:【南昌大学2002】类似)
"因为不会类 Pascal 语言, 所以我用类 C 语言代替"
答案:
semaphore seats = 50; // 用信号量 seats 表示可用的座位数, 初值为 50;
semaphore mutex = 1; // 信号量 mutex 表示登记表是否正在使用, 初值为 1.
while(true)
{
"进入阅览室的进程Pi()" // i = 1, 2, 3,...
while(true)
{
P(seats); // seats = seats - 1, 若 seats >= 0, 则可以进入阅览室, 否则被阻塞
P(mutex); // mutex = mutex - 1, 若 mutex >= 0, 则可以使用登记表, 否则被阻塞
填写登记表;
V(mutex);
进入阅览室阅读;
}
"离开阅览室的进程Li()" // i = 1, 2, 3,...
while(true)
{
P(mutex); // mutex = mutex - 1, 若 mutex >= 0, 则可以使用登记表, 否则被阻塞
注销掉登记;
V(mutex);
离开阅览室;
V(seats); // 最后才释放 “阅览室资源”, 和 Pi 中的 “P(seats);” 有一种对称关系
}
}
◆ 说明:这道题里面,可以把读者理解为 “即是生产者又是消费者”,进入阅览室就消费一个 “资源”,离开阅览室就生产一个 “资源”。只不过在一般的生产者消费者模型中,生产者和消费者是分开的,而这里的 “读者” 是结合在一起。
8.3 经典的读写问题
● 例题3、有两组并发进程:众多读者和众多写者,共享一个文件,要求:
(1)允许多个读者同时对文件进行读操作。
(2)只允许一个写者对文件进行写操作。
(3)任何写者在完成写操作前不允许其他读者或写者工作。
(4)写者在执行写操作前,应让已有的写者和读者全部退出。
▲ 先分析读者与读者、写者与写者、读者与写者之间的关系:
① 首先设定
R
i
R_i
Ri 为读者,
W
j
W_j
Wj 为写者,
i
=
1
,
2
,
.
.
.
,
a
i = 1,2,...,a
i=1,2,...,a,
j
=
1
,
2
,
.
.
.
,
b
j = 1,2,...,b
j=1,2,...,b 。
②
i
i
i 不同的
R
i
R_i
Ri 之间是不互斥的。
③
j
j
j 不同的
W
j
W_j
Wj 之间是互斥的。
④ 在
i
i
i和
j
j
j 相同的情况下,
P
i
P_i
Pi和
W
j
W_j
Wj 之间是互斥的。
显然这和生产消费类型问题不太一样,主要就是第 ② 条。那怎么解决这个不同点呢?也就是要做到 “不是所有进程都互斥进入缓冲区”。
根据题目(1)、(2)、(3)、(4)的要求,我们可以把读者分类。如下图所示,将读者分成三类,每类做的工作不同:
读 者 : { ① 第 一 个 进 入 的 读 者 : 打 开 “ 写 ” 屏 蔽 , 并 “ 读 ” ② 中 间 进 入 的 读 者 : “ 读 ” ③ 最 后 一 个 离 开 的 读 者 : 关 闭 “ 写 ” 屏 蔽 , 并 离 开 读者:\begin{cases}① 第一个进入的读者:打开 “写” 屏蔽,并 “读” \\ ② 中间进入的读者:“读” \\ ③ 最后一个离开的读者:关闭 “写” 屏蔽,并离开\\ \end{cases} 读者:⎩⎪⎨⎪⎧①第一个进入的读者:打开“写”屏蔽,并“读”②中间进入的读者:“读”③最后一个离开的读者:关闭“写”屏蔽,并离开
▲ 分析:为了区分三类读者,就必须引入一个计数器 rc 对 “读” 进程进行计数,即做加 1 或减 1 操作。但加 1 和减 1 的操作在机器内部是分三条语句完成的。为了不引起计数器的混乱,就必须将加减定义成原子操作,即规定加 1 或减 1 是一次性完成的。mutex 就是用于对计数器 rc 操作的互斥信号量,w 则表示是否允许 “写” 的信号量,也就是 “写” 屏蔽开关。
● 代码如下:
/* 基于 C 语言写的伪代码 */
int rc = 0;
semaphore w = 1;
semaphore mutex = 1;
while(true)
{
"读进程Ri():" // i = 1, 2, 3, ..., a
while(true)
{
P(mutex);
rc = rc + 1;
if(rc == 1)
{
P(w); // 如果是第一个读者才能执行, 开启 “写” 屏蔽
}
V(mutex);
读操作;
P(mutex);
rc = rc - 1;
if (rc == 0)
{
V(w); // 如果是最后一个读者才能执行, 关闭 “写” 屏蔽
}
V(mutex);
其他操作;
}
"写进程Wi():" // i = 1, 2, 3, ..., b
while(true)
{
...
P(w);
写操作;
V(w);
...
}
}
8.4 经典的独木桥问题
● 例题4、假定有如下独木桥问题:
(1)过桥时,同一方向的行人可连续过桥。当某一方有人过桥时,另一方向的行人必须等待。
(2)当某一方向无人过桥时,另一方向的行人可以过桥。
试用信号量机制解决。
▲ 分析:同向不互斥,异向互斥。
思路:
将独木桥的两个方向分别标记为 A 和 B.
再用整型变量 countA 和 countB 分别表示 A、B 方向上已在独木桥上的行人数。
countA 和 countB 的初值都设为 0.
另外需要设置三个初值都为 1 的互斥信号量:
SA 用来实现对 countA 的互斥访问,SB 用来实现对 countB 的互斥访问,mutex 用来实现对独木桥的互斥使用。
/* 基于 C 语言写的伪代码 */
int countA = 0;
int countB = 0;
semaphore SA = 1;
semaphore SB = 1;
semaphore mutex = 1;
while(true)
{
"向 A 方向过桥的行人进程Ai():" // i = 1, 2, 3, ..., a
while(true)
{
P(SA); // SA = SA - 1, 若 SA >= 0, 则实现对 countA 的互斥访问, 否则被阻塞
countA = countA + 1; // 想从 A 方向过桥的人数 + 1
if (countA == 1)
P(mutex); // mutex = mutex - 1, 若 mutex >= 0, 则获得桥的互斥使用权, 否则被阻塞
V(SA); // SA = SA + 1, 若 SA <= 0, 则唤醒被阻塞的 “想访问countA” 的某一进程, 否则不唤醒
Ai过桥();
P(SA); // SA = SA - 1, 若 SA >= 0, 则实现对 countA 的互斥访问, 否则被阻塞
countA = countA - 1; // 想从 A 方向过桥的人数 - 1
if (countA == 0)
V(mutex); // mutex = mutex + 1, 若 mutex <= 0, 则唤醒 “想获得桥的互斥使用权”的某一进城, 否则不唤醒
V(SA); // SA = SA + 1, 若 SA <= 0, 则唤醒被阻塞的 “想访问countA” 的某一进程, 否则不唤醒
}
"向 B 方向过桥的行人过程Bi():" // i = 1, 2, 3, ..., b
while(true) // 代码和 A 方向的类似
{
P(SB);
countB = countB + 1;
if (countB == 1)
P(mutex);
V(SB);
Bi过桥();
P(SB);
countB = countB - 1;
if (countB == 0)
V(mutex);
V(SB);
}
}
8.5 经典的独木桥问题 —— 升级版(2022/1/2补充的)
● 例题4、假定有如下独木桥问题:
(1)过桥时,同一方向的行人可连续过桥。当某一方有人过桥时,另一方向的行人必须等待。
(2)当某一方向无人过桥时,另一方向的行人可以过桥。
(3)桥上最多只能有 k 个人。
试用信号量机制解决。
/* 基于 C 语言写的伪代码 */
int countA = 0;
int countB = 0;
semaphore SA = 1;
semaphore SB = 1;
semaphore mutex = 1;
semaphore count = k; // 桥上最多只能有 k 个人
while(true)
{
"向 A 方向过桥的行人进程Ai():" // i = 1, 2, 3, ..., a
while(true)
{
P(SA); // SA = SA - 1, 若 SA >= 0, 则实现对 countA 的互斥访问, 否则被阻塞
countA = countA + 1; // 想从 A 方向过桥的人数 + 1
if (countA == 1)
P(mutex); // mutex = mutex - 1, 若 mutex >= 0, 则获得桥的互斥使用权, 否则被阻塞
V(SA); // SA = SA + 1, 若 SA <= 0, 则唤醒被阻塞的 “想访问countA” 的某一进程, 否则不唤醒
P(count);
Ai过桥();
V(count);
P(SA); // SA = SA - 1, 若 SA >= 0, 则实现对 countA 的互斥访问, 否则被阻塞
countA = countA - 1; // 想从 A 方向过桥的人数 - 1
if (countA == 0)
V(mutex); // mutex = mutex + 1, 若 mutex <= 0, 则唤醒 “想获得桥的互斥使用权”的某一进城, 否则不唤醒
V(SA); // SA = SA + 1, 若 SA <= 0, 则唤醒被阻塞的 “想访问countA” 的某一进程, 否则不唤醒
}
"向 B 方向过桥的行人过程Bi():" // i = 1, 2, 3, ..., b
while(true) // 代码和 A 方向的类似
{
P(SB);
countB = countB + 1;
if (countB == 1)
P(mutex);
V(SB);
P(count);
Bi过桥();
V(count);
P(SB);
countB = countB - 1;
if (countB == 0)
V(mutex);
V(SB);
}
}
● 重点掌握两个模板、几个经典例题的PV操作。然后在遇到问题时,能够回忆起这些模板和PV操作时,再结合具体情形,即可快速有效地解决问题。
九、参考附录:
[1] 《操作系统A》
上课用的慕课
链接: https://www.icourse163.org/course/NJUPT-1003219004?from=searchPage.
[2] 《操作系统教程》
上课用的教材
[3] 《PV操作示例详解》
链接: https://blog.csdn.net/wuxy720/article/details/78936912.
这篇写得很好,PV原理解释得很清楚
[4] 《信号量》
链接: https://blog.csdn.net/huangweiqing80/article/details/83038154.
基于Linux写的
[5] 《操作系统-PV操作-独木桥问题》
链接: https://blog.csdn.net/qq_39851917/article/details/109144914.
关于独木桥问题,这位学长也写得很好
上一篇文章地址链接: 【操作系统⑥】——进程联系与临界区管理【同步与互斥 Dekker算法 TS指令 SWAP指令】.
下一篇文章地址链接: 【操作系统⑧】——信号量与PV操作(下)【哲学家进餐问题 AND型信号量 信号量集机制】.
期末考试总复习——地址链接:《操作系统期末总复习——绝地求生版》.
⭐️ ⭐️