死锁应对.

发生死锁的原因

大部分人可能都见过交通阻塞,一大堆汽车因为争夺行路权,互不相让而造成阻塞。又或者因车辆发生故隙抛锚或两辆车相撞,造成道路阻塞。在这种情况下,所有的车都停下来,谁也无法前行,这就是死锁。死锁的发生,归根结底是因为对资源的竞争。 因为大家都想要某种资露,但又不能随心所欲地得到所有资源,在争夺的僵局中,导致任何人都无法继续推进。

在一个多道编程的环境里,一个系统里存在多个进程或线程,面这些远程和线程共享该计算机系统里的资源。因为资源竞争面造成系统无法继续推进就是难以避免的了。

这里所说的资源就是一个程序工作时需要的东西:磁盘驱动器、锁、信号量、数据表格等。资源既可以是硬件,如 CPU、内存、磁盘等,也可以是软件,看不见模不着,如锁、信号量等。根据资源是否可以抢占分为:可抢占资源和不可抢占资徽。可抢占资源当然是可以从持有人手上强行抢夺过来的资源,且不会发生系统运行紊乱;不可抢占资源则不能从持有人手上强行抢夺。如果强行抢夺,则将造成系统运行错误。

当然,从绝对概念上说,没有什么资源是不可抢占的。因此,我们的非可抢占只不过是相对意义上的概念。我们的目标是保证每个程序的正确运行。如果抢占一个进程所持有的资源后,我们还能找到某种方式让该程序正确运行下去,则该资源就是可抢占的,否则就是不可抢占的。

例如,CPU是可抢占资源。因为我可以将一个进程强行从CPU上拽下来,换上另一个进程来运行。被抢占的进程可以稍后继续运行,并正确结束。而CD刻录机就是不可抢占资源,如果在刻录光盘的中途将某个进程赶出来,接着刻录另一个进程的数据,则该张光盘将成为废品。锁、打印机等也属于不可抢占资源。

死锁的描述

我们知道,线程的执行需要资源,它们都要求以某种顺序来使用资源。如果请求被拒绝了,那就等着。线程使用资源的顺序通常如下:

  • 1)请求资源。
  • 2)使用资源。
  • 3)释放资源。

线程在资源请求没有被批准的情况下必须等待。这种等待有两种应对方式:一是阻塞等待,二是立即返回,执行别的事情,等以后再行请求。当然也可以失败退出,终结线程。就像我们在没有资源的情况下停止工作一样。

如果线程采用第2种方式(立即返回),则不会发生死锁,因为没有等待。但如果采用第1种方式,则死锁就有可能。例如,如果有n个线程,T1,…,Tn个资源,R1…,Rn。其中Ti持有资源Ri,但又请求资源Ri+1.,这样就将形成死锁。我们可以用有向图来表示资源的占用和需求关系。以方框代表资源,圆圈代表线程。从资源指向线程的实线表示该资源已被该线程获得,从线程指向资源的虚线表示该线程在等待该资源,则我们可以画出下图。
在这里插入图片描述
这样画出来的图称为资源使用图。从图中看出,每个线程都在等待某一个资源,因此没有线程可以推进,从而形成死锁。

到现在可以给死锁下一个正式定义了:如果有一组线程,每个线程都在等待一个事件的发生,而这个事件只能由该组线程里面的另一线程发出,则我们称这组线程发生了死锁。

这里的事件通常是资源的释放。在死锁状态中,没有线程可以执行、释放资源或被叫醒。

死锁的4个必要条件

从上面的论述中可以看出,死锁的发生必须具备一定的条件。最低限度,可以看出来,死锁的一个必要条件似乎是循环等待。除此之外,死锁还需要具备别的条件吗?当然。死锁的发生必须满足4个条件。分别细说如下。

条件1:死锁发生的必要条件是资源有限。 即一个系统里面的资源数量有限,以至于无法同时满足所有线程的资源需求。这个条件非常直观,如果每个线程都有足够资源同时推进,自然不会发生死锁。

这个条件也称为资源互斥条件。即资源不能共享,在一个时候只能由一个线程使用。这个和资源有限是等价的。因为,如果资源可以同时被多个线程使用,则将不会发生死锁。

条件2:死锁的另外一个必要条件是持有等待!即一个线程在请求新的资源时,其已经获得的资源并不释放,而是继续持有。

如果不是这样,死锁也不能发生。因为一个线程在请求资源时,其手上并没有持有任何资源,自然就不会阻挠别的线程运行。阻挠别的线程运行的线程必定已经获得了其所需的全部资源,而能够顺利执行到结束并释放资源。

条件3:死锁的另外一个条件是不能抢占。如果一个资源可以抢占,则死锁也不会发生。

条件4:循环等待条件。这是死锁的最后一个条件。如果你等我、我等你,大家这样都等着对方,就死锁。这个时候如果我们画出系统里线程的资源占用和请求图(即资源使用图),应该能看到一个循环。

死锁的应对

如何应对死锁呢?只要看看人类是如何应对生活中的死锁(难题),就可以推测出操作系统是如何应对死锁的。那么人有哪些办法来应对生活中的难题呢?从高级境界来看、只有两种策略,即:

  • 允许死锁发生。
  • 不允许死锁发生

每种策略又有两种子对策。对于第1种策略,其两个子对策就是:

1)假装没有看见,不予理睬。
2)在死锁发生后,想办法予以解决。

对于第2种策略,其两个子对策为:

1)通过生活中的周全考虑,避免难题出现。
2)通过将发生死锁的必要条件消除,杜绝死锁的发生

操作系统应对死锁的策略与人类应对难题的策略一样,也是两大种、四小种。下面我们分别予以论述。

不予理睬

此种策略就是操作系统不做任何措施,任由死锁的发生。 这看上去是一种非常糟糕的策略,很多人可能都认为没有什么实际操作系统采取的是这种策略。但这种策略真的很糟糕吗?

如果很糟糕,就不会有很多人在面对问题时选择这种策略了。实际上,这种策略是大多数人在大多数情况采取的策略。老子说过,无为而治,就是这个策略。你什么都不用做(实际上并不是什么都不做,而是尽量少做),事情慢慢就朝着有利的方向发展。

死锁也一样。只有你认为它是问题时,它才是问题。而我们研究商业操作系统的人不认为这是什么大问题,因为我们分析发现,死锁发生的频率不太高(当然这点有争议),所以不必管它。另外,死锁防止的代价很高,防治死锁比重启100次代价还高,因此不如直接重启。 即如果死锁发生的话,重启即可。这就是为什么Windows、Linux和其他商业操作系统都没有采取死锁防治措施,这也是你的电脑经常死机的原因。

在操作系统设计时我们经常不得不进行折中,是想尽量方便,偶尔出点错误呢?还是保证系统完全正确,但是费了很大力气?当然是前者。计算机科学的一个原则就是寻求方便。当然如果牵涉的是高可靠系统,实时控制系统,就另当别论了,那绝对不允许死锁。

死锁检测与修复

虽然刚刚说过,防治死锁非常麻烦,我们很愿意采取不予理睬的策略来应对死锁。但是在某些时候,还得对死锁进行防治。例如对待高可靠系统、实时系统等。

另外,如果你是一个不厌其烦的人,或者你是一个有决心、有意志力的人,当然就不会允许在面对困难时无所作为。你会绞尽脑汁,想尽办法来解决难题。而这也正是我们操作系统解决死锁的第2种策略:死锁检测与修复。

检测死锁

如果想纠正死锁,自然得知道是否已经死锁了。所以死锁修正的第一件事是要检测死锁。这个似乎很简单。死锁的原因是资源竞争,只要我们对资源的拥有和资源的请求都清楚,问题就解决了。或者说,将线程的资源占用和需求关系用一个有向图表示出来,然后查看图中有没有循环。如果有,就有死锁;如果没有,就没有发生死锁。

这个说起来很容易,但是要检测却不容易。看下面的资源关系图,你看出死锁了吗?
在这里插入图片描述
很显然,确定一个有向图是否含有循环并不是一件容易的事。事实上,这种检查算法的时间复杂性是n立方。如果在每次资源分配发生变化时,做一次这样的检查,系统效率将出现明显下降。因此,为了操作系统的效率,我们不会直接在图上进行操作。

一种效率较高的算法是利用矩阵。这种算法用到两个矩阵:一个叫资源分配矩阵,一个叫资源等待矩阵。矩阵的每一行代表一个进程,每一列代表一种资源。在资源分配矩阵里,行列交叉的数值代表该进程已经拥有该资源的数量;在资源等待矩阵里面,行列交叉的数值代表特定行还需要特定资源的数量。下图分别描述了资源分配和资源等待矩阵

在这里插入图片描述
除此之外,我们还维持两个矢量:一个系统资源总量矢量,表示系统中所有资源的总数是多少;另一个是系统当前可用资源矢量,代表系统现在还有多少可用的资源,如下所示。
在这里插入图片描述

有了上面的矩阵和矢量,我们就可以通过简单的矩阵运算来判断系统是否发生了死锁。怎样判断呢?先考虑,什么时候发生死锁?每个进程都不能推进。什么时候不能推进?就是要求的资源不能满足。你把可用的资源拿来与资源等待矩阵的每一行进行比较,你就知道谁不能满足。如果减出来,每一个进程都有负数,那就是发生了死锁。 通过这种办法你可以发现死锁,比在有向图找循环容易。

死锁的恢复

现在死锁的检测已经实现了,那怎么从死锁状态恢复呢?自然也有好几种办法。

首先可以抢占。即将某个线程所占的资源强行拿走,分配给别的线程。 当然了,被抢占的线程很有可能不能正确运行了。不过这就是纠正死锁的代价。这种策略的一个重要考虑是选择哪个线程作为牺牲品,而这往往不是容易作出的决策。一种选择是将占用资源最多的线程废掉,以期尽可能多地释故资源,不过这种办法后果很难预料。

另外一个选择是更进一步,不光是抢占某个线程的资源,而是将整个线程“杀掉“。 这其实并不会更加残忍。因为,抢占一个线程的资源有可能已经造成该线程无法再正确运行了。所以,干脆“杀掉”。与前种办法一样,后果难以预料。

最后一个办法是上翻(roll back),即将整个系统翻转到过去的某个状态, 大家从那个状态重新来过。那么这个就要定期对系统做记录。这是很麻烦的事情。对于一个负责的系统来说,上翻到一个可靠的状态十分困难。而且,更为重要的是,系统并不是在任何时候都可以上翻的。

那么死锁的检测与恢复这个办法可行吗?实际上根本行不通。在检测与恢复两个部分都存在巨大困难。首先,使用资源分配与等待矩阵来判断死锁是否可靠?如果减运算后每一项都有负数的话。死锁是不是就直的发生了?答案是否定的。

这种判断只能说死锁有可能发生,但并不能肯定死锁会发生。因为也许某个线程因某种原因突然退出,从而不会发生死锁。这样,死锁没发生你说它发生了,误判。另外,还可能出现死锁发生了你说没发生。我们有n个线程,有可能几个线程已经死锁了,但是有的线程还能推进。这样从矩阵上判断并没有发生死锁。

第二个问题,这个矩阵可能是一个巨大的矩阵。如果是多用户的话,线程数可能不计其数,因此,这个矩阵有可能非常的大。并且,资源每发生一下变化,就要更新这个矩阵,这样所要花费的计算资源也将很高。不过这还不是关键的问题。

此种策略最致命的问题是检查死锁的进程自己发生了死锁。 此时,死锁检查程序已经无法年进,自然无法检在死锁是否发生。
既然此种策略存在诸多弊端,那只能走另一条路:不让死锁发生!

死锁的动态避免

死锁的检测与修复虽然是一种积极的应对死锁的办法,但却不是什么高明的策略。因为让死锁发生再来检测与修复已经属于后发制人了。死锁的消极后果已经产生。即使修复,也已经浪费了时间,降低了效率,甚至造成了其他损失。

因此,我们就想,能不能更加积极主动一点,不要等死锁发生了再亡羊补牢,而是在运行中就小心谨慎,不让死锁发生。这就是死锁防治的第3种策略:动态避免。

进行动态避免的原则也很简单:在每次进行资源分配时,必须经过仔细计算,确保该资源请求批准后系统不会进入到死锁或潜在的死锁状态。 潜在的死锁
状态指的是尚未发生死锁的状态,但接下来的执行将一定产生死锁,这种状态我们又称之为“不安全状态”,与之相对的状态称之为“安全状态”。详细来说,安全状态指的从该状态开始,我们能够找到一种资源分配方法和顺序,使得所有进程都能获得其需要的资源,从而不会产生死锁而不安全状态则是从该状态开始,无论我们怎样,也不能找到一种资源分配方法和顺序来满足所有进程的资源需求。

假如我们有一种资源的数量为10个。我们有三个进程在运行。每个进程需要资源的最大数和当前已经占用的资源数,如表所示。
在这里插入图片描述

请问该状态是否为安全状态?答案是安全状态。因为从该状态开始,我们可以先给B分配两个资源来满足B的总需求,这样B将能够运行结束,然后释放其所持资源。这时总可用资源数将为5,正好可以再全部分给进程C。C的总需要满足后也可以顺利执行完毕,从而将所持资源释放,这样总可用资源数将变为7。我们可以将其中的6个分配给A,这样A也可以顺利执行完毕。这种分配过程如下图所示:

在这里插入图片描述
在这里插入图片描述

但假如,如果A请求再增加一个资源,我们不能批准。因为,批准该请求将使系统进入到下表所示的潜在死锁状态。
在这里插入图片描述
在上表的状态下,这时候虽然还没死锁,但是死锁已经不可避免了。因为已经不存在任何一种资源分配方法和顺序能够完全满足所有进程的资源需求。读者也许看出来我们可以将剩下的2个资源分配给B,B不就可以顺利执行完毕吗?是的。但问题是B完成后系统将只有4个可用资源,已经无法满足A或者C中任何一个进程的资源需求。因此,系统将就此进入死锁状态。

定义了安全状态后,死锁动态避免的策略就很简单了:就是要防止系统进入不安全状态。手段就是每次需要进行资源分配时,就计算一下该分配是否将系统带入非安全状态。如果是,就否决相关资源请求;否则,就批准。就像下棋,理想状态下,我们每个步骤都应该计算一下是否会导致输棋。如果不会,这一步就可以走。

动态避免的优点是无需等待死锁的发生,而是在死锁有可能发生时采取先发制人的措施,断然拒绝可能引导系统进入潜在死锁的资源请求。就像发现对方有敌意时采取先敌制人的手段。由于系统从来不进入到死锁状态,死锁所带来的各种问题自然不复存在。

但动态避免的缺点也是十分明显的。这就是计算一个状态是否安全不是一件容易的事。从上面的表格也许觉得这种计算不是十分困难。问题是,我们只是对一种资源,3个进程的超简单系统进行了计算。如果一个系统资源种类繁多,进程个数庞大,则这种计算将变得十分复杂和费时。不过这还不是动态避免最致命的缺陷。

假定我们有的是时间,等上一等也不是什么无法忍受的事。但动态避免策略的另一个问题却是你再有耐心也解决不了的。仔细看我们前面的表格:每个进程所需的最大资源数都已知。但是,我们怎么能够知道一个进程的最大资源需求呢?这不是需要预测将来吗?这当然是很困难的。也许有读者会说,不能准确预测将来的最大资源需求,可以粗略估算。只要估算值不低于实际需要值不就可以了。

问题是,在无法准确计算情况下,人类一般会超额估算自己的资源需求,这种超额估算带来的后果非常严重。首先是资源浪费,因为超额估算,使得本来可以分配给更多人的资源被少数线程占用。另外一个更为严重的问题是超额估算可能造成死锁误判,因为安全和非安全状态的判断依据之一是最大资源需求。如果该数值估算过大,超过进程的实际资源需求,将造成在实际上安全的情况下,系统被判为不安全,从而造成可以执行的任务也得不到执行。

死锁的静态防止

到目前为止,我们已经论述了三种死锁的应对策略,都不是很令人满意。虽然不予理睬省时省力,但不受用户欢迎,也不是一个负责研究者所应采取的态度。第2种策略虽然较第1种策略积极,但毕竟允许死锁发生,且死锁侦测存在种种困难,也不是我们追求完美的人所喜爱。第3种策略较前面两种又进了一步,已经不允许死锁发生了。不过,这种策略却存在实现困难的问题,设计实现和时间成本上都不理想。而且,从应对的境界上说,还是属于头痛医头、脚痛医脚的层次,即看到什么地方似乎要出问题了,即采取相应应对行动。

孙子兵法说:上兵伐谋,其次伐交,再下攻城。死锁的检测与修复只不过是攻城的下策,而死锁的动态避免只能算是其次伐交的中策。那我们有没有有应对死锁的上策呢?有,就是死锁的静态防止。

该策略的中心思想是清除死锁发生的土壤,而死锁发生的土壤就是死锁的4个必要条件。如果消除死锁发生的4个必要条件中的任何一个,则死锁将无法发生。下面我们就来看一下如何消除这些必要条件。

消除死锁的必要条件

消除资源有限条件

先看第一个条件,资源的有限和独占性。攻击这个条件有两个办法:将资源无限增加或把所有资源变为共享。当然了,我们在实际中并不需要将资源无限增加,而只要增加到能够满足所有进程的资源需要为止。但这一点并不实际。因为资源是有限的,没有什么东西是无限的。而且增加资源就要增加成本,因此此计不通。

那将资源变为共享是否可行呢?这要看是什么资源。有的资源看上去是独占资源,但却可以通过间接来实现共享。例如,磁盘和打印机。对于打印机来说,我们不允许任何进程直接占有,而是设计一个“精灵”程序daemon来管理。该程序daemon占用打印机,所有打印任务都必须由它发出。任何一个进程想要打印,不是直接请求打印机,而是将打印要求发给这个daemon指定的地方即可。这个daemon负责将打印任务完成。这很像我们前面说过的批处理。

不过这种办法并不适应所有的资源。例如,键盘输入就无法共享。

消除保持和请求条件

消除这个条件的办法很简单:就是一个进程必须一次请求其所需要的所有资源,而不是一般情况下的请求一点资源,做一点事情,到需要下一个资源的时候再请求一点,获得资源后再继续推进。由于一个进程一次就获得了其所需的所有资源,该进程自然就可以顺利执行,从而不会发生死锁。

这种办法的缺点就是一次将所有资源拿上,太过浪费。因为有的资源要到最后才需要,但也得在一开始就占用。显然不利于资源的有效利用。这种做法的另一个问题就是在一开始就需要知道一个进程所需要的所有资源,这是很困难的。

一种变通的办法是还像以前那样请求资源,即在需要资源的时候才请求。但加上一个条件:如果请求的资源被拒绝,则该进程需将其现在已经拥有的资源也释放掉。这样,因为阻塞的进程不占用任何资源,死锁自然也得以消除。

这种策略避免了上述问题,但也存在缺点。主要是在获得某个资源失败后,需要释放已经占用的所有资源,而这有可能造成问题。例如,如果一个进程已经获得一些锁,但在锁里的工作又没有做完。如果这个时候释放锁,很可能前功尽弃,从而造成浪费。

消除非抢占条件

另外一个办法就是把第三个条件拿掉,即允许对资源进行抢占。也就是说可以从一个进程手上将资源抢夺过来。典型的例子有CPU和内存空间。一个进程可以将CPU或内存空间从另一个进程手上抢过来,从而避免了因CPU和内存空间的竞争造成死锁。而这恰恰是操作系统里面CPU调度和内存管理的一个重要功能。

不过,该策略有局限性。因为不是所有的资源都可以被抢占而不产生不良后果。例如,锁就不能抢占。如果从一个进程手上将锁抢过来,后果有可能是不堪设想。

消除循环等待条件

死锁的最后一个必要条件是循环等待。而循环等待的原因是因为进程请求资源的顺序是随机的,即一个进程可以先请求资源A再请求资源B,也可以先请求资源B再请求资源A。这样,如果两个进程按照不同的顺序请求A、B两个资源,死锁就有可能发生。但如果我们规定对A、B两个资源的使用必须按照先A后B的顺序请求,则死锁就不能发生。

假定一个系统有1、2、3三种资源。如果我们要求请求资源的顺序必须是1→2一3,则系统将不会发生死锁。在给定了这种顺序后,如果一个进程只使用一种资源,则直接请求该资源即可;如果某个进程需要请求两个或以上的资源,必须按照给出的这个顺序获取资源。例如,如果一个进程需要使用1、3两个资源,则必须先请求资源1,再请求资源3,即使该进程先使用资源3,后使用资源1。这种按规定顺序请求资源的做法真的能消除死锁呢?

以上述例子来说明,一旦某个进程占用了资源3,则说明该进程的所有资源请求都已经获得满足,它将顺利完成而不会发生死锁。 而在其完成释放资源3和其他占用的资源后,其他等待进程就可以获得这些刚刚释放的资源,而获得资源3的进程将顺利完成。如果没有进程需要资源3,则占用资源2的进程将顺利进行,以此类推,该系统无法发生死锁。

这方面的典型例子是锁的使用。如果两个进程需要同时使用两把锁,则如果它们按照顺序请求这些锁,死锁将得以避免。下面所示的A、B两个线程将不会发生死锁:
在这里插入图片描述

这种按顺序请求资源的策略能够成功的原因是,其打断了循环等待的死锁条件。

死锁、活锁与饥饿

多道编程或者多线程编程除了要应对死锁之外还有一个问题必须面对:饥饿。
准确的说是资源饥饿。资源饥饿指的是某个进程或者线程一直等不到它所需要的资源,从而无法向前推进。就像个人因饥饿而无法成长一样。

饥饿在刚听上去像是一个与死锁不同的问题。但仔细考虑,却发现它实际上是死锁的一种通例。因为处于死锁状态的所有进程均无法获得其所需要的资源,死锁实际上就是饥饿。对于处于饥饿状态的进程来说,它因为无法往前推进,又有一点死锁的味道。因此,饥饿有时也称为死锁的"孪生兄弟”。

不过,这里需要注意的是,饥饿和死锁还是有所不同。处于死锁的进程必须多于1个,且处于一个死锁进程组的进程所需资源均为该组里其他进程所持有。而处于饥饿的进程可以只有1个,并且其所需要的资源可以被任何进程所占有。

除此之外,饥饿还有另外一种特例:活锁。在活锁状态下,处于活锁进程组里的进程状态可以改变,但是整个活锁组的进程无法推进。活锁可以用用两个人过一条很窄的小桥来比喻:为了让对方先过,两个人都想给对方让路而闪身到一边,但由于两个人都同时进行此种运动,有可能两个人都同时运动到左边,然后又同时运动到右边。这样,虽然两个人的状态一直在变化,但却都无法往前推进。由此可见,活锁是死锁的通例。

从上述分析可以看出,死锁,活锁,饥饿是一个包含与被包含的关系。死锁是活锁的特例,而活锁又是饥饿的特例。在多道编程时,我们不光要应对死锁,还需要应对活锁和饥饿,而由于它们之间存在类似性,死锁应对的手段也可以在一定程度上用来应对活锁和饥饿,因为活锁和饥饿是死锁的通例,,且死锁是无法完全避免的,活锁和饥饿也是不可完全避免的。
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值