簡介
在本章當中,我們將學習GNU/Linux管道。管道模型雖然很老但是就算是現在它仍然是一個十分有用的進程間通信機制。我們將會學習什么是半雙向管道以及有名管道。它們都提供了一個FIFO(先進先出)排隊模型來允許進程間通信。
管道模型
一個形象化管道的描述為——一個在兩個實體之間的單向連接器。例如,讓我們來看一看下面的這個GNU/Linux命令:
ls -1 | wc –l
這個命令創建了兩個進程,一個和ls -l關聯而另一個則和wc -l關聯。接著它通過設置第二個進程的標準輸入到個進程的標準輸出連接了這兩個進程(如圖11.1)。產生的結果是——計算了當前子目錄的文件數目。
我們的命令設置了一個在兩個GNU/Linux命令之間的管道。命令ls被執行之后它產生的輸出被用作第二個命令wc(word count)的輸入。這是一個單向管道——通信發生在一個方向上。兩個命令之間的連接由GNU/Linux來完成。我們也可以在應用程序當中做到這一點(等一下我們將會證明這一點)。
匿名管道和有名管道
一個匿名管道或者說單向管道,為一個進程提供了和它的一個子進程(匿名的種類)進行通信的方法。這是因為沒有可以在操作系統當中找到一個匿名進程的方法。它的通常的用法是在父進程建立一個匿名管道,然后將這個管道傳遞給它的子進程,然后它們就可以進行通信了。注意,如果需要雙向通信的話,我們考慮使用的API就應該是套接字(sockets)API了。
管道的另一種類型是有名管道。一個有名管道其功能和匿名管道差不多,差別就在于它是可以在文件系統中存在的并且所有進程都可以找到它。這意味著沒有血緣關系的進程之間可以使用它來進行通信。
在接下來的部分當中我們將會同時學習有名管道和匿名管道。我們將對管道進行一個快速的游覽,然后我們就詳細地學習管道API以及支持管道編程的GNU/Linux系統級的命令。
旋風式的游覽
讓我們以一個簡單的管道編程的模型的例子來開始我們的旋風式游覽。在這個簡單的例子當中,我們在一個進程當中創建了一個管道,然后對它寫入一個消息,接下來的就是從這個管道當中讀取前面寫入的消息,然后將它顯示出來。
清單11.1: 一個簡單的管道例子
1:#include <unistd.h>
2:#include <stdio.h>
3:#include <string.h>
4:
5:#define MAX_LINE 80
6:#define PIPE_STDIN 0
7: #define PIPE_STDOUT 1
8:
9: int main()
10: {
11: const char *string={"A sample message."};
12: int ret, myPipe[2];
13: char buffer[MAX_LINE+1];
14:
15: /* Create the pipe */
16: ret = pipe( myPipe );
17:
18: if (ret == 0) {
19:
20: /* Write the message into the pipe */
21: write( myPipe[PIPE_STDOUT], string, strlen(string) );
22:
23: /* Read the message from the pipe */
24: ret = read( myPipe[PIPE_STDIN], buffer, MAX_LINE );
25:
26: /* Null terminate the string */
27: buffer[ ret ] = 0;
28:
29: printf("%s\n", buffer);
30:
31: }
32:
33: return 0;
34: }
在清單11.1當中,我們在16行當中使用了函數pipe創建了我們的管道。我們向函數pipe傳入了一個擁有兩個元素的代表我們的管道的整型數組。管道被定義為一對分開的文件描述符——一個輸入和一個輸出。我們可以從管道的一端寫入然后從管道的另一端讀取。如果管道成功建立的話,API函數pipe就會返回0。基于返回,myPipe將會包括兩個新的代表管道的輸入(myPipe[1])和對管道的輸出(myPipe[0])的文件描述符。(PS: in文件描述符代表其所關聯的文件或者設備相當于一個輸入設備;out文件描述符則代表其所關聯的文件或者設備相當于一個輸出設備。)
在21行,我們使用函數write將我們的消息寫入管道。我們指定了stdout(標準輸出)描述符(這是從應用程序的角度來看的,而非管道)。管道現在包含了我們的消息,然后它能夠在24行被函數read讀取。在這里我們再次說明一下,從應用程序的角度來看,我們使用了stdin(標準輸入)描述符來從管道進行讀取。函數read將從管道當中讀取到的消息保存到了變量buffer。為了讓buffer真正地成為一個字符串,我們在其后添加了一個0 (NULL),然后我們就可以在29行使用函數printf顯示它了。
雖然這個例子很有趣,但是我們自己之間的通信的執行可以使用任意數量的機制。接下來我們將要學習提供了進程(不論是有關系的還是無關系的)間通信的更為復雜的例子。
詳細的回顧
雖然管道模型的絕大部分為函數pipe,但是還有兩個應該對它們的基于管道編程的可用性進行討論的其它函數。表格11.1列舉了我們在本章當中要詳細地討論的函數:
API函數 用處
pipe 創建一個匿名管道
dup 創建一個文件描述符的拷貝
mkfifo 創建一個有名管道(先進先出)
同時我們還將會學習一些可用于管道通信的其它函數,特別是那些能夠使用管道進行通信的函數。
注意:我們要記住一個管道不過是一對文件描述符而已,因此任何能夠在文件描述符上進行操作的函數都可以使用管道。 這就包括了select、read、write、fcntl以及freopen等等,但并不是僅僅限于這些函數。
pipe
API函數pipe創建了一個新的由一個包含兩個文件描述符的數組所代表的管道。函數pipe的原型如下:
#include
int pipe( int fds[2] );
如果函數pipe的操作成功的話,它將會返回0;反之則會返回-1,同時適當地設置了errno。如若成功返回,數組fds(它是按引用傳遞值)將會擁有兩個激活的文件描述符。數組的個元素是一個能夠被應用程序讀取的文件描述符;而第二個則是一個能夠被應用程序寫入的文件描述符。
現在讓我們來看一下一個應用管道于多進程應用程序的稍微更為復雜一點的例子。在這個應用程序,當中,我們將在14行當中創建一個管道,然后在16行使用函數fork在程序的父進程當中創建一個新的進程(子進程)。在子進程當中,我們嘗試從我們的管道的輸入文件描述符當中進行讀取(18 行),但是如果沒有什么可以讀取的話,我們就將這個子進程掛起。當進行讀取操作之后,我們就通過一個NULL將字符串終止(就是讓一個數組字符成為一個字符串),然后打印我們所讀取的字符串。父進程只是簡單地通過使用輸出(寫)文件描述符(管道結構的數組的第二個元素)將一個測試字符串寫入管道,然后使用函數wait等待子進程退出。
注意:除了我們的子進程繼承了父進程使用函數pipe創建的的文件描述符然后使用它們來和另一個子進程或者和父進程來通信,關于這個程序不再沒有什么可以引人注目的了。回想一下:一旦函數fork的操作完成,我們的進程是獨立的(除了子進程繼承了父進程的特征,比如管道描述符)。由于內存是分開的, pipe方法為我們提供了一個有趣的進程間通信的模型。
清單11.2: 舉例說明兩個進程間的管道模型
1: #include <stdio.h>
2: #include <unistd.h>
3: #include <string.h>
4: #include <wait.h>
5:
6: #define MAX_LINE 80
7:
8: int main()
9: {
10: int thePipe[2], ret;
11: char buf[MAX_LINE+1];
12: const char *testbuf={"a test string."};
13:
14: if ( pipe( thePipe ) == 0 ) {
15:
16: if (fork() == 0) {
17:
18: ret = read( thePipe[0], buf, MAX_LINE );
19: buf[ret] = 0;
20: printf( "Child read %s\n", buf );
21:
22: } else {
23:
24: ret = write( thePipe[1], testbuf, strlen(testbuf) );
25: ret = wait( NULL );
26:
27: }
28:
29: }
30:
31: return 0;
32: }
注意:在這些簡單的程序當中我們并沒有討論管道的關閉,這是因為一個進程一旦結束,和管道相關聯的資源將會被自動地釋放。雖然如此,調用函數close來關閉管道的描述符是一個十分良好的編程做法,比如:
ret = pipe( myPipe );
...
close( myPipe[0] );
close( myPipe[1] );
如果管道的寫入端被關閉,而一個進程想要從管道當中進行讀取的話,一個0將會被返回。這意味著管道不再被使用并且應該把它關閉。如果管道的讀取端被關閉,而一個進程想要對管道寫入的話,一個信號將會產生。這個信號(將會在“第十二章——套接字編程的簡介”當中討論)被叫做SIGPIPE。要對管道進行寫入操作的應用程序通常都包含一個捕獲這么一個狀況的信號句柄。
dup和dup2
函數dup和dup2能夠復制一個文件描述符的十分有用的函數。它們經常被用來重定向一個進程的stdin、stdout或者stderr。函數dup和dup2的原型如下:
#include
int dup( int oldfd );
int dup2( int oldfd, int targetfd );
函數dup允許我們復制一個描述符。我們向函數傳遞一個已經存在了的描述符,然后它返回了一個和前面那個一模一樣的新的描述符。這意味著兩個描述符共享著相同的結構。例如,我們用一個文件描述符執行了一個lseek(在文件當中查找),文件的當前位置(文件的內部指針)在第二個當中也是一樣的。函數 dup的使用如下面的代碼片段所示:
int fd1, fd2;
...
fd2 = dup( fd1 );
注意:在fork的調用之前創建一個描述符產生的效果和調用函數dup是一樣的。子進程接收了一個復制的描述符,就好像它模仿了dup的調用一樣。
函數dup2和dup類似,但是它允許調用者指定一個激活的描述符已經目標描述符的id。在函數dup2成功返回之后,新的目標描述符復制了個描述符(targetfd =oldfd)。讓我們來看一下下面的舉例說明dup2的用法的代碼片段:
int oldfd;
oldfd = open("app_log", (O_RDWR | O_CREATE), 0644 );
dup2( oldfd, 1 );
close( oldfd );
在這個例子當中,我們打開了一個叫做“app_log”的新的文件并且接收到了一個叫做fd1的文件描述符。我們通過使用oldfd和1調用了 dup2,就這樣我們用oldfd(我們新打開的文件)替換了文件描述符1(stdout標準輸出)。任何寫入標準輸出stdout的數據現在將會被寫入一個叫做“app_log”的文件當中。注意我們在復制了oldfd之后就關閉了它。然而這并沒有關閉了我們新打開的文件,這是因為文件描述符1現在就是它——我們可以這么認為。
現在讓我們來學習一個更為復雜的例子。回想本章的前面部分我們研究的讓ls –l的輸出成為wc –l的輸入。我們現在通過一個C應用程序來探討允許這個例子(清單11.3)。
在清單11.3的開始部分,我們在9行創建了我們的管道然后在13-16行創建了程序的子進程接下來在20-23行創建了父進程。我們在13行關閉了 stdout(標準輸出)描述符。子進程在這里提供了ls –l的功能并且不會寫入stdout當中去相反它被寫入了我們的管道(用了dup來作重定向)。在14行,我們使用了dup2來重定向stdout到我們的管道(pfds[1])當中去。一旦這樣的一個操作完成之后,我們關閉了我們的管道的輸入端(因為它永遠不會被使用)。,我們使用了函數 execlp來將我們的子進程的鏡像替換為命令ls –l的鏡像。一旦這個命令被執行了之后,任何產生的輸出將會被傳送到輸入當中去。
現在讓我們來看一看,管道的接收端。父進程充當了這么一個角色并遵循了一個相似的模式。我們首先在20行關閉了stdin(標準輸入),這是因為我們不會從它那里接收任何數據。接下來,我們再次使用函數dup2(21行)來使得stdin成為管道的輸出端。這是通過使文件描述符0(一般來說為 stdin)在功能上變得和pfds[0]一樣來完成的。我們關閉了管道的stdout標準輸出端(pfds[1]),這是因為在這里我們不會使用到它(22行)。,我們使用函數execlp來執行將管道當中的內容當成它的輸入的命令wc -1(23行)。
清單11.3: 流水線命令C程序
1: #include <stdio.h>
2: #include <stdlib.h>
3: #include <unistd.h>
4:
5: int main()
6: {
7: int pfds[2];
8:
9: if ( pipe(pfds) == 0 ) {
10:
11: if ( fork() == 0 ) {
12:
13: close(1);
14: dup2( pfds[1], 1 );
15: close( pfds[0] );
16: execlp( "ls", "ls", "-1", NULL );
17:
18: } else {
19:
20: close(0);
21: dup2( pfds[0], 0 );
22: close( pfds[1] );
23: execlp( "wc", "wc", "-l", NULL );
24:
25: }
26:
27: }
28:
29: return 0;
30: }
在這個程序當中有一點十分重要并值得我們記下來——這就是我們的子進程把它自己的輸出重定向到管道的輸入上,而父進程則將它自己的輸入重定向到管道的輸出——這是一個值得我們去記的十分有用的技術。
mkfifo
函數mkfifo被用來創建一個在文件系統當中的提供FIFO(先進先出)的功能的文件(也可以稱之為有名管道)。到目前為 止我們討論的都是匿名管道。它們是專用于一個父進程和它的子進程之間的通信。有名管道在文件系統當中是可見的,因此可以被任何進程使用。函數mkfifo原型如下:
#include
#include
int mkfifo( const char *pathname, mode_t mode );
命令mkfifo需要兩個參數。個(pathname)是要被創建在文件系統當中的一個特殊文件。第二個(mode)指定了FIFO的讀寫權限。成功的話命令mkfifo將會返回0,失敗的話將會返回-1(同時errno將會被適當地寫入)。讓我們來看一下一個使用函數mkfifo創建了一個 fifo的例子:
int ret;
...
ret = mkfifo( "/tmp/cmd_pipe", S_IFIFO | 0666 );
if (ret == 0) {
// Named pipe successfully created
} else {
// Failed to create named pipe
}
在這個例子當中,我們使用文件cmd_pipe在子目錄/tmp當中創建了一個fifo(有名管道)。接下來我們就打開了這個文件來進行讀寫,通過它我們就可以進行通信了。一旦我們打開了一個有名管道,我們可以使用典型的I/O命令進行讀寫了。例如,下面是一個使用fgets從管道當中進行讀取的代碼段:
pfp = fopen( "/tmp/cmd_pipe", "r" );
...
ret = fgets( buffer, MAX_LINE, pfp );
我們可以使用下面的代碼段來為上面的代碼對管道進行寫入操作:
pfp = fopen( "/tmp/cmd_pipe", "w+ );
...
ret = fprintf( pfp, "Here’s a test string!\n" );
關于有名管道的使人感興趣的地方是它們在被看做是集合點的地方工作,我們將會在對系統命令mkfifo的討論當中對這一點進行探討。一個讀取者是不能夠打開一個有名管道的,除非一個寫入者主動地打開管道的另一端。讀取者將會在調用open函數時被阻塞直到一個寫入者出現。盡管有這樣的一個限制,有名管道仍然是進程間通信的一個有用的機制。
系統命令
讓我們來看一下一個和用于IPC的管道模型相關的系統命令。命令mkfifo,就和API函數mkfifo一樣,允許我們在命令行上創建一個有名管道。
mkfifo
命令mkfifo是在命令行上創建有名管道(fifo特殊文件)的兩個方法當中的一個。命令mkfifo的一般用法如下:
mkfifo [options] name
在這里,選項(options)-m代表mode(權限)而name則是要創建的有名管道的名字(如果需要的話還要包含路徑)。如果沒有指定權限,默認的是0644。下面是一個例子,它在目錄/tmp當中創建了一個叫做cmd_pipe的有名管道:
$ mkfifo /tmp/cmd_pipe
我們可以簡單地通過使用選項-m調整選項。下面是一個將權限設置為0644的例子(但是我們必須首先刪除掉原先的那一個):
$ rm cmd_pipe
$ mkfifo -m 0644 /tmp/cmd_pipe
一旦權限被創建完畢,我們可以通過這個管道來在命令行上進行通信。在另一個終端上,我們使用命令echo向有名管道cmd_pipe寫入:
$ echo Hi > cmd_pipe
當這個命令結束的時候,我們的讀取者將會被喚醒并結束(下面清楚地完成了讀取者的命令序列):
$ cat cmd_pipe
Hi
$
以上舉例說明了有名管道不僅可以用于C程序當中還可以用在腳本當中。
有名管道的創建還可以使用命令mknod(緊隨其后的特殊文件可以是很多其它類型的文件)。下面我們使用命令mknod創建了一個有名管道;
$ mknod cmd_pipe p
在這里,有名管道cmd_pipe被創建在當前子目錄當中(p是有名管道的類型)。
總結
在這一章當中,我們對有名管道和匿名管道作了一次旋風式的游覽。我們回顧了創建管道的程序方法和命令行方法,同時還回顧了使用它們來進行通信的典型的 I/O機制。我們還回顧了如何使用函數dup和dup2來進行I/O的重定向。雖然管道十分有用,在其它某一特定情節當中這些命令或者函數依然十分地有用(無論在上面地方一個文件描述符被使用,比如一個套接字或者文件)。
管道編程API
#include
int pipe( int filedes[2] );
int dup( int oldfd );
int dup2( int oldfd, int targetfd );
int mkfifo( const char *pathname, mode_t mode );
GNU/Linux應用程序編程:用管道進行編程
更新時間: 2007-12-20 08:58:41來源: 粵嵌教育瀏覽量:796