【操作系统⑦】——信号量与PV操作(上)【互斥模板+同步模板+生产者消费者问题+典型的双缓冲问题+阅览室登记问题+读写问题+独木桥问题】


☕️



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型信号量 信号量集机制】.

期末考试总复习——地址链接《操作系统期末总复习——绝地求生版》.


⭐️ ⭐️

评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一支王同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值