《現代操作系統》讀書筆記之——進程間通信1

    很多時候,進程需要和其他的進程進行通信。比如shell中的管道命令:ps -ef | grep nginx,一個命令的輸出,作爲另一個進程的輸入,這就是進程間通信(Interprocess Communication)。

    進程間通信主要需要解決三個問題:

    1.一個進程如何給另一個進程傳遞信息

    2.如何確保進程之間不互相干擾、妨礙

    3.當進程間出現依賴關係時,該如何處理。

    儘管這裏討論的是進程之間的通信,但其實對於線程來說,他們之間的通信需要解決後兩個問題。由於多個線程處在相同的進程,因此也處在同一個地址空間中,所以第一個問題自然很好解決。但是第二個、第三個問題還是存在的,當然解決的方案其實與進程間通信在處理這兩個問題上採取的方案也是類似的。下面的內容會涉及上面的三個問題。

    1.競爭條件(Race Condition)

    在一些操作系統中,多個進程會共享一部分內存,每個進程都可以對他們進行讀寫操作。共享的內存有可能再內存中,也可能是一個共享的文件。爲了看看進程間通信之間的競爭條件,舉個簡單的例子加以說明:打印池。假如一個進程想打印一個文件,於是他將文件名輸入打印池目錄中。有一個負責打印的進程——打印機守護進程——每隔一段時間會查看一下打印池目錄中有沒有需要打印的文件。有的話就打印,沒有拉到。

    打印機目錄的示意圖如下:

    圖中的每個小格子可以存放一個待打印的文件名(實際上應該是需要打印的文件的指針,這裏只是爲了說明問題做的假設)。同樣,還需要假設兩個共享的變量:一個叫out存儲下一個輪到打印的文件的文件名;另一個叫in存儲上圖中下一個可以存放待打印文件文件名的小空格。這兩個變量可能被存儲再一個文件中,而這個文件共享給了所有的進程。

    上圖所示的時刻,單元1、2、3已經空了,也就是說,之前存在裏面的文件已經打印了。而5-9號空格還是空的,也就是說接下來需要打印的文件依次存放在下面的單元中。這一時刻,變量in存儲的應該是5。假設這時候,進程A讀取變量in,得到的值是5。於是,進程A將這個值存儲到他的局部變量next_free_slot中,這時候,恰好CPU時間片到了,進程A重新回到可運行狀態,而此時進程B獲得了時間片,開始運行,它也有文件需要打印,那麼它讀取in,獲得的值是5,於是它也將5這個值存儲進他的局部變量next_free_slot中。然後它根據這個變量的值,將它需要打印的文件的文件名,假設是ccc.txt存儲進單元格5中。等待打印。然後進程B開始幹別的事情了。又過了一段時間,進程B的CPU時間片用完了。而進程A又獲得了CPU時間片。它開始繼續運行,先讀取next_free_slot的值,得到的是5,於是,可怕的事情發生了,它把自己需要打印的文件名寫入單元格5,也就是說,覆蓋掉了進程B之前放置的需要打印的文件ccc.txt。進程B可能一直在等待打印的輸出,但是永遠都等不到了。

 

 2.臨界區(Critical Regions)

    如何避免競爭條件?避免出現這種麻煩,或者說在任何涉及到共享內存、文件或者其他一切共享資源的情況下的處理策略是防止多於一個進程在同一時刻讀寫共享數據。換句話說,互斥(Mutual Exclusion)。 

  對於大多數的操作系統來說,選擇合適的原語來實現互斥是一個主要的涉及手段。臨界區(Critical Region/Critical Section)是指進程中那些訪問共享內存並且可能導致競爭狀態的部分。要防止競爭狀態的出現,我們可以理解成要防止兩個需要共享同一塊內存的進程不要同時進入自己的臨界區。

    但是僅僅如此是不夠的,需要堅持下面的四個原則:

  • 不允許兩個進程同時進入各自的臨界區
  • 不要對任何關於CPU速度和CPU數量的假設
  • 不允許運行在自己臨界區外的進程阻塞別的進程
  • 不允許任何進程無休止的等待進入自己的臨界區

    上面描述的是一種抽象的解決問題的思路,可以簡單的使用一個圖作爲例子來描述使用臨界區則個概念來體現互斥操作。

   3.實現互斥的幾種方案之——禁用中斷(Disable Interrupt)

    在單處理器系統中,這時最爲簡單的方案。也就是當進程進入臨界區後,禁用中斷,等它離開臨界區再開啓。CPU能夠切換進程的前提是可以發生時鐘中斷或者其他中斷。這就意味着,當一個進程進入臨界區,他就不可能被剝奪CPU使用權,直到它離開臨界區。

    這種方案顯然沒有吸引力,其缺點有兩點:

  • 給用戶進程權利去禁用中斷是不明智的,如果有進程禁用了中斷並且再也不開啓,那系統最後將會死掉。
  • 如果多個CPU,當一個進程禁用中斷,只是它正在使用的CPU會被影響,而此時,另外的CPU上的進程可能還是會訪問臨界區,進入修改共享內存的內容 

   儘管如此,對於操作系統的內核來說,這卻是一個很方便的技巧。當內核正在更新就緒進程表時,它可能會在幾個指令的時間內禁用中斷。

   此外,現在使用這種方式實現互斥越累越少了,畢竟,現在的機器都是多核的。

    4.實現互斥的幾種方案之——鎖變量(Lock Variables)

    這種方案的原理是設計一個所變量,初始化爲0,某個進程要進入臨界區,先查看該變量的值,如果是0,則可以進入臨界區,並且將該變量的值設置爲1,等它離開臨界區再將這個變量設置爲0.簡而言之,這個變量爲0代表沒有進程正處在臨界區,1代表有進程處在臨界區。

    但是這個方案存在的問題和前面舉例的那個打印機問題一樣。

    5.實現互斥的幾種方案之——嚴格變更(strict alternation)

    這個方案的基本原理如下面的兩段代碼所示:

  1. while(TRUE) { 
  2.     while(turn != 0) { 
  3.     } 
  4.     critical_region(); 
  5.     turn = 1; 
  6.     noncritical_region(); 
  7.  
  8. while(TRUE) { 
  9.     while(turn != 1) { 
  10.     } 
  11.     critical_region(); 
  12.     turn = 0; 
  13.     noncritical_region(); 

    整型變量turn,用來跟蹤到底那個進程當前可以進入自己的臨界區。對於進程0來說,不斷檢測turn是否等於0,如果是則它可以進入自己的臨界區,如果turn不等於0,那麼它就繼續等待,並且持續的檢測這個變量的值。如果進程0進入了臨界區,當它出來之後,它將turn設置爲1,也就是輪到進程1(另外的一個進程)進入臨臨界區了。

    對於進程1來說,它執行的是第二段代碼,但是原理一樣。對於進程來說,必須不斷的檢查一個變量的值來判斷是否輪到自己進入臨界區,這種情況叫做忙等待(busy waiting)。這種情況是應該被避免的,因爲它很浪費CPU的時間。使用忙等待實現的鎖,叫做自旋鎖(spin lock)。

    這種方案還有一個問題。如果兩個進程執行的速度差距很大。我們假設進程0執行速度遠遠快於進程1。一開始,turn的值爲0,進程0進入臨界區,很快,進程0離開臨界區,將turn設置爲1,這時候,進程1開始進入臨界區,而此時進程0可能會已經離開非臨界區,並且又持續監測turn的值。進程1一離開臨界區,將turn再次設置爲0,進程0又進入臨界區,而進程1進入非臨界區。由於進程0執行速度很快,它很快再次執行完臨界區的代碼,將turn設置爲1,然後執行非臨界區代碼。由於進程0非常快,進程1很慢,很可能進程1還在第一次的非臨界區,這時,進程0已經執行完第二次非臨界區的代碼,而此時turn還是它剛纔設置的值——0,而其實另一個進程,進程1,並沒有在臨界區,而是在非臨界區掙扎。這種情況下,進程0卻只能乾等。

    所以,很明顯,這種方案非常不適合多個進程之間執行速度差距很大的情況。而我們這一節的標題叫做strict alternation,舉個例子說,就是像前面提到的打印池,strict alternation不允許一個進程一次提交大於1個需要打印的文件。這可能也是爲了不要是進城之間的執行速度差距過大吧。

    6.實現進程互斥的幾種方案之——Peterson方案

    這個方案的歷史我們這裏略過不說,其基本原理如下面的實例代碼:

  1. #define FALSE 0 
  2. #define TRUE 1 
  3. #define N 2 
  4.  
  5. int turn; 
  6. int interested[N]; 
  7.  
  8. void enter_region(int process) { 
  9.     int other; 
  10.     other = 1 - process; 
  11.     interested[process] = TRUE; 
  12.     turn = process; 
  13.     while(turn==process && intrested[other] == TRUE) 
  14. void leave_region(int process) { 
  15.     intrested[process] = FALSE; 

    進程在進入 臨界區之前,需要執行enter_region函數,並將自己的進程號作爲參數傳遞進去。當進程離開臨界區,需要執行和leave_region函數。

    我們具體看看原理:對於進程0和進程1。一開始誰都沒有在臨界區,現在進程0調用enter_region函數。首先,進程0將對應於自己的數組元素intrested[0]設置爲TRUE,並且將turn的值設置爲自己的進程號,也就是0。然後這一步很關鍵,做一個循環的檢查,如果輪到了自己(turn==process)但是另一個進程也很感興趣(intrested[other]==TRUE)。如果使這樣的話,那麼什麼也不做,等着。否則的話,真正開始進入臨界區。

    如果進程0執行完了臨界區代碼,那麼就將intrested[0]設置爲FALSE,也就是說表示自己此刻不需要再進入臨界區了。

    這種方案會不會發生兩個進程都在乾等着,最後死鎖呢?我們看看。假設進程0和進程1幾乎同時執行到turn=process代碼,假設進程0先執行這條語句,緊接着進程1也執行這條語句,從而擦除之前進程0設置的值。那麼接下來進程0就只能在那個while循環裏面等着了,而進程1則真的進入自己的臨界區。這也是爲什麼我在上面說這個裏層的while循環很重要了。

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