操作系统学习笔记【P17】——用临界区保护信号量

回顾

上节课讲了用信号量可以实现进程同步,但没有临界区的保护,信号量不能正常工作,
因此本节课的两个主题
1、为什么要保护信号量,为什么要引出临界区
2、怎么用临界区保护信号量

为什么要保护信号量

信号量是控制进程同步的关键,决定了进程的走与停,所以信号量必须是正确的
而信号量是共享变量,多个进程都会读写这个信号量,
进程调度顺序有多种可能(你不知道时间片停在进程的什么地方,切换哪个进程也是不确定的),这导致进程代码的执行顺序是不确定的,这种不确定会带来问题,导致信号量无法正确反应资源申请和释放的情况,如下例:
核心:在empty还没被彻底修改,就切到另一个进程了,另一个进程也会修改empty,那么empty含义就错误了

错误的进程执行顺序引起信号量含义的错误

分析清楚上述例子中怎么出错的,我们就很自然地知道怎么解决这个问题了
在写共享变量empty时阻止其他进程访问empty,也是让empty被一个进程彻底修改之后才能切换到下一个进程
就是说在一个进程中,修改共享变量的操作是原子操作,在执行这个操作时不能切换出去,
我们将这个只能互斥访问的共享变量,也称为临界资源,操作临界资源的代码成为临界区,在一组进程中,若一个进程在访问临界区,其他进程想访问临界区就必须等待,

怎么保证有一个进程在执行临界区代码时,其他进程无法执行呢?
将临界区保护起来,在进入临界区前加判断条件,退出临界区后释放权限,这部分代码怎么写呢?会讲3种方法

临界区代码保护原则

基本原则:互斥进入
好的临界区(减少CPU空转):有空让进 有限等待
(西电出版社汤小丹教材 空闲让进、忙则等待、有限等待、让权等待)

实现临界区方法(后三种最重要)

轮换法 —— 类似值日

谁值日谁执行,
两个进程A和B,turn=0,A执行;turn=1,B执行

行号进程A进程B
1while(turn != 0) ;while(turn != 1) ;
2临界区临界区
3turn=1;turn=0;

缺陷:AB必然是轮流值日的,若A执行完成后(turn=1),B因为其他原因阻塞,无法进入临界区;而turn一直等于1,A也进不了,不满足有空让进。
本质上是优先权的问题,A执行完成后,A的优先权会被降到很低,导致A无法获得执行临界区的机会,B因为其他原因不执行临界区,程序死锁。

标记法 —— 贴个便条

冰箱空了要给冰箱补充牛奶,丈夫发现冰箱空了,丈夫就贴个条告诉妻子自己将要去买,然后看看妻子有没有留条,留了就等待,没留就去买。妻子同理。

行号进程A进程B注释
1flag[0] = true;flag[1] = true;贴条
2while(flag[1]) ;while(flag[1]) ;看条
3临界区临界区
4flag[0] = false;flag[1] = false;摘条

缺陷:1、2行代码是进入区代码,但不是原子操作,因此考虑这种情况:A执行1后切换到B执行1,这样两个人都看到对方贴的条,都等待对方去买,出现无限等待。

个人思考:丈夫发现冰箱空了,也许应该先看看有没有条,再贴条?(还没细想代码怎么写,会出现什么问题)

非对称标记法 —— 贴个便条+一个人更勤劳

上一种对称的标记法出现的问题是:两个进程看到对方贴的条,都等待对方去买,进程切换导致他们一直等待,而冰箱里还是空的,
因为两个进程代码是对称的,进程切换时就出现上述问题,所以要破除这种对称结构
非对称标记法伪代码

Peterson算法(两个进程)—— 标记+轮转

进程要进入时先打标记,表示想进入。如果两个进程都打了标记,这时就通过轮转的turn判断谁优先权高,谁高谁执行,破除了互相看着对方标记空等的情况。

行号进程A进程B注释
1flag[0] = true;flag[1] = true;贴条,表示自己想执行
2turn = 1;turn = 0;谦让一下,把执行权给对方。如果两个都给了,后谦让的一个谦让成功,另一个执行
3while(flag[1]&&turn==1);while(flag[0]&&turn==0);只有对方想执行且对方获得执行权时才需等待
4临界区临界区
5flag[0] = false;flag[1] = false;摘条
如果交换turn赋值语句和flag赋值语句的位置,能否实现互斥访问临界区?

不可以。如果交换位置,意味着在没有访问意愿的情况下先谦让优先权。
考虑此执行顺序:B2-A2-A1-(flag[0] = true; turn = 1;)A进入临界区-B1-(flag[1] = true; turn = 1;A已经进入临界区了锁不住A)B进入临界区 —— AB都在临界区,互斥访问失败

peterson算法用于多进程

解法一:将线程分为两个一组,每组线程竞争同一个锁。在上一轮中获胜的线程会被重新分为一组,并在新的一轮中竞争。可以这样继续下去直到有一个线程在所有的轮次中均胜出,那么该线程获得锁。这种循环赛式的锁获取方式会产生较大的延迟。(参考《并行多核体系结构基础》汤孟岩 机械工业社2018年P162第六章课后习题,及P159解释说明。)
解法二:Peterson算法的推广Filter算法

Lamport 面包店算法(多进程)—— 轮转+标记+取号 (以上都是软件解法)

面包店的做法是: N个顾客要进入面包店采购,
首先按照次序给每个顾客安排一个号码,
顾客按照其号码由小到大依次进行购买,
完成购买的顾客号码重置为0,这样完成购买的顾客如果要再次购买,就必须重新排队。
以下代码注释来自此博客+部分个人理解

// 变量说明:
// i 表示当前进程PID
// j 表示当前迭代到的进程PID
// choosing[i] 表示当前进程i是否正在取号, 默认值为false
// number[i] 表示当前进程i的排队号, 默认值为0
// 对于一个进程i
process(i) {
    while (true) {
    	// 取号用choosing[i] = true、false包裹起来 再加上for循环里先while(choosing[j]); 
    	// 是为了保证每一个被迭代的进程都必须有号
    	
        // 当前进程i正在取号
        choosing[i] = true;
        // number为上一个已发放的排队号加1
        number[i] = 1 + max(number[1], number[2], ..., number[n-1]);
        // 当前进程i取号完毕
        choosing[i] = false;

        // 迭代所有进程
        for (j = 0; j < n; j++)
        {
            // 若当前迭代到的进程j正在取号, 则等待其取号完毕
            while(choosing[j]);

            // 同时满足, 当前进程才能通过
            while (number[j] != 0 && (number[j], j) < (number[i], i));
            //(a,b) < (c,d) 等价于 a<c 或者  a==c&&b<d  号码相同时,根据顺序选人,保证唯一性
        }

        // 临界区代码

        // 当前进程注销排队号
        // 一旦线程在临界区执行完毕,需要把自己的排队签到号码置为0,表示处于非临界区
        number[i] = 0;

        // 其它代码

    }
}

关中断(单CPU)

进程的代码都是由CPU执行的,当CPU在执行进程A的临界区时,由进程调度使CPU切换B的临界区去执行,这就导致多个进程访问临界区。联系之前学过的进程切换过程(用户栈、内核栈的切换),内核栈的切换是通过硬件中断实现的,如果我们禁止CPU检查中断,那进程就无法切换,也就实现了对临界区的独占访问
我们可以通过硬件指令cli()关中断,它的工作原理是:执行这条指令的CPU会将其CPU标志寄存器(即EFLAGS寄存器)中的中断允许标志位 IF(interrupt flag)设置为0,一旦这个位设置为0.这个CPU就不会在每次执行完指令时检查并处理中断了。
缺陷:对于一个多CPU(多核/多处理器)计算机来说,这个指令只能禁止掉一个CPU,对其他CPU没有影响,其他CPU仍然可以进入临界区。如果为了一个临界区把所有CPU都禁止了,这会极大影响计算机性能,很没有必要。
基于开关中断的临界区实现

硬件提供的原子指令

为什么不能通过信号量来保护临界区?

回顾我们是怎么引出临界区的:我们要使用临界区来保护信号量,使得其含义正确,从而正确控制进程同步。
如果用互斥信号量实现临界区,然后再用临界区保护信号量,这不是等价于用信号量保护信号量,再用信号量保护信号量,这将是个没完没了的循环
我们用其他的机制,如硬件机制保证互斥信号量操作的原子性,就以可以用这个互斥信号量去实现临界区,再用临界区去保护别的信号量,就能解决这个问题。

TestandSet指令

保护临界区的互斥信号量只取0和1两个数值,所以这个信号量的P、V操作就会简单而规整,完全可以让硬件来实现这个P、V操作的原子性。由于信号量只有0和1两个数值,很像是一个锁,所以这个信号量通常也被命名为 lock,现在要用硬件实现lock 操作的原子性。这种方法被称为硬件原子指令法。
如果 lock的当前值为0,说明没有上锁,可以继续执行进入临界区,同时要将lock 修改为1(上锁),这个工作要做成一条硬件指令,因为一条硬件指令的执行过程是绝对不会被中途打断的,从而实现了指令的原子操作。如果 lock 的当前值为1,则进程自旋等待。这条指令就是TestandSet。

bool test_and_set(bool *target) 
{
	// 通过地址寻址,真实地修改target的值
	bool rv = *target;
	*target = true;
	// 返回的是传入时target的值
	return rv;
}

指令执行结果分析:
1:传入参数为false,那么该函数会返回false,但过程中会真实地修改target所指向内存地址的值为true。
2:传入参数为true,那么该函数会返回true,此时过程中对target所指向内存地址的修改操作将不会引起变化。

指令使用代码示例:

do {
	// 进入区代码
	while(test_and_set(&lock));  
		
	// 临界区资源操作
	criricalSectionOperation();

	// 退出区代码
	lock = false;
		
	// 剩余区
	doSomethingElse();
	
} while (true);

  • 8
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值