挑戰408——作業系統(9)——程序的同步與互斥
作業系統中的併發程序有些是獨立的有些需要相互協作,獨立的程序在系統中執行不影響其他程序,也不被其他程序影響(因為他們沒有共同需要一起用到的資源)。而另外一些程序則需要與其他程序共享資料,以完成一項共同的任務。 因此,為了保證作業系統的正常活動,使得程式的執行具有可再現性,保證執行結果的正確性。作業系統必須為這種協作的程序提供某種機制。 程序間的協作關係分為:互斥,同步,通訊。(實習面試的時候,有個考官問過我這個問題)
程序間的協作關係
- 互斥。是指多個程序不允許同時使用同一資源。當某個程序使用某種資源的時候,其他程序必須等待。所以資源不能被多個程序同時使用。在這種情況下,程式的執行與其他程序無關。
- 同步。是指多個程序中發生的事件存在某種先後順序。即某些程序的執行必須先於另一些程序(我們之前畫的前驅圖)。其任務是使得併發執行的諸多程序有效的共享資源和相互合作,從而使程式的執行具有可再現性。這種情況下,程序間接知道對方。
- 通訊。是指多個程序間要傳遞一定的資訊。這個時候程序直接得知對方。
臨界資源和臨界區
在計算機中,某段時間內只允許一個程序使用的資源稱為臨界資源,比如印表機,共享變數等等。而每個程序訪問臨界資源的那段程式程式碼稱為臨界區。 顯然,幾個程序共享一個臨界資源,它們必須互斥使用。為此每個程序在進入臨界區的時候,要對訪問的臨界資源進行檢查,看它是否正在被訪問,若是則不進入臨界區,否則進入臨界區,並設定標誌,表明我這個程序正在訪問臨界資源,這段程式碼稱為進入區。其餘的程式碼稱為剩餘區。
當然,不是所有的程式都必須等待資源的釋放,當滿足下面幾個條件的時候可以允許進入(具體的後面會解釋,感覺配合案例好理解): 1. 空閒讓進 2. 忙則等待 3. 有限等待 4. 讓權等待
這裡說說臨界資源與共享資源的區別*: 臨界資源是指某段時間內只允許一個程序使用的資源。(比如印表機) 共享資源是指某段時間內允許多個程序同時使用的資源。(比如磁碟,公用佇列等,可以多個程序同時讀取資料,但不能同時修改) 再注意一下臨界區的概念:每個程序訪問臨界資源的那段程式程式碼稱為臨界區。 注意:臨界區是程序的一段程式碼,有n個程序就會有n個臨界區。
實現互斥的方法
實現互斥常用的方法有:軟體實現法,硬體實現法,pv訊號量法
軟體實現法
為了更好的理解思想,我們採用程式語言的思想去分析過程
單標誌法
先看下面的兩段程式碼:
/*程序p1*/ /*程序p2*/
while(turn != 0){ while(turn != 1){
什麼也不做 什麼也不做
} }
/**p1的臨界區**/ /**p2的臨界區**/
turn = 1; turn = 0;
/**剩餘區**/ /**剩餘區**/
當採用單標誌法的時候,我們設定公共的bool變數turn。對於程序P1而言,只有當turn =0的時候才允許它進入臨界區,當它退出的時候,將turn置1,這個時候才輪到P2進入,因此實現了兩個程序互斥進入臨界區,保證了任何時刻都至多隻有一個程序可以進入臨界區。 但是這種方法帶來了新的問題:這種做法強制程序輪流進入臨界區,而且這時候如果出現沒有程序在訪問臨界資源的話,資源空閒,但是可以進入。不能保證空閒讓進,也就是當資源空閒的時候,不能保證有程序進入。 舉個例子:程序p1進入臨界區並順利執行後離開,並將turn 置1,按照正常流程應該輪到p2執行,但是如果p2遲遲不來呢?(比如某些原因阻塞了),那麼臨界資源空閒,而turn = 1一直成立,導致其他需要使用這個資源的程序必須等待。 所以不能做到空閒讓進。
雙標誌先檢查法
那麼既然前一種方式不能做到空閒讓進,那麼我們就設法克服這一點,看下面的程式碼:
/*程序p1*/ /*程序p2*/
while(flag[1]){ //(1) while(flag[0]){//(2)
什麼也不做 什麼也不做
} }
flag[0] = true; //(3) flag[1] = true;//(4)
/**p1的臨界區**/ /**p2的臨界區**/
flag[0] = false; flag[1] = false;
/**剩餘區**/ /**剩餘區**/
這樣想,既然我們可以用一個標誌位保證程序間只能允許一個程序訪問臨界資源,那麼我們是否也可以再採用一個標誌位來保證空閒讓進呢? 我們設定一個BOOL型別的陣列flag[2],初始值為false,用來表示一開始時所有程序都未進入臨界區,若flag[0] = true,則表示p1允許進入臨界區並執行。 與(1)不同,在進入之前,檢查是否有程序在使用該資源。如果有,那麼就等待,如果沒有就進入。 我們來分上面兩個程序:
while(flag[1]) //如果此刻p2正在使用臨界資源,那麼等待
flag[0] = true;//否則,將flag[0]置為true,進入臨界區,也表明這個資源我在用。
flag[0] = false;//離開臨界區,表明這個資源我用完了或者沒在用
//再來看看P2:
while(flag[0]) //如果此時p1正在使用臨界資源,那麼等待
同上
這樣程序在使用之前都檢查一下是否有其他程序在使用臨界資源,沒有就進入,所以保證了空閒讓進。 潛在的問題:由於程序是併發執行的,所以執行的步驟或者說推進的速度都是不一致的,如果推進的速度是這樣的:(1)->(2)->(3)->(4),那麼(1)(2)步驟一開始都是false,繼續執行(3)(4),發現這個時候flag[2]全部都是true,也就是雙方的while迴圈都是對的,因此同時進入臨界區。這樣就違背了“忙則等待”。即有程序在訪問臨界資源的時候,其他程序必須等待。
雙標誌,先修改後檢查
同樣的,先看看方法(2)的問題所在,原因就是它先進行檢查,卻忽略了程序執行速度對檢查的影響。那麼我們在檢查之前進修改一下呢?
/*程序p1*/ /*程序p2*/
flag[0 ] = true; flag[1] = true;
while(flag[1]){ while(flag[0]){
什麼也不做 什麼也不做
} }
flag[0] = true; flag[1] = true;
/**p1的臨界區**/ /**p2的臨界區**/
flag[0] = false; flag[1] = false;
/**剩餘區**/ /**剩餘區**/
其實,對比上面的方法(2),只是先執行了賦值操作,我們分析一下: flag[0 ] = true; //P1想進入臨界區 while(flag[1]) //於是檢查一下p2是否在使用臨界資源,如果是,那麼什麼都不做 flag[0] = false;//否則進入臨界區,並且離開的時候,置為false,表明資源使用完畢。這樣,在獲得進入臨界區的權利後,準備進入之前,看看有沒有程序也想進入,有的話就讓給對方。克服了兩個程序同時進入臨界區的問題 但是,又考慮一種極端的情況: 假設P1和P2都同時想進入臨界區,併發執行的順序同方法二,那麼都將自己置為true,此時while迴圈執行,發現對方也想進入,於是相互謙讓,導致誰都訪問不了這個資源,產生飢餓現象。
Peterson’s演算法
也稱為先修改,後檢查,後修改者等待演算法。很拗口,但是理解起來不難,該演算法可以看錯做是方法(1)(3)的結合。用方法一中的turn標誌實現臨界資源的訪問,用(3)的雙標誌個修改來維護臨界區進入準則。
/*程序p1*/ /*程序p2*/
flag[0 ] = true; flag[1] = true;
turn = 1; turn = 0;
while(flag[1]&&turn ==1){ while(flag[0]&&turn ==0){
什麼也不做 什麼也不做
} }
/**p1的臨界區**/ /**p2的臨界區**/
flag[0] = false; flag[1] = false;
/**剩餘區**/ /**剩餘區**/
演算法通過修改同一標誌turn來描述標誌修改的前後。我們同樣分析一下p1的程序: flag[0 ] = true; //表明P1想進入臨界區 turn = 1;//設定標誌位為1, while(flag[1]&&turn ==1) //如果p2想用,並且p1的推進速度較慢,那麼讓出給p2. flag[0] = false //否則進入臨界區,並且離開的時候,置為false,表明資源使用完畢
為什麼turn的值會受推進速度的影響?我們同樣考慮之前的極端情況,按照順序(1)(2)(3)(4),turn的值先從1,再變為0,說明P2的賦值語句turn = 0對比P1的賦值語句 turn = 1執行的較晚,根據後修改的程序等待的原則,這個時候給P1執行。
所以這個方法實現了“空閒讓進”和"忙則等待"
硬體實現法
硬體的實現方法主要有兩種:禁止中斷和專用機器指令
- 禁止中斷: 這個方法其實很簡單:
靜止中斷
/**臨界區**/
開中斷
/**剩餘區**/
這種方式主要用於單處理機,因為單處理機中程序不能併發執行,所以只要保證一個程序不被中斷即可。 但是這樣子程序被限制只能交替進行。
- 專用機器指令(TS指令, swap指令) TS指令是指讀出指定標記後記為true,設定一個bool變數lock,當lock為true時,代表資源正在使用,反之false表示空閒: 當程序進入臨界區時,由於while迴圈的存在,於是不會主動放棄CPU。這種方法適用範圍廣,並且簡單,同時支援多個臨界區。
但是卻不能做到“讓權等待”,並且程序是隨機選擇的,可能造成某個程序飢餓。