1. 由來:
從簡單的 Process 定義開始,所謂的 Process 其實可以看作正在執行中的程式碼。在作業系統中,同時執行的 Process 通常不只一個,Process 可以大致分成兩類。
1. 獨立(independent process):
這類的 Process 可以想做是獨行俠,他們彼此之間各自做各自的,互相不干擾,所以基本上只要作業系統可以確保 Process 可以正常執行,不會因為切換 Process 而導致一些問題的話,不會有什麼狀況發生。
2. 合作(cooperating process):
第二種我們稱作合作 Process,如果一個 Process 可以影響別人,或是可能被其他 Process 影響,我們就稱這些 Process 為 合作 Process。這時候就出現一些問題了,比方說有 word 要列印,印表機必須要等到 word 都把資料傳完才能開始列印。顯而易見的兩個問題是,一,資料要傳到哪?這裏顯然需要 Process 之間的溝通。二,如何確保印表機不會讀到舊的資料?
如何讓兩個 Process 之間溝通並傳資料常見的方法有,Share Memory(分享記憶體)及 Message Passing(訊息傳遞),本篇主要探討的是 Share Memory 的部分,並討論其衍生的 Race Condition 問題以及常見的三種解法討論。
2. Share Memory:
所謂的 Share Memory 代表的是如果兩個 Process 需要溝通,則他們跟老大哥作業系統要一塊共同的記憶體空間,並透過這塊共同擁有的記憶體空間溝通。
如上圖所示, Process A 先把想要給 Process B 的資料,放進 Shared Memory 中,之後 Process B 再去拿出來使用。一切看似很完美,卻還是藏著一些問題,也就是 Process B 如何知道資料是最新的?如何可以確保 Process B 不會拿到改到一半的資料?更複雜一點來說,要是兩個 Process 同時對同一個變數進行更改會怎樣?如下圖,兩個 Process 同時要對 count 加一,會如何?
這時候就會出現一個奇妙的狀況,程式最後執行的結果會跟程式之間運行的順序有關,以下面這個例子來看,我們假定 count 一開始為 4 ,並根據下面的順序執行。
count 最後結果是 5,而正確結果卻應該是 6 才對。
這種因為執行順序不同而導致結果不同的現象稱作 Race Condition,也是我們今天要討論的問題。
3. Race Condition:
講了這麼多,現在終於要進入今天的主角 Race Condition 了,仔細觀察剛剛的例子會發現,會出現 Race Condition 現象主要是因為放在 Share Memory 中的共用變數被同時存取了。換句話說,要是我們可以限制共用變數一次只能被一個 Process 所使用,Race Condition 就迎刃而解了。
那要如何讓共用變數一次只被一個 Process 所使用呢?主要有兩種方法:
1. Disable Interrupt (停止使用中斷):
即使現在的作業系統看起來都可以同時執行好多程式,但實際上作業系統還是只能一次執行一個程式,只是透過中斷、排班等技術快速在不同程式間切換。所以只要作業系統不要因為需要處理某些突發事件而中斷現在的 Process 並切換到其他 Process,這樣就不會有機會讓其他 Process 同時存取到同一個變數了。如下圖這般設計:
乍看之下很像很完美,但其實有很多缺陷,比方說這個方法不太適用於多核心處理器,因為只停用某一顆 CPU 的中斷沒意義,但全部停用,花費的代價太高,再者停用中斷其實很危險,如果有很緊急的狀況出現就不能馬上處理。
2. Critical Section Design (臨界區間設計):
所謂的臨界區間就是就是會對共用變數存取的程式碼區域。而設計臨界區間,主要就是設計進去的條件跟出來的條件解除,使其可以保證在離開臨界區間之前,即使換其他 Process 執行也不能進去存取同樣變數的臨界區間去存取共用變數。
大體上臨界區間會長這樣,首先會有一個區域來判斷 Process 可不可以進去,要離開的時候再把進入條件更改,讓其他想進來的人可以進來。
如果以我們之前的例子來看,我們就是要把更改 count 這行當成臨界區間,所以在之前要先判斷是否可以進去,離開時也要給予其他 Process 機會。
4. Critical Section:
再來就是 Critical Section Design 所需要的要求了,要達成一次只有一個程式進去,且不會造成大家都進不去需要滿足三個條件,分別是 Mutual Exclusion、Progress 以及 Bounded Waiting。
1. Mutual Exclusion:
這也是臨界區間的最基本要求,一次最多只能有一個 Process 在裡面存取共用變數。
2. Progress:
Progress 有兩個要求。
+ 不想進入臨界區間的 Process 不可以阻礙其它 Process 進入
(即:不可參與進入臨界區間的決策過程)
+ 必須在有限的時間內,自那些想進入的 Process 中,挑選出一個 Process 進入。
(即:不可以出現 DeadLock)
3. Bounded Waiting:
當 Process 申請要進入臨界區間到真正進入這段等待時間是有限的,即:若有n個 Processes 想進入,則任一 Process 至多等待n-1次,即可進入。
臨界區間設計有很多種方式,如透過演算法設計的方式,或是硬體保證不會被中斷的進入判斷,還有高階資料結構的 Semaphore、Monitor 等,本篇主要集中在兩個 Processes 的軟體演算法設計的三種著名方法。雖然其中有兩種是錯誤的,且最後一種在現今的架構之下也未必能保證正確,但可由這三個例子明白 Critical Section Design 所需要的三個要求的重要性及其精神。
5. 軟體演算法:
以下三個演算法皆是討論只有兩個 Processes Pi,Pj 的情況之下要如何設計,其中展現出來的 Pseudo Code 都是代表Pi 的。
讓我們來分析三個條件是否有滿足:
1. Mutual Exclusion:
滿足,因為 turn 只能是 i 或 j 所以一次最多只能有一個 Process 進去臨界區間。
2. Progress:
不滿足,假設 turn 的初始值是 i,但是 Pi 完全沒有想要進去,換句話說沒有機會把 turn 改成 j,這時如果Pj 想進去就沒辦法進去。
3. Bounded Waiting:
滿足,因為每一個 Process 在離開之時會把 turn 設成另外一個的,相當於把優先權讓出去,即使馬上想要在進入,也只能等待另外一個 Process 出來把 turn 改過後才能進去。換句話說,每一個 Process 至多等待一次就可以進入。
同樣的,來分析看看三個條件:
1. Mutual Exclusion:
滿足,因為在進去之前就會把自己的 flag 改成 true 所以其他人無法進入。
2. Progress:
不滿足,如下面這個順序執行,則最終大家都進不去。
3. Bounded Waiting:
滿足,因為是讓別人先進去的概念,所以如果我出來之後想馬上再進去,要先讓之前在等待的先進去。換句話說,每一個 Process 至多等待一次就可以進入。
分析看看三個條件,就能明白這個方法可以保證臨界區間正確:
1. Mutual Exclusion:
滿足,若 Pi 與 Pj 皆想進入自已的臨界區間,代表 flag[i] = flag[j] = true,而且會分別執行到 turn = i 及 turn = j 之設定 (只是先後次序不同)。因此,turn 値僅會是 i 或 j,絶不會兩者皆是。 ∴ Mutual Exclusion得以確保。
2. Progress:
滿足
+ 若 Pi 不想進入臨界區間,則表示 flag[i] = false。此時若 Pj 想進入自已的臨界區間,必可進入,而不會被 Pi 阻礙。
+ 若 Pi 與 Pj 皆想進入自已的臨界區間,則在有限的時間內,Pi 與 Pj 會分別執行到 turn = i 及 turn = j 之設定 (只是先後次序不同),turn値必為 i 或 j。∴ Pi (或 Pj ) 會在有限的時間內進入自已的臨界區間。
3. Bounded Waiting:
滿足,假設兩個 Process 同時都想進入自己的臨界區間,如果最後是Pi 順利進入而Pj 等待。假如 Pi 在執行完臨界區間之後想馬上再進去,但因為在進入前會把 turn 設成 j 且因為 Pj 在等待中而無法進入,相當於無法搶先 Pj,換句話說,每一個 Process 至多等待一次就可以進入。
其實第三個演算法叫 Peterson’s algorithm (或 Peterson’s solution) 是一個純粹靠軟體而不使用硬體提供的 Test and Set 或 Swap 來實現臨界區間設計的方法,此方法是 1981 年由 Gary L. Peterson 提出的。
最後,其實臨界區間設計還有很多方法,比方硬體保證不會被中斷的驗證指令(Test and Set 或 Swap),或是可以減輕程式員負擔的高階資料結構 Semaphore 或 Monitor 等,這些之後有機會會在撰文說明及討論。
6. Reference:
1. 許多圖跟例子來自杰哥作業系統數位教室
2. wikipedia