经典PV问题系列三:习题归纳

1、另类PV操作问题

问题描述:有一个系统,定义P、V操作如下:
P(s):
s.count --;
if s<0 then
将本进程插入相应队列末尾等待;
V(s):
s.count ++;
if s<=0 then
从相应等待队列队尾唤醒一个进程,将其插入就绪队列;

a.这样定义P、V操作有什么问题?
这种P、V的定义方法与上一节所示的P、V定义唯一的区别是:这里V操作从队尾唤醒进程,而我们从队首唤醒进程(FIFO)。假如不断有进程持续到来加入信号量队尾,可能导致队首进程饥饿。

b.用这样的P、V操作实现无上述问题的N个进程竞争使用某一共享变量的互斥机制。
可以思考:在什么情况下堆栈可以模拟队列呢?思路1、使用两个堆栈互转。但是在这里,V操作在队尾唤醒进进程后,直接将其插入系统就绪队列,这是我们无法操纵的,所以我们这种思路可能不行。思路2、设立多个队列,保证每个队列上最多只有一个元素。

由此我们可以想到Peterson在N进程上应用的那个图,设立一个井,有N-1层,最上层可容纳N个进程,可以漏下N-1个进程,下一层比上一层的容量小1,到临界区里就仅能容纳一个进程。

经典PV问题系列三:习题归纳_第1张图片

我们设立 N-1 个信号量代表 N-1 个阶段,分别初始化 S[i] = n-1-i 。可以进入第i个阶段的有N-i个进程,最多可以通过N-i-1个进程,最多只会在该信号量上挂一个进程。

Semaphore S[N-1];	// S[i] = N-i-1
void func()
{
	for(int i=0 ; i=0 ; i--)
		V(S[i]);
}

我身边的一位大神同学提出了另一种解法:设立一个N层的互斥量(均初始化为1的信号量)井,每个进程从上到下开始拿锁,同一时刻最多拿两个锁,然后放开拿到的上层锁再去拿下一层的锁。

Semaphore S[N];	// S[i] = 1;
void func()
{
	P(S[0]);
	for(int i=1 ; i
能提出这个解法已经非常不容易,乍看之下确实可以工作,每个进程把后来的进程堵在井上面的部分,这样保证了越靠近井底必先进临界区。

不过这种解法其实是有一定问题的。如果在刚完成某个P操作后瞬间到来大量进程序列,则这些进程序列都会被阻塞在该互斥量的队列上,而取出时则是以堆栈的方式后进先出,这样还是存在前端的饥饿问题。

反观为什么标准解法是正确的呢?标准解法实际上是将“谁先进”这个包袱抛给了操作系统调度算法,调度让某个程序先执行,则必然先抢占井中容量,我们不能定义“先”是指先执行到某条P(S[i])操作,而是由调度算法决定的“谁先”。

2、复杂的消息缓冲问题

问题描述:消息缓冲区为k个,有m个发送进程,n个接收进程,每个接收进程对发送来的消息都必须取一次。

这个问题实际上是生产者-消费者和读者-写者问题的综合拓展,需要保证每个消费者都对缓冲区取一次,每个消费者之间可以并行读。

semaphore send[K] = {1}, receive[K][N] = {0};
semaphore mutex[K] = {1}, mutex1 = 1;
int count[K] = {0}, cur = 0;

void Sender() {
	while(true) {
		int i;	// 这里只是利用局部变量i减少临界区
		P(mutex1);	// mutex1只用于cur的保护
		i = cur;
		cur = (cur + 1) % K;
		V(mutex1);
		P(send[i]);	// 一旦一个Sender进程拿到send[i]锁,其他Sender进程便无法进入,因此无需多余的保护
		对buffer[i]写入信息;
		for(int j=0 ; j			V(receive[i][j]);	// 让这一列都可接收
	}
}

void Receive(int id) {
	for(int i=0 ; ; i=(i+1)%K) {
		P(receive[i][id]);
		从buffer[i]读信息;	// 读进程之间可以并行,而send[i]和receive[i][j](存在一个j)之间必有一个为0一个为1,保证了读写之间的互斥
		P(mutex[i]);	// mutex[i]用于保护count[i]
		count[i]++;
		if(count[i] == N) {
			count[i] = 0;
			V(send[i]);
		}
		V(mutex[i]);
	}
}

3、商店与供应商问题

问题描述:某商店有两种食品A和B, 最大数量各为m个。该商店将A、B两种食品搭配出售,每次各取一个。为避免食品变质,遵循先到食品先出售的原则。有两个食品公司分别不断地供应A、B两种食品(每次一个)。为保证正常销售,当某种食品的数量比另一种的数量超过k(k

semaphore buff_numA = m, buff_numB = m;	//A、B的缓冲区个数, 初值m
semaphore numA = 0, numB = 0;	// A、B的个数,初值为0
semaphore mutex = 1;	// 用于缓冲区的互斥
void shop()
{
	while(TRUE)
	{
		P(numA);
		P(numB);
		P(mutex);
		取出A、B食品各一个;
		V(mutex);
		V(buff_numA);
		V(buff_numB);
		销售;
	}
}

semaphore A_B = K;	// “A食品加1,而B食品不变”这种情形允许出现的次数(许可证的数量),其值等于//k-(A-B),初值为K
semaphore B_A = K;	// “B食品加1,而A食品不变”这种情形允许出现的次数(许可证的数量),其值等于//k-(B-A),初值为k
void Producer_A()
{
	while(TRUE)
	{
		生产A食品;
		P(buff_numA);
		P(A_B);
		P(mutex);
		放进一个A食品;
		V(mutex);
		V(B_A);
		V(numA);
	}
}
void Producer_B()
{
	while(TRUE)
	{
		生产B食品;
		P(buff_numB);
		P(B_A);
		P(mutex);
		放进一个B食品;
		V(mutex);
		V(A_B);
		V(numB);
	}
}

这里详细解释一下producer_A:相当于生产者消费者问题中的生产者,每次先生产一个产品,首先将空槽的数量减少一个,由于A产品数量增加了一个,所以A减B的量的允许值也少了一个(与k比较)。然后开始临界区操作,使用mutex来对A产品有关的临界区访问进行互斥。最后将满槽的数目加一,由于B相对于A减一,故允许的B减A的差值加一。

可以看出,这也是生产者和消费者问题的变形。后两个信号量是必要的,可以保证生产者在食品数量超过限额时等在一个信号量上。当然还可以优化,比如把mutex分解为三个互斥量,同上一节对生产者-消费者问题的优化。

4、三峡大坝船闸问题

问题描述:三峡大坝有五级船闸,T1~T5。由上游驶来的船需经由各级船闸到下游;由下游驶来的船需经由各级船闸到上游。假设船闸只能允许单方向通行。

相当于五级的独木桥问题。对每个船闸,如果一个方向上已经有人进入,则另一个方向上必须等待这个方向上的人全部走完才能出发。

semaphore mutex[5] = 1;	// 互斥对各级船闸的使用
int countdown[5] = 0;	// 记录各级船闸正在下行的数目
semaphore S_down[5] = 1;	// 互斥对countdown的使用
int countup[5] = 0;
semaphore S_up[5] = 1;

void down()
{
	for(int i=0 ; i<5 ; i++)
	{
		P(S_down[i]);
		countdown[i]++;
		if(countdown[i] == 1)
			P(mutex[i]);	// 下行拿锁
		V(S_down[i]);
		过闸
		P(S_down[i]);
		countdown[i]--;
		if(countdown[i] == 0)
			V(mutex[i]);	// 下行放锁
		V(S_down[i]);
	}
}
void up()
{
	// 只需将上述所有'down'替换为'up'即可
}

5、阅览室问题

问题描述:假定一个阅览室最多可容纳100人,读者进入和离开阅览室时都必须在阅览室门口的一个登记表上进行登记,而且每次只允许一人进行登记操作。

semaphore empty = 100, full = 0, mutex = 1;
void getin()
{
	P(empty);
	P(mutex);
	登记;
	V(mutex);
	V(full);
}
void getout()
{
	P(full);
	P(mutex);
	取消登记;
	V(mutex);
	V(empty);
}

6、两进程相互生产消费问题

问题描述:两个进程PA、PB通过两个FIFO(先进先出)缓冲区队列连接(如图)

PA从Q2取消息,处理后往Q1发消息;PB从Q1取消息,处理后往Q2发消息,每个缓冲区长度等于传送消息长度。 Q1队列长度为n,Q2队列长度为m. 假设开始时Q1中装满了消息,试用P、V操作解决上述进程间通讯问题。

// Q1队列当中的空闲缓冲区个数,初值为0
semaphore  empty1;  
// Q2队列当中的空闲缓冲区个数,初值为m 
semaphore  empty2;    
// Q1队列当中的消息数量,初值为n 
semaphore  full1;
// Q2队列当中的消息数量,初值为0 
semaphore  full2;

void PA( )
{
    while(1)
    {
        P(full2);
        从Q2当中取出一条消息;
        V(empty2);
        处理消息;
        生成新的消息;
        P(empty1);
        把该消息发送到Q1当中;
        V(full1);
    } 
}
void PB( )
{
    while(1)
    {
        P(full1);
        从Q1当中取出一条消息;
        V(empty1);
        处理消息;
        生成新的消息;
        P(empty2);
        把该消息发送到Q2当中;
        V(full2);
    } 
}

这里为什么不需要对缓冲区加锁互斥呢?对每个缓冲区,本题中有且仅有一个生产者和消费者。唯一可能的竞争来自两进程同取同放一个槽,但empty和full可以保证在队列中当两指针相等时,必有一个进程已被阻塞在这两个信号量上。

7、课程考试问题

问题描述:《操作系统》课程的期末考试即将举行,假设把学生和监考老师都看作进程,学生有N人,教师1人。考场门口每次只能进出一个人,进考场的原则是先来先进。当N个学生都进入了考场后,教师才能发卷子。学生交卷后即可离开考场,而教师要等收上来全部卷子并封装卷子后才能离开考场。

semaphore  S_Door;		// 能否进出门,初值1
semaphore  S_StudentReady;	// 学生是否到齐,初值为0
semaphore  S_ExamBegin;	// 开始考试,初值为0
semaphore  S_ExamOver;	// 考试结束,初值为0

int nStudentNum = 0;	// 学生数目
semaphore  S_Mutex1;	//互斥信号量,初值为1
int  nPaperNum = 0;	// 已交的卷子数目
semaphore  S_Mutex2;	//互斥信号量,初值为1

void  student( )
{
	P(S_Door);
	进门;
	V(S_Door);
	P(S_Mutex1);
	nStudentNum ++;	// 增加学生的个数
	if(nStudentNum == N)
		V(S_StudentReady);
	V(S_Mutex1);
	P(S_ExamBegin);  // 等老师宣布考试开始
	考试中…
	交卷;
	P(S_Mutex2);
	nPaperNum ++;	// 增加试卷的份数
	if(nPaperNum == N) 
	V(S_ExamOver);
	V(S_Mutex2);
	P(S_Door);
	出门;
	V(S_Door);
}
void  teacher( )
{
	P(S_Door);
	进门;
	V(S_Door);
	P(S_StudentReady);	//等待最后一个学生来唤醒
	发卷子;
	for(i = 1; i <= N; i++)   
		V(S_ExamBegin);
	P(S_ExamOver);	//等待考试结束
	封装试卷;
	P(S_Door);
	出门;
	V(S_Door);
}

题目并不难,但过程较多,仔细一些便可做对。

8、公寓浴室共用问题

题目描述:在一栋学生公寓里,只有一间浴室,而且这间浴室非常小,每一次只能容纳一个人。公寓里既住着男生也住着女生,他们不得不分享这间浴室。因此,楼长制定了以下的浴室使用规则:
(1)每一次只能有一个人在使用;
(2)女生的优先级要高于男生,即如果同时有男生和女生在等待使用浴室,则女生优先;
(3)对于相同性别的人来说,采用先来先使用的原则。

semaphore S_mutex;	// 互斥信号量,初值均为1
semaphore S_boys;	// 男生等待队列,初值为0
semaphore S_girls;	// 女生等待队列,初值为0
int boys_waiting = 0;	// 正在等待的男生数;
int girls_waiting = 0;	// 正在等待的女生数;
int using = 0;	// 当前是否有人在使用浴室;

void boy()
{
	P(S_mutex);
	if(using == 0)
	{
		using  =  1;
		V(S_mutex);
	}
	else
	{
		boys_waiting ++;
		V(S_mutex);
		P(S_boys);
	}
	沐浴
	P(S_mutex);
	if(girls_waiting  >  0)  // 优先唤醒女生
	{
		girls_waiting --;
		V(S_girls);
	}
	else if(boys_waiting  >  0)
	{
		boys_waiting --;
		V(S_ boys);
	}
	else
		using  =  0;	 // 无人在等待
	V(S_mutex);
}

对于girl()而言,显然沐浴之后的操作是通用的。之前的部分,只需将 boys_waiting ++ 换为为 girl_waiting ++ 并且将 P(S_boys) 换为 P(S_girls) 即可。
另外,这里的解法与网上解法有一点不同:第11行处网上的操作是:"if((using == 0) && (girls_waiting == 0))",不过当 using 为0时必然有 girls_waiting 为0,所以在这里将其去掉。

9、父母子女水果盘问题

问题描述:一家四人父、母、儿子、女儿围桌而坐;桌上有一个水果盘; (1)  当水果盘空时,父亲可以放香蕉或者母亲可以放苹果,但盘中已有水果时,就不能放,父母等待。当盘中有香蕉时,女儿可吃香蕉,否则,女儿等待;当盘中有苹果时,儿子可吃,否则,儿子等待。

解设信号量:SE=1 (空盘子);SA=0 (放了苹果的盘子);SB=0 (放了香蕉的盘子)

void father() {
	while(TRUE) {
		剥香蕉
		P(SE);
		放香蕉;
		V(SB);
	}
}
void mother() {
	while(TRUE) {
		削苹果;
		P(SE);
		放苹果;
		V(SA);
	}
}
void son() {
	P(SA);
	拿苹果;
	V(SE);
	吃苹果;
}
void daughter() {
	P(SB);
	拿香蕉;
	V(SE);
	吃香蕉;
}

(2)把(1)改为:儿子要吃苹果时,请母亲放苹果,女儿要吃香蕉时,请父亲放香蕉,(还是盘子为空时才可以放)。

(2)解:再增加两个信号量:SF=0, SM=0

void father() {
	while(TRUE) {
		P(SF);
		剥香蕉
		P(SE);
		放香蕉;
		V(SB);
	}
}
void mother() {
	while(TRUE) {
		P(SM);
		削苹果;
		P(SE);
		放苹果;
		V(SA);
	}
}
void son() {
	V(SM);
	P(SA);
	拿苹果;
	V(SE);
	吃苹果;
}
void daughter() {
	V(SF);
	P(SB);
	拿香蕉;
	V(SE);
	吃香蕉;
}

虽然简单,不过也值得一做。盘子缓冲区数量可以扩展到N(se = N)

10、更多习题链接

大连交通大学吧-信号量习题及答案

计算机操作系统 习题答案

各大学信号量典型过往习题

你可能感兴趣的:(OS)