管道(pipe)是進程間通訊的一種實現方式。在 Linux 系統中,管道本質上是一種特殊的文件,它的主要用途是實現進程間的通訊。文中演示所用環境爲 Ubuntu 18.04 desktop。shell
在 shell 中執行下面的命令:編程
$ echo abc | cat
echo 命令的輸出經過管道做爲了 cat 命令的輸入。這裏面的具體操做是由 shell 程序完成的。數組
管道的一個顯著特色是:建立一個管道後,會得到兩個文件描述符,分別用於對管道進行讀取和寫入操做。一般將這兩個文件描述符稱爲管道的讀取端和寫入端,從寫入端寫入管道的任何數據均可以從讀取端讀取。
對一個進程來講,管道的寫入和讀取操做與寫入和讀取一個普通文件沒有什麼區別,只是在內核中經過這種機制來實現進程間的通訊而已。數據結構
建立管道使用的系統調用的函數聲明以下:函數
#include <unistd.h> int pipe(int filedescriptors[2]);
pipe 函數相對來講是一個比較底層的函數,它建立一個管道(相對於命名管道而言,這個管道又被稱爲匿名管道)。參數 filedescriptors 是一個長度爲 2 的整型數組,用於存放調用該函數所建立管道的文件描述符。其中 filedescriptors[0] 存放管道讀取端的文件描述符,filedescriptors[1] 存放管道寫入端的文件描述符。調用成功時,返回值爲 0;調用失敗時,返回值爲 -1。
由此可知,管道自己是一個抽象的概念,其本質是經過對特殊文件的讀寫實現進程間的通訊。一個管道實際上就是個只存在於內存中的文件,對這個文件的操做要經過兩個文件描述符進行,它們分別表明管道的兩端。所以管道是一種特殊的文件,它不屬於某一種文件系統,而是一種獨立的文件系統,有其本身的數據結構。spa
調用 pipe 函數建立了一個管道後,還不能實現經過管道在兩個進程間通訊的目的,由於此時管道的讀取端和寫入端的文件描述符都屬於同一個進程。咱們知道,在 Linux 系統中,經過 fork 系統調用建立子進程時,父進程中打開的文件描述符仍將保持打開狀態。因此,常見的作法是:先在父進程中建立管道,而後經過 fork 調用建立子進程,這時就能夠經過管道在父子進程間傳遞數據了。
實際的使用中,經常在子進程中調用 exec 族函數執行特定的程序,而後根據數據傳輸的方向分別關閉父進程和子進程中的一個文件描述符(注意,此時只能單向傳輸數據。若是要雙向傳輸數據,最好是建立兩個單向傳輸的管道)。例如:要實現父進程向子進程傳輸數據,則關閉父進程中的讀取端(filedescriptors[0])文件描述符和子進程中的寫入端文件描述符(filedescriptors[1])。設計
下面的 demo 演示瞭如何在父子進程和子進程之間創建管道:code
#include <sys/types.h> #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <string.h> int main(void) { int file_pipes[2]; pid_t fork_result; if(pipe(file_pipes) == 0) { fork_result = fork(); if(fork_result == -1) { fprintf(stderr, "Fork failure"); exit(EXIT_FAILURE); } if(fork_result == 0) { close(file_pipes[1]); exit(EXIT_SUCCESS); } else { close(file_pipes[0]); } } exit(EXIT_SUCCESS); }
上面的代碼建立了一個由父進程向子進程傳輸數據的管道。blog
因爲管道是一種特殊的文件,用戶在使用中徹底能夠像讀寫普通文件同樣對管道進行讀寫,所使用的函數爲 read 和 write。
系統定義的常數 PIPE_BUF 規定了管道緩衝區的大小,當寫入數據超過規定的大小時,就會發生數據錯亂。
雖然管道是一種特殊的文件,它的讀寫操做和普通文件的讀寫操做也徹底相同,但管道不是一個真實存在的文件,它只在內核中存儲,而不存在於文件系統中。進程
下面是擴展後的 demo,演示瞭如何在父子進程和子進程之間創建管道並傳輸數據:
#include <sys/types.h> #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <string.h> int main(void) { int data_processed; int file_pipes[2]; const char some_data[] = "123"; char buffer[BUFSIZ + 1]; pid_t fork_result; memset(buffer, '\0', sizeof(buffer)); if(pipe(file_pipes) == 0) { fork_result = fork(); if(fork_result == -1) { fprintf(stderr, "Fork failure"); exit(EXIT_FAILURE); } if(fork_result == 0) { close(file_pipes[1]); data_processed = read(file_pipes[0], buffer, BUFSIZ); printf("Read %d bytes: %s\n", data_processed, buffer); exit(EXIT_SUCCESS); } else { close(file_pipes[0]); data_processed = write(file_pipes[1], some_data, strlen(some_data)); printf("Write %d bytes\n", data_processed); } } exit(EXIT_SUCCESS); }
把上面的代碼保存到文件 pipedemo.c 中,並編譯:
$ gcc -Wall pipedemo.c -o pipe_demo
而後運行程序:
$ ./pipe_demo
這個程序先調用 pipe 函數建立一個管道,接着調用 fork 函數建立出一個子進程。若是 fork 函數調用成功,父進程就向管道中寫入數據,而子進程則從管道中讀取數據。父子進程都在只調用了一次 write 或 read 以後退出。
管道雖然被普遍使用,可是也有其侷限性。管道的最大特色就是要求進程之間具備同源性,即它們必須是最終由同一個進程所派生的進程。固然這個缺點能夠經過命名管道解決。
管道的另外一個缺點是半雙工的工做方式,即只容許單向傳輸數據。這一點管道和命名管道是相同的,能夠建立兩個單向管道來實現數據的雙向傳輸。
參考:
《Linux 程序設計》
《Linux 環境下 C 編程指南》