信号量实现同步互斥经典案例

上一篇介绍了进程同步、互斥的基本概念跟方法进程的互斥、同步_码农诗人的博客-CSDN博客_进程互斥同步;本篇在上篇的基础上对信号量概念延伸,通过结合5个经典的案例,来进一步理解操作系统如何通过信号量对多个进程实现互斥、同步的。

生产者-消费者(一对一)

问题描述
        系统中有一组生产者进程和一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者进程每次从缓冲区中取出一个产品并使用。(这里的“产品”理解为某种数据)
        生产者消费者共享一个初始为空,大小为n的缓冲区。
        只有缓冲区没有满时,生产者才能把产品放入缓冲区,否则必须等待。
        只有缓冲区没有空时,消费者才能消费缓冲区中的产品,否则必须等待。
        缓冲区是临界资源,各进程之间访问必须互斥。

问题分析
        如何使用信号量来实现该问题中的同步、互斥呢?首先得分析问题中的互斥关系和同步关系:
        同步关系(2对):
              1、一旦缓冲池中所有缓冲区均装满产品时,生产者必须等待消费者提供空缓冲区;
              2、一旦缓冲池中所有缓冲区全为空时,消费者必须等待生产者提供满缓冲区。
        互斥关系(1对):
              1、由于缓冲池是临界资源,所以任何进程在对缓冲区进行存取操作时都必须和其他进程互斥进行。

前驱关系分析
        当缓冲区为空时:生产者是消费者的前驱,生产者每次要消耗(P)一个空闲缓冲区,并生产(V)一个产品。
        当缓冲区为满时:消费者是生产者的前驱,消费者每次要消费(P)一个产品,并释放(V)一个缓冲区。往缓冲区放入/取走产品需要互斥。具体关系如下图:

综合分析题目可得:首先需要设置一个互斥信号量,和两个同步信号量,具体如下:

semaphore mutex = 1;  //互斥信号量
semaphore empty = n;  //同步信号量。空闲缓冲区的数量
semaphore full = 0;   //同步信号量。产品的数量,非空缓冲区的数量

// 生产者进程
producer(){
    while(1){
        生成一个产品;
        P(empty); // 消耗一个空闲缓冲区(同步P操作)
        P(mutex); // (互斥P操作)
        把产品放入缓冲区;
        V(mutex);
        V(full);   //增加一个产品
    }
}

// 消费者进程
consumer(){
    while(1){
        P(full);   // 消耗一个产品
        P(mutex);  // (互斥P操作)
        从缓冲区取出一个产品;
        V(mutex);
        V(empty);  //增加一个空闲缓冲区
        使用产品;
    }
}

上面代码将同步P操作放在互斥P操作之前,考虑一下如果这两个P操作先后顺序调换一下会发生什么?是的,会发生死锁,自己可以推理一下。因此得出结论:同步P操作必须放在互斥P操作之前

生产者-消费者(多对多)

问题描述
        桌子上有一只盘子,每次只能向其中放入一个水果。爸爸专向盘子中放苹果,妈妈专向盘子中放橘子,儿子专等着吃盘子中的橘子,女儿专等着吃盘子中的苹果。只有盘子空时,爸爸或妈妈才可向盘子中放一个水果。仅当盘子中有自己需要的水果时,儿子或女儿可以从盘子中取出水果。用PV操作实现上述过程。

问题分析
       由题目分析可得同步关系及互斥关系为:
       同步关系(3对):
              1、盘子为空时(即没苹果也没橘子),爸爸可以往盘子放苹果或妈妈可以往盘子放橘子;
              2、盘子有苹果时,此时必须为女儿吃。
              3、盘子有橘子时,此时必须为儿子吃。
       互斥关系(1对):
              1、由于盘子是临界资源,所以任何进程在对盘子的存取操作时都必须和其他进程互斥进行。

由上节【生产者-消费者(一对一)】很容易分析得,需要设置一个互斥信号量,和三个同步信号量,具体如下:

semaphore mutex 	= 1;  //互斥信号量
semaphore plate 	= 1;  //同步信号量。盘子刚开始为空,可以放一个苹果或橘子
semaphore apple		= 0;  //同步信号量。盘子中放的苹果个数
semaphore orange 	= 0;  //同步信号量。盘子中放的橘子个数

// 生产者进程
dad(){
    while(1){
        取一个苹果;
        P(plate); // 消耗一个盘子(同步P操作)
        P(mutex); // (互斥P操作)
        把苹果放入盘子;
        V(mutex);
        V(apple);   //增加一个苹果
    }
}

mom(){
    while(1){
        取一个橘子;
        P(plate); // 消耗一个盘子(同步P操作)
        P(mutex); // (互斥P操作)
        把橘子放入盘子;
        V(mutex);
        V(orange);   //增加一个橘子
    }
}

// 消费者进程
son(){
    while(1){
        P(orange);   // 消耗一个橘子
        P(mutex);  // (互斥P操作)
        从盘子取出一个橘子;
        V(mutex);
        V(plate);  //增加一个盘子(位置)
        吃橘子;
    }
}

daughter(){
    while(1){
        P(apple);   // 消耗一个苹果
        P(mutex);  // (互斥P操作)
        从盘子取出一个苹果;
        V(mutex);
        V(plate);  //增加一个盘子(位置)
        吃苹果;
    }
}

再进一步考虑,同步关系能不能保证互斥关系的成立?也就是说当上述3个同步关系确定时,能不能保证互斥关系的存在。

问题分析
    情况1:若当dad生产者进程执行P(plate)操作,plate会变为0;此时当mom生产者进程执行P(plate)时会阻塞;此时son消费者跟daughter消费者进程由于orange、apple信号量都为0也会阻塞。
    情况2:若当mom生产者进程执行P(plate)操作,与上述情况1相同。
    情况3:若当son消费者进程执行P(orange)操作,此时说明mom生产者进程已执行P(plate)、V(orange)操作,此时plate信号量肯定是0;mom生产者跟dad生产者一定阻塞;由于最近一次P(plate)是mom生产者进程执行的,所以此时apple信号量肯定是0,daughter消费者进程也一定阻塞。
    情况4:若当daughter消费者进程执行P(apple)操作,与上述情况3相似。

由上述4中情况分析可得出:同步关系可以保证互斥关系的成立。因此进一步优化实现如下:

semaphore plate 	= 1;  //同步信号量。盘子刚开始为空,可以放一个苹果或橘子
semaphore apple		= 0;  //同步信号量。盘子中放的苹果个数
semaphore orange 	= 0;  //同步信号量。盘子中放的橘子个数

// 生产者进程
dad(){
    while(1){
        取一个苹果;
        P(plate); // 消耗一个盘子(同步P操作)
        把苹果放入盘子;
        V(apple);   //增加一个苹果
    }
}

mom(){
    while(1){
        取一个橘子;
        P(plate); // 消耗一个盘子(同步P操作)
        把橘子放入盘子;
        V(orange);   //增加一个橘子
    }
}

// 消费者进程
son(){
    while(1){
        P(orange);   // 消耗一个橘子
        从盘子取出一个橘子;
        V(plate);  //增加一个盘子(位置)
        吃橘子;
    }
}

daughter(){
    while(1){
        P(apple);   // 消耗一个苹果
        从盘子取出一个苹果;
        V(plate);  //增加一个盘子(位置)
        吃苹果;
    }
}

吸烟者问题

问题描述
        假设一个系统有三个抽烟者进程和一个供应者进程。每个抽烟者不停地卷烟并抽掉,但是要卷起并抽掉一支烟,需要三种材料:烟草、纸、胶水。三个抽烟者中,每一个第一个拥有烟草,第二个拥有纸,第三个拥有胶水。供应者无限提供三种材料,供应者每次将两种材料放桌子上,拥有剩下材料的抽烟者卷一支烟并抽掉它,并给供应者一个信号完成了,供应者就会把另外两种材料再放桌子上,这个过程一直重复(三个抽烟者轮流抽烟)。

问题分析
       由题目分析可得同步关系及互斥关系为:
       同步关系(4对):
              1、桌子为空时且收到供应者完成吸烟信号,供应者进程放两种材料(三种材料轮流);
              2、桌子上有烟草、纸时,有胶水材料的吸烟者取。
              3、桌子上有烟草、胶水时,有纸材料的吸烟者取。
              4、桌子上有胶水、纸时,有烟草材料的吸烟者取。
       互斥关系(1对):
              1、由于桌子是临界资源,所以任何进程在对盘子的存取操作时都必须和其他进程互斥进行。

进一步按照上边的分析可以确定同步关系可以保证互斥关系的成立。可以实现如下:

semaphore finish 	= 0;  //供应者完成吸烟信号量。
semaphore tag_AB	= 0;  //桌子上有烟草、纸信号量。
semaphore tag_AC 	= 0;  //桌子上有烟草、胶水信号量。
semaphore tag_BC 	= 0;  //桌子上有纸、胶水信号量。

// 生产者进程
give(){
    int i = 0;	// 保证轮流供给
    while(1){
        switch(i++%3)
        {
        case 0:
	    取烟草、纸给有胶水的吸烟者;
	    放入桌子上;
	    V(tag_AB);
	    break;
        case 1:
	    取烟草、胶水给有纸的吸烟者;
	    放入桌子上;
	    V(tag_AC);
	    break;
        case 2:
	    取胶水、纸给有烟草的吸烟者;
	    放入桌子上;
	    V(tag_BC);
	    break;
        default:break;
        }
        
        P(finish);
    }
}

// 消费者进程
get_AB(){
    while(1){
        P(tag_AB);
        取烟草、纸结合自己的胶水完成吸烟;
        V(finish);
    }
}

get_AC(){
    while(1){
        P(tag_AC);
        取烟草、胶水合自己的纸完成吸烟;
        V(finish);
    }
}

get_BC(){
    while(1){
        P(tag_BC);
        取胶水、纸合自己的烟草完成吸烟;
        V(finish);
    }
}

读者-写者问题

问题描述
        有两组并发进程:读者和写者,共享一个文件资源。读者可以同时读取文件;读者和写者不能同时对文件进行操作;两个写者也不能同时对文件进行操作。

问题分析
        该问题实质只包含互斥关系,只是相较于之前而言更复杂一些。如果用一个互斥量来表示文件是否被读或写,在读或写之前对互斥量进行P操作,之后执行V操作;这样尽使得所有读写进程都互斥。不满足读者进程之间不互斥!如何使得读进程之间不互斥?解决这种问题也是读者写者问题的核心所在!
        也就是说在读者进程在读文件之前加锁是有条件的!在读文件之后解锁也是有条件的。必须保证第一个读进程加锁,其余读进程不加锁;而最后一个读进程解锁,其余进程不解锁。因此可以这样解决:定义一个记录读进程个数的变量count_r=0,每个读进程要读文件之前先对count_r加1,读文件之后对count_r减1。当count_r由0变1时肯定是读进程的第一个进程,而当count_r由1变0时肯定是读进程的最后一个进程。

由上述分析可以实现如下:

semaphore mutex_wr     	= 1;		// 共享文件互斥信号量
int count_r		= 0;		// 记录读进程的个数
semaphore mutex_count_r = 1;		// 保护记录读进程的个数互斥信号量

// 写进程
P_Write(){
    while(1){
	P(mutex_wr);
	写文件;
	V(mutex_wr);
    }
}

// 读进程
P_Read()(){
    while(1){
	P(mutex_count_r);
	if(count_r++ == 0)	// 第一个读进程负责加锁
	{
	    P(mutex_wr);
	}
	V(mutex_count_r);
		
	读文件;
		
	P(mutex_count_r);
	if(--count_r == 0)	// 最后一个读进程负责解锁
	{
	    V(mutex_wr);
	}
	V(mutex_count_r);
    }
}

分析上述代码逻辑,当有读进程源源不断的读时(count_r这个读进程计数器一直保持大于0),会一直造成写进程的无穷等待!这种情况会造成写进程“饥饿”。如何避免这种情况?我们可以再加一个互斥信号量,当写进程运行时对该信号量执行P操作,执行完后进行V操作;当读进程对count_r变量判断操作之前对该信号量执行P操作,对count_r变量判断操作完后进行V操作;这样就更好的解决了该问题。具体代码逻辑实现如下:

semaphore mutex_wr     	= 1;		// 共享文件互斥信号量
int count_r		= 0;		// 记录读进程的个数
semaphore mutex_count_r = 1;		// 保护记录读进程的个数互斥信号量
semaphore mutex         = 1;		// 解决写进程饿死互斥量


// 写进程
P_Write(){
    while(1){
        P(mutex);
	P(mutex_wr);
	写文件;
	V(mutex_wr);
        V(mutex);
    }
}

// 读进程
P_Read()(){
    while(1){
        P(mutex);    
	P(mutex_count_r);
	if(count_r++ == 0)	// 第一个读进程负责加锁
	{
	    P(mutex_wr);
	}
	V(mutex_count_r);
        V(mutex);
		
	读文件;
		
	P(mutex_count_r);
	if(--count_r == 0)	// 最后一个读进程负责解锁
	{
	    V(mutex_wr);
	}
	V(mutex_count_r);
    }
}

分析一下这种代码逻辑。假设进程调度顺序为:写进程1->读进程1->写进程2->读进程2;分析可得处理机调度的顺序也是这种顺序的!因此这种处理方法也是类似于FCFS(先来先服务)算法,是相对较公平的一种算法。

哲学家进餐问题

问题描述
        有五个哲学家,他们的生活方式是交替地进行思考和进餐。他们共用一张圆桌,分别坐在五张椅子上。在圆桌上有五支筷子,平时一个哲学家进行思考,饥饿时便试图取用其左、右最靠近他的筷子,只有在他拿到两支筷子时才能进餐。进餐完毕,放下筷子又继续思考。

问题分析
    哲学家左右的两只筷子都属于临界资源,需要互斥信号量实现互斥操作;每个哲学家进餐得用自己左右两个筷子(临界资源)。i为哲学家编号:0,1,2,3,4;筷子编号:i的左侧为i,右侧为(i+1)%5。这样还有一种情况就是当所有哲学家都想吃饭时,都拿起自己左边的筷子,这样都登着自己右边筷子的释放,这样会形成死锁

如何解决上述死锁问题?以下整理了三种解决该问题的方案,我们一起来分析一下吧!

方案一:简单的想法是加一个互斥量保证哲学家拿左右边筷子时“为不可中断”。这样就可以得到如下逻辑代码:

semaphore chopstick[5] = {1, 1, 1, 1, 1};	// 筷子的互斥信号量
semaphore mutex = 1;    // 操作两个筷子的互斥量

// 哲学家i进程
P_Philosopher_i(){
    while(1){
        P(mutex);    // 保证取左右两边筷子的互斥性
	P(chopstick[i]);          // 取左边筷子的互斥性
	P(chopstick[(i+1)%5]);    // 取右边筷子的互斥性
        V(mutex);

        吃饭;

	V(chopstick[i]);
	V(chopstick[(i+1)%5]);
	
        思考;
    }
}

方案二:对哲学家同时进餐的进程加以限制,可以让最大4个哲学家进程同时拿去筷子。这样就可以得到如下逻辑代码:

semaphore chopstick[5] = {1, 1, 1, 1, 1};	// 筷子的互斥信号量
semaphore mutex = 4;    // 哲学家同时进餐互斥量

// 哲学家i进程
P_Philosopher_i(){
    while(1){
        P(mutex);    // 保证最多4个哲学家同时取筷子
	P(chopstick[i]);          // 取左边筷子的互斥性
	P(chopstick[(i+1)%5]);    // 取右边筷子的互斥性
        V(mutex);

        吃饭;

	V(chopstick[i]);
	V(chopstick[(i+1)%5]);
	
        思考;
    }
}

方案三:对编号偶数哲学家来说先取左边的筷子,而对于编号奇数哲学家来说先取右边的筷子,这样也会避免死锁。得到如下逻辑代码:

semaphore chopstick[5] = {1, 1, 1, 1, 1};	// 筷子的互斥信号量

// 哲学家i进程
P_Philosopher_i(){
    while(1){        
        if(i%2 == 0){    // 编号为偶数哲学家
	    P(chopstick[i]);          // 取左边筷子的互斥性
	    P(chopstick[(i+1)%5]);    // 取右边筷子的互斥性
        }
        else{        // 编号为奇数哲学家
            P(chopstick[(i+1)%5]);    // 取右边边筷子的互斥性
	    P(chopstick[i]);          // 取左边筷子的互斥性
        }

        吃饭;

	V(chopstick[i]);
	V(chopstick[(i+1)%5]);
	
        思考;
    }
}

总结分析:上边的五个经典案例,都是通过信号量来对进程的互斥同步进行限制的,里面的逻辑及对信号量的操作都值得仔细推敲、反复斟酌理解,这样对信号量的理解运用才会如鱼得水。

  • 15
    点赞
  • 80
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值