操作系统原理——Dekker互斥算法详解

Dekker互斥算法详解


大家好,这是本人的第一个技术博客(也是第一个博客),曾经看过《一个程序员的奋斗史》,从而萌生了写博客的想法。本人目前正在自学嵌入式方向,是一个不折不扣的小鸟。这篇博客亦来自于我学习计算机操作系统原理过程中的总结成果(视频下载地址http://xidong.net/File001/File_53948.html)。如果有人也正在学习这方面内容,我非常希望能够帮到你。如果有不对的地方欢迎大家指出和改正。

大多数系统允许多个进程共享资源(如CPU,IO设备,硬盘等),而为了保证进程间能够互不影响、安全正确地访问这些共享资源,就必须对进程访问共享资源采取某种控制。对于某一时刻仅允许一个进程访问的共享资源就叫临界资源,而访问这些临界资源的程序代码段就叫做临界区。而计算机术语中,对进程排它地访问临界资源的这种控制手段就叫做互斥(也就是说某一时刻临界区的进程只能为一个)。

解决进程互斥的方法有很多,比如软件方法、硬件方法、信号量方法、管程等方法,而今天我所说的Dekker互斥算法就是软件方法中的一种。


菜鸟设想:先让我们看看一个初学者所能想到的最简单的互斥访问临界区的伪代码:

操作系统原理——Dekker互斥算法详解_第1张图片

初学者看见这段代码,可能有人会觉得很奇怪。flag初始化为0,然后执行process方法的时候,while判断不通过,故执行后面的代码,而最后方法结束的时候,flag的值任然为0,这不是相当于while循环体永远不会执行吗???注意!!不能再用单进程编程那样一段代码从头执行到底的“直线式”思维了。这里可是多进(线)程编程!程序完全有可能在执行到任意一条语句时被中断,从而跳到另外一个进程执行另外一段代码。

上述代码中,设置一个状态值用于判断进程是否可以进入临界区,有程序进入临界区的时候便将flag修改为忙碌,等到出临界区的时候则重置为空闲,从而释放资源。表面上来看仿佛做到了互斥(当一个进程在临界区时阻止了其他进程进入)。可是仔细看,其实上述代码根本没有达到互斥的功能!!!为什么呢?先休息一下。等我谈到改进设想1的时候再说原因。^_^


总之,上述的代码并没有达到我们所需要的功能,那么我现在就推出一个很好的解决了互斥功能的代码吧(仅仅是解决了互斥),这就是初步设想。

初步设想:代码献上:

操作系统原理——Dekker互斥算法详解_第2张图片

注意到这段代码,其实这段代码的思路很清晰:将每个要访问临界区的进程设置一个序号,每个进程必须按照这个序号依次访问临界区。看上去貌似可以达到互斥。实际上的确如此,他完美的达到了互斥,仅此而已。程序严格地按照一定的次序(0->1->0->1...)访问临界区,从而当一个进程进入临界区时,完全没有留给任何进入临界区的机会给其他进程。为什么呢?同样等到改进设想1的时候再说原因。^_^

基础比较好的同学肯定会注意到:这样做留下了好几个要命的问题:首先访问临界区的两个进程完全依赖彼此对number值的修改,假设一个进程在将序号值修改成其他进程需要的序号值之前,出现了错误直接退出进程,那么序号将永远得不到修改,那么依赖这个序号值的进程将永远得不到执行!这是很要命的,它违背了进程“有限等待、空闲让进”的互斥原则。此外,当两个进程访问临界资源的频率不同时将会严重影响系统效率。比如,进程0每1分钟要访问临界区,进程1每10分钟访问临界区。当进程1退出临界区后,十分钟后才会进入临界区,而进程0的每一分钟就要进入临界区,可是当进程0退出临界区后必须等进程1退出临界区后才能进入,而此时进程1并不需要进入临界区,进程0必须在临界区空闲的情况下在9分钟后等到临界区被进程1访问并退出后才能进入,而且每进行一个周期就要等一个9分钟!也就是说访问频率低的进程严重影响了访问频率高的进程的执行效率,这明显违背了“空闲让进”原则。

可能菜鸟们(本人也是)对以上的表述;理解得不是太清楚。下面我来类比这样一个例子:上面的情形就好像两个人想进入一个仅有一个蹲位的厕所,门口有一个大爷将两个人分别给了一个号码牌(0、1),且规定必须按照“01010101...”的规则进入厕所。假如人A在上厕所的时候一个不小心将号码牌0丢进了下水道,那么人B就悲剧了,因为守门的大爷很奇怪,他永远要等到号码牌0从厕所里出来才会让人B进去。。。。另外一钟情况,假如人A每一天上一次厕所,而人B肾功能不太好每5分钟上一次厕所,那么等人A上完厕所后,人B也第一次上完厕所后。。。天啊!他不给憋死才怪。

这下大家应该知道这种制度的可恨之处了吧,介于初步设想有这么多弊端,下面进行了第一次改进。


改进设想1:代码如下:

操作系统原理——Dekker互斥算法详解_第3张图片

改进设想1取消了初步设想的序号访问方式,使得上面的大部分问题得到解决。此方法其实与菜鸟设想大同小异,它们给了临界区一个状态标识,当标识为空闲则允许进程进入,使得临界区得到充分利用,不同地方在于改进设想1状态信息更为精确,它使得程序知道是哪个进程占用了临界区,加大了编程的灵活性,但是却限制了互斥算法的使用条件:仅能控制两个进程进行互斥操作,其实这也是Dekker算法的缺点之一。

不幸的是,改进设想1菜鸟设想一样,同样不能实现真正的互斥,这到底是为什么呢?我前面提到,多进程的并发具有不确定性,程序执行过程中可能会不停地进行进程切换。让我们看一下这样一种情形:进程0一路执行完(1)处,由于flag[1]被初始化为0,因而跳过循环体,准备开始执行(2),此时进程突然被切换,转而执行进程1,等到进程1执行到(3)时,因为(2)处还未得到执行,flag[0]的值还没有来的修改,还是初始值0,结果进程1同样绕过了循环体顺利的进入了临界区,这样就导致了进程0和进程1同时进入了临界区,使得互斥失效。同样的道理菜鸟设想也犯了这个致命的错误。然而为什么序号访问就能顺利的互斥呢?这是因为无论如何切换,number的值不是0就是1,这就决定了两个while循环(一个判断是否为0,一个判断是否为1)只有一个能顺利“绕过”循环体,并进入临界区,从而达到互斥访问。

回到刚才我类比的例子,就好比此时厕所上装了一个信号灯,用来表示厕所里是否有人,当厕所外面的人看见信号灯亮则表示里面有人而不能进厕所,反之则表示无人可进。这时候假如人A看见信号灯是黑的,进去之后还没来得急按下信号灯的开关,而此时恰好人B又看见灯是黑的,于是也进入了厕所,这时,就尴尬了。。。

可见由于进程的切换的不确定性导致了进程同时进入了临界区,而上述情况的原因在于修改状态的“时机”不对,因此就有了改进设想2。


改进设想2:代码如下:

操作系统原理——Dekker互斥算法详解_第4张图片

改进设想2将状态的修改放在了循环之前,即进程访问临界区时需提前表达访问临界区的“意愿”,这样一来,无论进程如何切换等到其中一个进程开始执行循环条件判断时,要么仅有一个flag为0,要么两个均为1,这就保证了两个进程无法同时绕过循环体。可是细心的人会发现这样一个问题:当进程0一路执行完(1)处,由于flag[1]被初始化为0,因而跳过循环体,准备开始执行(2),此时进程若被切换至进程1,等进程1执行完(3)时,又被切换回进程0执行,而此时flag[1]被修改为1满足循环条件进入循环体,在循环的某一时刻又切换到(4)执行,同样由于flag[0]被修改为1进入循环体,结果可想而知,两个进程都进入了循环体而不能修改自己状态值,导致两个进程永远都不能跳出循环。这种情形在计算机中叫做“死锁”,除非某一进程主动放弃资源,或系统主动干涉,否则进程永远占用资源但却又得不到推进。

用厕所的类比来说,此种方法就是给了每个人一个遥控器,使之能在进入厕所之前就能够远程点亮信号灯,从而避免同时进入厕所,可是倘若人A点亮了属于自己的信号灯,但还没来得及进入厕所时,人B也不甘示弱的点亮了自己的信号灯,两个人都互不谦让,从而造成谁也进不了厕所。。。

可以看出上述问题出现的根结所在,由于两个进程均提前表达了进入临界区的企图,可是谁也不肯放弃自己进入临界区的意愿,互不退让,并且不断查看状态值,从而导致死锁。

改进设想3:代码如下:    

     操作系统原理——Dekker互斥算法详解_第5张图片

改进的思路很简单,让进程在发现对方进入想临界区的愿望后,将自己的意愿停止一个随机时长,从而达到相互谦让地表达自身的意愿。这样一来即使双发进程在某一刻因为对方的状态值均为1而都进入了while循环体,可是由于进程将会放弃进入意愿而使得某一进程总会因为不满足循环条件而跳出循环,从而进入临界区。由于停止时间是随机的,或许会经过很多次循环判断的时候对方的状态都为1,可是,总会有一时刻

使得两个进程的执行进度会得到错开,使得双方进行循环判断的时候不会两个flag都为1。虽然此种方法不会进入死锁,可是却存在一种发生可能性非常低的僵局,即双方在谦让过后可能同时又进行循环判断,从而一次又一次的谦让下去,导致程序效率降低,甚至出现双方的谦让导致两个进程的阻塞时间接近于无穷大的情况。

再用到厕所的例子来说明:人A与人B看见对方进入厕所的信号灯都是亮着的,便都主动礼貌地谦让,关掉了自己的信号灯,可是呢,两人实在过于默契,两人都在同一时刻关灯,并且又在同一时间开灯,双发就这么一直让来让去,两人就这么僵持着很长一段时间,谁也没能进入临界区。。。

然而,程序设计必须保持绝对的严谨,虽然在停止了一个随机时间的情况下,两个进程仍然保持同步推进的情况实在微乎其微,但是即使有%0.00...001的可能性使得程序死锁,那么这个设计便是不完美的。为了彻底杜绝决形成这种僵局的可能性,便诞生了最终算法也就是大名鼎鼎的Dekker互斥算法。(抱歉,这一段,实在不知道该怎么表达才通俗易懂)

最终设想(Dekker互斥算法):

操作系统原理——Dekker互斥算法详解_第6张图片

Dekker算法终于出炉了,可以看出Dekker算法是基于改进设想3的谦让思路的基础上,采取的一种“谦让而又不过分谦让”的折中思想。如何避免双方不停的谦让呢?Dekker算法采用了初步设想的序号访问思想,使得进入循环的进程按照序号来决定到底该不该一直谦让,有人会问,采用序号的是否使得进程出现按照序号访问临界区的老毛病呢?答案是不会的,至少是不会严格的按照序号轮流访问临界区,原因在于,按照序号来谦让有一个前提条件:两个进程必须因为对方的状态值恰好都为忙碌而同时进入循环体,而在此之前,两个进程进入临界区的概率是相同的,跟序号是没有关系的。也就是说想要序号访问临界区还需要一点“运气”。

总之,Dekker算法首先使用状态值的方式解决了序号访问临界区的弊端,又利用改进思想2使得进程可以互斥的访问临界资源,同时又采用了改进思想3的方法避免了死锁现象,而最后结合了序号谦让方式解决了因为避免死锁而产生的僵局现象,就这样循序渐进地用软件方法解决了进程的互斥的问题。


然而,Dekker方法就真是完美无缺的吗?很遗憾,就算是Dekker算法也无法避免软件互斥方法的一个通病,那就是忙等现象。大家注意以上的每一个设想中,都不可或缺地使用while循环,而循环体中,执行地都是毫无意义的代码,也就是说软件方法利用无意义的循环使得进程无法向前推进来达到阻塞进程的目的,然而让CPU去执行无意义的代码本身就是一种严重资源浪费,进程既占用了CPU,然而却没有生产任何有效数据,可以说,这样做严重降低了CPU的效率。这是整个软件方法都无法回避的问题。

此外,大家可以看见Dekker算法仅能进行两个进程的互斥,对于两个以上的互斥问题,实现起来相当复杂。

并且,软件方法在实现互斥的时候,需要相当小心,一会不小就是互斥失败啦,死锁啦,之类的。


第一篇博客终于写完了,写完之后总觉得是否太啰嗦了,而且有些地方总觉得没解释太清楚,看来有些事情真的只可意会不可言传啊,如果有读者觉得有哪里不对,或者需要补充什么,欢迎来喷。谢谢!










你可能感兴趣的:(操作系统原理)