管道是linux進程間的一種通信方式,兩個進程可以通過一個共享內存區域來傳遞信息,并且管道中的數據只能是單向流動的,也就是說只能有固定的寫進程和讀進程。目前在任何一個shell中,都可以使用“|”連接兩個命令,shell會將前后兩個進程的輸入輸出用一個管道相連,以便達到進程間通信的目的。
本教程操作環境:linux7.3系統、Dell G3電腦。
管道是UNIX環境中歷史最悠久的進程間通信方式。本文主要說明在Linux環境上如何使用管道。
什么是管道?
管道,英文為pipe。管道是Linux進程間的一種通信方式,兩個進程可以通過一個共享內存區域來傳遞信息,并且管道中的數據只能是單向流動的,也就是說只能有固定的寫進程和讀進程。
管道的發明人是道格拉斯.麥克羅伊,這位也是UNIX上早期shell的發明人。他在發明了shell之后,發現系統操作執行命令的時候,經常有需求要將一個程序的輸出交給另一個程序進行處理,這種操作可以使用輸入輸出重定向加文件搞定,比如:
[zorro@zorro-pc?pipe]$?ls??-l?/etc/?>?etc.txt [zorro@zorro-pc?pipe]$?wc?-l?etc.txt? 183?etc.txt
但是這樣未免顯得太麻煩了。所以,管道的概念應運而生。目前在任何一個shell中,都可以使用“|”連接兩個命令,shell會將前后兩個進程的輸入輸出用一個管道相連,以便達到進程間通信的目的:
[zorro@zorro-pc?pipe]$?ls?-l?/etc/?|?wc?-l 183
對比以上兩種方法,我們也可以理解為,管道本質上就是一個文件,前面的進程以寫方式打開文件,后面的進程以讀方式打開。這樣前面寫完后面讀,于是就實現了通信。實際上管道的設計也是遵循UNIX的“一切皆文件”設計原則的,它本質上就是一個文件。Linux系統直接把管道實現成了一種文件系統,借助VFS給應用程序提供操作接口。
雖然實現形態上是文件,但是管道本身并不占用磁盤或者其他外部存儲的空間。在Linux的實現上,它占用的是內存空間。所以,Linux上的管道就是一個操作方式為文件的內存緩沖區。
管道的分類和使用
Linux上的管道分兩種類型:
-
匿名管道
-
命名管道
這兩種管道也叫做有名或無名管道。匿名管道最常見的形態就是我們在shell操作中最常用的”|”。它的特點是只能在父子進程中使用,父進程在產生子進程前必須打開一個管道文件,然后fork產生子進程,這樣子進程通過拷貝父進程的進程地址空間獲得同一個管道文件的描述符,以達到使用同一個管道通信的目的。此時除了父子進程外,沒人知道這個管道文件的描述符,所以通過這個管道中的信息無法傳遞給其他進程。這保證了傳輸數據的安全性,當然也降低了管道了通用性,于是系統還提供了命名管道。
我們可以使用mkfifo或mknod命令來創建一個命名管道,這跟創建一個文件沒有什么區別:
[zorro@zorro-pc?pipe]$?mkfifo?pipe [zorro@zorro-pc?pipe]$?ls?-l?pipe? prw-r--r--?1?zorro?zorro?0?Jul?14?10:44?pipe
可以看到創建出來的文件類型比較特殊,是p類型。表示這是一個管道文件。有了這個管道文件,系統中就有了對一個管道的全局名稱,于是任何兩個不相關的進程都可以通過這個管道文件進行通信了。比如我們現在讓一個進程寫這個管道文件:
[zorro@zorro-pc?pipe]$?echo?xxxxxxxxxxxxxx?>?pipe
此時這個寫操作會阻塞,因為管道另一端沒有人讀。這是內核對管道文件定義的默認行為。此時如果有進程讀這個管道,那么這個寫操作的阻塞才會解除:
[zorro@zorro-pc?pipe]$?cat?pipe? xxxxxxxxxxxxxx
大家可以觀察到,當我們cat完這個文件之后,另一端的echo命令也返回了。這就是命名管道。
Linux系統無論對于命名管道和匿名管道,底層都用的是同一種文件系統的操作行為,這種文件系統叫pipefs。大家可以在/etc/proc/filesystems文件中找到你的系統是不是支持這種文件系統:
[zorro@zorro-pc?pipe]$?cat?/proc/filesystems?|grep?pipefs nodev????pipefs
觀察完了如何在命令行中使用管道之后,我們再來看看如何在系統編程中使用管道。
PIPE
我們可以把匿名管道和命名管道分別叫做PIPE和FIFO。這主要因為在系統編程中,創建匿名管道的系統調用是pipe(),而創建命名管道的函數是mkfifo()。使用mknod()系統調用并指定文件類型為為S_IFIFO也可以創建一個FIFO。
使用pipe()系統調用可以創建一個匿名管道,這個系統調用的原型為:
#include?<unistd.h> int?pipe(int?pipefd[2]);</unistd.h>
這個方法將會創建出兩個文件描述符,可以使用pipefd這個數組來引用這兩個描述符進行文件操作。pipefd[0]是讀方式打開,作為管道的讀描述符。pipefd[1]是寫方式打開,作為管道的寫描述符。從管道寫端寫入的數據會被內核緩存直到有人從另一端讀取為止。我們來看一下如何在一個進程中使用管道,雖然這個例子并沒有什么意義:
[zorro@zorro-pc?pipe]$?cat?pipe.c #include?<stdlib.h> #include?<stdio.h> #include?<unistd.h> #include?<string.h> #define?STRING?"hello?world!" int?main() { ????int?pipefd[2]; ????char?buf[BUFSIZ]; ????if?(pipe(pipefd)?==?-1)?{ ????????perror("pipe()"); ????????exit(1); ????} ????if?(write(pipefd[1],?STRING,?strlen(STRING))?</string.h></unistd.h></stdio.h></stdlib.h>
這個程序創建了一個管道,并且對管道寫了一個字符串之后從管道讀取,并打印在標準輸出上。用一個圖來說明這個程序的狀態就是這樣的:

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

如圖中描述,fork產生的子進程會繼承父進程對應的文件描述符。利用這個特性,父進程先pipe創建管道之后,子進程也會得到同一個管道的讀寫文件描述符。從而實現了父子兩個進程使用一個管道可以完成半雙工通信。此時,父進程可以通過fd[1]給子進程發消息,子進程通過fd[0]讀。子進程也可以通過fd[1]給父進程發消息,父進程用fd[0]讀。程序實例如下:
[zorro@zorro-pc?pipe]$?cat?pipe_parent_child.c #include?<stdlib.h> #include?<stdio.h> #include?<unistd.h> #include?<string.h> #include?<sys> #include?<sys> #define?STRING?"hello?world!" int?main() { ????int?pipefd[2]; ????pid_t?pid; ????char?buf[BUFSIZ]; ????if?(pipe(pipefd)?==?-1)?{ ????????perror("pipe()"); ????????exit(1); ????} ????pid?=?fork(); ????if?(pid?==?-1)?{ ????????perror("fork()"); ????????exit(1); ????} ????if?(pid?==?0)?{ ????????/*?this?is?child.?*/ ????????printf("Child?pid?is:?%dn",?getpid()); ????????if?(read(pipefd[0],?buf,?BUFSIZ)?</sys></sys></string.h></unistd.h></stdio.h></stdlib.h>
父進程先給子進程發一個消息,子進程接收到之后打印消息,之后再給父進程發消息,父進程再打印從子進程接收到的消息。程序執行效果:
[zorro@zorro-pc?pipe]$?./pipe_parent_child? Parent?pid?is:?8309 Child?pid?is:?8310 Message?from?parent:?My?pid?is:?8309 Message?from?child:?My?pid?is:?8310
從這個程序中我們可以看到,管道實際上可以實現一個半雙工通信的機制。使用同一個管道的父子進程可以分時給對方發送消息。我們也可以看到對管道讀寫的一些特點,即:
在管道中沒有數據的情況下,對管道的讀操作會阻塞,直到管道內有數據為止。當一次寫的數據量不超過管道容量的時候,對管道的寫操作一般不會阻塞,直接將要寫的數據寫入管道緩沖區即可。
當然寫操作也不會再所有情況下都不阻塞。這里我們要先來了解一下管道的內核實現。上文說過,管道實際上就是內核控制的一個內存緩沖區,既然是緩沖區,就有容量上限。我們把管道一次最多可以緩存的數據量大小叫做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進行設置。
#define?BUFSIZE?65536 ...... ret?=?fcntl(pipefd[1],?F_GETPIPE_SZ); if?(ret?
PIPEBUF和PIPESIZE對管道操作的影響會因為管道描述符是否被設置為非阻塞方式而有行為變化,n為要寫入的數據量時具體為:
O_NONBLOCK關閉,n
n個字節的寫入操作是原子操作,write系統調用可能會因為管道容量(PIPESIZE)沒有足夠的空間存放n字節長度而阻塞。
O_NONBLOCK打開,n
如果有足夠的空間存放n字節長度,write調用會立即返回成功,并且對數據進行寫操作??臻g不夠則立即報錯返回,并且errno被設置為EAGAIN。
O_NONBLOCK關閉,n > PIPE_BUF:
對n字節的寫入操作不保證是原子的,就是說這次寫入操作的數據可能會跟其他進程寫這個管道的數據進行交叉。當管道容量長度低于要寫的數據長度的時候write操作會被阻塞。
O_NONBLOCK打開,n > PIPE_BUF:
如果管道空間已滿。write調用報錯返回并且errno被設置為EAGAIN。如果沒滿,則可能會寫入從1到n個字節長度,這取決于當前管道的剩余空間長度,并且這些數據可能跟別的進程的數據有交叉。
以上是在使用半雙工管道的時候要注意的事情,因為在這種情況下,管道的兩端都可能有多個進程進行讀寫處理。如果再加上線程,則事情可能變得更復雜。實際上,我們在使用管道的時候,并不推薦這樣來用。管道推薦的使用方法是其單工模式:即只有兩個進程通信,一個進程只寫管道,另一個進程只讀管道。實現為:
[zorro@zorro-pc?pipe]$?cat?pipe_parent_child2.c #include?<stdlib.h> #include?<stdio.h> #include?<unistd.h> #include?<string.h> #include?<sys> #include?<sys> #define?STRING?"hello?world!" int?main() { ????int?pipefd[2]; ????pid_t?pid; ????char?buf[BUFSIZ]; ????if?(pipe(pipefd)?==?-1)?{ ????????perror("pipe()"); ????????exit(1); ????} ????pid?=?fork(); ????if?(pid?==?-1)?{ ????????perror("fork()"); ????????exit(1); ????} ????if?(pid?==?0)?{ ????????/*?this?is?child.?*/ ????????close(pipefd[1]); ????????printf("Child?pid?is:?%dn",?getpid()); ????????if?(read(pipefd[0],?buf,?BUFSIZ)?</sys></sys></string.h></unistd.h></stdio.h></stdlib.h>
這個程序實際上比上一個要簡單,父進程關閉管道的讀端,只寫管道。子進程關閉管道的寫端,只讀管道。整個管道的打開效果最后成為下圖所示:

此時兩個進程就只用管道實現了一個單工通信,并且這種狀態下不用考慮多個進程同時對管道寫產生的數據交叉的問題,這是最經典的管道打開方式,也是我們推薦的管道使用方式。另外,作為一個程序員,即使我們了解了Linux管道的實現,我們的代碼也不能依賴其特性,所以處理管道時該越界判斷還是要判斷,該錯誤檢查還是要檢查,這樣代碼才能更健壯。
FIFO
命名管道在底層的實現跟匿名管道完全一致,區別只是命名管道會有一個全局可見的文件名以供別人open打開使用。再程序中創建一個命名管道文件的方法有兩種,一種是使用mkfifo函數。另一種是使用mknod系統調用,例子如下:
[zorro@zorro-pc?pipe]$?cat?mymkfifo.c #include?<stdio.h> #include?<sys> #include?<sys> #include?<stdlib.h> int?main(int?argc,?char?*argv[]) { ????if?(argc?!=?2)?{ ????????fprintf(stderr,?"Argument?error!n"); ????????exit(1); ????} /* ????if?(mkfifo(argv[1],?0600)?</stdlib.h></sys></sys></stdio.h>
我們使用第一個參數作為創建的文件路徑。創建完之后,其他進程就可以使用open()、read()、write()標準文件操作等方法進行使用了。其余所有的操作跟匿名管道使用類似。需要注意的是,無論命名還是匿名管道,它的文件描述都沒有偏移量的概念,所以不能用lseek進行偏移量調整。
相關推薦:《Linux視頻教程》