JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

資源配置圖與環

四個必要條件告訴你死結「何時可能」發生;資源配置圖則讓你「親眼看見」它。學會把「誰握著什麼、誰在等什麼」畫出來,並弄清楚這張圖裡的「環」究竟在什麼時候代表系統真的卡死了。

從四個條件到一張圖

在上一篇導覽裡,你學到死結需要四個必要條件同時成立,而其中最後一個——循環等待——正是把陷阱合攏的那一個:一串行程,每個都握著下一個所需要的東西,繞了一圈又繞回自己。光是讀程式碼,這種循環等待很難看出來。所以這篇導覽要給你一個能把它「看見」的工具:資源配置圖(resource-allocation graph),一張小小的圖,畫出「誰握著什麼、誰在等什麼」。一旦你會畫它,死結就不再是抽象的擔憂,而變成你能用手指真的描出來的東西。

回想這個階梯一開頭談過的系統資源模型:作業系統管理各種「資源類型」(一台印表機、一台磁帶機、一把鎖、一塊記憶體),而每種類型可能有一個或多個一模一樣的「實體(instance)」。一個行程提出對某資源的需求(request)、被授予一段時間(持有)、然後歸還(release)。這張圖不過是這套帳目在某一瞬間的誠實快照。把核心想成一位拿著夾板的大樓管理員:夾板上記著現在哪位住戶拿著哪把鑰匙、又有哪位住戶提了申請、正在櫃台前等著。資源配置圖就是把這塊夾板重畫成一堆點與箭頭。

點與箭頭:怎麼讀這張圖

這張圖只有兩種點和兩種箭頭,整套詞彙就這麼多。一個圓圈是一個行程。一個方框是一種資源類型,方框裡為該類型的每個實體畫一個小點——一台印表機就一個點,三台一模一樣的磁帶機就三個點。箭頭負責說故事。需求邊(request edge)從行程指向資源(這個行程正在要求、正在等待):把它畫成從圓圈指向方框的箭頭。配置邊(assignment edge)則從資源方框內某個特定的實體點指回某個行程(這個行程目前握著那個實體):把它畫成從方框裡的某個點指向圓圈的箭頭。

一次需求的自然節奏是這樣的。行程 P 先畫一條指向資源 R 的需求邊——一支從 P 指向 R 的箭頭——意思是「我想要一個 R,現在正在等。」當核心批准時,這條需求邊就翻轉過來:它變成一條從 R 裡某個實體點指回 P 的配置邊,意思是「P 現在握著這一個。」等到 P 用完、釋放這個資源,那條配置邊就被直接擦掉,把該實體釋放給正在等的人。所以任一瞬間,這張圖都是活的——隨著需求進來而長出箭頭、隨著批准而翻轉、隨著釋放而消失。而死結,就是當這幅活生生的圖再也無法改變時,我們所看見的東西。

Two processes, two single-instance resources (R1, R2):

   (P1) --- request ---> [ R2 . ]
    ^                       |
    |                   assignment
  assignment                |
    |                       v
   [ R1 . ] <--- request --- (P2)

P1 holds R1, and is waiting for R2.
P2 holds R2, and is waiting for R1.

Follow the arrows:  P1 -> R2 -> P2 -> R1 -> P1
That path comes back to where it started: a CYCLE.
Neither can proceed. This is a deadlock.
用資源配置圖畫出的經典死結:每個行程都握著一個資源(配置邊),又在要求對方握著的那一個(需求邊)。順著箭頭走,會繞回起點——一個環。

環在什麼時候才代表死結?

這裡有個關於這類圖最重要、也最常被誤解的事實,請你慢慢讀。如果每一種資源類型都恰好只有一個實體,那麼圖裡有環就代表有死結——句點。在單一實體的情況下,環既是必要、也是充分條件:找到一個迴圈,系統就卡死;沒有迴圈,系統就安全。上面那張圖正是這種情形,而它恰好對應到循環等待條件,因為那個環,其實就是「等待的循環鏈」被攤開來畫出來而已。

但一旦某種資源類型有好幾個實體,環就不再保證有死結——它只剩下「警訊」的身分,是必要條件、卻不是充分條件。為什麼?因為環有可能從外部被打破。假設迴圈是 P1 指向 R、R 指向 P2、P2 指向 S、S 又指回 P1——紙面上確實是個環。但如果資源類型 R 還有第二個實體,而那第二個實體被環外的某個行程 P3 握著,那麼當 P3 做完、釋放它手上那個 R 實體時,正在等的行程就能搶到它,它的需求邊翻轉成配置邊,環就被剪斷了。從頭到尾根本沒有人真的被卡住。所以在多實體的情況下:沒有環,就「絕對」沒有死結;但有環,只代表「也許」——你得湊近一點看。

更精簡的畫法:等待圖

當每個資源都是單一實體時,還有一種更精簡的畫法,乾脆把資源方框整個丟掉。把「P1 在等 R、R 被 P2 握著」這串東西,整個摺成一支箭頭「P1 在等 P2」。現在圖裡就只剩行程點、以及唯一一種箭頭了。這就是等待圖(wait-for graph),它正好就是把資源節點擠掉之後的資源配置圖。這裡有環的意思和先前一樣——代表死結——但圖更小、檢查更快,而這恰恰就是你之後會碰到的死結偵測機制偏好它的原因。

那麼,程式實際上要怎麼在這種圖裡找出一個環?它做的正是你用手做的事,只是更有條理:它沿著箭頭走。在有向圖裡偵測環,是個標準又便宜的操作——你可以沿著邊一路走下去,留意有沒有走到「目前這條路徑上已經造訪過」的節點。當然,核心不會真的去重畫圖;它把同樣的資訊存成「誰握著什麼」與「誰在等什麼」的表格,然後在這些表格上跑環檢查。底下就是這個搜尋,一步一步走給你看。

  1. 挑一個還沒造訪過的行程節點,開始走。把它標記為「在目前這條路徑上」。
  2. 順著這個行程往外的等待箭頭,走到它所等待的那個行程。
  3. 如果下一個行程已經被標記為「在目前這條路徑上」,代表你繞回到它了——存在一個環,而這些行程就陷入死結了。
  4. 如果它是全新的,就也把它標記為「在目前這條路徑上」,繼續順著它的箭頭走,並重複檢查。
  5. 如果你走到一個什麼都不等的行程(死路),就退回來、把這條路徑上的節點取消標記,再從另一個還沒造訪的節點重新走一遍。如果每一次走訪都在沒有重訪路徑節點的情況下結束,那就沒有環——因此也沒有死結。

打破環,就是打破一個條件

這張圖不只是診斷;它還悄悄告訴你解藥。要打破死結,你必須從環裡移除一條邊,而每一種移除邊的方式,都對應回「否定四個條件之一」。藉由強制把某個資源從某行程手上奪走,來擦掉一條配置邊——這就是否定不可搶佔。在某行程兩手空空之前,拒絕為它畫出新的需求邊——這攻擊的是持有並等待。讓所有行程都依照同一個固定的全域順序去要資源,好讓箭頭永遠不可能「往回」指、把迴圈合攏——這就是預防循環等待的排序技巧。每一招都是一把剪刀,對準同一個結上不同的那一股。

而同一張圖,也解釋了為什麼多數真實系統在執行時「什麼都不做」。在每一次資源需求上都去畫圖、重新掃描有沒有環,是要花時間的,而真正的死結在實務上很罕見。所以 Linux、Windows 之類的系統,通常走的是你之後會碰到的務實路線——偶爾才跑一次的偵測並復原,或乾脆無視這問題、在罕見的當機時重開機。就算核心從來沒有真的建出一張圖,這張圖仍然是思考死結的正確方式;它是「避免」策略與「偵測」策略背後共同的心智模型。下一篇,我們會從「畫出這張圖」轉向「一開始就別惹上麻煩」:靠銀行家演算法,讓系統維持在安全狀態。