【轉】進程間通信(IPC):共享內存和消息隊列原理詳解

操作系統內的併發執行進程可以是獨立的也可以是協作的:

  • 如果一個進程不能影響其他進程或受其他進程影響,那麼該進程是獨立的,換句話說,不與任何其他進程共享數據的進程是獨立的;
  • 如果一個進程能影響其他進程或受其他進程所影響,那麼該進程是協作的。換句話說,與其他進程共享數據的進程爲協作進程。


提供環境允許進程協作,具有許多理由:

  • 信息共享:由於多個用戶可能對同樣的信息感興趣(例如共享文件),所以應提供環境以允許併發訪問這些信息。
  • 計算加速:如果希望一個特定任務快速運行,那麼應將它分成子任務,而每個子任務可以與其他子任務一起並行執行。注意,如果要實現這樣的加速,那麼計算機需要有多個處理核。
  • 模塊化:可能需要按模塊化方式構造系統,即將系統功能分成獨立的進程或線程。
  • 方便:即使單個用戶也可能同時執行許多任務。例如,用戶可以並行地編輯、收聽音樂、編譯。


協作進程需要有一種進程間通信機制(簡稱 IPC),以允許進程相互交換數據與信息。進程間通信有兩種基本模型:共享內存和消息傳遞(消息隊列):

  • 共享內存模型會建立起一塊供協作進程共享的內存區域,進程通過向此共享區域讀出或寫入數據來交換信息。
  • 消息傳遞模型通過在協作進程間交換消息來實現通信。


圖 1 給出了這兩種模型的對比。


通信模型
圖 1 通信模型

 

上述兩種模型在操作系統中都常見,而且許多系統也實現了這兩種模型。消息傳遞對於交換較少數量的數據很有用,因爲無需避免衝突。對於分佈式系統,消息傳遞也比共享內存更易實現。共享內存可以快於消息傳遞,這是因爲消息傳遞的實現經常採用系統調用,因此需要消耗更多時間以便內核介入。與此相反,共享內存系統僅在建立共享內存區域時需要系統調用;一旦建立共享內存,所有訪問都可作爲常規內存訪問,無需藉助內核。

對具有多個處理核系統的最新研究表明,在這類系統上,消息傳遞的性能要優於共享內存。共享內存會有高速緩存一致性問題,這是由共享數據在多個高速緩存之間遷移而引起的。隨着系統的處理核的數量的日益增加,可能導致消息傳遞作爲 IPC 的首選機制。

共享內存系統

採用共享內存的進程間通信,需要通信進程建立共享內存區域。通常,共享內存區域駐留在創建共享內存段的進程地址空間內。其他希望使用這個共享內存段進行通信的進程應將其附加到自己的地址空間。

回憶一下,通常操作系統試圖阻止一個進程訪問另一進程的內存。共享內存需要兩個或更多的進程同意取消這一限制,這樣它們通過在共享區域內讀出或寫入來交換信息。數據的類型或位置取決於這些進程,而不是受控於操作系統。另外,進程負責確保它們不向同一位置同時寫入數據。

爲了說明協作進程的概念,我們來看一看生產者-消費者問題,這是協作進程的通用範例。生產者進程生成信息,以供消費者進程消費。例如,編譯器生成的彙編代碼可供彙編程序使用,而且彙編程序又可生成目標模塊以供加載程序使用。

生產者-消費者問題同時還爲客戶機-服務器範例提供了有用的比喻。通常,將服務器當作生產者,而將客戶機當作消費者。例如,一個 Web 服務器生成(提供)HTML 文件和圖像,以供請求資源的 Web 客戶瀏覽器使用(讀取)。

解決生產者-消費者問題的方法之一是採用共享內存。爲了允許生產者進程和消費者進程併發執行,應有一個可用的緩衝區,以被生產者填充和被消費者清空。這個緩衝區駐留在生產者進程和消費者進程的共享內存區域內。當消費者使用一項時,生產者可產生另一項。生產者和消費者必須同步,這樣消費者不會試圖消費一個尚未生產出來的項。

緩衝區類型可分兩種:

  • 無界緩衝區沒有限制緩衝區的大小。消費者可能不得不等待新的項,但生產者總是可以產生新項。
  • 有界緩衝區假設固定大小的緩衝區。對於這種情況,如果緩衝區空,那麼消費者必須等待;並且如果緩衝區滿,那麼生產者必須等待。


下面深入分析,有界緩衝區如何用於通過共享內存的進程間通信。以下變量駐留在由生產者和消費者共享的內存區域中:


 
  1. #define BUFFER_SIZE 10
  2. typedef struct {
  3. ...
  4. }item;
  5. item buffer [BUFFER_SIZE];
  6. int in = 0;
  7. int out = 0;

共享 buffer 的實現採用一個循環數組和兩個邏輯指針:in 和 out。變量 in 指向緩衝區的下一個空位;變量 out 指向緩衝區的第一個滿位。當 in == out 時,緩衝區爲空;當 (in + 1)%BUFFER SIZE == out 時,緩衝區爲滿。

生產者進程和消費者進程的代碼爲:


 
  1. //生產者進程
  2. while (true) {
  3. /* produce an item in next .produced */
  4. while (((in + 1) %BUFFER_SIZE) == out)
  5. ;/* do nothing */
  6. buffer [in] = next_produced;
  7. in = (in + 1) % BUFFER.SIZE;
  8. }
  9.  
  10. //消費者進程
  11. item next_consumed;
  12. while (true) {
  13. while (in == out)
  14. ;/* do nothing */
  15. next_consumed = buffer[out];
  16. out = (out + 1) %BUFFER_SIZE;
  17. /* consume the item in next-consumed */
  18. }

生產者進程有一個局部變量 next_produced,以便存儲生成的新項;消費者進程有一個局部變量 next_consumed,以便存儲所要使用的新項。

消息傳遞系統(消息隊列)

前面講解了協作進程如何可以通過共享內存進行通信。此方案要求這些進程共享一個內存區域,並且應用程序開發人員需要明確編寫代碼,以訪問和操作共享內存。達到同樣效果的另一種方式是,操作系統提供機制,以便協作進程通過消息傳遞功能進行通信。

消息傳遞提供一種機制,以便允許進程不必通過共享地址空間來實現通信和同步。對分佈式環境(通信進程可能位於通過網絡連接的不同計算機),這特別有用。

例如,可以設計一個互聯網的聊天程序以便聊天參與者通過交換消息相互通信。消息傳遞工具提供至少兩種操作:send(message) 和 receive(message)。

進程發送的消息可以是定長的或變長的。如果只能發送定長消息,那麼系統級實現就簡單。不過,這一限制使得編程任務更加困難。相反,變長消息要求更復雜的系統級實現,但是編程任務變得更爲簡單。在整個操作系統設計中,這種折中很常見。

如果進程 P 和 Q 需要通信,那麼它們必須互相發送消息和接收消息:它們之間要有通信鏈路。該鏈路的實現有多種方法。這裏不關心鏈路的物理實現(如共享內存、硬件總線或網絡等),而只關心鏈路的邏輯實現。

這裏有幾個方法,用於邏輯實現鏈路和操作 send()/receive():

  • 直接或間接的通信;
  • 同步或異步的通信;
  • 自動或顯式的緩衝;


下面研究這些特徵的相關問題。

命名

需要通信的進程應有一個方法,以便互相引用。它們可以使用直接或間接的通信。

對於直接通信,需要通信的每個進程必須明確指定通信的接收者或發送者。採用這種方案,原語 send() 和 receive() 定義如下:

  • send(P,message):向進程P發送 message。
  • receive(Q,message):從進程 Q 接收 message。


這種方案的通信鏈路具有以下屬性:

  • 在需要通信的每對進程之間,自動建立鏈路。進程僅需知道對方身份就可進行交流。
  • 每個鏈路只與兩個進程相關。
  • 每對進程之間只有一個鏈路。


這種方案展示了尋址的對稱性,即發送和接收進程必須指定對方,以便通信。這種方案的一個變形採用尋址的非對稱性,即只要發送者指定接收者,而接收者不需要指定發送者。採用這種方案,原語 send() 和 receive() 的定義如下:

  • send(P,message):向進程 P 發送 message。
  • receive(id, message):從任何進程,接收 message,這裏變量 id 被設置成與其通信進程的名稱。


這兩個方案(對稱和非對稱的尋址)的缺點是:生成進程定義的有限模塊化。更改進程的標識符可能需要分析所有其他進程定義。所有舊的標識符的引用都應找到,以便修改成爲新標識符。通常,任何這樣的硬編碼技術(其中標識符需要明確指定),與下面所述的採用間接的技術相比要差。

在間接通信中,通過郵箱或端口來發送和接收消息。郵箱可以抽象成一個對象,進程可以向其中存放消息,也可從中刪除消息,每個郵箱都有一個唯一的標識符。

例如,POSIX 消息隊列採用一個整數值來標識一個郵箱。一個進程可以通過多個不同郵箱與另一個進程通信,但是兩個進程只有擁有一個共享郵箱時才能通信。原語 send() 和 receive() 定義如下:

  • send(A, message):向郵箱 A 發送 message。
  • receive(A,message):從郵箱 A 接收 message。


對於這種方案,通信鏈路具有如下特點:

  • 只有在兩個進程共享一個郵箱時,才能建立通信鏈路。
  • 一個鏈路可以與兩個或更多進程相關聯。
  • 兩個通信進程之間可有多個不同鏈路,每個鏈路對應於一個郵箱。


現在假設進程 P1、P2 和 P3 都共享郵箱 A。進程 P1 發送一個消息到 A,而進程 P2 和 P3 都對 A 執行 receive()。哪個進程會收到 P1 發送的消息?

答案取決於所選擇的方案:

  • 允許一個鏈路最多隻能與兩個進程關聯。
  • 允許一次最多一個進程執行操作 receive ()。
  • 允許系統隨意選擇一個進程以便接收消息(即進程 P2 和 P3 兩者之一都可以接收消息,但不能兩個都可以)。系統同樣可以定義一個算法來選擇哪個進程是接收者(如輪轉,進程輪流接收消息)。系統可以讓發送者指定接收者。


郵箱可以爲進程或操作系統擁有。如果郵箱爲進程擁有(即郵箱是進程地址空間的一部分),那麼需要區分所有者(只能從郵箱接收消息)和使用者(只能向郵箱發送消息)。由於每個郵箱都有唯一的標識符,所以關於誰能接收發到郵箱的消息沒有任何疑問。當擁有郵箱的進程終止,那麼郵箱消失。任何進程後來向該郵箱發送消息,都會得知郵箱不再存在。

與此相反,操作系統擁有的郵箱是獨立存在的;它不屬於某個特定進程。因此,操作系統必須提供機制,以便允許進程進行如下操作:

  1. 創建新的郵箱。
  2. 通過郵箱發送和接收消息。
  3. 刪除郵箱。


創建新郵箱的進程缺省爲郵箱的所有者。開始時,所有者是唯一能通過該郵箱接收消息的進程。不過,通過系統調用,擁有權和接收特權可以傳給其他進程。當然,這樣可以導致每個郵箱具有多個接收者。

同步

進程間通信可以通過調用原語 send() 和 receive() 來進行。實現這些原語有不同的設計方案。消息傳遞可以是阻塞或非阻塞,也稱爲同步或異步:

  • 阻塞發送:發送進程阻塞,直到消息由接收進程或郵箱所接收。
  • 非阻塞發送:發送進程發送消息,並且恢復操作。
  • 阻塞接收:接收進程阻塞,直到有消息可用。
  • 非阻塞接收:接收進程收到一個有效消息或空消息。


不同組合的 send() 和 receive() 都有可能。當 send() 和 receive() 都是阻塞的,則在發送者和接收者之間就有一個交會。當採用阻塞的 send() 和 receive()時,生產者-消費者問題的解決就簡單了。生產者僅需調用阻塞 send() 並且等待,直到消息被送到接收者或郵箱。同樣,當消費者調用 receive() 時,它會阻塞直到有一個消息可用。這種情況如下代碼所示:


 
  1. //採用消息傳遞的生產者進程
  2. message next_produced;
  3. while (true) {
  4. /* produce an item in next_produced */
  5. send (next_produced);
  6. }
  7. //採用消息隊列的消費者進程
  8. message next_consumed;
  9. while (true) {
  10. receive (next_consumed);
  11. /* consume the item in next .consumed */
  12. }

緩存

不管通信是直接的還是間接的,通信進程交換的消息總是駐留在臨時隊列中。簡單地講,隊列實現有三種方法:

  1. 零容量:隊列的最大長度爲 0。因此,鏈路中不能有任何消息處於等待。對於這種情況,發送者應阻塞,直到接收者接收到消息。
  2. 有限容量:隊列長度爲有限的 n。因此,最多只能有 n 個消息駐留其中。如果在發送新消息時隊列未滿,那麼該消息可以放在隊列中(或者複製消息或者保存消息的指針),且發送者可以繼續執行而不必等待。如果鏈路已滿,那麼發送者應阻塞,直到隊列空間有可用的爲止。
  3. 無限容量:隊列長度可以無限,因此,不管多少消息都可在其中等待。發送者從不阻塞。


零容量情況稱爲無緩衝的消息系統,其他情況稱爲自動緩衝的消息系統。

 

 

文章出處:http://c.biancheng.net/view/1208.html

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