Linux程式間通訊

choubou發表於2021-09-09


我們在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

 

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4548/viewspace-2811765/,如需轉載,請註明出處,否則將追究法律責任。

相關文章