死鎖偵測 (Deadlock Detection)
本系列文章內容參考自經典教材 Operating System Concepts, 10th Edition (Silberschatz, Galvin, Gagne)。本文對應章節:Section 8.7 Deadlock Detection。
為什麼需要偵測?
前兩篇分別介紹了死鎖預防(Prevention)與死鎖規避(Avoidance)。這兩種做法都在死鎖發生之前介入,透過限制資源請求或拒絕可能導致不安全狀態的分配,來確保死鎖永遠不會出現。
但代價是明顯的: 預防必須永久放棄其中一個必要條件(例如強制所有 Thread 一次申請全部資源),規避則要求每個 Thread 事先宣告最大需求量(Maximum Claim),且所有配置都要通過安全性演算法的篩選。這些限制在很多實際系統中過於嚴苛,導致資源使用效率低落。
如果系統不採用預防或規避機制,死鎖就可能在執行期間悄悄出現。面對這種情況,系統需要提供兩樣東西:
- 偵測演算法:定期檢查系統狀態,判斷是否已發生死鎖,以及哪些 Thread 陷入了死鎖
- 恢復機制:一旦確認死鎖存在,採取行動打破循環等待,讓系統得以繼續運作
值得注意的是,偵測加恢復這套機制本身也有成本:除了維護額外資料結構與定期執行演算法的運行時開銷之外,恢復過程本身(例如終止 Thread、回滾執行狀態)也可能造成部分計算成果的損失。
8.7.1 單一實例資源:等待圖 (Wait-for Graph)
當系統中每種資源類型都只有一個實例時,可以使用一種稱為等待圖(Wait-for Graph) 的資料結構來偵測死鎖。
從資源分配圖推導等待圖
等待圖由資源分配圖(Resource-Allocation Graph) 衍生而來。推導方式很直接:把資源分配圖中所有代表資源類型的節點(方塊)移除,再把通過資源節點相連的兩條邊「收合」成一條直接連接 Thread 的邊。
具體規則是:若資源分配圖中存在兩條邊 Ti → Rq(Ti 請求 Rq)與 Rq → Tj(Rq 已分配給 Tj),則在等待圖中新增一條邊 Ti → Tj,代表 Ti 正在等待 Tj 釋放它所需的資源。
下圖呈現了資源分配圖與對應等待圖的並排比較:
圖中各元素的含義:
- (a)資源分配圖:圓形節點為 Thread,方形節點為資源(方形內的實心圓點代表一個資源實例)。紅色虛線箭頭(T→R)是請求邊,綠色實線箭頭(R→T)是分配邊。整張圖描述了五個 Thread(T1–T5)各自持有與等待的資源:T5 持有 R3、R4 並等待 R1;T1 持有 R1 並等待 R2;T4 持有 R2 並等待 R5;T2 持有 R5 並等待 R3;T3 等待 R4。
- (b)等待圖:移除所有資源節點後,每對「Ti → Rq → Tj」的間接連線直接收合成「Ti → Tj」。圖中可見一個明確的環狀結構:T5 → T1 → T4 → T2 → T5,這個 4 節點的循環就是死鎖的所在。T3 → T5 則代表 T3 也在等待死鎖鏈中的 T5,因此 T3 雖不在循環內,卻同樣無法繼續執行。
這張圖最核心的洞察是:等待圖中只要存在一個環(Cycle),系統就處於死鎖狀態。在單一實例資源的情況下,等待圖中有環等價於死鎖。
偵測演算法的運作
為了持續偵測死鎖,系統必須:
- 維護等待圖:在 Thread 申請資源或資源被釋放時即時更新圖的結構
- 定期執行環偵測(Cycle Detection)演算法:搜尋等待圖中是否存在環
在等待圖中搜尋環需要 O(n²) 次運算,其中 n 是圖中節點數(即 Thread 數量)。
Linux 的 BCC(BPF Compiler Collection)工具集提供了一個名為 deadlock_detector 的工具,能夠偵測使用 Pthreads mutex lock 的 User Process 中潛在的死鎖。
其運作原理是在 pthread_mutex_lock() 與 pthread_mutex_unlock() 函式上插入探針(Probe)。每當目標 Process 呼叫這兩個函式時,工具就在記憶體中即時維護該 Process 的 mutex 等待圖,一旦偵測到環就立即回報可能存在的死鎖。
這是一種動態偵測的實作範例,不需要修改應用程式原始碼,直接在執行期間觀察 Thread 的鎖定行為。
8.7.2 多實例資源:偵測演算法
當資源類型具有多個實例時,等待圖不再適用,因為一個資源節點可以同時分配給多個 Thread,單純移除資源節點會丟失重要的數量資訊。此時需要一套更完整的偵測演算法,其結構與 Banker's Algorithm 的安全性檢查非常相似,但目的不同:這裡不是預測未來,而是判斷當前系統是否已陷入死鎖。