操做系統內的併發執行進程能夠是獨立的也能夠是協做的:算法
提供環境容許進程協做,具備許多理由:編程
協做進程須要有一種進程間通訊機制(簡稱 IPC),以容許進程相互交換數據與信息。進程間通訊有兩種基本模型:共享內存和消息傳遞(消息隊列):數組
圖 1 給出了這兩種模型的對比。瀏覽器
上述兩種模型在操做系統中都常見,並且許多系統也實現了這兩種模型。消息傳遞對於交換較少數量的數據頗有用,由於無需避免衝突。對於分佈式系統,消息傳遞也比共享內存更易實現。共享內存能夠快於消息傳遞,這是由於消息傳遞的實現常常採用系統調用,所以須要消耗更多時間以便內核介入。與此相反,共享內存系統僅在創建共享內存區域時須要系統調用;一旦創建共享內存,全部訪問均可做爲常規內存訪問,無需藉助內核。對具備多個處理核系統的最新研究代表,在這類系統上,消息傳遞的性能要優於共享內存。共享內存會有高速緩存一致性問題,這是由共享數據在多個高速緩存之間遷移而引發的。隨着系統的處理核的數量的日益增長,可能致使消息傳遞做爲 IPC 的首選機制。緩存
採用共享內存的進程間通訊,須要通訊進程創建共享內存區域。一般,共享內存區域駐留在建立共享內存段的進程地址空間內。其餘但願使用這個共享內存段進行通訊的進程應將其附加到本身的地址空間。回憶一下,一般操做系統試圖阻止一個進程訪問另外一進程的內存。共享內存須要兩個或更多的進程贊成取消這一限制,這樣它們經過在共享區域內讀出或寫入來交換信息。數據的類型或位置取決於這些進程,而不是受控於操做系統。另外,進程負責確保它們不向同一位置同時寫入數據。爲了說明協做進程的概念,咱們來看一看生產者-消費者問題,這是協做進程的通用範例。生產者進程生成信息,以供消費者進程消費。例如,編譯器生成的彙編代碼可供彙編程序使用,並且彙編程序又可生成目標模塊以供加載程序使用。生產者-消費者問題同時還爲客戶機-服務器範例提供了有用的比喻。一般,將服務器看成生產者,而將客戶機看成消費者。例如,一個 Web 服務器生成(提供)HTML 文件和圖像,以供請求資源的 Web 客戶瀏覽器使用(讀取)。解決生產者-消費者問題的方法之一是採用共享內存。爲了容許生產者進程和消費者進程併發執行,應有一個可用的緩衝區,以被生產者填充和被消費者清空。這個緩衝區駐留在生產者進程和消費者進程的共享內存區域內。當消費者使用一項時,生產者可產生另外一項。生產者和消費者必須同步,這樣消費者不會試圖消費一個還沒有生產出來的項。緩衝區類型可分兩種:服務器
下面深刻分析,有界緩衝區如何用於經過共享內存的進程間通訊。如下變量駐留在由生產者和消費者共享的內存區域中:網絡
#define BUFFER_SIZE 10 typedef struct { ... }item; item buffer [BUFFER_SIZE]; int in = 0; int out = 0;
共享 buffer 的實現採用一個循環數組和兩個邏輯指針:in 和 out。變量 in 指向緩衝區的下一個空位;變量 out 指向緩衝區的第一個滿位。當 in == out
時,緩衝區爲空;當 (in + 1)%BUFFER SIZE == out
時,緩衝區爲滿。生產者進程和消費者進程的代碼爲:併發
//生產者進程 while (true) { /* produce an item in next .produced */ while (((in + 1) %BUFFER_SIZE) == out) ;/* do nothing */ buffer [in] = next_produced; in = (in + 1) % BUFFER.SIZE; } //消費者進程 item next_consumed; while (true) { while (in == out) ;/* do nothing */ next_consumed = buffer[out]; out = (out + 1) %BUFFER_SIZE; /* consume the item in next-consumed */ }
生產者進程有一個局部變量 next_produced,以便存儲生成的新項;消費者進程有一個局部變量 next_consumed,以便存儲所要使用的新項。異步
前面講解了協做進程如何能夠經過共享內存進行通訊。此方案要求這些進程共享一個內存區域,而且應用程序開發人員須要明確編寫代碼,以訪問和操做共享內存。達到一樣效果的另外一種方式是,操做系統提供機制,以便協做進程經過消息傳遞功能進行通訊。消息傳遞提供一種機制,以便容許進程沒必要經過共享地址空間來實現通訊和同步。對分佈式環境(通訊進程可能位於經過網絡鏈接的不一樣計算機),這特別有用。例如,能夠設計一個互聯網的聊天程序以便聊天參與者經過交換消息相互通訊。消息傳遞工具提供至少兩種操做:send(message) 和 receive(message)。進程發送的消息能夠是定長的或變長的。若是隻能發送定長消息,那麼系統級實現就簡單。不過,這一限制使得編程任務更加困難。相反,變長消息要求更復雜的系統級實現,可是編程任務變得更爲簡單。在整個操做系統設計中,這種折中很常見。若是進程 P 和 Q 須要通訊,那麼它們必須互相發送消息和接收消息:它們之間要有通訊鏈路。該鏈路的實現有多種方法。這裏不關心鏈路的物理實現(如共享內存、硬件總線或網絡等),而只關心鏈路的邏輯實現。這裏有幾個方法,用於邏輯實現鏈路和操做 send()/receive():分佈式
下面研究這些特徵的相關問題。
須要通訊的進程應有一個方法,以便互相引用。它們可使用直接或間接的通訊。對於直接通訊,須要通訊的每一個進程必須明確指定通訊的接收者或發送者。採用這種方案,原語 send() 和 receive() 定義以下:
這種方案的通訊鏈路具備如下屬性:
這種方案展現了尋址的對稱性,即發送和接收進程必須指定對方,以便通訊。這種方案的一個變形採用尋址的非對稱性,即只要發送者指定接收者,而接收者不須要指定發送者。採用這種方案,原語 send() 和 receive() 的定義以下:
這兩個方案(對稱和非對稱的尋址)的缺點是:生成進程定義的有限模塊化。更改進程的標識符可能須要分析全部其餘進程定義。全部舊的標識符的引用都應找到,以便修改爲爲新標識符。一般,任何這樣的硬編碼技術(其中標識符須要明確指定),與下面所述的採用間接的技術相比要差。在間接通訊中,經過郵箱或端口來發送和接收消息。郵箱能夠抽象成一個對象,進程能夠向其中存放消息,也可從中刪除消息,每一個郵箱都有一個惟一的標識符。例如,POSIX 消息隊列採用一個整數值來標識一個郵箱。一個進程能夠經過多個不一樣郵箱與另外一個進程通訊,可是兩個進程只有擁有一個共享郵箱時才能通訊。原語 send() 和 receive() 定義以下:
對於這種方案,通訊鏈路具備以下特色:
如今假設進程 P一、P2 和 P3 都共享郵箱 A。進程 P1 發送一個消息到 A,而進程 P2 和 P3 都對 A 執行 receive()。哪一個進程會收到 P1 發送的消息?
答案取決於所選擇的方案:
郵箱能夠爲進程或操做系統擁有。若是郵箱爲進程擁有(即郵箱是進程地址空間的一部分),那麼須要區分全部者(只能從郵箱接收消息)和使用者(只能向郵箱發送消息)。因爲每一個郵箱都有惟一的標識符,因此關於誰能接收發到郵箱的消息沒有任何疑問。當擁有郵箱的進程終止,那麼郵箱消失。任何進程後來向該郵箱發送消息,都會得知郵箱再也不存在。與此相反,操做系統擁有的郵箱是獨立存在的;它不屬於某個特定進程。所以,操做系統必須提供機制,以便容許進程進行以下操做:
建立新郵箱的進程缺省爲郵箱的全部者。開始時,全部者是惟一能經過該郵箱接收消息的進程。不過,經過系統調用,擁有權和接收特權能夠傳給其餘進程。固然,這樣能夠致使每一個郵箱具備多個接收者。
進程間通訊能夠經過調用原語 send() 和 receive() 來進行。實現這些原語有不一樣的設計方案。消息傳遞能夠是阻塞或非阻塞,也稱爲同步或異步:
不一樣組合的 send() 和 receive() 都有可能。當 send() 和 receive() 都是阻塞的,則在發送者和接收者之間就有一個交會。當採用阻塞的 send() 和 receive()時,生產者-消費者問題的解決就簡單了。生產者僅需調用阻塞 send() 而且等待,直到消息被送到接收者或郵箱。一樣,當消費者調用 receive() 時,它會阻塞直到有一個消息可用。這種狀況以下代碼所示:
//採用消息傳遞的生產者進程 message next_produced; while (true) { /* produce an item in next_produced */ send (next_produced); } //採用消息隊列的消費者進程 message next_consumed; while (true) { receive (next_consumed); /* consume the item in next .consumed */ }
無論通訊是直接的仍是間接的,通訊進程交換的消息老是駐留在臨時隊列中。簡單地講,隊列實現有三種方法:
零容量狀況稱爲無緩衝的消息系統,其餘狀況稱爲自動緩衝的消息系統。