[OS] 進程間通信

進程之間需要通信,而且最好使用一種結構良好的方式,不要使用中斷。

競爭條件

在一些操作系統中,協作的進程可能共享一些彼此都能讀寫的公用存儲區。
這個公用存儲區可能在內存中(可能是在內核數據結構中),也可能是一個共享文件。
兩個或多個進程讀寫某些共享數據,而最后的結果取決于進程運行的精確時序,稱為競爭條件(race condition)。
調試包含有競爭條件的程序是一件很頭疼的事。
大多數的測試運行結果都很好,但在極少數情況下會發生一些無法解釋的奇怪現象。

臨界區

怎樣避免競爭條件?
實際上凡涉及共享內存,共享文件以及共享任何資源的情況下都會引發與前面類似的錯誤,要避免這種錯誤,關鍵是要找出某種途徑來阻止多個進程同時讀寫共享的數據。
換言之,我們需要的是互斥,即以某種手段確保當一個進程在使用一個共享變量或文件時,其他進程不能做同樣的操作。
一個進程的一部分時間做內部計算或另外一些不會引發競爭條件的操作。
在某些時候進程可能需要訪問共享內存或共享文件,或執行另外一些會導致競爭的操作。
我們把對共享內存進行訪問的程序片段稱為臨界區域(critical region)或臨界區(critical section)。
如果我們能夠適當的安排,使得兩個進程不可能同時處于臨界區中,就能避免競爭條件。

忙等待互斥

(1)屏蔽中斷
在單處理器系統中,最簡單的方法是使每個進程在剛剛進入臨界區后立即屏蔽所有中斷,并在就要離開之前再打開中斷。
屏蔽中斷后,時鐘中斷也被屏蔽。
CPU只有發生時鐘中斷或其他中斷才會進行進程切換,這樣,在屏蔽中斷之后CPU將不會切換到其他進程。
屏蔽中斷對于操作系統本身而言是一項很有用的技術,但對于用戶進程則不是一種合適的通用互斥機制。

(2)鎖變量
設想有一個共享(鎖)變量,其初始值為0。
當一個進程想進入其臨界區時,它首先測試這把鎖。
如果該鎖的值為0,則該進程將其設置為1并進入臨界區。
若這把鎖的值已經是1,則該進程將等待直到其值變為0。
但是,這種想法包含了疏漏,假設一個進程讀出鎖變量的值并發現它為0,而恰好在將其值設置為1之前,另一個進程被調度運行,將該鎖變量設置為1。
當第一個進程再次能運行時,它同樣也將該鎖設置為1,則此時同時有兩個進程進入臨界區中。

(3)嚴格輪換法
整型變量turn,初始值為0,用于記錄輪到哪個進程進入臨界區,并檢查或更新共享內存。
開始時,進程0檢查turn,發現其值為0,于是進入臨界區。
進程1也發現其值為0,所以在一個等待循環中不停的測試turn,看其值何時變成1。
連續測試一個變量知道某個值出現為止,稱為忙等待(busy waiting)。
由于這種方式浪費CPU時間,所以通常應該避免。

只有在有理由認為等待時間是非常短的情形下,才使用忙等待。
用于忙等待的鎖,稱為自旋鎖(spin lock)。

進程0離開臨界區時,它將turn的值設置為1,以便允許進程1進入其臨界區。
但是如果一個進程比另一個慢了很多的情況下,輪流進入臨界區并不是一個好辦法。

(4)Peterson算法
一開始,沒有任何進程處于臨界區中,現在進程0調用enter_resion。
它通過設置其數組元素和將turn置為0來標識它希望進入臨界區。
由于進程1并不想進入臨界區,所以enter_resion很快便返回。
如果進程1現在調用enter_region,進程1將在此處掛起知道interested[0]變成FALSE。
該事件只有在進程0調用leave_resion退出臨界區時才會發生。
現在考慮兩個進程幾乎同時調用enter_resion的情況。
它們都將自己的進程號存入turn,但只有后被保存進去的進程號才有效,前一個因被重寫而丟失。

(5)TSL指令
某些計算機中,特別是那些設計為多處理器的計算機,都有下面一條指令。
TSL RX, lock
稱為測試并加鎖(Test and Set Lock),它將一個內存字lock讀到寄存器RX中,然后在該內存地址上存一個非零值。
讀字和寫字操作保證是不可分割的,即該指令結束之前其他處理器均不允許訪問該內存字。
執行TSL指令的CPU將鎖住內存總線,以禁止其他CPU在本指令結束之前訪問內存。

著重說明一下,鎖住內存總線不同于屏蔽中斷。
屏蔽中斷,然后在讀內存字之后跟著寫操作并不能阻止總線上的第二個處理器在讀操作和寫操作之間訪問該內存字。
事實上,在處理器1上屏蔽中斷對處理器2根本沒有任何影響。
讓處理器2遠離內存直到處理器1完成的唯一方法就是鎖住總線。
這需要一個特殊的硬件設施。

信號量

信號量,使用一個整型變量來累計喚醒次數,供以后使用。
信號量的取值可以為0(表示沒有保存下來的喚醒操作)或者正值(表示有一個或多個喚醒操作)。

對一個信號量執行down操作,則是檢查其值是否大于0。
若該值大于0,則將其值減1(即用掉一個保存的喚醒信號)并繼續。
若該值為0,則進程將睡眠,而且此時down操作并未結束。
up操作對信號量的值增1。

檢查數值,修改變量值以及可能發生的睡眠操作均作為一個單一的,不可分割的原子操作完成。
保證一旦一個信號量操作開始,則在該操作完成或阻塞之前,其他進程均不允許訪問該信號量。
這種原子性對于解決同步問題和避免競爭條件是絕對必要的。

如果一個或多個進程在該信號量上睡眠,無法完成一個先前的down操作,則由系統選擇其中的一個(如隨機挑選)并允許該進程完成它的down操作。
于是,對一個有進程在其上睡眠的信號量執行一次up操作之后,該信號量的值仍舊是0,但在其上睡眠的進程卻少了一個。
信號量的值增1和喚醒一個進程同樣也是不可分割的。

為確保信號量能正確工作,最重要的是要采用一種不可分割的方式來實現它。
通常是將up和down作為系統調用實現,而且操作系統只需在執行以下操作時暫時屏蔽全部中斷:測試信號量,更新信號量,以及在需要時使某個進程睡眠。
由于這些動作只需要幾條指令,所以屏蔽中斷不會帶來什么副作用。
如果使用多個CPU,則每個信號量應由一個鎖變量進行保護。
通過TSL或XCHG指令來確保同一時刻只有一個CPU在對信號量進行操作。

互斥量

如果不需要信號量的計數能力,有時可以使用信號量的一個簡化版本,稱為互斥量(mutex)。
互斥量僅僅適用于管理共享資源或一小段代碼。
互斥量是一個可以處于兩態之一的變量:解鎖和加鎖。

當一個進程需要訪問臨界區時,它調用nutex_lock。
如果該互斥量當前是解鎖的(即臨界區可用),此調用成功,調用進程可以自由進入該臨界區。
另一方面,如果該互斥量已經加鎖,調用進程被阻塞,知道臨界區中的線程完成并調用mutex-unlock。
如果多個線程被阻塞在該互斥量上,將隨機選擇一個線程并允許它獲得鎖。

共享

在用戶級線程包中,多個線程訪問同一個互斥量是沒有問題的,因為所有線程都在一個公共地址空間中操作。
但是諸如Peterson算法和信號量,都有一個未說明的前提,即這些多個進程至少應該訪問一些共享內存,也許僅僅是一個字。
有兩種方案。第一種,有些共享數據結構,如信號量,可以存放在內核中,并且只能通過系統調用來訪問。
第二種,多數現代操作系統,提供一種方法,讓進程與其他進程共享其部分地址空間。
在這種方法中,緩沖區和其他數據結構可以共享。
在最壞的情形下,如果沒有可共享的途徑,則可以使用共享文件。

如果兩個或多個進程共享其全部或大部分地址空間時,進程和線程之間的差別就變得模糊起來,但無論怎樣,兩者的差別還是有的。
共享一個公共地址空間的兩個進程仍舊有各自的打開文件,報警定時器以及其他一些單個進程的特性,而在單個進程中的線程,則共享進程全部特性。
另外,共享一個公共地址空間的多個線程絕不會擁有用戶級線程的效率。

管程

使用信號量時要非常小心,一處很小的錯誤將導致很大的麻煩。
為了更易于編寫正確的程序,Brinch Hansen和Hoare提出了一種高級同步原語,稱為管程。
一個管程是一個由過程,變量及數據結構等組成的一個集合,它們組成一個特殊的模塊或軟件包。
進程可在任何需要的時候調用管程中的進程,但它們不能再管程之外聲明的過程中直接訪問管程內的數據結構。

管程有一個很重要的特性,即任一時刻管程中只能有一個活躍進程,這一特性使管程能有效的完成互斥。
管程是編程語言的組成部分,編譯器知道它們的特殊性,因此可以采用與其他過程調用不同的方法來處理對管程的調用。
進入管程時的互斥由編譯器負責,但通常的做法是用一個互斥量或二元信號量。
因為是由編譯器而非程序員來安排互斥,所以出錯的可能性要小得多。
在任一時刻,寫管程的人無須關心編譯器是如何實現互斥的。
他只需知道將所有的臨界區轉換成管程過程即可,絕不會有兩個進程同時執行臨界區中的代碼。

消息傳遞

這種進程間通信的方法使用兩條原語send和service,它們像信號量而不像管程,是系統調用而不是語言成分。
消息傳遞系統面臨著許多信號量和管程所未涉及的問題和設計難點,特別是位于網絡中不同機器上的通信進程的情況。
例如,消息有可能被網絡丟失。為了防止消息丟失,發送方和接收方可以達成如下一致:
一旦接收到消息,接收方馬上回送一條特殊的確認消息。
如果發送方在一段時間間隔內未收到確認,則重發消息。

現在考慮消息本身被正確接收,而返回給發送者的確認信息丟失的情況。
發送者將重發信息,這樣接收者將收到兩次相同的消息。
對于接收者來說,如何區分新的消息和一條重發的老消息是非常重要的。
通常采用在每條原始消息中嵌入一個連續的序號來解決此問題。
如果接收者收到一條消息,它具有與前面某一條消息一樣的序號,就知道這條消息是重復的,可以忽略。

屏障

在有些應用中,劃分了若干階段。
并且規定,除非所有的進程都就緒準備著手下一個階段,否則任何進程都不能進入下一個階段。
可以通過在每個階段的結果安置屏障(barrier)來實現這種行為。
當一個進程到達屏障時,它就被屏障阻攔,直到所有進程都到達該屏障為止。

參考

現代操作系統 2.3

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容