天天看點

Linux 的程序間通信:管道

管道是UNIX環境中曆史最悠久的程序間通信方式。本文主要說明在Linux環境上如何使用管道。閱讀本文可以幫你解決以下問題:

什麼是管道和為什麼要有管道?

管道怎麼分類?

管道的實作是什麼樣的?

管道有多大?

管道的大小是不是可以調整?如何調整?

管道,英文為pipe。這是一個我們在學習Linux指令行的時候就會引入的一個很重要的概念。它的發明人是道格拉斯.麥克羅伊,這位也是UNIX上早期shell的發明人。他在發明了shell之後,發現系統操作執行指令的時候,經常有需求要将一個程式的輸出交給另一個程式進行處理,這種操作可以使用輸入輸出重定向加檔案搞定,比如:

但是這樣未免顯得太麻煩了。是以,管道的概念應運而生。目前在任何一個shell中,都可以使用“|”連接配接兩個指令,shell會将前後兩個程序的輸入輸出用一個管道相連,以便達到程序間通信的目的:

對比以上兩種方法,我們也可以了解為,管道本質上就是一個檔案,前面的程序以寫方式打開檔案,後面的程序以讀方式打開。這樣前面寫完後面讀,于是就實作了通信。實際上管道的設計也是遵循UNIX的“一切皆檔案”設計原則的,它本質上就是一個檔案。Linux系統直接把管道實作成了一種檔案系統,借助VFS給應用程式提供操作接口。

雖然實作形态上是檔案,但是管道本身并不占用磁盤或者其他外部存儲的空間。在Linux的實作上,它占用的是記憶體空間。是以,Linux上的管道就是一個操作方式為檔案的記憶體緩沖區。

Linux上的管道分兩種類型:

匿名管道

命名管道

這兩種管道也叫做有名或無名管道。匿名管道最常見的形态就是我們在shell操作中最常用的”|”。它的特點是隻能在父子程序中使用,父程序在産生子程序前必須打開一個管道檔案,然後fork産生子程序,這樣子程序通過拷貝父程序的程序位址空間獲得同一個管道檔案的描述符,以達到使用同一個管道通信的目的。此時除了父子程序外,沒人知道這個管道檔案的描述符,是以通過這個管道中的資訊無法傳遞給其他程序。這保證了傳輸資料的安全性,當然也降低了管道了通用性,于是系統還提供了命名管道。

我們可以使用mkfifo或mknod指令來建立一個命名管道,這跟建立一個檔案沒有什麼差別:

可以看到建立出來的檔案類型比較特殊,是p類型。表示這是一個管道檔案。有了這個管道檔案,系統中就有了對一個管道的全局名稱,于是任何兩個不相關的程序都可以通過這個管道檔案進行通信了。比如我們現在讓一個程序寫這個管道檔案:

此時這個寫操作會阻塞,因為管道另一端沒有人讀。這是核心對管道檔案定義的預設行為。此時如果有程序讀這個管道,那麼這個寫操作的阻塞才會解除:

大家可以觀察到,當我們cat完這個檔案之後,另一端的echo指令也傳回了。這就是命名管道。

Linux系統無論對于命名管道和匿名管道,底層都用的是同一種檔案系統的操作行為,這種檔案系統叫pipefs。大家可以在/etc/proc/filesystems檔案中找到你的系統是不是支援這種檔案系統:

觀察完了如何在指令行中使用管道之後,我們再來看看如何在系統程式設計中使用管道。

我們可以把匿名管道和命名管道分别叫做PIPE和FIFO。這主要因為在系統程式設計中,建立匿名管道的系統調用是pipe(),而建立命名管道的函數是mkfifo()。使用mknod()系統調用并指定檔案類型為為S_IFIFO也可以建立一個FIFO。

使用pipe()系統調用可以建立一個匿名管道,這個系統調用的原型為:

這個方法将會建立出兩個檔案描述符,可以使用pipefd這個數組來引用這兩個描述符進行檔案操作。pipefd[0]是讀方式打開,作為管道的讀描述符。pipefd[1]是寫方式打開,作為管道的寫描述符。從管道寫端寫入的資料會被核心緩存直到有人從另一端讀取為止。我們來看一下如何在一個程序中使用管道,雖然這個例子并沒有什麼意義:

這個程式建立了一個管道,并且對管道寫了一個字元串之後從管道讀取,并列印在标準輸出上。用一個圖來說明這個程式的狀态就是這樣的:

Linux 的程式間通信:管道

一個程序自己給自己發送消息這當然不叫程序間通信,是以實際情況中我們不會在單個程序中使用管道。程序在pipe建立完管道之後,往往都要fork産生子程序,成為如下圖表示的樣子:

Linux 的程式間通信:管道

如圖中描述,fork産生的子程序會繼承父程序對應的檔案描述符。利用這個特性,父程序先pipe建立管道之後,子程序也會得到同一個管道的讀寫檔案描述符。進而實作了父子兩個程序使用一個管道可以完成半雙工通信。此時,父程序可以通過fd[1]給子程序發消息,子程序通過fd[0]讀。子程序也可以通過fd[1]給父程序發消息,父程序用fd[0]讀。程式執行個體如下:

父程序先給子程序發一個消息,子程序接收到之後列印消息,之後再給父程序發消息,父程序再列印從子程序接收到的消息。程式執行效果:

從這個程式中我們可以看到,管道實際上可以實作一個半雙工通信的機制。使用同一個管道的父子程序可以分時給對方發送消息。我們也可以看到對管道讀寫的一些特點,即:

在管道中沒有資料的情況下,對管道的讀操作會阻塞,直到管道内有資料為止。當一次寫的資料量不超過管道容量的時候,對管道的寫操作一般不會阻塞,直接将要寫的資料寫入管道緩沖區即可。

當然寫操作也不會再所有情況下都不阻塞。這裡我們要先來了解一下管道的核心實作。上文說過,管道實際上就是核心控制的一個記憶體緩沖區,既然是緩沖區,就有容量上限。我們把管道一次最多可以緩存的資料量大小叫做PIPESIZE。核心在處理管道資料的時候,底層也要調用類似read和write這樣的方法進行資料拷貝,這種核心操作每次可以操作的資料量也是有限的,一般的操作長度為一個page,即預設為4k位元組。我們把每次可以操作的資料量長度叫做PIPEBUF。POSIX标準中,對PIPEBUF有長度限制,要求其最小長度不得低于512位元組。PIPEBUF的作用是,核心在處理管道的時候,如果每次讀寫操作的資料長度不大于PIPEBUF時,保證其操作是原子的。而PIPESIZE的影響是,大于其長度的寫操作會被阻塞,直到目前管道中的資料被讀取為止。

在Linux 2.6.11之前,PIPESIZE和PIPEBUF實際上是一樣的。在這之後,Linux重新實作了一個管道緩存,并将它與寫操作的PIPEBUF實作成了不同的概念,形成了一個預設長度為65536位元組的PIPESIZE,而PIPEBUF隻影響相關讀寫操作的原子性。從Linux 2.6.35之後,在fcntl系統調用方法中實作了F_GETPIPE_SZ和F_SETPIPE_SZ操作,來分别檢視目前管道容量和設定管道容量。管道容量容量上限可以在/proc/sys/fs/pipe-max-size進行設定。

PIPEBUF和PIPESIZE對管道操作的影響會因為管道描述符是否被設定為非阻塞方式而有行為變化,n為要寫入的資料量時具體為:

O_NONBLOCK關閉,n <= PIPE_BUF:

n個位元組的寫入操作是原子操作,write系統調用可能會因為管道容量(PIPESIZE)沒有足夠的空間存放n位元組長度而阻塞。

O_NONBLOCK打開,n <= PIPE_BUF:

如果有足夠的空間存放n位元組長度,write調用會立即傳回成功,并且對資料進行寫操作。空間不夠則立即報錯傳回,并且errno被設定為EAGAIN。

O_NONBLOCK關閉,n > PIPE_BUF:

對n位元組的寫入操作不保證是原子的,就是說這次寫入操作的資料可能會跟其他程序寫這個管道的資料進行交叉。當管道容量長度低于要寫的資料長度的時候write操作會被阻塞。

O_NONBLOCK打開,n > PIPE_BUF:

如果管道空間已滿。write調用報錯傳回并且errno被設定為EAGAIN。如果沒滿,則可能會寫入從1到n個位元組長度,這取決于目前管道的剩餘空間長度,并且這些資料可能跟别的程序的資料有交叉。

以上是在使用半雙工管道的時候要注意的事情,因為在這種情況下,管道的兩端都可能有多個程序進行讀寫處理。如果再加上線程,則事情可能變得更複雜。實際上,我們在使用管道的時候,并不推薦這樣來用。管道推薦的使用方法是其單工模式:即隻有兩個程序通信,一個程序隻寫管道,另一個程序隻讀管道。實作為:

這個程式實際上比上一個要簡單,父程序關閉管道的讀端,隻寫管道。子程序關閉管道的寫端,隻讀管道。整個管道的打開效果最後成為下圖所示:

Linux 的程式間通信:管道

此時兩個程序就隻用管道實作了一個單工通信,并且這種狀态下不用考慮多個程序同時對管道寫産生的資料交叉的問題,這是最經典的管道打開方式,也是我們推薦的管道使用方式。另外,作為一個程式員,即使我們了解了Linux管道的實作,我們的代碼也不能依賴其特性,是以處理管道時該越界判斷還是要判斷,該錯誤檢查還是要檢查,這樣代碼才能更健壯。

命名管道在底層的實作跟匿名管道完全一緻,差別隻是命名管道會有一個全局可見的檔案名以供别人open打開使用。再程式中建立一個命名管道檔案的方法有兩種,一種是使用mkfifo函數。另一種是使用mknod系統調用,例子如下:

我們使用第一個參數作為建立的檔案路徑。建立完之後,其他程序就可以使用open()、read()、write()标準檔案操作等方法進行使用了。其餘所有的操作跟匿名管道使用類似。需要注意的是,無論命名還是匿名管道,它的檔案描述都沒有偏移量的概念,是以不能用lseek進行偏移量調整。

關于管道的其它議題,比如popen、pclose的使用等話題,《UNIX環境進階程式設計》中的相關章節已經講的很清楚了。如果想學習補充這些知識,請參見此書。

希望這些内容對大家進一步深入了解管道有幫助。如果有相關問題,可以在我的微網誌、微信或者部落格上聯系我。

大家好,我是Zorro!

如果你喜歡本文,歡迎在微網誌上搜尋“orroz”關注我,位址是:http://weibo.com/orroz

大家也可以在微信上搜尋:Linux系統技術 關注我的公衆号。

我的所有文章都會沉澱在我的個人部落格上,位址是:http://liwei.life。

歡迎使用以上各種方式一起探讨學習,共同進步。