好像是dekker算法的小历史

实在是无奈,OS老师一定要求自学这篇博文的内容。

大神,我就在此处全篇引用你的代码啦,包涵。

可能内容有点冗杂,可以分版本阅读。

以下正文

………………………………………………………………………………

对于每份代码,要求对代码写注释,写原理,还写缺陷。

对于图示的一些说明:

其中的interrupt是中断的意思,是从一个程序切换到另一个程序去执行。

其中的critical section,是临界区的意思。

版本一:

int flag = 0;//全局变量,表示临界区是否被使用
void process() {
	...
	while(flag != 0) {//表示当前有程序访问临界区,则当前程序等待
		/*do nothing*/
	}
	flag = 1;//标记当前程序进入临界区
	/*访问临界区*/
	flag = 0;//访问完成之后,释放临界区资源
	...
}

版本一的原理:

从临界区角度出发,考虑到临界区只有两种状态(占用,空闲),用一个全局变量,表示这两种状态(对应程序的第一行)。

当临界区处于占用状态时,让试图进入临界区的程序等到(对应程序中的while循环)。等待到临界区空闲,则按下面步骤执行。

当临界区处于空闲状态时,让试图进入临界区的程序直接进入,进入之前将临界区状态标记为占用(对应程序中flag = 1),在临界区内执行程序,

执行完成之后,释放临界区,使临界区处于空闲状态(对应程序中的flag = 0),让其他程序访问临界区。

版本一的缺陷:

由于中断执行的不确定性,可能会发生如下情况

很显然,当程序0判断临界区是否为空闲时,发现空闲同时发生中断,执行程序1并又判断临界区是否为空闲,此时当然也判断为空闲;

之后就是两个程序先后将临界区标记为占用,两个程序又先后进入临界区执行若干周期。

这个不满足忙则等待,也就是不能控制只用一个程序占用临界区执行。版本一因此也会发生重大的错误。

版本二:

int number = 0;//全局变量,表示当前那个程序占用临界区
void process0(){
	...
	while(number != 0) {//对于0号程序而言,如果发现其他程序占用临界区,则等待
		/*do nothing*/
	}
	number = 0;//将临界区标记为0号程序使用
	/*访问临界区*/
	number = 1;//将临界区标记其他程序使用
	... 
}
void process1(){//对于此处的代码,只是对象发生改变,思想不变。
	...
       while(number != 1){
		 /*do nothing*/
	}
 	number = 1;
	/*访问临界区*/
	number = 0;
	...
}

版本二原理:

依旧是从临界区角度出发,并细化了版本一中全局变量的含义,即由原来单一的临界区资源是否被占用,进一步表示为那个程序占用临界区(对应程序第一行)。

当程序0试图进入临界区时,发现已经有其他程序编号占用临界区,则等待(对应程序中的while循环)至其他程序退出临界区,将临界区分配给它之后,按以下步骤;

当程序0试图进入临界区时,发现当前临界区正被其占用,则将临界区标记为自己的程序编号,并访问临界区,访问完成之后,将临界区分配给其他程序。

版本二缺陷:

我们现在考虑这样一个情况,比如说当前number的值为0,程序0中需要访问临界区的代码相对整个程序而言,位置比较靠后,而程序1中需要访问的代码却相对靠前;

也就是当程序1试图进入临界区时,程序1发现其他程序正在占用临界区,则只能等待;

事实是却是这样的,程序0还未进入临界区,

也就是临界区空闲,但是对于程序1而言是被占用的。

这就违背了空闲则入,这势必会影响整个系统的效率。

如果存在极端情况,比如说number值一直是0,然而程序0一直不访问临界区,哈哈哈哈(原谅我笑一下,C语言中的死循环听过么?)。那么程序1真的要等到海枯石烂了。

这就不能做到有限等待啦。(用户体验那么糟糕的系统,如果有一定是我写出来的)

版本三:

int flag[2] = {0, 0};//全局变量,表示每一个程序是否占用临界区
void process0(){
	...
	while(flag[1] != 0) {//当程序0试图进入临界区时,判断程序1是否占用临界区;如果占用,则等待
		/*do nothing*/
	}
	flag[0] = 1;//程序0标记占用临界区,之后访问
	/*访问临界区*/
	flag[0] = 0;//程序0标记释放临界区。
	...
}
void process1(){//思想相同,对象不同
	...
	while(flag[0] != 0) {
		/*do nothing*/
	}
	flag[1] = 1;
	/*访问临界区*/
	flag[1] = 0;
	...
}

版本三的原理:

从上面两个版本看来,临界区自顾自己的感受,不考虑程序本身处境。

这次从程序的角度看,程序是否占用临界区状态也是两种(占用,释放),每个程序各有标识(对应程序第一行)。

当程序0试图进入临界区的时候,判断是否有其他程序正在占用临界区,如果占用则等待,直到其他程序释放临界区(对应程序中的while循环)。释放之后,按以下步骤。

当程序0判断发现其他程序并未占用临界区,则将自己的状态标记为占有临界区,之后访问临界区,之后释放临界区资源。

版本三的缺陷:

同样的,对于先判断临界区是否被占用,后改变状态的思路来说,都有一个致命的缺陷。那就是在中断的帮助下,绕过等待,各自进入同时(宏观上)临界区。

同版本一,由于中断的不确定性,两个程序有在临界区会师成功,尽管这是我们所不想看到的,但是还是发生了。

先判断后标记的思路,经过两次尝试,都有一个共同且致命的bug,不满足忙则等待。

版本四:

int flag[2] = {0, 0};//全局变量,表示某个程序是否占用临界区
void process0(){
	...
	flag[0] = 1;//表明程序0试图进入临界区的意图
	while(flag[1] != 0) {//判断是否有其他程序使用,如果有则等待
		/*do nothing*/
	}
	/*访问临界区*/
	flag[0] = 0;//释放临界区
	... 
}
void process1(){//思路相同,对象不同
	...
	flag[1] = 1;
	while(flag[0] != 0) {
		/*do nothing*/ 
	}
	/*访问临界区*/
	flag[1] = 0;
	...
}

版本四的原理:

依旧从程序角度出发,因为版本三的思路不正确,此处改用,先标记后判断的方法(有点类似版本二),还是保留版本三的数据结构。

当程序0试图进入程序时,先表明意图(对应程序中的flag[0] = 1;)

然后判断其他程序是否占用临界区(对应程序中的wheil循环),如果有其他程序占用临界区,则等待至其释放临界区。释放之后按以下步骤。

然后访问临界区,访问完成之后释放临界区资源。

版本四的缺陷:

此处你即将了解到,中断是如何一次又一次巧妙的阻碍我们走向成功的(真是个小妖精)。

如果中断发生的恰到时候,那么flag[0]和flag[1]都将被标记为1,也就是都表示对方在占用临界区,哈哈哈(C语言的死循环)

这样的一个坏结果是,发生死锁现象。这个结果的确挺糟糕的。

版本五:

int flag[2] = {0, 0};//全局变量,表示某个程序是否占用临界区
void process0(){
	...
	flag[0] = 1;//先表明意图,希望进入临界区
	while(flag[1] != 0) {//判断是否有其他程序占用临界区
		flag[0] = 0;//如果有其他程序占用临界区,则退让并等待。
		/*程序暂停一个随机时间*/
		flag[0] = 1;//在随机时间之后,重新表明进入临界区的想法
	}
	/*访问临界区*/
	flag[0] = 0; //释放临界区
	...
}
void process1(){//此处依旧不做多余描述
	...
	flag[1] = 1;
	while(flag[0] != 0){
		flag[1] = 0;
		/*程序暂停一个随机时间*/
		flag[1] = 1;
	}
	/*访问临界区*/
	flag[1] = 0;
	... 
}

版本五的原理:

在版本四的基础上又做了改进,具体如下。

当其他程序占用时,将自己的意图暂时收起来,避免了版本四的死锁现象。也就是从程序角度上看是对程序增加了退让等待的属性。

程序0先表明希望进入程序的意图,

判断是否有其他程序占用临界区,若有,则退让并等待一些时间(while循环中的flag[0] = 0;),之后重新表明意图(while循环中的flag[0] = 1;),直到其他程序释放临界区。释放之后,按以下步骤。

访问临界区,访问完之后释放临界区。

版本五的缺陷:

虽然避免了死锁的出现,但是如果程序之间退让过于默契,那么仍然会出现程序之间相互等待的情况,也会导致系统性能的降低,甚至出现长时间的等待。

版本六(dekker's algorithm):

int flag[2] = {0, 0};//全局变量,表示某个程序是否占用临界区
int number = 0;//全局变量,表示当前临界区被那个程序占用
void process0(){
	...
	flag[0] = 1;//表明试图占用临界区
	while(flag[1] != 0) {//判断其他程序,是否占用
		flag[0] = 0;//若占用,则退让
		if(number != 0) {//通过临界区判断,当前是否空闲
			while(1) {//临界区并非空闲,则当前程序等待
				/*do nothing*/
			}
		}
		flag[0] = 1;//申请占用
	}
	/*访问临界区*/
	number = 1, flag[0] = 0;//将临界区状态分配给其他程序,当前程序释放临界区
	... 
}
void process1(){//由于代码思想相同,此处不做赘述
	...
	flag[1] = 1;
	while(flag[0] != 0) {
		flag[1] = 0;
		if(number != 1) {
			while(1) {
				/*do nothing*/
			}
		}
		flag[1] = 1;
	}
	/*访问临界区*/
	number = 0, flag[1] = 0;
	... 
}

版本六的原理:

单从程序看来也是行不通的,版本六则将临界区和程序两者结合起来。通过程序主动申请占用临界区,发现对方占用时主动退让,同时利用临界区只能有一个程序被占用的特点来决定两个同时退让的程序更优先占用临界区,避免了死锁和长时间等待的出现。

当程序0试图进入临界区时(对应程序中第5行),

判断其他程序是否进入临界区(对应程序中第6行),若有,则退让(对应第7行),

通过临界区判断(对应第8行),临界区被谁占用,若有其他程序占用,则等待。等待结束,则按以下骤。

重新标记临界区被程序0占用,由于此时程序1释放临界区(分两种情况考虑,一:在版本五的情况下,两者相互退让,程序1释放临界区;二:程序1正常访问临界区后退出)

所以程序0可以访问临界区,之后释放临界区。

版本六的缺陷:

该版本是解决了前五个版本的所有问题,但是从原理分析这个来看,考虑情况极多,代码也相对上面各版本复杂。

同时while循环的引入,会占用CPU资源,出现忙等待现象。

该算法对于多处理机就显得不适用。

综述:

无论是版本一还是版本三,通过判断临界区是否被占用还是程序是否占用临界区,都不能达到互斥的目的;

对于版本二和版本四,通过预先判断临界区被哪个程序占用还是判断哪个程序正在占用临界区,都可能发生死锁或长时间等待。

对于版本五,理所当然的放弃了对版本一和版本三的治疗,转而对版本四做了改进。意识到版本四的互不相让,带来的死锁,也就有了相互之间的退让的版本五。

可惜过犹不及,退让的太默契也不是好事。这个时候,再次从临界区这个被占用者角度出发,利用其唯一占用性,调解过于退让的局面出现。

dekker算法做到了合适的调解了程序之间的关系,即不但能达到互斥,也能在相互退让时给出决策,避免死锁。


啰里啰嗦的写了那么多字,有不当之处,请指出,给初学者一个学习的机会。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值