天天看點

Linux程序間通信——使用匿名管道

在前面,介紹了一種程序間的通信方式:使用信号,我們建立通知事件,并通過它引起響應,但傳遞的資訊隻是一個信号值。這裡将介紹另一種程序間通信的方式——匿名管道,通過它程序間可以交換更多有用的資料。

一、什麼是管道

如果你使用過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 <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);  

}  

運作結果如下:

Linux程式間通信——使用匿名管道

從運作結果來看,達到了資訊篩選的目的。程式在程序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函數的原型如下:

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,代碼如下:

    int data_processed = 0;  

    int filedes[2];  

    const char data[] = "Hello pipe!";  

    pid_t pid;  

    //清空緩沖區  

    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);  

運作結果為:

Linux程式間通信——使用匿名管道

可見,子程序讀取了父程序寫到filedes[1]中的資料,如果在父程序中沒有sleep語句,父程序可能在子程序結束前結束,這樣你可能将看到兩個輸入之間有一個指令提示符分隔。

四、把管道用作标準輸入和标準輸出

下面來介紹一種用管道來連接配接兩個程序的更簡潔方法,我們可以把檔案描述符設定為一個已知值,一般是标準輸入0或标準輸出1。這樣做最大的好處是可以調用标準程式,即那些不需要以檔案描述符為參數的程式。

為了完成這個工作,我們還需要兩個函數的輔助,它們分别是dup函數或dup2函數,它們的原型如下

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,代碼如下:

    int pipes[2];  

    const char data[] = "123";  

    if(pipe(pipes) == 0)  

            fprintf(stderr, "Fork failure!\n");  

            //使标準輸入指向fildes[0]  

            close(0);  

            dup(pipes[0]);  

            //關閉pipes[0]和pipes[1],隻剩下标準輸入  

            close(pipes[0]);  

            close(pipes[1]);  

            //啟動新程序od  

            execlp("od", "od", "-c", 0);  

            //關閉pipes[0],因為父程序不用讀取資料  

            data_processed = write(pipes[1], data, strlen(data));  

            //寫完資料後,關閉pipes[1]  

            printf("%d - Wrote %d bytes\n", getpid(), data_processed);  

    exit(EXIT_SUCCESS);  

Linux程式間通信——使用匿名管道

從運作結果中可以看出od程序正确地完成了它的任務,與在shell中直接輸入od -c和123的效果一樣。

五、關于管道關閉後的讀操作的讨論

現在有這樣一個問題,假如父程序向管道file_pipe[1]寫資料,而子程序在管道file_pipe[0]中讀取資料,當父程序沒有向file_pipe[1]寫資料時,子程序則沒有資料可讀,則子程序會發生什麼呢?再者父程序把file_pipe[1]關閉了,子程序又會有什麼反應呢?

當寫資料的管道沒有關閉,而又沒有資料可讀時,read調用通常會阻塞,但是當寫資料的管道關閉時,read調用将會傳回0而不是阻塞。注意,這與讀取一個無效的檔案描述符不同,read一個無效的檔案描述符傳回-1。

六、匿名管道的缺陷