【操作系統】第九章:臨界區的概念和互斥的理解

背景

計算機系統裏會有多個進程存在,這多個進程相互之間還會進行交互。交互會引起對共享資源的訪問,如果對這些共享資源訪問操作不當,就可能出現一些問題(死鎖、飢餓)之類。
在這裏插入圖片描述
這些問題出現的原因也和調度有關。如果進程相對獨立(不需要訪問共享資源,進程間不需要交互),則每一個進程和線程的執行是確定的,執行的流程也是可以重複的。
但是如果進程和線程需要協助操作,可能會交互或者訪問共同資源,則在調度器管理下會有不同順序的調度。而調度的順序由很多因素決定,可能導致對於當個進程而言它的執行時間不確定(其他進程可能會搶佔),且結果不確定(因爲其他進程訪問共享資源而導致結果不同)而且結果可能無法重現(其他進程的搶佔時機、次數和資源等變量過多)。 這種不確定性和不可重複性會引入難以發現的bug,會出現一些不穩定的現象,而這種現象又很難重複。


在這裏插入圖片描述

進程間合作是必需的。因爲資源需要共享,不能爲每一個訪問者拷貝一份相同的資源,從負載上來說是不可能的(想象一下給每個人設置一家銀行)。其次通過並行或者併發操作,可以提高系統效率,實現更有效的資源利用。在計算機中,我們爲了提高效率會把大的資源拆分成小的資源,把大的工作分成小的工作,模塊化之後,不同模塊化之間是需要共享和交互的,因爲他們本身就是一個整體。


例子)原本沒有問題的代碼段在並行中的問題
在這裏插入圖片描述

在這裏插入圖片描述
P1執行了兩個彙編指令後產生上下文切換(調度),P2搶佔P1。P2完成後調度P1再執行,這個執行過程裏P1的PID是100,和預期相同。P2得到的PID也是100而且next pid應該是102,這裏確實101。
一開始執行時,newpid確實是100.在這個時候做了一次調度,切換到進程2。進程2也要完成同樣的操作,第一步要LOAD nexpid給寄存器1.而這個時候的nextpid是100導致接下來的三條操作,會給newpid賦100而且nextpid是101。到此爲止沒有問題。
但是從P2切回P1時,P1要做個+1操作,而這時候的寄存器1還是100。但是進程2切換到進程1時,P1得到的PID(存在於NEWPID)的值等於P2的值都是100,且nextpid都是101,並沒有變102。因爲上下文切換後,進程1的寄存器回覆之後,寄存器保存的值依然是100的值使得nextpid無法進一步更新成102。
這個問題就因爲四條指令產生的上下文切換就使得結果不一致了。

在這裏插入圖片描述

我們希望無論多個線程如何切換,交替執行,程序都要正常執行。不確定性要求並行程序的正確性。


我們把這些現象稱爲:
在這裏插入圖片描述
出現了競態條件就會導致出現不確定性和不可重現性的現象。爲了避免這種現象:其實很簡單,只要那四條指令不會被打斷,不會被切換,就不會出現這種現象了。那麼爲了保證指令不被打斷,就需要原子操作:
在這裏插入圖片描述
原子操作就是不可被打斷的操作,避免了不同切換點出現問題的現象。但是實際系統中,指令間是可以被打斷的。通過某種軟硬件結合的方式,使得指令可以按照原子操作方式執行而不是隨時被打斷的方式執行。
爲了理解舉一個例子:
在這裏插入圖片描述
兩個線程,一個加操作一個減操作,誰先完成誰就贏(打印出一段話)。調度這兩個線程的內存讀取和存儲是原子操作,但是+1 -1不是原子操作(4條彙編指令構成)。根據不同調度策略結果不同,除了A贏或者B贏,還有一種情況:誰都打印不出來,線程持續執行沒有結果。

如果線程執行到i=i+1時,這時候線程切換到B,執行i=i-1。就會出現i=i+1-1=i,就會可能出現線程AB在while循環裏一直在各自的循環內出不來,也就說誰都無法執行完畢。

互斥

爲了理解同步互斥,需要簡述以下概念:
在這裏插入圖片描述


臨界區一段代碼需要訪問的資源是共享資源。訪問共享資源的代碼稱爲臨界區。
互斥:臨界區的進程只有一個,不允許出現多個進程進入臨界區訪問。多個進程訪問會出現不確定性
死鎖:當兩個進程都擁有一定的資源且還需要其他資源時,有可能相互等待。A等B、B等A,誰也無法繼續執行。
飢餓:一個進程持續等待,而且可能長期存在而無限期等待。


接下來根據這個問題來講解同步互斥
在這裏插入圖片描述
把上述現象視爲進程活動,就會出現買麪包事件執行了兩次,而我們不希望這樣。

那麼出現這種情況的原因有:
1.沒有限制只能有一個人買麪包(設定爲最多一個人有一個)
2.如果需要,就會有人去買麪包
那麼解決的方案有以下幾種:


## 加鎖
鎖上鎖冰箱,粒度過大,因爲冰箱內可能有其他東西是其他人需要的,所以這種方式並不可行。


在這裏插入圖片描述
方案:標籤
設置標籤視爲加鎖操作,去掉標籤視爲解鎖操作。
邏輯:任一個體打開冰箱第一步看有木有面包,如果有則空過。如果沒有再看有木有標籤,如果有,則空過;如果沒有說明他是第一個發現冰箱沒有 麪包的人,貼一個標籤然後去買麪包。麪包買到後回來撕下標籤並將麪包放入冰箱。
那麼這件事映射到計算機程序會怎麼樣呢?
如果控制邏輯變成併發進程去執行,仍然會出現問題。如果以進程的方式來執行,那麼在執行過程中任何時間是可能發生上下文切換的。也就說進程A執行到一定程度也可以被OS調度切換到另一個進程去執行。這種情況下:
在這裏插入圖片描述
12步發現沒有面包後,產生進程切換。跳到進程2,也會判斷面包標籤,發現也沒有,這時候也會觸發購買麪包事件。然後再次跳到進程A,A買麪包,之後B也會完成同樣的行爲。也就說,標籤沒有起到鎖的保護機制,並沒有限定到只讓一個人去買麪包的功能,控制邏輯失效了。而且,出現這種情況後,很難去重現這種情況,因爲下一次產生調度可能不是按照這種情況進行上下文切換的。會導致間歇性失效。
調度的不確定性使得競態條件隨時會發生,不想要的結果仍然會出現。


那麼如果先留標籤再檢查麪包和標籤:
在這裏插入圖片描述
在這裏插入圖片描述
如果這樣放置,進程調度可能會發生誰都不去買麪包的情況。A和B都放置標籤,然後兩個都去檢查是否有標籤,結果有標籤,則都不會去執行接下來的行爲。


考慮到上述行爲,也許是因爲標籤沒有標識,也據說不知道這個標籤是誰留的。我們給標籤增加一個標識,嘗試着去解決這個問題。
在這裏插入圖片描述
進程A放置標籤1,進程B放置標籤2.然後對於進程A,他會檢查是否有標籤2,如果沒有標籤2就意味着進程B沒有放置標籤,則會判斷是否有面包並執行買麪包操作。
在這裏插入圖片描述
但是仍然會出現問題:
在上下文切換後,標籤1和2全部留下,然後AB都檢查到對方已留標籤,則都不會繼續向下執行。所以仍然會發生誰都不買麪包的情況。

也就說增加標籤的屬性仍然不能解決這個問題。


更加複雜的標籤方案:
在這裏插入圖片描述
現在這種情況對於進程而言,可以自己買也可以等別人去買,也就說進程。
進程A:如果發現B留了標籤,則會一直等待B買完麪包
對於B:如果發現A的標籤,則可以去買麪包;否則,離開。如果B離開了,則A可以去買麪包。
進程AB的控制邏輯不同,但是也存在問題。對於進程A判斷有標籤,A不會去掉自己的標籤或者執行其他操作,而是忙等,直到B去掉標籤纔會繼續執行下一個語句。對於進程B而言,判斷A是否留標籤,如果設置了標籤並不是忙等,而是去掉標籤2。使得進程A如果執行while循環時有很大概率迅速獲得後續的控制邏輯並進一步執行。
這樣確實解決了問題,確保了總有一方去購買麪包而且不會沒有人去買麪包。而且A有更大的概率買麪包,B相對於A購買麪包的次數會少一些。
這種方法的弊端:
在這裏插入圖片描述

1.只是解決了兩個進程的問題,如果有很多進程,則這種方法無法自然擴展。
2.調度和進程設計來說,希望A與B能夠均等的去執行,但是這種設計出現明顯的不對稱性和不公平性。
所以這個方法治標不治本。

==> 通過互斥的手段來解決

在這裏插入圖片描述
臨界區:有一段代碼會對共享資源進行操作,這個例子裏,麪包是共享資源。臨界區裏只允許一個進程去訪問和執行,這段代碼主要就是對共享資源進行讀或者寫操作,如果已經有一個進程在臨界區執行,則其他進程必須等待。
互斥:確保只有一個進程在臨界區。保證互斥就可以保證結果確定。
我們要確保互斥的執行臨界區代碼,也就意味着任何時候只有一個進程在執行臨界區的內容。
改善代碼:
在這裏插入圖片描述
這樣就可以確保任何時候只有一個進程在臨界區中執行,其他進程在臨界區外等待,只有等臨界區內進程執行完畢之後,等待的進程中也只能有一個進入臨界區。很好地解決了這個麪包問題。

臨界區的設計

在這裏插入圖片描述
臨界區的屬性:
1.互斥:任何時候只有一個線程或者進程可以訪問臨界區
2.前進(Progress):一個進程或者線程想進入臨界區,則最終他會進入臨界區,不會一直等待。
3.有限等待:如果一個進程只需等待有限的時間段就能確保它可以進入臨界區去執行。
4.無忙等待【可選】:在臨界區之外等着,如果是在做死循環這種忙等,如果不能確保很快進入臨界區,則過於消耗CPU資源。

禁用硬件中斷

在這裏插入圖片描述
中斷除了產生硬件事件讓OS去響應事件之外,當進程要進行切換或者調度時,我們需要時鐘中斷。時鐘中斷可以使得當前進程即使它在執行也可以被打斷,切換到OS,讓OS完成調度切換其他進程去執行。這個中斷使得OS有強制打斷進程正常執行完成進程切換的能力。這個能力是用於OS調度系統管理的重要機制,同時這個機制也會得到上述不確定結果的原因,也就說如果我們執行臨界區代碼時禁用該功能(退出臨界區時再恢復),也就不會有面包問題了。
雖然可以解決麪包問題,但是也存在其他問題:中斷主要用來響應外部事件,比如外設產生的事件,用於外界交互。比如這時候來了一個網絡包或者時鐘信號,完成磁盤塊的讀寫等。如果這時候屏蔽中斷,也就意味着這些硬件事件無法及時響應,對整體效率有很大影響。其次臨界區本身執行的時間長短是不確定的,如果很長的話,長時間禁用會對系統造成很大影響。
也就說只針對臨界區很小的情況是有效的。
另外兩個CPU並行的進程要執行該臨界區資源時,那麼中斷機制只屏蔽其中一個CPU是無法解決問題的。也就說中斷機制對於多CPU是有限定的。當一個CPU執行屏蔽中斷指令時,會把自身的響應中斷能力屏蔽,但是不會屏蔽其他CPU的中斷能力,也就意味着其他CPU可以繼續執行繼續產生中斷,導致多CPU情況下屏蔽中斷無法解決互斥問題。

基於軟件(同步)的解決方法

在這裏插入圖片描述
如何設計進入和離開臨界區才能使得任何時刻只有一個進程或線程在臨界區執行以及確定其他進程可以在有限時間內必然能夠進入臨界區?
第一種方法:
在這裏插入圖片描述
進程turn(次序)決定誰先進入臨界區。這種情況下,turn=0意味着輪到進程0來進入臨界區了。
對於每個進程,如果不是輪到自己,則會在while裏打轉,當輪到自己,則會進入臨界區執行,執行完畢後,將turn值變更(另一個合法的值J賦給turn),然後離開臨界區,跳出循環。
這種情況下,可以保證互斥。保證只有一個進程可以進入臨界區執行,因爲turn只有一個值。
但是不滿足前進:如果A完成臨界區內的操作後決定不再進入臨界區執行,而B再完成臨界區操作後決定再進入臨界區執行,但是A因爲不打算進入臨界區,就不會執行進入臨界區的代碼,也就不會改變turn值,導致B無法繼續前進。

改進
在這裏插入圖片描述
設置一個flag標識,表示是否想要進入臨界區。
對於進程ii而言:判斷flag[j]flag[j]是否等於1,意味着jj是否想要進入臨界區。對方想進入臨界區,ii就要等待對方退出臨界區之後。等待jj退出臨界區,退出時標識應該是0,也就意味着自身有機會可以執行了。
也就說,一開始其他進程會進入臨界區,則謙讓一下,讓其他進程先執行。如果沒有,則跳出循環向下執行,會把自身標識變成1然後進入臨界區,執行後再賦0。
但是這個代碼無法滿足互斥。開始時,所有flag都是0,也就意味着如果進程執行到while循環跳出後切換,會令兩個進程都會跳出循環,來到第二語句,給各自的flag賦1,這兩個進程同時都會去執行臨界區代碼,無法保證互斥(多買麪包問題)。
再修正
在這裏插入圖片描述
交換語序,這個代碼確實在任何時刻不能兩個while循環都能進入(保證互斥)。但是存在死鎖,兩個都被擋在while循環誰都進不去。
進程A的F是1,切換到B並把F賦1。切換回來後,因爲兩個進程都是1,則兩者都會死循環,誰都跳不出循環。
正解:Peterson算法
在這裏插入圖片描述
用了兩個變量,標識進入臨界區的順序和是否準備好進入臨界區。(Flag是包含n個變量的數組,所以變量可增加)
在這裏插入圖片描述

進入臨界區,如果要進入臨界區:
flag[j]是否爲真且當前turn是否等於j。意味着進程j想進入臨界區且現在輪到他進入。此時讓給j執行臨界區,否則自己執行臨界區並且退出,此時訪問臨界區的需求改爲否。
滿足互斥與前進。
更復雜的算法:Dekkers算法
在這裏插入圖片描述
擴展:N線程的方法
在這裏插入圖片描述
針對n個進程保持互斥。算法細節不展開講,介紹一下大致思路:對於進程i而言,前面的進程如果有已經進入臨界區或者要進入臨界區的,那麼i進程等待。i進程後面的進程也要等待i進程執行後纔行進入臨界區執行,前提是i想要進入臨界區。
通過這種方式實現有序循環的方式來完成n個進程有序進入臨界區執行的思路。
Bakery算法:
在這裏插入圖片描述
進程id本身和票號來排序決定進入臨界區的順序,要比兩個進程的算法複雜不少。

小結

在這裏插入圖片描述
當進程無法進入臨界區時,目前的設計是忙等,這樣會導致CPU佔用時間過大。
硬件需求低,只需要LOAD STORE操作是原子的就可以。

更高級的抽象

基於硬件的原子操作的高層抽象實現。硬件如果能過提供原子操作,就能用原子指令來輔助我們實現進入臨界區和退出臨界區的操作。
在這裏插入圖片描述
獲得鎖:可以進入臨界區執行
獲得鎖的過程就是進入臨界區的時間過程。
釋放鎖的過程就是退出臨界區的時間過程。
抽象的實現:
在這裏插入圖片描述

Test and Set:這是一條機械指令。完成了通常的讀寫操作的兩條指令的功能,它包含三個語義{從內存中讀取,判斷值(返回真假),讀的內存值設置成1}
Exchange:輸入參數是兩個內存的單元,把兩個單元的值交換並返回。
這兩條指令如果有一條就能比較簡單的實現臨界區的進入和退出。
在這裏插入圖片描述
這兩個指令被封裝爲機械指令,也就意味着執行這些指令時,是不允許被中斷或者切換的。只有執行完整個指令的語義之後纔可能產生切換或者中斷。
在這裏插入圖片描述
某一進程想進入臨界區,首先要獲得LOCK。 while循環內的t-a-s操作一開始是0,代表沒有任何一個進程進入臨界區。做完t-a-s操作後,值變爲1且返回0,然後跳出循環繼續執行。接下來下一個進程想要進入臨界區,他會調用Lock::Acquire(),這時候他發現,值是1,也就意味着返回值也是1,再賦值還是1,會一直循環出不來(忙等)。進入臨界區完成之後,會做出一個 退出操作,退出操作Release()就是把值賦0.一旦值賦0也就意味着其他等待Lock::Acquire()的進程可以跳出循環了,因爲這時候值爲0了。
從這個結構看來,他除了可以很簡單的支持兩個進程以外,還能支持N個進程。一個訪問,其他的等while循環。一個退出,值覆蓋爲0,其他等待進程有機會執行臨界區。而且兩個進程和N個進程的操作很簡潔,都是一樣的結構。
改進
while循環是忙等,也就意味着臨界區很長,CPU開銷會比較大。
在這裏插入圖片描述
等待的進程在等待時,可以去睡眠。如果一個進程在等待且暫時不能進入臨界區,我們可以令其進入睡眠。也就意味着把進程掛回等待隊列,就可以把CPU讓出來給其他進程使用,一旦當前臨界區內執行的進程退出,他會把值賦0還會同時完成喚醒操作,通過喚醒操作使得等待進入臨界區的進程從新去判斷可以完成t-a-s/跳出循環。如果跳不出,再次進入睡眠,否則進入臨界區。
與忙等的差別:
如果臨界區足夠短,則我們選擇忙等。因爲他不需要完成上下文切換,節省開銷。
如果臨界區長,開銷遠遠大於上下文切換,我們願意選擇基於上下文切換非忙等的機制來使用。


在這裏插入圖片描述
如果k=1,做交換操作,交換lock和key。lock和key都是兩個內存單元,第一個進程執行時,lock值爲0,執行完exchange後lock是1,key是0.一旦key是0,while循環條件被打破,可以進入臨界區去執行。
進入臨界區時,lock已經從0變成1.其他進程再想進入時,由於key和lock都是1,所以while死循環。直到臨界區內的進程退出時,會完成一個操作lock=0,一旦lock爲0,那麼其他等待的進程會再次執行交換,第一個執行exchange的進程他會發現lock變0 了,那麼此時key會變0,lock會變1,跳出循環,執行進入臨界區的操作。


基於原子操作機械指令的同步互斥.優點:
在這裏插入圖片描述
實現簡單
容易擴展
開銷比較小

缺點
在這裏插入圖片描述
忙等可能會耗費更多CPU開銷
While循環會去搶lock,搶lock是一個比較隨機的過程,那麼可能導致某個進程一直搶不到的飢餓現象。
高優先級進入忙等,但是低優先級沒有機會釋放鎖。可以通過優先級反轉有效解決。


通過鎖可以有效解決互斥問題。鎖機制的實現需要一定的計算機體系結構的硬件支持。目前而言,單機OS選擇基於原子操作的機械指令來完成同步互斥來進入/退出臨界區是常見的方式。根據臨界區執行長短的特徵來完成忙等和非忙等的方式來實現同步互斥。

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