操作系统-并发:死锁和饥饿
本章介紹并發處理中需要解決的兩個問題:死鎖和饑餓。本章首先討論死鎖的基本原理和饑餓的相關問題;接著分析處理死鎖的三種常用方法:預防、檢測和避免;然后考慮用于說明同步和死鎖的一個經典問題;哲學家就餐問題。
作為對全局知識的把控,這里給出學習目標,作為參考:
- 列舉并解釋死鎖產生的條件
- 定義死鎖預防,針對死鎖產生的條件給出死鎖預防的策略
- 理解死鎖預防與死鎖避免的區別
- 掌握死鎖避免的兩種方法
- 理解死鎖檢測與死鎖預防、死鎖檢測與死鎖避免的本質區別
- 掌握設計綜合死鎖解決策略的方法
- 分析哲學家就餐問題
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)來支持資源搶占,只有這樣才能把進程及其資源恢復到以前的適當狀態,使得進程最終可以重復其動作。前三個條件都只是死鎖存在的必要條件而非充分條件。要產生死鎖,還需要第四個條件:
第四個條件實際上是前三個條件的潛在結果,即假設前三個條件存在,那么可能發生的一系列事件會導致不可解的循環等待。這個不可解的循環等待實際上就是死鎖的定義。條件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)和(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
總結
以上是生活随笔為你收集整理的操作系统-并发:死锁和饥饿的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 操作系统-并发性:互斥与同步
- 下一篇: Java Web - Ajax技术