操作系统-并发:死锁和饥饿

    xiaoxiao2024-12-23  4

    知识架构

    本章介绍并发处理中需要解决的两个问题:死锁和饥饿。本章首先讨论死锁的基本原理和饥饿的相关问题;接着分析处理死锁的三种常用方法:预防、检测和避免;然后考虑用于说明同步和死锁的一个经典问题;哲学家就餐问题。

    作为对全局知识的把控,这里给出学习目标,作为参考:

    列举并解释死锁产生的条件定义死锁预防,针对死锁产生的条件给出死锁预防的策略理解死锁预防与死锁避免的区别掌握死锁避免的两种方法理解死锁检测与死锁预防、死锁检测与死锁避免的本质区别掌握设计综合死锁解决策略的方法分析哲学家就餐问题

    6.1 死锁原理

    死锁定义为一组相互竞争系统资源或进行通信的进程间的“永久”阻塞。当一组进程中的每个进程都在等待某个事件(典型情况下是等待释放所请求的资源),而仅有这组进程中被阻塞的其他进程才可触发该事件时,就称这组进程发生了死锁。因为没有事件能够被触发,故死锁是永久性的。与并发进程管理中的其他问题不同,死锁问题并无有效的通用解决方案。

    6.1.1 可重用资源

    资源通常分为两类:可重用资源和可消耗资源。可重用资源是指一次仅供一个进程安全使用且不因使用而耗尽的资源。进程得到资源单元并使用后,会释放这些单元供其他进程再次使用。可重用资源的例子包括处理器、I/O通道、内存和外存、设备,以及诸如文件、数据库和信号量之类的数据结构。

    下面给出一个涉及可重用资源死锁的例子。考虑相互竞争的两个进程都要独占访问磁盘文件D和磁带设备T的情形。程序执行如图6.4所示的操作。每个进程都占用一个资源并请求另一个资源时,就会发生死锁;例如,多道程序设计系统交替地执行两个进程时会发生死锁,如下所示:

    这看起来更像程序设计错误而非操作系统设计人员的问题。由于并发程序设计非常具有挑战性,因此这类死锁的确会发生,而发生的原因通常隐藏在复杂的程序逻辑中,因此检测非常困难。处理这类死锁的一个策略是,给系统设计施加关于资源请求顺序的约束。

    可重用资源死锁的另一个例子是内存请求。假设可用的分配空间为200KB,且发生的请求序列如下所示: 两个进程都行进到它们的第二个请求时,就会发生死锁。因为对方都不释放内存,那么另外一个进程就无法申请到新的内存,如果不申请到新的内存,就不会继续执行以释放内存,这就是产生死锁的根本原因所在。 若事先并不知道请求的存储空间总量,则很难通过系统设计约束来处理这类死锁。解决这类特殊问题的最好办法是,使用虚存有效地消除这种可能性。

    在可重用资源上发生死锁是非常典型的现象。有的人可能对死锁的影响也只停留于于此,但是还有一种可消耗资源上的死锁,也很常见。

    6.1.2 可消耗资源

    可消耗资源是指可被创建(生产)和销毁(消耗)的资源。某种类型可消耗资源的数量通常没有限制,无阻塞生产进程可以创建任意数量的这类资源。消费进程得到一个资源时,该资源就不再存在。可消耗资源的例子有中断、信号、消息和I/O缓冲区中的信息。

    作为一个涉及可消耗资源死锁的例子,考虑下面的进程对,其中的每个进程都试图从另一个进程接收消息,然后再给那个进程发送一条消息: Receive阻塞(即接收进程被阻塞直到收到消息)时,发生死锁。同样,引发死锁的原因是一个设计错误。这类错误比较微妙,因而难以发现。此外,罕见的事件组合也会导致死锁,因此只有当程序使用了相当长的一段时间甚至几年后,才可能出现这类问题(即发生死锁)。

    不存在解决所有类型死锁的有效策略。表6.1概括了已有方法中最重要的那些方法的要素:检测、预防和避免。首先详细阐述死锁的条件,然后依次分析每种方法(现在看来这张表可能有些抽象,实际上这是下面关于解决死锁问题讨论的总结,可以先看下面的讨论,而不用纠结于这张表)。

    6.1.3 资源分配图

    表征进程资源分配的有效工具是Holt引入的资源分配图(resource allocation graph)。资源分配图是有向图,它说明了系统资源和进程的状态,其中每个资源和进程用节点表示。图中从进程指向资源的边表示进程请求资源但还未得到授权,如图6.5(a)所示。资源节点中,圆点表示资源的一个实例。I/O设备是有多个资源实例的资源类型,它由操作系统中的资源管理模块分配。图中从可重用资源节点中的点到一个进程的边表示请求已被授权,如图6.5(b)所示;也就是说,该进程已被安排了一个单位的资源。图中从可消耗资源节点中的点到一个进程的边表示进程是资源生产者。 图6.5(c)是一个死锁的例子。资源Ra和Rb都仅拥有一个单元的资源。进程P1持有Rb同时请求Ra;进程P2持有Ra同时请求Rb。图6.5(d)和图6.5(c)的拓扑结构相同,但图6.5(d)不会发生死锁,因为每个资源有多个实例。

    图6.6所示资源分配图的死锁情况与图6.1(b)类似。与6.5(c)不同的是,图6.6不是两个进程彼此拥有对方所需资源的简单情况,而是存在进程和资源的环,因此导致了死锁(但本质和图6.5(c)还是一样的)。

    6.1.4 死锁条件

    死锁有三个必要条件:

    互斥(独占性)。一次只有一个进程可以使用一个资源。其他进程不能访问已分配给其他进程的资源。占有且等待。当一个进程等待其他进程时,继续占有已分配的资源。不可抢占。不能强行抢占进程已占有的资源。

    在很多情况下,这些条件很容易满足。例如,要确保结果的一致性和数据库的完整性,互斥是非常有必要的。同理,不能随意地进行资源抢占。比如,在涉及数据资源时,必须提供回滚恢复机制(rollback recovery mechanism)来支持资源抢占,只有这样才能把进程及其资源恢复到以前的适当状态,使得进程最终可以重复其动作。前三个条件都只是死锁存在的必要条件而非充分条件。要产生死锁,还需要第四个条件:

    循环等待。存在一个闭合的进程链,每个进程至少占有此链中下一个进程所需的一个资源,如图6.5(c)和图6.6所示。

    第四个条件实际上是前三个条件的潜在结果,即假设前三个条件存在,那么可能发生的一系列事件会导致不可解的循环等待。这个不可解的循环等待实际上就是死锁的定义。条件4中列出的循环等待之所以不可解,是因为有前面三个条件的存在。因此,这四个条件一起构成了死锁的充分必要条件。

    6.2 死锁的预防

    简单地讲,死锁预防(deadlock prevention)策略是试图设计一种系统来排除发生死锁的可能性。死锁预防方法分为两类。一类是间接死锁预防方法,即防止前面列出的三个必要条件中的任何一个条件的发生(见6.4.1节到6.4.3节);另一类是直接死锁预防方法,即防止循环等待的发生(见6.4.4节)。下面具体分析与这4个条件相关的技术问题。

    6.2.1 互斥

    一般来说,在所列出的4个条件中,第一个条件不可能禁止。如果需要对资源进行互斥访问,那么操作系统就必须支持互斥。某些资源,如文件,可能允许多个读访问,但只能允许互斥的写访问,此时若有多个进程请求写权限,则也可能发生死锁。

    6.2.2 占有且等待

    为预防占有且等待的条件,可以要求进程一次性地请求所有需要的资源,并阻塞这个进程直到所有请求都同时满足(即:要不就不请求资源,请求就一下子全部请求,并一次全部得到,或者不得到)。这种方法有两个方面的低效性。首先,一个进程可能被阻塞很长时间,以等待满足其所有的资源请求。而实际上,只要有一部分资源,它就可以继续执行。其次,分配给一个进程的资源可能会在相当长的一段时间不会被该进程使用,且不能被其他进程使用。另一个问题是一个进程可能事先并不知道它所需要的所有资源。

    这也是应用程序在使用模块化程序设计或多线程结构时产生的实际问题。要同时请求所需的资源,应用程序需要知道其以后将在所有级别或所有模块中请求的所有资源。

    6.2.3 不可抢占

    预防不可抢战的方法有几种。首先,占有某些资源的一个进程进一步申请资源时若被拒绝,则该进程必须释放其最初占有的资源,必要时可再次申请这些资源和其他资源。其次,一个进程请求当前被另一个进程占有的一个资源时,操作系统可以抢占另一个进程,要求它释放资源。只有在任意两个进程的优先级都不同的条件下,后一种方案才能预防死锁。

    此外,只有在资源状态可以很容易地保存和恢复的情况下(就像处理器一样),这种方法才是实用的。

    6.3.4 循环等待

    循环等待条件可通过定义资源类型的线性顺序来预防。若一个进程已分配了R类型的资源,则其接下来请求的资源只能是那些排在R类型之后的资源。

    为证明这种策略的正确性,我们给每种资源类型指定一个下标。当i</时,资源R,排在资源R 前面。现在假设两个进程A和B死锁,原因是A获得R并请求R,而B获得R,并请求R,那么这个条件不可能,因为这意味着i<j且j<i。

    类似于占有且等待的预防方法,循环等待的预防方法可能是低效的,因此它会使进程执行速度变慢,且可能在没有必要的情况下拒绝资源访问。

    6.3 死锁避免

    解决死锁问题的另一种方法是死锁避免(deadlock avoidance),它和死锁预防的差别很小。在死锁预防(deadlock prevention)中,约束资源请求至少可破坏四个死锁条件中的一个条件。这可通过防止发生三个必要条件中的一个(互斥、占有且等待、非抢占)间接完成,也可通过防止循环等待直接完成,但都会导致低效的资源使用和低效的进程执行。死锁避免则相反,它允许三个必要条件,但通过明智地选择,可确保永远不会到达死锁点,因此死锁避免与死锁预防相比,可允许更多的并发。在死锁避免中,是否允许当前的资源分配请求是通过判断该请求是否可能导致死锁来决定的。因此,死锁避免需要知道未来进程资源请求的情况。

    本节给出了两种死锁避免方法:

    若一个进程的请求会导致死锁,则不启动该进程。若一个进程增加的资源请求会导致死锁,则不允许这一资源分配。

    关于这两点的进一步讨论显得有些无聊,如果感兴趣的话,可以直接去查看原书中的描述,如果做了解的话,到这一步已经足够了。下面的6.4.1死锁检测算法也是一样的。

    6.4 死锁检测

    死锁预防策略非常保守,它们通过限制访问资源和在进程上强加约束来解决死锁问题。死锁检测策略则完全相反,它不限制资源访问或约束进程行为。对于死锁检测(deadlock detection)来说,只要有可能,就会给进程分配其所请求的资源。操作系统周期性地执行一个算法来检测前面的条件(4),即图6.6中描述的循环等待条件。

    6.4.1 死锁检测算法

    死锁检测可以频繁地在每个资源请求发生时进行,也可进行得少一些,具体取决于发生死锁的可能性。在每次请求资源时检查死锁有两个优点:可以尽早地检测死锁情况;算法相对比较简单,因为这种方法基于系统状态的逐渐变化情况。然而,这种频繁的检测会耗费相当多的处理器时间。

    具体的检测算法不做说明,只要知道这种算法可以检测出来发生死锁即可。

    6.4.2 恢复

    检测到死锁后,就需要某种策略来恢复死锁。下面按复杂度递增的顺序列出可能的方法:

    取消所有的死锁进程。这是操作系统中最常采用的方法。把每个死锁进程回滚到前面定义的某些检查点,并重新启动所有进程。此时,要求在系统中构建回滚和重启机制。这种方法的风险是原来的死锁可能再次发生。但是,并发进程的不确定性通常能保证不会发生这种情况。连续取消死锁进程直到不再存在死锁。所选取消进程的顺序应基于某种最小代价原则。在每次取消后,必须重新调用检测算法,以测试是否仍存在死锁。连续抢占资源直到不再存在死锁。和(3)一样,需要使用一种基于代价的选择方法,且需要在每次抢占后重新调用检测算法。一个资源被抢占的进程必须回滚到获得这个资源之前的某一状态。

    对于(3)和(4),选择原则如采用如下之一:

    目前为止消耗的处理器时间最少。目前为止产生的输出最少。预计剩下的时间最长。目前为止分配的资源总量最少。优先级最低。

    在这些原则中,有些原则更易于测度。预计剩下的时间最值得怀疑。此外,除优先级测度外,相对于整个系统的代价而言,其他原则对用户而言没有任何代价。

    6.5 一种综合的死锁策略

    如表6.1所示,解决死锁的所有策略都各有优缺点。与其将操作系统机制设计为只采用其中的一种策略,不如在不同情况下使用不同的策略。[HOWA73]提出了一种方法:

    把资源分成几组不同的资源类。为预防在资源类之间由于循环等待产生死锁,可使用前面定义的线性排序策略。在一个资源类中,使用该类资源最适合的算法。

    作为这种技术的一个例子,我们考虑如下资源类:

    可交换空间:进程交换所用外存中的存储块。进程资源:可分配的设备,如磁带设备和文件。内存:可按页或按段分配给进程。内部资源:诸如I/O通道。

    前面给出的次序表明了资源分配的次序。考虑到一个进程在其生命周期中可能会遵循这样的顺序,因此这个次序是最合理的。在每一类中,可采用以下策略:

    可交换空间:要求一次性分配所有请求的资源来预防死锁,就像古有等待预防策略一样。若知道最大存储需求(通常情况下都知道),则这个策略是合理的,死锁避免也是可能的。进程资源:对于这类资源,死锁避免策略通常是很有效的,因为进程可以事先声明它们需要的这类资源。采用资源排序的预防策略也是可能的。内存:对于内存,基于抢占的预防是最适合的策略。当一个进程被抢占后,它仅被换到外存,释放空间以解决死锁。内部资源:可以使用基于资源排序的预防策略。

    6.6 哲学家就餐问题

    6.6.1 基于信号量的解决方案

    图6.12给出了基于信号量的解决方案。每位哲学家首先拿起左边的叉子,然后拿起右边的叉子。 在哲学家吃完面后,这两把叉子又被放回到餐桌上。这个解决方案会导致死锁:如果所有哲学家在同一时刻都感到饥饿,那么他们都会坐下来,都拿起左边的叉子,都伸手拿右边的叉子,但都没有拿到。在这种有损尊严的状态下,所有哲学家都会处于饥饿状态。

    为避免死锁的危险,可再买5把叉子(一种更卫生的解决方案)或教会哲学家仅用一把叉子吃面。另一种方法是,考虑增加一位服务员,他/她只允许4位哲学家同时进入餐厅,由于最多有4位哲学家就座,因而至少有一位哲学家可以拿到两把叉子。图6.13显示了这种方案,这里再次使用了信号量。这个方案不会发生死锁和饥饿。

    6.6.2 基于管程的解决方案

    图6.14给出了基于管程的哲学家就餐问题的解决方案。这种方案定义了一个含有5个条件变量的向量,每把又子对应一个条件变量。这些条件变量用来标示哲学家等待的叉子可用情况。另外,用一个布尔向量记录每把叉子的可用情况(true表示叉子可用)。管程包含了两个过程。get_forks函数表示哲学家取他/她左边和右边的叉子。如果至少有一把叉子不可用,那么哲学家进程就会在条件变量的队列中等待。这可让其他哲学家进程进入管程。release-forks函数用来标示两把叉子可用。注意,这种解决方案的结构和图6.12中的信号量解决方案相似。在这两种方案中,哲学家都是先取左边的叉子,然后取右边的叉子。与信号量不同的是,管程不会发生死锁,因为在同一时刻只有一个进程进入管程。比如,第一位哲学家进程进入管程保证了只要他拿起左边的叉子,其右边的哲学家可以拿到其左边的叉子之前(即这位哲学家右边的叉子),就一定可以拿到右边的叉子。

    6.7 小结

    死锁是指一组争用系统资源的进程或互相通信的进程被阻塞的现象。这种阻塞是永久性的,除非操作系统采取某些非常规行动,如杀死一个或多个进程,或强迫一个或多个进程进行回滚。死锁可能涉及可重用资源或可消耗资源。可重用资源是指不会因使用而被耗尽或销毁的资源,如I/O通道或一块内存区域。可消耗资源是指当被一个进程获得时就销毁的资源,这类资源的例子有消息和I/O缓冲区中的信息。

    处理死锁通常有三种方法:预防、检测和避免。死锁预防通过确保不满足死锁的一个必要条件来避免发生死锁。操作系统总是同意资源请求时,需要进行死锁检测。操作系统必须周期性地检查死锁,并采取行动打破死锁。死锁避免涉及分析新的资源请求,以确定它是否会导致死锁,且仅当不可能发生死锁时才同意该请求。

    在程序编写过程中也会经常会遇到死锁的情况,这就需要使用语言提供的相应的机制来打破,打破的方法,也就是从上面提到的方法中入手。作为参考,可以来看看java中是如何解决死锁问题的。 https://www.cnblogs.com/xiaoxi/p/8311034.html

    最新回复(0)