死結迴避 (Deadlock Avoidance)
本系列文章內容參考自經典教材 Operating System Concepts, 10th Edition (Silberschatz, Galvin, Gagne)。本文對應章節:Section 8.6 Deadlock Avoidance。
前幾節介紹了兩種主動防範死結的方式,並深入討論了其中一種:死結預防(Deadlock Prevention),透過結構性約束使四個必要條件中的至少一個在系統設計階段就被永久消除。然而,預防的代價是犧牲資源使用率,或讓程式設計的彈性受到顯著限制。
本節介紹另一種方式:死結迴避(Deadlock Avoidance)。迴避的思路截然不同。系統不從結構上消除死結的必要條件,而是在執行期間動態判斷每一次資源請求是否安全。它要求執行緒在進入系統前,事先聲明(Declare) 自己在整個執行過程中對每種資源可能需要的最大數量,OS 再根據這份額外資訊,在資源請求發生的當下決定是立即授予還是讓執行緒等待,確保系統永遠不會進入可能導致死結的狀態。
8.6.1 安全狀態 (Safe State)
安全狀態的定義
迴避演算法的一切判斷都以「安全狀態」為核心。在理解定義之前,先建立直覺:想像系統中有若干執行緒,每個執行緒的剩餘資源需求都已知。如果存在某種為每個執行緒分配資源的順序,使得每個執行緒都能在最終取得所需資源、完成任務並釋放資源,那麼系統就是安全的。
正式定義: 若系統可以依照某種順序依序為每個執行緒分配資源(直到達到其最大需求量)並讓其完成任務,而不發生死結,則稱系統處於安全狀態(Safe State)。
這個「某種順序」就是安全序列(Safe Sequence)。
安全序列的核心概念是一種連鎖(Chain) 關係,可以用接力賽來理解:T₁ 先取得資源、完成任務、歸還資源;T₂ 再接棒,從「剩餘可用資源 + T₁ 剛歸還的資源」中取得所需,完成後再歸還;T₃ 又從更多的可用資源中取得所需……依此類推,直到所有執行緒都完成為止。
下圖呈現這個連鎖結構,其中 W 代表系統在每個階段的可用資源總量:
圖示說明:
- W₀:系統目前的可用資源(Available)。
- 每個 Tᵢ 的完成條件:剩餘需求 Needᵢ 不超過輪到它時的可用資源 Wᵢ₋₁。
- W 的累積:每當一個執行緒完成,它持有的資源全數歸還,使下一棒的可用資源比上一棒更多。
有一個容易誤解的細節:Tᵢ 不必在「現在」就能立刻取得資源。如果暫時資源不足,Tᵢ 可以等待排在它前面的 T₁…Tᵢ₋₁ 依序完成並釋放資源後再繼續,只要最終一定能拿到即可。安全序列保證的是「每個執行緒最終都一定有辦法完成」,而不是「每個執行緒現在馬上就能執行」。
正式定義:⟨T₁, T₂, ..., Tₙ⟩ 是安全序列,若且唯若每個 Tᵢ 的剩餘需求(Needᵢ)都能被以下總和滿足:
若系統找不到任何一條這樣的序列,系統便處於不安全狀態(Unsafe State)。
安全、不安全、死結三者的關係
下圖呈現三種狀態的包含關係:
從圖中可以得出幾個重要結論:
- 安全狀態一定不是死結狀態:若系統能找到安全序列,就代表每個執行緒最終都能完成,死結不可能發生。
- 死結狀態一定是不安全狀態:一旦陷入死結,找不到任何讓所有執行緒推進的序列。
- 不安全狀態不一定是死結狀態:不安全只代表「目前沒有已知的安全序列」,執行緒的實際行為可能讓系統恢復,但 OS 無法保證這一點。
這個包含關係揭示了迴避演算法的核心策略:只要系統始終維持在安全狀態,死結就絕不可能發生。因此,每當執行緒請求資源時,OS 先「模擬」授予,若模擬後系統仍處於安全狀態則授予,否則讓執行緒等待。
安全與不安全狀態的具體範例
以下範例具體說明安全狀態如何在一次錯誤的資源分配後轉變為不安全狀態。
假設系統共有 12 個相同資源,以及 3 個執行緒 T₀、T₁、T₂,各自的最大需求(Maximum Needs)與當前分配量(Current Allocation)如下:
| 執行緒 | Maximum Needs | Current Allocation | 仍需(Need = Max − Alloc) |
|---|---|---|---|
| T₀ | 10 | 5 | 5 |
| T₁ | 4 | 2 | 2 |
| T₂ | 9 | 2 | 7 |
已分配總量:5 + 2 + 2 = 9,剩餘可用資源:12 − 9 = 3。
時間點 t₀:安全狀態
檢驗序列 ⟨T₁, T₀, T₂⟩ 是否為安全序列:
- T₁:Need = 2 ≤ Available = 3,可立即取得所需資源。T₁ 完成後釋放 2 個資源,Available 變為 5。
- T₀:Need = 5 ≤ Available = 5,可取得所需資源。T₀ 完成後釋放 5 個資源,Available 變為 10。
- T₂:Need = 7 ≤ Available = 10,可取得所需資源。T₂ 完成後釋放 2 個資源,Available 回到 12。
序列 ⟨T₁, T₀, T₂⟩ 滿足安全條件,系統在 t₀ 處於安全狀態。
時間點 t₁:不安全狀態
若此時 T₂ 多請求一個資源(從 2 個增加到 3 個),系統即進入不安全狀態:
| 執行緒 | Current Allocation | Need |
|---|---|---|
| T₀ | 5 | 5 |
| T₁ | 2 | 2 |
| T₂ | 3 | 6 |
可用資源:12 − 10 = 2。
只有 T₁(Need = 2 ≤ Available = 2)可以立即推進。T₁ 完成後,Available 變為 4。此時:
- T₀ 需要 5 個 資源,但只有 4 個可用,必須等待。
- T₂ 需要 6 個資源,但只有 4 個可用,必須等待。
系統無法繼續讓任何執行緒推進,陷入死結。犯錯的時機是允許 T₂ 多拿一個資源。若當時讓 T₂ 等待直到 T₀ 或 T₁ 完成,死結就不會發生。
在迴避方案下,即使一個資源當下是閒置可用的,OS 也可能拒絕立即分配,強制執行緒等待,因為立即分配會讓系統進入不安全狀態。這意味著資源使用率可能比沒有迴避機制時更低,是迴避演算法在實務上的主要代價。
8.6.2 資源分配圖演算法 (Resource-Allocation-Graph Algorithm)
在每種資源類型只有一個實例(Single Instance) 的系統中,可以使用一種基於資源分配圖(Resource-Allocation Graph)的迴避演算法。
聲索邊 (Claim Edge) 的引入
在標準資源分配圖中,只有兩種邊:請求邊(Request Edge,T→R)和分配邊(Assignment Edge,R→T)。為了實現迴避,加入第三種邊:聲索邊(Claim Edge)。
聲索邊 Tᵢ → Rⱼ 表示執行緒 Tᵢ 在未來的某個時間點可能會請求資源 Rⱼ。它在圖中以虛線(Dashed Line) 表示,外觀類似請求邊但尚未成真。
聲索邊的生命週期如下:
聲索邊有一個重要限制:執行緒在開始執行之前,其所有可能用到的資源的聲索邊必須已經出現在圖中。也就是說,執行緒必須在啟動前預先聲明最大需求,不能在執行中途才宣告新的聲索。
演算法核心:請求前的循環檢測
當執行緒 Tᵢ 請求資源 Rⱼ 時,系統的判斷流程如下:
- 將 Tᵢ → Rⱼ 的聲索邊(虛線)在圖中臨時轉換為分配邊 Rⱼ → Tᵢ(假設已授予)。
- 對轉換後的圖執行循環偵測演算法(Cycle-Detection Algorithm),時間複雜度為 O(n²),n 為執行緒數量。
- 若沒有循環:授予請求,系統維持安全狀態。
- 若發現循環:拒絕授予,Tᵢ 必須等待,系統恢復原圖。
注意,循環偵測時聲索邊也參與計算,這正是迴避演算法能在死結實際發生前就預測 風險的關鍵。
具體圖例說明
以下圖呈現一個初始的安全狀態:
圖中各邊的含義:
- R1 → T1(綠色實線,分配邊):R1 已被分配給 T1,T1 目前持有 R1。
- T2 → R1(紅色實線,請求邊):T2 正在等待 R1,但 R1 被 T1 持有中。
- T1 → R2(紫色虛線,聲索邊):T1 在未來某個時間點可能會請求 R2。
- T2 → R2(紫色虛線,聲索邊):T2 在未來某個時間點也可能會請求 R2。
此時系統安全:R2 目前空閒,T1 和 T2 都只有聲索邊指向它(代表「未來可能要」),尚無衝突。
現在假設 T2 請求 R2(R2 目前是空閒的)。直覺上 R2 可用,應該直接分配,但系統先模擬「若授予會發生什麼」:
若授予 T2,T2 的聲索邊 T2→R2 轉換為分配邊 R2→T2(綠色),形成下圖的狀態:
圖中出現一個循環:
T2 → R1 → T1 → R2 → T2
這條循環是怎麼形成的?關鍵在於 T1 對 R2 的聲索邊。循環偵測時,聲索邊也被視為潛在路徑納入計算,因此形成了這條循環:
| 循環路徑段 | 邊的類型 | 含義 |
|---|---|---|
| T2 → R1 | 請求邊(紅) | T2 正在等待 R1(T1 持有) |
| R1 → T1 | 分配邊(綠) | R1 目前分配給 T1 |
| T1 → R2 | 聲索邊(紫虛) | T1 將來可能請求 R2 |
| R2 → T2 | 分配邊(綠) | R2 被分配給 T2(新增的邊) |
分配 R2 給 T2 後,系統進入不安全狀態,代表在某些未來的執行路徑上,死結無法避免。具體場景如下:
假設 T1 繼續執行,並在某個時間點真的對 R2 提出請求(聲索邊變為真實請求):
- T2 目前的狀態:持有 R2,正在等待 R1(被 T1 持有),無法繼續。
- T1 目前的狀態:持有 R1,執行到需要 R2 的地方,發出請求。
- R2 被 T2 持有,T1 只好等待 T2 釋放 R2。
- T2 需要 R1 才能完成並釋放 R2,但 R1 被 T1 持有,T1 正在等 R2。
- 結果:T1 等 T2 釋放 R2,T2 等 T1 釋放 R1,兩者互相等待,永遠無法推進。
正是因為迴避演算法預見了這條路徑,它在 T2 請求 R2 的當下就拒絕授予,讓系統永遠不進入這個狀態。
因此,系統判定授予 T2 的請求會讓系統進入不安全狀態,拒絕此次請求,T2 必須繼續等待,直到系統狀態變化(例如 T1 完成並釋放 R1 後,T2 可先取得 R1,再取得 R2)。
這個演算法只適用於每種資源類型恰好有一個實例的系統。當資源有多個實例時,圖中可能不存在循環但系統仍處於不安全狀態(這個問題在 8.3 節的 RAG 部分已探討過)。對於多實例資源,必須使用下一節介紹的銀行家演算法。