程序間有哪些通訊方式?
直接開講!
每個程序的使用者地址空間都是獨立的,一般而言是不能互相訪問的,但核心空間是每個程序都共享的,所以程序之間要通訊必須通過核心。
Linux 核心提供了不少程序間通訊的機制,我們來一起瞧瞧有哪些?
一、管道
如果你學過 Linux 命令,那你肯定很熟悉「|
」這個豎線。
$ ps auxf | grep mysql
上面命令列裡的「|
」豎線就是一個管道,它的功能是將前一個命令(ps auxf
)的輸出,作為後一個命令(grep mysql
)的輸入,從這功能描述,可以看出管道傳輸資料是單向的,如果想相互通訊,我們需要建立兩個管道才行。
同時,我們得知上面這種管道是沒有名字,所以「|
」表示的管道稱為匿名管道
管道還有另外一個型別是命名管道,也被叫做 FIFO
,因為資料是先進先出的傳輸方式。
在使用命名管道前,先需要通過 mkfifo
命令來建立,並且指定管道名字:
$ mkfifo myPipe
myPipe 就是這個管道的名稱,基於 Linux 一切皆檔案的理念,所以管道也是以檔案的方式存在,我們可以用 ls 看一下,這個檔案的型別是 p,也就是 pipe(管道) 的意思:
$ ls -l
prw-r--r--. 1 root root 0 Jul 17 02:45 myPipe
接下來,我們往 myPipe 這個管道寫入資料:
$ echo "hello" > myPipe // 將資料寫進管道
// 停住了 ...
你操作了後,你會發現命令執行後就停在這了,這是因為管道里的內容沒有被讀取,只有當管道里的資料被讀完後,命令才可以正常退出。
於是,我們執行另外一個命令來讀取這個管道里的資料:
$ cat < myPipe // 讀取管道里的資料
hello
可以看到,管道里的內容被讀取出來了,並列印在了終端上,另外一方面,echo 那個命令也正常退出了。
我們可以看出,管道這種通訊方式效率低,不適合程序間頻繁地交換資料。當然,它的好處,自然就是簡單,同時也我們很容易得知管道里的資料已經被另一個程序讀取了。
1.那管道如何建立呢,背後原理是什麼?
匿名管道的建立,需要通過下面這個系統呼叫:
int pipe(int fd[2])
這裡表示建立一個匿名管道,並返回了兩個描述符,一個是管道的讀取端描述符 fd[0]
,另一個是管道的寫入端描述符 fd[1]
。注意,這個匿名管道是特殊的檔案,只存在於記憶體,不存於檔案系統中。
其實,所謂的管道,就是核心裡面的一串快取。從管道的一段寫入的資料,實際上是快取在核心中的,另一端讀取,也就是從核心中讀取這段資料。另外,管道傳輸的資料是無格式的流且大小受限。
看到這,你可能會有疑問了,這兩個描述符都是在一個程序裡面,並沒有起到程序間通訊的作用,怎麼樣才能使得管道是跨過兩個程序的呢?
我們可以使用 fork
建立子程序,建立的子程序會複製父程序的檔案描述符,這樣就做到了兩個程序各有兩個「 fd[0]
與 fd[1]
」,兩個程序就可以通過各自的 fd 寫入和讀取同一個管道檔案實現跨程序通訊了。
管道只能一端寫入,另一端讀出,所以上面這種模式容易造成混亂,因為父程序和子程序都可以同時寫入,也都可以讀出。那麼,為了避免這種情況,通常的做法是:
- 父程序關閉讀取的 fd[0],只保留寫入的 fd[1];
- 子程序關閉寫入的 fd[1],只保留讀取的 fd[0];
所以說如果需要雙向通訊,則應該建立兩個管道。
到這裡,我們僅僅解析了使用管道進行父程序與子程序之間的通訊,但是在我們 shell 裡面並不是這樣的。
在 shell 裡面執行 A | B
命令的時候,A 程序和 B 程序都是 shell 創建出來的子程序,A 和 B 之間不存在父子關係,它倆的父程序都是 shell。
所以說,在 shell 裡通過「|
」匿名管道將多個命令連線在一起,實際上也就是建立了多個子程序,那麼在我們編寫 shell 指令碼時,能使用一個管道搞定的事情,就不要多用一個管道,這樣可以減少建立子程序的系統開銷。
我們可以得知,對於匿名管道,它的通訊範圍是存在父子關係的程序。因為管道沒有實體,也就是沒有管道檔案,只能通過 fork 來複制父程序 fd 檔案描述符,來達到通訊的目的。
另外,對於命名管道,它可以在不相關的程序間也能相互通訊。因為命令管道,提前建立了一個型別為管道的裝置檔案,在程序裡只要使用這個裝置檔案,就可以相互通訊。
不管是匿名管道還是命名管道,程序寫入的資料都是快取在核心中,另一個程序讀取資料時候自然也是從核心中獲取,同時通訊資料都遵循先進先出原則,不支援 lseek 之類的檔案定位操作。
二、訊息佇列
前面說到管道的通訊方式是效率低的,因此管道不適合程序間頻繁地交換資料。
對於這個問題,訊息佇列的通訊模式就可以解決。比如,A 程序要給 B 程序傳送訊息,A 程序把資料放在對應的訊息佇列後就可以正常返回了,B 程序需要的時候再去讀取資料就可以了。同理,B 程序要給 A 程序傳送訊息也是如此。
再來,訊息佇列是儲存在核心中的訊息連結串列,在傳送資料時,會分成一個一個獨立的資料單元,也就是訊息體(資料塊),訊息體是使用者自定義的資料型別,訊息的傳送方和接收方要約定好訊息體的資料型別,所以每個訊息體都是固定大小的儲存塊,不像管道是無格式的位元組流資料。如果程序從訊息佇列中讀取了訊息體,核心就會把這個訊息體刪除。
訊息佇列生命週期隨核心,如果沒有釋放訊息佇列或者沒有關閉作業系統,訊息佇列會一直存在,而前面提到的匿名管道的生命週期,是隨程序的建立而建立,隨程序的結束而銷燬。
訊息這種模型,兩個程序之間的通訊就像平時發郵件一樣,你來一封,我回一封,可以頻繁溝通了。
但郵件的通訊方式存在不足的地方有兩點,一是通訊不及時,二是附件也有大小限制,這同樣也是訊息佇列通訊不足的點。
訊息佇列不適合比較大資料的傳輸,因為在核心中每個訊息體都有一個最大長度的限制,同時所有佇列所包含的全部訊息體的總長度也是有上限。在 Linux 核心中,會有兩個巨集定義 MSGMAX
和 MSGMNB
,它們以位元組為單位,分別定義了一條訊息的最大長度和一個佇列的最大長度。
訊息佇列通訊過程中,存在使用者態與核心態之間的資料拷貝開銷,因為程序寫入資料到核心中的訊息佇列時,會發生從使用者態拷貝資料到核心態的過程,同理另一程序讀取核心中的訊息資料時,會發生從核心態拷貝資料到使用者態的過程。
三、共享記憶體
訊息佇列的讀取和寫入的過程,都會有發生使用者態與核心態之間的訊息拷貝過程。那共享記憶體的方式,就很好的解決了這一問題。
現代作業系統,對於記憶體管理,採用的是虛擬記憶體技術,也就是每個程序都有自己獨立的虛擬記憶體空間,不同程序的虛擬記憶體對映到不同的實體記憶體中。所以,即使程序 A 和 程序 B 的虛擬地址是一樣的,其實訪問的是不同的實體記憶體地址,對於資料的增刪查改互不影響。
共享記憶體的機制,就是拿出一塊虛擬地址空間來,對映到相同的實體記憶體中。這樣這個程序寫入的東西,另外一個程序馬上就能看到了,都不需要拷貝來拷貝去,傳來傳去,大大提高了程序間通訊的速度。