操作系統5:進程同步
進程同步存在問題,原因就是一個CPU要為兩個以上的進程服務,而這其實是現在的操作系統也沒有完美解決的
臨界區問題
如果不加處理的話,就會出現問題:假設兩個進程要訪問同一個資源,由於CPU調度具有一定的隨機性,而先訪問的進程會對資源進行修改,這就使得進程對資源的訪問結果具有一定的隨機性,這顯然是不可接受的
這里以生產者-消費者問題為例:
如果這兩個操作在不被打斷的情況下執行,那么就不會有同步問題
也就是說,涉及到共享資源的寫時,操作需要是獨立不受干擾地執行的
但是要實現原子操作不是直觀看上去那么簡單的,因為它在實際執行時可能根本不是一條語句,而是多條(例如像例子中的這種情況,不是所有的CPU都有直接對內存變量進行修改的指令,許多CPU指令集,特別是精簡指令集,就像下面展示的那樣,還需要借助寄存器)
注意,單條機器指令在執行的過程中是不會被中斷的,這是因為此時是關中斷了的
除了指令集本身的限制外,還可能有編譯程序的限制。由於編譯器要在不同平台上提供服務,雖然每個平台都是單獨設計的,但是不一定能針對該平台充分優化。例如上圖這種情況,如果編譯器優化不到位,在所有平台遇到++都翻譯為上述三條指令,那當然在各個平台都可以運行,雖然不是最優的。這也使得同步問題更加地突出
這雖然不是通常的情況,但是是的確可能發生的
上面的問題出在修改的值沒有及時從寄存器寫入內存
共享數據的最終穩定值取決於最后完成操作的那個進程
注意臨界區這個詞是形容進程代碼的,並且不包括所有進程只讀不寫的情況
臨界區是和進程相關
什么是臨界區問題呢?
顯然,實現這一點是不容易的
對於這個問題,我們可以提出一些條件來檢驗解決方案是否有效
什么是remainder section呢?
entry section和exit section是為了解決臨界區問題而加入的、原本代碼中沒有的代碼塊,而臨界區問題的解決方案也就是確定這兩個區域的代碼內容的問題
注意,當我們討論一個資源的時候,另外的資源代碼都算成remainder section。也就是說,選舉必須是直接相關的進程才能做的
不在意這些進程是否都在持續運行,也不考慮運行速度:注意,進程同步和進程調度是在同步執行的,也就是說各干各的,至今為止也沒辦法做到這兩個模塊之間的協調,否則性能損失非常大
我們先討論兩個進程的情況
雙進程算法1
這個算法就是為了解決臨界區問題的一個算法
turn不是原來的程序中的變量,是為了解決問題另外定義的
注意do中的while語句后面有個分號,表示的就是空操作
那像這種本來CPU已經分配給了進程時間片,結果它卻在while死循環是不是一種資源的浪費呢?是的,但是在並發下,保證共享資源的同步正確性顯然更重要
做完操作之后執行turn=j,表示主動還掉turn
為什么它不符合progress呢?
例如,如果進程0正在remainder section中IO中斷,並且將永久中斷(例如讓用戶輸入而用戶不輸入),此時進程0不更新turn,如果此時turn已經被進程1之前設置為了指自己,進程1就永遠沒有機會執行,反復循環,turn也不會變。這實際上就是,進程0在remainder section,卻依然影響了選舉
為什么滿足mutual exclusion是很容易證明的,這里就不說了
雙進程算法2
while(flag[j]);表示,如果對方進程想要訪問,就優先對方訪問,等對方訪問完了、flag撤銷了,自己再訪問
同樣不滿足progress
這種算法很明顯,有死鎖的問題,就是兩個進程都設置了flag,結果都卡在while死循環,這就違背了在有限時間內完成選舉的要求
雙進程算法3:Peterson算法
也就是說,只有j既被允許,同時又申請的情況下,i才會做空操作等待
這個算法是符合這三個原則的,有興趣的可以自己證明下
兩個進程的算法就討論到這里了
下面開始討論N個進程的算法:
N進程面包房算法
這個n是有限的,也就是說需要提前確定的,這是因為這個算法中使用到了數組,數組需要確定的大小
其實思路就是排號,為啥叫面包房算法,可能就是因為作者那里的面包房排號吧
number的分配通過max()+1以及number一致時候的判斷邏輯來實現在並發下的正確性
因為設置number的過程不是一個原子操作,所以需要使用choosing和第一個while,相當於加一個鎖。在第一個while循環的邏輯下,所有進程的choosing過程都不會被打斷
第二個while循環就是實現了分配順序。
注意在進程同步中,我們關心的是資源的正確性,而不是誰先操作誰后操作,也就是進程調度在這里暫時不是重點
-
互斥是否滿足?
滿足,因為根據number和進程號一定是可比較的
-
bounded waiting是否滿足?
選舉的有限次數也可以證明:每次while等待一個進程,這個進程只能加入臨界區一次。這是因為它下一次進入的時候需要重新獲取number.而獲取的number一定是比其他所有都大的,這樣現在正在等待的進程就不需要給它讓步了
-
process是否滿足?
這個從判斷的條件中就可看出。如果在remainder section的話,它的Number就是0,這在前面的分配時就會被直接略去。由於bounded waaiting符合,所以while語句的執行是有限時間的,所以等待時間是有限時間
第一個問題很好理解,就是並行的結果,因為number的賦值操作不是原子的
第二個問題:
choosing數組的存在是為了保證while循環中比較時候所用到的number都是穩定的,防止出現一個進程計算number的過程還沒有完成時間片就被搶走時,其他進程和該進程比較時用的還是老的number(也就是0),從而認為該進程沒有在臨界區,最后導致多於一個進程同時訪問臨界區的結果
硬件指令解決方案
除此之外,增加硬件指令也會導致CPU結構變得更加復雜,是一個需要很慎重的事情
特別是在多CPU中,關中斷難道要關閉所有CPU的中斷信號嗎?所有這種方法是不行的,還需要找其他方法
追求的是原子的效果
- 測試並賦值
不是說硬件指令嗎,為什么是一個函數?其實在計算機組成中我們學習了微指令,CPU的一些復雜的硬件指令也都是通過組合微指令而成的,寫成函數的形式其實很自然,這里只不過是用C語言的格式描述了而已
由於這是一個硬件指令,所以在它執行完之前,是不可能有中斷發生的,這一點就避免了許多許多問題
有了硬件指令后,就可以很簡單地加一個鎖就完事了
同樣,這種方式沒有對進程的數量進行限制,是無限的
但是這個方法不是沒有問題的,它不滿足bounded waiting:例如,一個進程設置了鎖為true,但是時間片到期,CPU調度給了其他進程,但是由於已經上鎖,其他進程無法訪問共享資源,只有等待原進程被重新調度。但是如果原進程被調度后執行完了一次后又循環,又加上了鎖,這就可能導致其他進程永遠不能被執行,違背了bounded waiting的有限次數要求
這個指令的效果和用TestAndSet沒有區別,所以它也不符合bounded waiting
另外,上面的辦法都有一個問題沒有解決,就是對應用程序編寫者要求太高,上面的代碼都是要加在應用程序開發代碼中的,這對於應用程序開發者是一個很大的負擔。而且,一個程序中可能有多個共享資源,也就有了多個臨界區,要求開發者對每一個地方都插入上述的代碼並進行調試顯然是強人所難
這就是下面的解決方案要解決的問題
信號量
信號量試圖將用戶要做的額外步驟都放在一起,並且保證其是原子操作
注意這里說的是等待隊列
如果進程的value小於0,就將自己掛到信號量里面的等待隊列中並重新進行調度
如何應用呢?
這種方法對用戶的要求是比較低的,如果有同步問題的話,只要定義一個信號量然后在上下分別加上wait和signal就好了
對於第一個進程,它把value從1減到0,正常執行;第二個、第三個等等之后的進程因為value值運算后都小於0,所以被wait掛起到等待隊列。value值如果小於0的話,它的絕對值就是等待的進程個數。等到第一個進程執行完后,value+1,此時value依然是小於0的,就從等待隊列拿一個進程喚醒,它訪問完共享資源之后繼續調用signal,使得value加一,直到value大於0,也就是恢復到1
bounded waiting條件能滿足的條件是signal里從等待隊列中選進程時使用先來先服務或者輪轉法,不能按優先級排列,否則如果一直有高優先級的進程進來的話,低優先級的進程也會出現永遠得不到服務的情況
那么,又回到了原本的問題:如何保證wait和signal操作是原子的?最簡單的,可以使用關中斷的方式,但是這種方法還是一樣,代價太高。現在Linux實現了不需要關中斷就能保證原子性的算法。不僅如此,它還給應用程序開發人員提供了信號量組的操作,可以一次性對多個信號量進行操作
信號量還可以解決其他的進程同步問題:
挺簡單的,不解釋了
經典同步問題
將信號量作為基本方法
這里給出一個使用信號量實現的解
利用信號量,使用wait和signal保護counter
但是這不是最好的解,因為還留下了兩個while語句,本質上還是使用了忙等待,在效率上會有損失,我們可以想辦法消除這些忙等待:
full可以表示當前緩沖區被占用的格子的數量,empty表示當前緩沖器空閑格子數量
每次生產empty減一,如果減到0了就阻塞了
每次消費full減一,等減到0就阻塞
信號量的使用次序也是很講究的,如果使用不當就可能會造成死鎖:
consumer通過wait(mutex),但發現full是0,此時會被掛起,但是還沒有執行signal(mutex),所以producer仍無法執行,程序陷入死鎖
飢餓是理論上不是死鎖,但是實際上由於其他情況卻發生了的進程無法被調度的情況
讀進程之間不需要互斥,但是寫進程和讀寫都互斥
問題是,讀寫進程同時排隊時,先調度讀還是先調度寫
第一類是讀者優先,第二類是寫者優先
有算法可以避免飢餓的情況發生,這里就不細說了
這里只以第一類問題為例來使用信號量求解
mutex在讀者之間協調
readcount是為讀的進程計數
wrt解決所有的讀者和不同的寫者之間的互斥
mutex是為了給readcount加鎖,使得readcount保持正確
if(readcount1) wait(wrt)是檢測現在是否有寫者,如果有的話就阻塞自己,如果沒有的話,wait(wrt)也能避免之后讀者在讀數據的時候有寫者想進入。而正因為有了這樣的操作,所以只要讀者不是第一個,也就是readcount1的話,就不需要管寫者了,所以才會有這個if語句
之后的if(readcount==0)signal(wrt)是解開寫者的鎖,這也只有最后一個讀者才會去解
如果能寫代碼更方便的話:
理想目標就是只使用一個信號量,然后提供四個系統調用,程序員只把調用寫好,剩下的都是由操作系統去做
哲學家只有兩種狀態:思考和吃飯
只有五根筷子
避免死鎖和飢餓
--若干進程之間分配若干資源
死鎖是顯然的:因為每個進程需要兩個資源才能正常運行,很容易死鎖
可能的解決方法:
- 留一個空閑資源,不要一次性所有人上座:加一個信號量,初始化為4
- 一次性獲得資源,不要先后拿取兩根筷子,要么拿兩根要不不拿
- 不對稱解:五個哲學家中一部分先拿左手邊,一部分先拿右手。這種可以解決死鎖(因為只要這些人中有不一致的,就一定有至少一個人能從左右手邊拿到一對筷子),但是不能解決飢餓
這些經典的同步問題可以用來檢驗新的同步算法