在一文中我們討論了在一個共享內存的環境中進行通訊。這要求了進程間共享一塊內存區域,並且可以讓應用編程人員通過代碼訪問和操縱這塊共享內存。達到相同效果的另一種方法是,作業系統提供用於進程之間通過消息傳遞的工具來實現相互通信的。
消息傳遞提供了一種機制,這種機制允許進程在不分享相同地址空間的情況下,通訊和同步他們的行為。這在分布式系統中特別有用,因為通訊的進程為了連結在網絡中的不同計算機上。比方說,一個網際網路聊天程序被設計用於聊天的參與方與另一方交換信息。
一個消息傳遞機制至少提供兩種操作:
進程發送的消息的大小可以是固定的,也可以是可變的。如果只能發送固定大小的消息,則系統級實現是直接的。但是,這種限制使編程任務更加困難。相反,可變大小的消息需要更複雜的系統級實現,但編程任務變得更簡單。這是在整個作業系統設計中常見的一種折衷。
如果進程P和Q想到通信,他們必須可以相互發送和接受消息:它們兩個之間必須要有一條通訊鏈路。這個鏈路可以由好幾種方式實現。這裡我們關注的不是鏈路的物理實現而是邏輯上的實現。下下面是幾種邏輯上的實現鏈路的方法。
想要通訊的進程必須要有一種互相指待的方法。他們可以直接或間接通訊。
在直接通訊中,每個想要通訊的進程都必須顯示地對通訊和發送或接受方命名。在這樣的模式下,原本的send和receives會定義如下:
這樣模式下的通訊鏈路有如下屬性:
該方案在尋址方面表現出對稱性。也就是說,發送方進程和接收方進程都必須命名對方才能進行通信。該方案的變體在尋址中採用不對稱性。在這裡,只有發件人為收件人命名;收件人無需為發件人命名。在這個方案中send()和receive()原語定義如下:
這兩種方式(對稱和非對稱)的缺點就是限制了進程定義的模塊化程度。更改一個進程的名稱可能必須要檢查其它所有進程的定義。必須要發現所有對原名稱的引用,以便於更換為新名稱。總的來說,任何這樣硬編碼的技術,也是就是說標識符必須顯性陳述的,都不是大家所想要的。
通過非直接通信,消息可以通過mailboxs或ports來發送和接收。一個mailbox可以被認為是一個抽象的目標,在這裡消息可以被進程放入或移除。每個mailbox有一個獨一無二的標識符。比方說,POSIX消息隊列使用一個整數值來標識mailbox。一個進程可以通過不同的mailbox進行通訊,但兩個進程只有共享一個mailbox是才可以通信。在這個方案中send()和receive()原語定義如下:
在這個方案中,通信鏈路有如下屬性:
現在,假定進程P1,P2,P3,共享mailbox A。進程P1發了一個消息給A,P2和P3執行receive()從A那裡接收到消息。哪一個進程會接收到來自P1的消息。答案取決於如下挑選的方法:
一個mailbox可以被一個進程或作業系統所擁有。如果mailbox是歸進程所有的,那麼我們把他們區分為owner(只能從mailbox接收消息)和user(只能向mail發送消息)。因為每個mailbox都有一個獨一無二的owner,哪個進程應該接收發送至mailbox的消息就不會有困擾。但一個擁有mailbox的進程終止時,mailbox就沒有了。任何之後發送給mailbox消息的進程必須被告知mailbox不再存在。
相反,一個被作業系統擁有的mailbox有他自己的地址空間。它是獨立並且不附加於任何指定的進程。作業系統然後必須提供一個機制,這個機制允許進程的如下行為:
創建mailbox的進程默認是mailbox的owner。一開始,owner只能從這個mailbox接收消息。然而,ownership和接收特權可以通過過適當的系統調用而傳遞出去。當然,這條規定可能會導致每個mailbox裡都有大量的接收者。
進程間的通信可以通過調用原語send()和receive()來發生。對於每個原語的實現有著不同的設計選擇,消息傳遞可以是阻塞或非阻塞的,也被稱為同步和異步。
send()和receives()存在著不同形式的組合。當send()和receive()都阻塞時,我們在發送者和接收者之間有一個集合點。當使用阻塞的send()和receive()語句時,生產者-消費者問題的解決方案就變得微不足道了。生產者僅調用阻塞的send()調用,並等待直到消息傳遞到接收者或郵箱。同樣,當使用者調用receive()時,它將阻塞直到有消息可用為止。如下圖所示:
無論通訊是直接還是間接的,通訊的進程的消息交換是位於一個臨時的隊列中的。基本上,這樣的隊列可以有三種方式實現:
zero-capacity有時被指待沒有緩衝的消息系統。其他幾種情況指待帶有自動緩衝的系統。