Linux處理序間通訊——使用匿名管道

來源:互聯網
上載者:User
在前面,介紹了一種進程間的通訊方式:使用訊號,我們建立通知事件,並通過它引起響應,但傳遞的資訊只是一個訊號值。這裡將介紹另一種處理序間通訊的方式——匿名管道,通過它進程間可以交換更多有用的資料。一、什麼是管道如果你使用過Linux的命令,那麼對於管道這個名詞你一定不會感覺到陌生,因為我們通常通過符號“|"來使用管道,但是管理的真正定義是什麼呢?管道是一個進程串連資料流到另一個進程的通道,它通常是用作把一個進程的輸出通過管道串連到另一個進程的輸入。舉個例子,在shell中輸入命令:ls -l | grep string,我們知道ls命令(其實也是一個進程)會把目前的目錄中的檔案都列出來,但是它不會直接輸出,而是把本來要輸出到螢幕上的資料通過管道輸出到grep這個進程中,作為grep這個進程的輸入,然後這個進程對輸入的資訊進行篩選,把存在string的資訊的字串(以行為單位)列印在螢幕上。二、使用popen函數1、popen函數和pclose函數介紹有靜就有動,有開就有關,與此相同,與popen函數相對應的函數是pclose函數,它們的原型如下:
#include <stdio.h>FILE* popen (const char *command, const char *open_mode);int pclose(FILE *stream_to_close);
poen函數允許一個程式將另一個程式作為新進程來啟動,並可以傳遞資料給它或者通過它接收資料。command是要啟動並執行程式名和相應的參數。open_mode只能是"r(唯讀)"和"w(唯寫)"的其中之一。注意,popen函數的傳回值是一個FILE類型的指標,而Linux把一切都視為檔案,也就是說我們可以使用stdio I/O庫中的檔案處理函數來對其進行操作。如果open_mode是"r",主調用程式就可以使用被調用程式的輸出,通過函數返回的FILE指標,就可以能過stdio函數(如fread)來讀取程式的輸出;如果open_mode是"w",主調用程式就可以向被調用程式發送資料,即通過stdio函數(如fwrite)向被調用程式寫資料,而被調用程式就可以在自己的標準輸入中讀取這些資料。pclose函數用於關閉由popen建立出的關聯檔案流。pclose只在popen啟動的進程結束後才返回,如果調用pclose時被調用進程仍在運行,pclose調用將等待該進程結束。它返回關閉的檔案流所在進程的退出碼。2、例子很多時候,我們根本就不知道輸出資料的長度,為了避免定義一個非常大的數組作為緩衝區,我們可以以塊的方式來發送資料,一次讀取一個塊的資料並發送一個塊的資料,直到把所有的資料都發送完。下面的例子就是採用這種方式的資料讀取和發送方式。源檔案名稱為popen.c,代碼如下:
#include <unistd.h>#include <stdlib.h>#include <stdio.h>#include <string.h>int main(){FILE *read_fp = NULL;FILE *write_fp = NULL;char buffer[BUFSIZ + 1];int chars_read = 0;//初始化緩衝區memset(buffer, '\0', sizeof(buffer));//開啟ls和grep進程read_fp = popen("ls -l", "r");write_fp = popen("grep rwxrwxr-x", "w");//兩個進程都開啟成功if(read_fp && write_fp){//讀取一個資料區塊chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);while(chars_read > 0){buffer[chars_read] = '\0';//把資料寫入grep進程fwrite(buffer, sizeof(char), chars_read, write_fp);//還有資料可讀,迴圈讀取資料,直到讀完所有資料chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);}//關閉檔案流pclose(read_fp);pclose(write_fp);exit(EXIT_SUCCESS);}exit(EXIT_FAILURE);}
運行結果如下:從運行結果來看,達到了資訊篩選的目的。程式在進程ls中讀取資料,再把資料發送到進程grep中進行篩選處理,相當於在shell中直接輸入命令:ls -l | grep rwxrwxr-x。3、popen的實現方式及優缺點當請求popen調用運行一個程式時,它首先啟動shell,即系統中的sh命令,然後將command字串作為一個參數傳遞給它。這樣就帶來了一個優點和一個缺點。優點是:在Linux中所有的參數擴充都是由shell來完成的。所以在啟動程式(command中的命令程式)之前先啟動shell來分析命令字串,也就可以使各種shell擴充(如萬用字元)在程式啟動之前就全部完成,這樣我們就可以通過popen啟動非常複雜的shell命令。而它的缺點就是:對於每個popen調用,不僅要啟動一個被請求的程式,還要啟動一個shell,即每一個popen調用將啟動兩個進程,從效率和資源的角度看,popen函數的調用比正常方式要慢一些。三、pipe調用如果說popen是一個進階的函數,pipe則是一個底層的調用。與popen函數不同的是,它在兩個進程之間傳遞資料不需要啟動一個shell來解釋請求命令,同時它還提供對讀寫資料的更多的控制。pipe函數的原型如下:
#include <unistd.h>int pipe(int file_descriptor[2]);
我們可以看到pipe函數的定義非常特別,該函數在數組中牆上兩個新的檔案描述符後返回0,如果返回返回-1,並設定errno來說明失敗原因。數組中的兩個檔案描述符以一種特殊的方式串連起來,資料基於先進先出的原則,寫到file_descriptor[1]的所有資料都可以從file_descriptor[0]讀回來。由於資料基於先進先出的原則,所以讀取的資料和寫入的資料是一致的。特別提醒:1、從函數的原型我們可以看到,它跟popen函數的一個重大區別是,popen函數是基於檔案流(FILE)工作的,而pipe是基於檔案描述符工作的,所以在使用pipe後,資料必須要用底層的read和write調用來讀取和發送。2、不要用file_descriptor[0]寫資料,也不要用file_descriptor[1]讀資料,其行為未定義的,但在有些系統上可能會返回-1表示調用失敗。資料只能從file_descriptor[0]中讀取,資料也只能寫入到file_descriptor[1],不能倒過來。例子:首先,我們在原先的進程中建立一個管道,然後再調用fork建立一個新的進程,最後通過管道在兩個進程之間傳遞資料。源檔案名稱為pipe.c,代碼如下:
#include <unistd.h>#include <stdlib.h>#include <stdio.h>#include <string.h>int main(){int data_processed = 0;int filedes[2];const char data[] = "Hello pipe!";char buffer[BUFSIZ + 1];pid_t pid;//清空緩衝區memset(buffer, '\0', sizeof(buffer));if(pipe(filedes) == 0){//建立管道成功//通過調用fork建立子進程pid = fork();if(pid == -1){fprintf(stderr, "Fork failure");exit(EXIT_FAILURE);}if(pid == 0){//子進程中//讀取資料data_processed = read(filedes[0], buffer, BUFSIZ);printf("Read %d bytes: %s\n", data_processed, buffer);exit(EXIT_SUCCESS);}else{//父進程中//寫資料data_processed = write(filedes[1], data, strlen(data));printf("Wrote %d bytes: %s\n", data_processed, data);//休眠2秒,主要是為了等子進程先結束,這樣做也只是純粹為了輸出好看而已//父進程其實沒有必要等等子進程結束sleep(2);exit(EXIT_SUCCESS);}}exit(EXIT_FAILURE);}
運行結果為:可見,子進程讀取了父進程寫到filedes[1]中的資料,如果在父進程中沒有sleep語句,父進程可能在子進程結束前結束,這樣你可能將看到兩個輸入之間有一個命令提示字元分隔。四、把管道用作標準輸入和標準輸出下面來介紹一種用管道來串連兩個進程的更簡潔方法,我們可以把檔案描述符設定為一個已知值,一般是標準輸入0或標準輸出1。這樣做最大的好處是可以調用標準程式,即那些不需要以檔案描述符為參數的程式。為了完成這個工作,我們還需要兩個函數的輔助,它們分別是dup函數或dup2函數,它們的原型如下
#include <unistd.h>int dup(int file_descriptor);int dup2(int file_descriptor_one, int file_descriptor_two);
dup調用建立一個新的檔案描述符與作為它的參數的那個已有檔案描述符指向同一個檔案或管道。對於dup函數而言,新的檔案描述總是取最小的可用值。而dup2所建立的新檔案描述符或者與int file_descriptor_two相同,或者是第一個大於該參數的可用值。所以當我們首先關閉檔案描述符0後調用dup,那麼新的檔案描述符將是數字0.例子在下面的例子中,首先開啟管道,然後fork一個子進程,然後在子進程中,使標準輸入指向讀管道,然後關閉子進程中的讀管道和寫管道,只留下標準輸入,最後調用execlp函數來啟動一個新的進程od,但是od並不知道它的資料來源是管道還是終端。父進程則相對簡單,它首先關閉讀管道,然後在寫管道中寫入資料,再關閉寫管道就完成了它的任務。源檔案為pipe2.c,代碼如下:
#include <unistd.h>#include <stdlib.h>#include <stdio.h>#include <string.h>int main(){int data_processed = 0;int pipes[2];const char data[] = "123";pid_t pid;if(pipe(pipes) == 0){pid = fork();if(pid == -1){fprintf(stderr, "Fork failure!\n");exit(EXIT_FAILURE);}if(pid == 0){//子進程中//使標準輸入指向fildes[0]close(0);dup(pipes[0]);//關閉pipes[0]和pipes[1],只剩下標準輸入close(pipes[0]);close(pipes[1]);//啟動新進程odexeclp("od", "od", "-c", 0);exit(EXIT_FAILURE);}else{//關閉pipes[0],因為父進程不用讀取資料close(pipes[0]);data_processed = write(pipes[1], data, strlen(data));//寫完資料後,關閉pipes[1]close(pipes[1]);printf("%d - Wrote %d bytes\n", getpid(), data_processed);}}exit(EXIT_SUCCESS);}
運行結果為:從運行結果中可以看出od進程正確地完成了它的任務,與在shell中直接輸入od -c和123的效果一樣。五、關於管道關閉後的讀操作的討論現在有這樣一個問題,假如父進程向管道file_pipe[1]寫資料,而子進程在管道file_pipe[0]中讀取資料,當父進程沒有向file_pipe[1]寫資料時,子進程則沒有資料可讀,則子進程會發生什麼呢?再者父進程把file_pipe[1]關閉了,子進程又會有什麼反應呢?當寫資料的管道沒有關閉,而又沒有資料可讀時,read調用通常會阻塞,但是當寫資料的管道關閉時,read調用將會返回0而不是阻塞。注意,這與讀取一個無效的檔案描述符不同,read一個無效的檔案描述符返回-1。六、匿名管道的缺陷看了這麼多相信大家也知道它的一個缺點,就是通訊的進程,它們的關係一定是父子進程的關係,這就使得它的使用受到了一點的限制,但是我們可以使用具名管道來解決這個問題。具名管道將在下一篇文章:Linux處理序間通訊——使用具名管道中介紹。
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.