挑戰408——操作系統(9)——進程的同步與互斥

操作系統中的併發進程有些是獨立的有些需要相互協作,獨立的進程在系統中執行不影響其他進程,也不被其他進程影響(因爲他們沒有共同需要一起用到的資源)。而另外一些進程則需要與其他進程共享數據,以完成一項共同的任務。
因此,爲了保證操作系統的正常活動,使得程序的執行具有可再現性,保證執行結果的正確性。操作系統必須爲這種協作的進程提供某種機制。
進程間的協作關係分爲:互斥,同步,通信。(實習面試的時候,有個考官問過我這個問題)

進程間的協作關係

  • 互斥是指多個進程不允許同時使用同一資源。當某個進程使用某種資源的時候,其他進程必須等待。所以資源不能被多個進程同時使用。在這種情況下,程序的執行與其他進程無關。
  • 同步是指多個進程中發生的事件存在某種先後順序。即某些進程的執行必須先於另一些進程(我們之前畫的前驅圖)。其任務是使得併發執行的諸多進程有效的共享資源和相互合作,從而使程序的執行具有可再現性。這種情況下,進程間接知道對方。
  • 通信是指多個進程間要傳遞一定的信息。這個時候進程直接得知對方。

臨界資源和臨界區

在計算機中,某段時間內只允許一個進程使用的資源稱爲臨界資源,比如打印機,共享變量等等。而每個進程訪問臨界資源的那段程序代碼稱爲臨界區。
顯然,幾個進程共享一個臨界資源,它們必須互斥使用。爲此每個進程在進入臨界區的時候,要對訪問的臨界資源進行檢查,看它是否正在被訪問,若是則不進入臨界區,否則進入臨界區,並設置標誌,表明我這個進程正在訪問臨界資源,這段代碼稱爲進入區。其餘的代碼稱爲剩餘區。
在這裏插入圖片描述

當然,不是所有的程序都必須等待資源的釋放,當滿足下面幾個條件的時候可以允許進入(具體的後面會解釋,感覺配合案例好理解):
1. 空閒讓進
2. 忙則等待
3. 有限等待
4. 讓權等待

這裏說說臨界資源與共享資源的區別*:
臨界資源是指某段時間內只允許一個進程使用的資源。(比如打印機)
共享資源是指某段時間內允許多個進程同時使用的資源。(比如磁盤,公用隊列等,可以多個進程同時讀取數據,但不能同時修改)
再注意一下臨界區的概念:每個進程訪問臨界資源的那段程序代碼稱爲臨界區。 注意:臨界區是進程的一段代碼,有n個進程就會有n個臨界區。

實現互斥的方法

實現互斥常用的方法有:軟件實現法,硬件實現法,pv信號量法。其中軟件實現方法雖然用的比較少,但是其算法的思想還是比較重要的,考試喜歡在選擇題中考。硬件的實現瞭解一下,考的比較多。而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執行。

所以這個方法實現了“空閒讓進”和"忙則等待"

硬件實現法

硬件的實現方法主要有兩種:禁止中斷和專用機器指令

  1. 禁止中斷:
    這個方法其實很簡單:
靜止中斷
/**臨界區**/
開中斷
/**剩餘區**/

這種方式主要用於單處理機,因爲單處理機中進程不能併發執行,所以只要保證一個進程不被中斷即可。
但是這樣子進程被限制只能交替進行。

  1. 專用機器指令(TS指令, swap指令)
    TS指令是指讀出指定標記後記爲true,設置一個bool變量lock,當lock爲true時,代表資源正在使用,反之false表示空閒:
    在這裏插入圖片描述
    當進程進入臨界區時,由於while循環的存在,於是不會主動放棄CPU。這種方法適用範圍廣,並且簡單,同時支持多個臨界區。

但是卻不能做到“讓權等待”,並且進程是隨機選擇的,可能造成某個進程飢餓。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章