作者:Vamei 出處:http://www.cnblogs.com/vamei 歡迎轉載,也請保留這段聲明。謝謝!
我們在Linux信號基礎中已經說明,信號可以看作一種粗糙的進程間通信(IPC, interprocess communication)的方式,用以向進程封閉的內存空間傳遞信息。為了讓進程間傳遞更多的信息量,我們需要其他的進程間通信方式。這些進程間通信方式可以分為兩種:
- 管道(PIPE)機制。在Linux文本流中,我們提到可以使用管道將一個進程的輸出和另一個進程的輸入連接起來,從而利用文件操作API來管理進程間通信。在shell中,我們經常利用管道將多個進程連接在一起,從而讓各個進程協作,實現復雜的功能。
- 傳統IPC (interprocess communication)。我們主要是指消息隊列(message queue),信號量(semaphore),共享內存(shared memory)。這些IPC的特點是允許多進程之間共享資源,這與多線程共享heap和global data相類似。由於多進程任務具有並發性 (每個進程包含一個進程,多個進程的話就有多個線程),所以在共享資源的時候也必須解決同步的問題 (參考Linux多線程與同步)。
管道與FIFO文件
一個原始的IPC方式是所有的進程通過一個文件交流。比如我在紙(文件)上寫下我的名字和年紀。另一個人讀這張紙,會知道我的名字和年紀。他也可以在同一張紙上寫下他的信息,而當我讀這張紙的話,同樣也可以知道別人的信息。但是,由於硬盤讀寫比較慢,所以這個方式效率很低。那么,我們是否可以將這張紙放入內存中以提高讀寫速度呢?
在Linux文本流中,我們已經講解了如何在shell中使用管道連接多個進程。同樣,許多編程語言中,也有一些命令用以實現類似的機制,比如在Python子進程中使用Popen和PIPE,在C語言中也有popen庫函數來實現管道 (shell中的管道就是根據此編寫的)。管道是由內核管理的一個緩沖區(buffer),相當於我們放入內存中的一個紙條。管道的一端連接一個進程的輸出。這個進程會向管道中放入信息。管道的另一端連接一個進程的輸入,這個進程取出被放入管道的信息。一個緩沖區不需要很大,它被設計成為環形的數據結構,以便管道可以被循環利用。當管道中沒有信息的話,從管道中讀取的進程會等待,直到另一端的進程放入信息。當管道被放滿信息的時候,嘗試放入信息的進程會等待,直到另一端的進程取出信息。當兩個進程都終結的時候,管道也自動消失。
從原理上,管道利用fork機制建立(參考Linux進程基礎和Linux從程序到進程),從而讓兩個進程可以連接到同一個PIPE上。最開始的時候,上面的兩個箭頭都連接在同一個進程Process 1上(連接在Process 1上的兩個箭頭)。當fork復制進程的時候,會將這兩個連接也復制到新的進程(Process 2)。隨后,每個進程關閉自己不需要的一個連接 (兩個黑色的箭頭被關閉; Process 1關閉從PIPE來的輸入連接,Process 2關閉輸出到PIPE的連接),這樣,剩下的紅色連接就構成了如上圖的PIPE。
由於基於fork機制,所以管道只能用於父進程和子進程之間,或者擁有相同祖先的兩個子進程之間 (有親緣關系的進程之間)。為了解決這一問題,Linux提供了FIFO方式連接進程。FIFO又叫做命名管道(named PIPE)。
FIFO (First in, First out)為一種特殊的文件類型,它在文件系統中有對應的路徑。當一個進程以讀(r)的方式打開該文件,而另一個進程以寫(w)的方式打開該文件,那么內核就會在這兩個進程之間建立管道,所以FIFO實際上也由內核管理,不與硬盤打交道。之所以叫FIFO,是因為管道本質上是一個先進先出的隊列數據結構,最早放入的數據被最先讀出來(好像是傳送帶,一頭放貨,一頭取貨),從而保證信息交流的順序。FIFO只是借用了文件系統(file system, 參考Linux文件管理背景知識)來為管道命名。寫模式的進程向FIFO文件中寫入,而讀模式的進程從FIFO文件中讀出。當刪除FIFO文件時,管道連接也隨之消失。FIFO的好處在於我們可以通過文件的路徑來識別管道,從而讓沒有親緣關系的進程之間建立連接。
傳統IPC
這幾種傳統IPC實際上有很悠久的歷史,所以其實現方式也並不完善 (比如說我們需要某個進程負責刪除建立的IPC)。一個共同的特征是它們並不使用文件操作的API。對於任何一種IPC來說,你都可以建立多個連接,並使用鍵值(key)作為識別的方式。我們可以在一個進程中中通過鍵值來使用的想要那一個連接 (比如多個消息隊列,而我們選擇使用其中的一個)。鍵值可以通過某種IPC方式在進程間傳遞(比如說我們上面說的PIPE,FIFO或者寫入文件),也可以在編程的時候內置於程序中。
在幾個進程共享鍵值的情況下,這些傳統IPC非常類似於多線程共享資源的方式(參看Linux多線程與同步):
- semaphore與mutex類似,用於處理同步問題。我們說mutex像是一個只能容納一個人的洗手間,那么semaphore就像是一個能容納N個人的洗手間。其實從意義上來說,semaphore就是一個計數鎖(我覺得將semaphore翻譯成為信號量非常容易讓人混淆semaphore與signal),它允許被N個進程獲得。當有更多的進程嘗試獲得semaphore的時候,就必須等待有前面的進程釋放鎖。當N等於1的時候,semaphore與mutex實現的功能就完全相同。許多編程語言也使用semaphore處理多線程同步的問題。一個semaphore會一直存在在內核中,直到某個進程刪除它。
- 共享內存與多線程共享global data和heap類似。一個進程可以將自己內存空間中的一部分拿出來,允許其它進程讀寫。當使用共享內存的時候,我們要注意同步的問題。我們可以使用semaphore同步,也可以在共享內存中建立mutex或其它的線程同步變量來同步。由於共享內存允許多個進程直接對同一個內存區域直接操作,所以它是效率最高的IPC方式。
消息隊列(message queue)與PIPE相類似。它也是建立一個隊列,先放入隊列的消息被最先取出。不同的是,消息隊列允許多個進程放入消息,也允許多個進程取出消息。每個消息可以帶有一個整數識別符(message_type)。你可以通過識別符對消息分類 (極端的情況是將每個消息設置一個不同的識別符)。某個進程從隊列中取出消息的時候,可以按照先進先出的順序取出,也可以只取出符合某個識別符的消息(有多個這樣的消息時,同樣按照先進先出的順序取出)。消息隊列與PIPE的另一個不同在於它並不使用文件API。最后,一個隊列不會自動消失,它會一直存在於內核中,直到某個進程刪除該隊列。
多進程協作可以幫助我們充分利用多核和網絡時代帶來的優勢。多進程可以有效解決計算瓶頸的問題。互聯網通信實際上也是一個進程間通信的問題,只不過這多個進程分布於不同的電腦上。網絡連接是通過socket實現的。由於socket內容龐大,所以我們不在這里深入。一個小小的注解是,socket也可以用於計算機內部進程間的通信。
總結
PIPE, FIFO
semaphore, message queue, shared memory; key
歡迎閱讀“騎着企鵝采樹莓”系列文章