用訊號量解決程式的同步與互斥探討

發表於2015-05-07

現代作業系統採用多道程式設計機制,多個程式可以併發執行,CPU在程式之間來回切換,共享某些資源,提高了資源的利用率,但這也使得處理併發執行的多個程式之間的衝突和相互制約關係成為了一道難題。如果對併發程式的排程不當,則可能會出現執行結果與切換時間有關的情況,令結果不可再現,影響系統的效率和正確性,嚴重時還會使系統直接崩潰。就比如你只有一臺印表機,有兩個程式都需要列印檔案,如果直接讓他們簡單地併發訪問印表機,那麼你很可能什麼都列印不出來或者列印的檔案是…anyway,我們需要增加一些機制來控制併發程式間的這種相互制約關係。

程式間通訊的很多問題的根本原因是我們不知道程式何時切換。

概念

首先我們瞭解一下臨界資源與臨界區的概念:臨界資源就是一次只允許一個程式訪問的資源,一個程式在使用臨界資源的時候,另一個程式是無法訪問的,作業系統也不能夠中途剝奪正在使用者的使用權利,正所謂“潑出去的女兒嫁出去的水”是也。即臨界資源是不可剝奪性資源。那麼臨界區呢?所謂臨界區就是程式中範文臨界資源的那段程式程式碼,注意,是程式程式碼,不是記憶體資源了,這就是臨界資源與臨界區的區別。我們規定臨界區的使用原則(也即同步機制應遵循的準則)十六字訣:“空閒讓進,忙則等待,有限等待,讓權等待”–strling。讓我們分別來解釋一下:

(1)空閒讓進:臨界資源空閒時一定要讓程式進入,不發生“互斥禮讓”行為。

(2)忙則等待:臨界資源正在使用時外面的程式等待。

(3)有限等待:程式等待進入臨界區的時間是有限的,不會發生“餓死”的情況。

(4)讓權等待:程式等待進入臨界區是應該放棄CPU的使用。

好了,我們進入下一部分。

程式間通常存在著兩種制約關係:直接制約關係和間接制約關係,就是我們通常所說的程式的同步與互斥。顧名思義,一個是合作關係,一個是互斥關係。程式互斥說白了就是“你用的時候別人都不能用,別人用的時候,你也不能去用”,是一種源於資源共享的間接制約關係。程式同步指的是“我們大家利用一些共同的資源區,大家一起合作,完成某些事情,但是我在幹某些小事的時候,可能要等到你做完另一些小事”,是一種源於相互合作的直接制約關係。兩者區別在於互斥的程式間沒有必然的聯絡,屬於競爭者關係,誰競爭到資源(的使用權),誰就使用它,直到使用完才歸還。就比如洗衣房的洗衣機這個資源,去洗衣的同學並不需要有必然聯絡,你們可以互不認識,但是誰競爭到洗衣機的使用權,就可以使用,直到洗完走人。而同步的程式間是有必然聯絡的,即使競爭到使用權,如果合作者沒有發出必要的資訊,該程式依然不能執行。就比如排隊打水,即使排到你了,如果水箱沒水了,你就打不了水,說明你和水箱是有著必然聯絡的,你得從它裡面取水,你們是同步關係,你們合作完成“打水”這個過程。

那麼先來討論如何實現程式的互斥控制。有下列幾種方法:嚴格輪換(每個程式每次都從頭執行到尾,效率不高,可能等待很久),遮蔽中斷(剛剛進入臨界區時就遮蔽中斷,剛要出臨界區就開啟中斷),專用機器指令test_and_set,test_and_clear,加鎖,軟體方法,訊號量機制。講一下加鎖和軟體方法,加鎖方法如下:設定一個鎖標誌K表示臨界資源的狀態,K=1表示臨界資源正在被使用,K=0表示沒有程式在訪問臨界資源。如果一個程式需要訪問臨界資源,那麼先檢查鎖標誌K:

離開臨界區時設定鎖標誌K為0. 軟體方法類似,如愛斯基摩人的小屋協議,愛斯基摩人的小屋很小,每次只能容納一個人進入,小屋內有一個黑板,上面標誌這能夠進入臨界區的程式。若程式申請進入臨界區,則先進入小屋檢查黑板標誌,如果是自己,那麼離開小屋進入臨界區,執行完後進入小屋修改黑板標誌為其他程式,離開小屋。如果小屋黑板標誌不是自己,那麼反覆進入小屋考察黑板標誌是不是自己。這兩種方法都實現了互斥訪問,但是都違反了四條原則之一:讓權等待,都需要不斷的迴圈重複檢測標誌,霸佔了CPU資源,不是很好的方法。

到後來,荷蘭電腦科學家Dijkstra於1965年提出瞭解決程式同步與互斥問題的訊號量機制,收到了很好的效果,被一直沿用至今,廣泛應用與單處理機和多處理機系統以及計算機網路中。訊號量機制就是說兩個或者多個程式通過他們都可以利用的一個或多個訊號來實現準確無誤不衝突的併發執行。如果臨界資源不夠,就會有一個訊號表示出來,如果程式此時想訪問,那麼就會阻塞到一個佇列中,等待排程。當臨界資源使用完畢,一個程式改變訊號,並及時喚醒阻塞的程式,這就實現了程式間的同步和互斥問題。

訊號量分為整型訊號量,記錄型訊號量,AND訊號量以及訊號量集。最初的訊號量就是整型訊號量,定義訊號量為一個整型變數,僅能通過兩個原子操作P,V來訪問,所謂原子操作就是指一組相聯的操作要麼不間斷地執行,要麼不執行。這兩個操作又稱為wait和signal操作或者down和up操作。之所以叫P,V操作是因為Dijkstra是荷蘭人,P指的是荷蘭語中的“proberen”,意為“測試”,而V指的是荷蘭語中的“verhogen”,意為“增加”。最初P,V操作被描述為:

但是這樣明顯違反了“讓權等待的原則”,後來發展為記錄型訊號量,記錄型訊號量的資料結構是一個兩元組,包含訊號量的值value和關於此訊號量的阻塞佇列Q,value具有非負初值,一般反映了資源的數量,只能由P,V操作改變其值。(還有另一種定義,訊號量由value和P組成,value為訊號量的值,P為指向PCB佇列的指標)。

記錄型訊號量的P,V操作原語為:

我們來詳細解釋一下這兩個操作的含義:

首先,P操作,首先將S.value減1,表示該程式需要一個臨界資源,如果S.value<0,那麼說明原來的S.value <= 0,即已經沒有資源可用了,於是將程式阻塞到與訊號量S相關的阻塞佇列中去,如果S.value<0,那麼|S.value|其實就表示阻塞佇列的長度,即等待使用資源的程式數量。然後,V操作:首先S.value加1,表示釋放一個資源,如果S.value <= 0,那麼說明原來的S.value < 0,阻塞佇列中是由程式的,於是喚醒該佇列中的一個程式。那麼,為什麼S.value > 0時不喚醒程式呢,很簡單,因為阻塞佇列中沒有程式了。

P操作相當於“等待一個訊號”,而V操作相當於“傳送一個訊號”,在實現同步過程中,V操作相當於傳送一個訊號說合作者已經完成了某項任務,在實現互斥過程中,V操作相當於傳送一個訊號說臨界資源可用了。實際上,在實現互斥時,P,V操作相當於申請資源和釋放資源。

我們將訊號量初值設定為1時通常可實現互斥,因為訊號量表示資源可用數目,互斥訊號量保證只有一個程式訪問臨界資源,相當於只有一個訪問權可用。設定為0或者N時可以用來實現同步。我們後面將會在生產者-消費者問題中看到這點。用P,V操作實現互斥類似於加鎖的實現,在臨界區之前加P操作,在臨界區之後加V操作,即可互斥控制程式進入臨界區,訪問臨界資源。記錄型訊號量由於引入了阻塞機制,消除了不讓權等待的情況,提高了實現的效率。

經典問題

下面通過一些例項詳細講解如何使用訊號量機制解決程式同步與互斥問題。先說明一條規律,即:同步與互斥實現的P,V操作雖然都是成對出現,但是互斥的P,V操作出現在同一個程式的程式裡,而同步的P,V操作出現在不同程式的程式中。

問題1:生產者-消費者問題

經典的同步互斥問題,也稱作“有界緩衝區問題”。具體表現為:

1.兩個程式對同一個記憶體資源進行操作,一個是生產者,一個是消費者。

2.生產者往共享記憶體資源填充資料,如果區域滿,則等待消費者消費資料。

3.消費者從共享記憶體資源取資料,如果區域空,則等待生產者填充資料。

4.生產者的填充資料行為和消費者的消費資料行為不可在同一時間發生。

生產者-消費者之間的同步關係表現為緩衝區空,則消費者需要等待生產者往裡填充資料,緩衝區滿則生產者需要等待消費者消費。兩者共同完成資料的轉移或傳送。生產者-消費者之間的互斥關係表現為生產者往緩衝區裡填充資料的時候,消費者無法進行消費,需要等待生產者完成工作,反之亦然。

既然瞭解了互斥與同步關係,那麼我們就來設定訊號量:

由於有互斥關係,所以我們應該設定一個互斥量mutex控制兩者不能同時操作緩衝區。此外,為了控制同步關係,我們設定兩個訊號量empty和full來表示緩衝區的空槽數目和滿槽數目,即有資料的緩衝區單元的個數。mutex初值為1,empty初值為n,即緩衝區容量,代表初始沒有任何資料,有n個空的單元,類似的,full初值為0.

下面進行生產者-消費者行為設計:

這樣我們的分析也就完成了,http://www.cnblogs.com/whatbeg/p/4419979.html 這篇文章裡有我用Windows API實現的用訊號量實現生產者-消費者問題。

下面,問題來了,我們的生產者和消費者裡面都有兩個P,兩個V操作,那麼兩個P操作可否調換順序呢?V操作呢?想一想。

答案是P操作不可對換,V操作可以。為什麼呢?想象一下這種情況,生產者執行P(mutex)把互斥量鎖住,然後再P(empty),此時empty < 0,鎖住,無法繼續生產,等待消費者消費,消費者倒是也想消費,可是mutex被鎖住了啊,於是兩個人就等啊等,就成了等待戈多了。。但是V操作是可以隨意調換的,因為V操作是解鎖和喚醒,不會因為它鎖住什麼。

問題2:讀者-寫者問題

第二個經典問題是讀者-寫著問題,它為資料庫的訪問建立了一個模型。規則如下:

1.一個程式在讀的時候,其他程式也可以讀。

2.一個程式在讀/寫的時候,其他程式不能進行寫/讀。

3.一個程式在寫的時候,其他程式不能寫。

我們來分析他們的關係,首先,這個問題沒有明顯的同步關係,因為在這個問題裡,讀和寫並不要合作完成某些事情。但是是有互斥關係的,寫者和寫者,寫者和讀者是有互斥關係的,我們需要設定一個mutex來控制其訪問,但是單純一個訊號量的話會出現讀者和讀者的互斥也出現了,因為我們可能有多個讀者,所以我們設定一個變數ReadCount表示讀者的數量,好,這個時候,對於ReadCount又要實現多個讀者對他的互斥訪問,所以還要設定一個RC_mutex。這樣就好了。然後是行為設計:

其實,這個方法是有一定問題的,只要趁前面的讀者還沒讀完的時候新一個讀者進來,這樣一直保持,那麼寫者會一直得不到機會,導致餓死。有一種解決方法就是在一個寫者到達時,如果後面還有新的讀者進來,那麼先掛起那些讀者,先執行寫者,但是這樣的話併發度和效率又會降到很低。有人提出了一種寫者優先的解法,有點不好理解,這裡給出實現:

問題3:哲學家就餐問題

哲學家就餐問題描述如下:

有五個哲學家,他們的生活方式是交替地進行思考和進餐,哲學家們共用一張圓桌,分別坐在周圍的五張椅子上,在圓桌上有五個碗和五支筷子,平時哲學家進行思考,飢餓時便試圖取其左、右最靠近他的筷子,只有在他拿到兩支筷子時才能進餐,進餐完畢,放下筷子又繼續思考。

約束條件
(1)只有拿到兩隻筷子時,哲學家才能吃飯。
(2)如果筷子已被別人拿走,則必須等別人吃完之後才能拿到筷子。
(3)任一哲學家在自己未拿到兩隻筷子吃飯前,不會放下手中拿到的筷子。
(4)用完之後將筷子返回原處

分析:筷子是臨界資源,每次只被一個哲學家拿到,這是互斥關係。如果筷子被拿走,那麼需要等待,這是同步關係。

容易想到一種錯誤的解法,所以設定一個訊號量表示一隻筷子,有5只筷子,所以設定5個訊號量,哲學家每次飢餓時先試圖拿左邊的筷子,再試圖拿右邊的筷子,拿不到則等待,拿到了就進餐,最後逐個放下筷子。這種情況可能會產生死鎖,因為我們不知道程式何時切換(這也是很多IPC問題的根本原因),如果5個哲學家同時飢餓,同時試圖拿起左邊的筷子,也很幸運地都拿到了,那麼他們拿右邊的筷子的時候都會拿不到,而根據第三個約束條件,都不會放下筷子,這就產生了死鎖。《現代作業系統》中記載的一種解法是僅當一個哲學家左右的筷子都可用時,才拿起筷子,將“試圖獲取兩個筷子”作為臨界資源,用一個互斥量mutex實現對其的互斥控制,然後用n個變數記錄哲學家的狀態(飢餓,進餐,思考<可有可無,因為除了前兩者以外只會思考>),然後用一個同步訊號量陣列,每個訊號量對應一個哲學家,來保證哲學家得不到自己所需筷子的時候阻塞。演算法如下:

 

還有一種解法是讓奇數號與偶數號的哲學家拿筷子的先後順序不同,以破壞環路等待條件。還可以只允許4個哲學家同時進餐(4個人都拿起一隻筷子的時候,第5個人不能再拿筷子,這樣就會空出一隻筷子)

例題分析

至此,我們已經可以總結出一點用訊號量解決同步互斥問題的基本規律和一般步驟:

(1)分析各程式間的制約關係,從而得出同步與互斥關係

(2)根據(1)中的分析,設定訊號量

(3)編寫虛擬碼,實施P,V操作

同步:多個程式在執行次序上的協調,相互等待訊息

互斥:對臨界資源的使用

要注意的是,雖然P,V操作在每一個程式中都是成對出現的,但不一定是針對一個訊號量。互斥訊號量的P,V操作總是出現在一個程式中的臨界區的前後,而同步訊號量的P,V操作總是出現在具有同步關係的兩個程式中,需要等待訊息的一方執行P操作,發出訊息的一方執行V操作。

下面通過諸多例題來熟悉,掌握及訓練用訊號量解決同步與互斥問題的一般方法。

 

問題4:放水果問題

桌上有一空盤,最多允許存放一隻水果。爸爸可向盤中放一個蘋果或放一個桔子。

兒子專等吃盤中的桔子,女兒專等吃蘋果。

試用P、V操作實現爸爸、兒子、女兒三個併發程式的同步。

分析:臨界資源是盤子,放的時候不能取,取的時候不能放,取的時候不能再取。同步關係:爸爸與盤子為空,兒子與盤中有桔,女兒與盤中有蘋果。

所以設定一個mutex互斥訊號量來控制對盤子的訪問,用empty,orange,apple分別代表以上同步關係。程式如下:

問題5:讀檔案問題

四個程式A、B、C、D都要讀一個共享檔案F,系統允許多個程式同時讀檔案F。但限制是程式A和程式C不能同時讀檔案F,程式B和程式D也不能同時讀檔案F。為了使這四個程式併發執行時能按系統要求使用檔案,現用P、V操作進行管理。

分析:互斥關係:A和C讀檔案時互斥,B和D讀檔案時互斥,沒有同步關係。

所以設定兩個互斥訊號量:AC_mutex,BD_mutex即可。虛擬碼如下:

問題6:閱覽室問題 / 圖書館問題

有一閱覽室,讀者進入時必須先在一張登記表上進行登記,該表為每一座位列一表目,包括座號和讀者姓名。讀者離開時要消掉登記訊號
,閱覽室中共有100個座位。用PV操作控制這個過程。

分析:

由於每個讀者都會進行一樣的操作:登記->進入->閱讀->撤銷登記->離開,所以建立一個讀者模型即可。

臨界資源有:座位,登記表

讀者間有座位和登記表的互斥關係,所以設訊號量empty表示空座位的數量,初始為100,mutex表示對登記表的互斥訪問,初始為1。

P,V操作如下:

 

問題7:單行道問題

一段雙向行駛的公路,由於山體滑坡,一小段路的一般車道被阻隔,該段每次只能容納一輛車通過,一個方向的多個車輛可以緊接著通過,試用P,V操作控制此過程。

分析:

臨界資源為一半被阻隔的一小段區域,所以需要一個mutex來控制每個方向第一輛車通過該路段的訪問,類似於讀者-寫者問題,車輛從兩邊通過相當於兩個讀者,我們設立兩個計數器A和B分別代表兩個方向的汽車數量,還要設定兩個訊號量A_mutex和B_mutex來實現對計數器的互斥訪問。於是程式如下(PV操作包含其中):

執行結果:

從其中可以看出,車輛正常交替順序通過該路段。數字重複出現是因為執行緒被重複地排程執行。

問題8:理髮師問題

理髮店理有一位理髮師、一把理髮椅和n把供等候理髮的顧客坐的椅子 如果沒有顧客,理髮師便在理髮椅上睡覺。 一個顧客到來時,它必
須叫醒理髮師 如果理髮師正在理髮時又有顧客來到,則如果有空椅子可坐,就坐下來等待,否則就離開。用PV操作管理該過程。

分析:

法1:首先設定一個count表示等待的人數(包括理髮椅上的那個人),初值為0,以供後來者判斷是否應該離開。同時對count的訪問要保證互斥,所以設定mutex訊號量來保證互斥,初值為1。

臨界資源:凳子,理髮椅。 分別設定waitchair,barchair訊號量,初值分別為n和1,表示臨界資源數量。

同步關係:顧客和理髮師之間有同步關係,用ready和done訊號量來表示,初值均為0,ready表示顧客有沒有準備好,done表示理髮師是否完成一次理髮。

注意:並非每一個程式都需要while(1)無限迴圈,比如此例,顧客剪完一次頭髮就走了,不可能馬上再來剪,而以前的生產者-消費者不同,他們都是可以不斷生產消費的。

寫出P,V操作如下:

法2:將凳子和理髮椅看做同一種資源,因為只要理髮椅空就一定會有人湊上去,所以相當於每個位置都是理髮椅,理髮師只需要去每個有人的座位理髮即可。

還是設定count表示正在理髮店中的人數,以便決定後來者是否離開。

同步關係仍用ready和done來表示。

演算法:

 

好了,先說這麼多,例題會持續更新增加,感興趣的朋友可以關注下。

鄙人學力有限,有不足或錯誤之處敬請指出,不勝感激。

 

參考文獻:

1.《現代作業系統》 –Andrew S. Tanenbaum

2.《作業系統設計與實現》 –Andrew S. Tanenbaum

3.《作業系統精髓與設計原理》 –Strling

4.《2015作業系統高分筆記》 –劉泱主編

 

相關文章