管道是UNIX系統IPC的最古老的形式,並且所有UNIX系統都提供此種通信機制。管道有下面兩種局限性:
(1)歷史上,它們是半雙工的(即數據只能在一個方向上流動)。現在,某些系統提供全雙工管道,但是為了最佳的可移植性,我們決不應預先假定系統使用此特性。
(2)它們只能在具有公共祖先的進程之間使用。通常,一個管道由一個進程創建,然后該進程調用fork,此后父、子進程之間就可應用該管道。
(FIFO沒有第二種局限性,UNIX域套接字和命名流管道則沒有這兩種局限性。)
盡管有這兩種局限性,半雙工管道仍是最常用的IPC形式。每當你在管道線中鍵入一個由shell執行的命令序列時,shell為每一條命令單獨創建一進程,然后將前一條命令進程的標准輸出用管道與后一條命令的標准輸入相連接。
管道是由調用pipe函數而創建的:
#include <unistd.h> int pipe(int filedes[2]); 返回值:若成功則返回0,若出錯則返回-1
經由參數filedes返回的兩個文件描述符:filedes[0]為讀而打開,filedes[1]為寫而打開。filedes[1]的輸出是filedes[0]的輸入。
POSIX.1允許實現支持全雙工管道。對於這些實現,filedes[0]和filedes[1]以讀/寫方式打開。
有兩種方式來描繪一個半雙工管道,見圖15-1。左半圖顯示了管道的兩端在一個進程中相互連接,右半圖則說明數據通過內核在管道中流動。
圖15-1 觀察半雙工管道的兩種方法
fstat函數(見http://www.cnblogs.com/nufangrensheng/p/3501385.html)對管道的每一端都返回一個FIFO類型的文件描述符,可以用S_ISFIFO()宏來測試管道。
POSIX.1規定stat結構的st_size成員對於管道是未定義的。但是當fstat函數應用於管道讀端的文件描述符時,很多系統在st_size中存放管道中可用於讀的字節數。但是,這是不可移植的。
單個進程中的管道幾乎沒有任何用處。通常,調用pipe的進程接着調用fork,這樣就創建了從父進程到子進程(或反向)的IPC通道。圖15-2顯示了這種情況。
圖15-2 調用fork之后的半雙工通道
調用fork之后做什么取決於我們想要有的數據流的方向。對於從父進程到子進程的管道,父進程關閉管道的讀端(fd[0]),子進程則關閉寫端(fd[1])。圖15-3顯示了在此之后描述符的安排。
圖15-3 從父進程到子進程的管道
為了構造從子進程到父進程的管道,父進程關閉fd[1],子進程關閉fd[0]。
當管道的一端被關閉后,下列兩條規則其作用:
(1)當讀一個寫端已被關閉的管道時,在所有數據都被讀取后,read返回0,以指示達到了文件結尾處。(從技術方面考慮,管道的寫端還有進程時,就不會產生文件的結束。可以復制一個管道的描述符,使得有多個進程對它具有寫打開文件描述符。但是,通常一個管道只有一個讀進程、一個寫進程。而對於一個單一的FIFO常常有多個寫進程。)
(2)如果寫一個讀端已被關閉的管道,則產生信號SIGPIPE。如果忽略該信號或者捕捉該信號並從其處理程序返回,則write返回-1,errno設置為EPIPE。
在寫管道(或FIFO)時,常量PIPE_BUF規定了內核中管道緩沖區的大小。如果對管道調用write,而且要求寫的字節數小於等於PIPE_BUF,則此操作不會與其他進程對同一管道(或FIFO)的write操作穿插進行(當然,其他進程要求寫的字節數也都小於等於PIPE_BUF)。但是,若有多個進程同時寫一個管道(或FIFO),而且有進程(一個或幾個)要求寫的字節數超過PIPE_BUF字節數時,則寫操作的數據可能相互穿插。用pathconf或fpathconf函數(見http://www.cnblogs.com/nufangrensheng/p/3496323.html中表6)可以確定PIPE_BUF的值。
實例
程序清單15-1創建了一個從父進程到子進程的管道,並且父進程經由該管道向子進程傳送數據。
程序清單15-1 經由管道父進程向子進程傳送數據
#include "apue.h" int main(void) { int n; int fd[2]; pid_t pid; char line[MAXLINE]; if(pipe(fd) < 0) err_sys("pipe error"); if((pid = fork()) < 0) { err_sys("fork error"); } else if(pid > 0) /* parent */ { close(fd[0]); write(fd[1], "hello world\n", 12); } else /* child */ { close(fd[1]); n = read(fd[0], line, MAXLINE); write(STDOUT_FILENO, line, n); } exit(0); }
在上面的例子中,直接對管道描述符調用read和write。更好的方法是將管道描述符復制為標准輸入和標准輸出。在此之后通常子進程執行另一個程序,該程序或者從標准輸入(已創建的管道)讀數據,或者將數據寫至其標准輸出(該管道)。
實例
試編寫一個程序,其功能是每次一頁顯示已產生的輸出。已經有很多UNIX系統實用程序具有分頁功能,因此無需再構造一個新的分頁程序,而是調用用戶最喜愛的分頁程序。為了避免先將所有數據寫到一個臨時文件中,然后在調用系統中有關程序顯示該文件,我們希望將輸出通過管道直接送到分頁程序。為此,先創建一個管道,調用fork產生一個子進程,使子進程的標准輸入成為管道的讀端,然后調用exec,執行用戶喜愛的分頁程序。程序清單15-2顯示了如何實現這些操作。(本例要求在命令行中有一個參數說明要顯示文件的名稱。通常,這種類型的程序要求在終端上顯示的數據已經在存儲器中。)
程序清單15-2 將文件復制到分頁程序
#include "apue.h" #include <sys/wait.h> #define DEF_PAGER "/bin/more" /* default pager program */ int main(int argc, char *argv[]) { int n; int fd[2]; pid_t pid; char *pager, *argv0; char line[MAXLINE]; FILE *fp; if(argc != 2) err_quit("usage: a.out <pathname>"); if((fp = fopen(argv[1], "r")) == NULL) err_sys("can't open %s", argv[1]); if(pipe(fd) < 0) err_sys("pipe error"); if((pid = fork()) < 0) { err_sys("fork error"); } else if(pid > 0) /* parent */ { close(fd[0]); /* close read end */ /* parent copies argv[1] to pipe */ while(fgets(line, MAXLINE, fp) != NULL) { n = strlen(line); if(write(fd[1], line, n) != n) err_sys("write error to pipe"); } if(ferror(fp)) err_sys("fgets error"); close(fd[1]); /* close write end of pipe for reader */ if(waitpid(pid, NULL, 0) < 0) err_sys("waitpid error"); exit(0); } else /* child */ { close(fd[1]); /* close write end */ if(fd[0] != STDIN_FILENO) { if(dup2(fd[0], STDIN_FILENO) != STDIN_FILENO) err_sys("dup2 error to stdin"); close(fd[0]); /* don't need this after dup2 */ } /* get arguments for execl() */ if((pager = getenv("PAGER")) == NULL) pager = DEF_PAGER; if((argv0 = strrchr(pager, '/')) != NULL) argv0++; /* step past rightmost slash */ else argv0 = pager; /* no slash in pager */ if(execl(pager, argv0, (char *)0) < 0) err_sys("execl error for %s", pager); } exit(0); }
在調用fork之前先創建一個管道。fork之后父進程關閉其讀端,子進程關閉其寫端。子進程然后調用dup2,使其標准輸入成為管道的讀端。當執行分頁程序時,其標准輸入將是管道的讀端。
當我們將一個描述符復制到另一個時(在進程中,fd[0]復制到標准輸入),應當注意在復制之前該描述符的值並不是所希望的值。如果該描述符已經具有所希望的值,並且我們先調用dup2,然后調用close則將關閉此進程中只有該單個描述符所代表的打開文件。(回憶http://www.cnblogs.com/nufangrensheng/p/3498736.html中所述,當dup2中的兩個參數值相等時的操作。)
請注意,我們是如何使用環境變量PAGER試圖獲得用戶分頁程序名稱的。如果這種操作沒有成功,則使用系統默認值。這是環境變量的常見用法。
實例
回憶http://www.cnblogs.com/nufangrensheng/p/3510306.html中的5個函數:TELL_WAIT、TELL_PARENT、TELL_CHILD、WAIT_PARENT以及WAIT_CHILD。http://www.cnblogs.com/nufangrensheng/p/3516427.html中的程序清單10-17提供了一個使用信號的實現。程序清單15-3則是一個使用管道的實現。
程序清單15-3 使父、子進程同步的例程
#include "apue.h" static int pfd1[2], pfd2[2]; void TELL_WAIT(void) { if(pipe(pfd1) < 0 || pipe(pfd2) < 0) err_sys("pipe error"); } void TELL_PARENT(pid_t pid) { if(write(pfd2[1], "c", 1) != 1) err_sys("write error"); } void WAIT_PARENT(void) { char c; if(read(pfd1[0], &c, 1) != 1) err_sys("read error"); if(c != 'p') err_quit("WAIT_PARENT: incorrect data"); } void TELL_CHILD(pid_t pid) { if(write(pfd1[1], "p", 1) != 1) err_sys("write error"); } void WAIT_CHILD(void) { char c; if(read(pfd2[0], &c, 1) != 1) err_sys("read error"); if(c != "c") err_quit("WAIT_CHILD: incorrect data"); }
如圖15-4所示,在fork之前創建了兩個管道。父進程在調用TELL_CHILD時,寫一個字符“p”至上一個管道,子進程在調用TELL_PARENT時,經由下一個管道寫一個字符“c”。相應的WAIT_xxx函數調用read讀這個字符,沒有讀到字符時阻塞(睡眠等待)。
圖15-4 用兩個管道實現父子進程同步
請注意,每一個管道都有一個額外的讀取進程,這沒有關系。也就是說,除了子進程從pfd1[0]讀取,父進程也有上一個管道的讀端。因為父進程並沒有執行對該管道的讀操作,所以這不會產生任何影響。
本篇博文內容摘自《UNIX環境高級編程》(第二版),僅作個人學習記錄所用。關於本書可參考:http://www.apuebook.com/。