Linux 上實現雙向進程間通信管道

轉載於:https://www.ibm.com/developerworks/cn/linux/l-pipebid/


問題和常見方法

Linux 提供了 popen 和 pclose 函數 (1),用於創建和關閉管道與另外一個進程進行通信。其接口如下:

FILE *popen(const char *command, const char *mode);
int pclose(FILE *stream);

遺憾的是,popen 創建的管道只能是單向的 -- mode 只能是 "r" 或 "w" 而不能是某種組合--用戶只能選擇要麼往裏寫,要麼從中讀,而不能同時在一個管道中進行讀寫。實際應用中,經常會有同時進行讀寫的要求,比如,我們可能希望把文本數據送往sort工具排序後再取回結果。此時popen就無法用上了。我們需要尋找其它的解決方案。

有一種解決方案是使用 pipe 函數 (2)創建兩個單向管道。沒有錯誤檢測的代碼示意如下:

int pipe_in[2], pipe_out[2];
pid_t pid;
pipe(&pipe_in);	// 創建父進程中用於讀取數據的管道
pipe(&pipe_out);	// 創建父進程中用於寫入數據的管道
if ( (pid = fork()) == 0) {	// 子進程
    close(pipe_in[0]);	// 關閉父進程的讀管道的子進程讀端
    close(pipe_out[1]);	// 關閉父進程的寫管道的子進程寫端
    dup2(pipe_in[1], STDOUT_FILENO);	// 複製父進程的讀管道到子進程的標準輸出
    dup2(pipe_out[0], STDIN_FILENO);	// 複製父進程的寫管道到子進程的標準輸入
    close(pipe_in[1]);	// 關閉已複製的讀管道
    close(pipe_out[0]);	// 關閉已複製的寫管道
    /* 使用exec執行命令 */
} else {	// 父進程
    close(pipe_in[1]);	// 關閉讀管道的寫端
    close(pipe_out[0]);	// 關閉寫管道的讀端
    /* 現在可向pipe_out[1]中寫數據,並從pipe_in[0]中讀結果 */
    close(pipe_out[1]);	// 關閉寫管道
    /* 讀取pipe_in[0]中的剩餘數據 */
    close(pipe_in[0]);	// 關閉讀管道
    /* 使用wait系列函數等待子進程退出並取得退出代碼 */
}

當然,這樣的代碼的可讀性(特別是加上錯誤處理代碼之後)比較差,也不容易封裝成類似於popen/pclose的函數,方便高層代碼使用。究其原因,是pipe函數返回的一對文件描述符只能從第一個中讀、第二個中寫(至少對於Linux是如此)。爲了同時讀寫,就只能採取這麼累贅的兩個pipe調用、兩個文件描述符的形式了。

一個更好的方案

使用pipe就只能如此了。不過,Linux實現了一個源自BSD的socketpair調用 (3),可以實現上述在同一個文件描述符中進行讀寫的功能(該調用目前也是POSIX規範的一部分 (4))。該系統調用能創建一對已連接的(UNIX族)無名socket。在Linux中,完全可以把這一對socket當成pipe返回的文件描述符一樣使用,唯一的區別就是這一對文件描述符中的任何一個都可讀和可寫。

這似乎可以是一個用來實現進程間通信管道的好方法。不過,要注意的是,爲了解決我前面的提出的使用sort的應用問題,我們需要關閉子進程的標準輸入通知子進程數據已經發送完畢,而後從子進程的標準輸出中讀取數據直到遇到EOF。使用兩個單向管道的話每個管道可以單獨關閉,因而不存在任何問題;而在使用雙向管道時,如果不關閉管道就無法通知對端數據已經發送完畢,但關閉了管道又無法從中讀取結果數據。——這一問題不解決的話,使用socketpair的設想就變得毫無意義。

令人高興的是,shutdown調用 (5)可解決此問題。畢竟socketpair產生的文件描述符是一對socket,socket上的標準操作都可以使用,其中也包括shutdown。——利用shutdown,可以實現一個半關閉操作,通知對端本進程不再發送數據,同時仍可以利用該文件描述符接收來自對端的數據。沒有錯誤檢測的代碼示意如下:

int fd[2];
pid_t pid;
socketpair(AF_UNIX, SOCKET_STREAM, 0, fd);	// 創建管道
if ( (pid = fork()) == 0) {	// 子進程
    close(fd[0]);	// 關閉管道的父進程端
    dup2(fd[1], STDOUT_FILENO);	// 複製管道的子進程端到標準輸出
    dup2(fd[1], STDIN_FILENO);	// 複製管道的子進程端到標準輸入
    close(fd[1]);	// 關閉已複製的讀管道
    /* 使用exec執行命令 */
} else {	// 父進程
    close(fd[1]);	// 關閉管道的子進程端
    /* 現在可在fd[0]中讀寫數據 */
    shutdown(fd[0], SHUT_WR);	// 通知對端數據發送完畢
    /* 讀取剩餘數據 */
    close(fd[0]);	// 關閉管道
    /* 使用wait系列函數等待子進程退出並取得退出代碼 */
}

很清楚,這比使用兩個單向管道的方案要簡潔不少。我將在此基礎上作進一步的封裝和改進。

封裝和實現

直接使用上面的方法,無論怎麼看,至少也是醜陋和不方便的。程序的維護者想看到的是程序的邏輯,而不是完成一件任務的各種各樣的繁瑣細節。我們需要一個好的封裝。

封裝可以使用C或者C++。此處,我按照UNIX的傳統,提供一個類似於POSIX標準中popen/pclose函數調用的C封裝,以保證最大程度的可用性。接口如下:

FILE *dpopen(const char *command);
int dpclose(FILE *stream);
int dphalfclose(FILE *stream);

關於接口,以下幾點需要注意一下:

  • 與pipe函數類似,dpopen返回的是文件結構的指針,而不是文件描述符。這意味着,我們可以直接使用fprintf之類的函數,文件緩衝區會緩存寫入管道的數據(除非使用setbuf函數關閉文件緩衝區),要保證數據確實寫入到管道中需要使用fflush函數。

  • 由於dpopen返回的是可讀寫的管道,所以popen的第二個表示讀/寫的參數不再需要。

  • 在雙向管道中我們需要通知對端寫數據已經結束,此項操作由dphalfclose函數來完成。

具體的實現請直接查看程序源代碼,其中有詳細的註釋和doxygen文檔註釋 (6)。我只略作幾點說明:

  • 本實現使用了一個鏈表來記錄所有dpopen打開的文件指針和子進程ID的對應關係,因此,在同時用dpopen打開的管道的多的時候,dpclose(需要搜索鏈表)的速度會稍慢一點。我認爲在通常使用過程中這不會產生什麼問題。如果在某些特殊情況下這會是一個問題的話,可考慮更改dpopen的返回值類型和dpclose的傳入參數類型(不太方便使用,但實現簡單),或者使用哈希表/平衡樹來代替目前使用的鏈表以加速查找(接口不變,但實現較複雜)。

  • 當編譯時在gcc中使用了"-pthread"命令行參數時,本實現會啓用POSIX線程支持,使用互斥量保護對鏈表的訪問。因此本實現可以安全地用於POSIX多線程環境之中。

  • 與popen類似 (7),dpopen會在fork產生的子進程中關閉以前用dpopen打開的管道。

  • 如果傳給dpclose的參數不是以前用dpopen返回的非NULL值,當前實現除返回-1表示錯誤外,還會把errno設爲EBADF。對於pclose而言,這種情況在POSIX規範中被視爲不確定(unspecified)行爲 (8)

  • 實現中沒有使用任何平臺相關特性,以方便移植到其它POSIX平臺上。

下面的代碼展示了一個簡單例子,將多行文本送到sort中,然後取回結果、顯示出來:

#include <stdio.h>
#include <stdlib.h>
#include "dpopen.h"
#define MAXLINE 80
int main()
{
    char    line[MAXLINE];
    FILE    *fp;
    fp = dpopen("sort");
    if (fp == NULL) {
        perror("dpopen error");
        exit(1);
    }
    fprintf(fp, "orange\n");
    fprintf(fp, "apple\n");
    fprintf(fp, "pear\n");
    if (dphalfclose(fp) < 0) {
        perror("dphalfclose error");
        exit(1);
    }
    for (;;) {
        if (fgets(line, MAXLINE, fp) == NULL)
            break;
        fputs(line, stdout);
    }
    dpclose(fp);
    return 0;
}

輸出結果爲:

apple
orange
pear

總結

本文闡述了一個使用socketpair系統調用在Linux上實現雙向進程通訊管道的方法,並提供了一個實現。該實現提供的接口與POSIX規範中的popen/pclose函數較爲接近,因而非常易於使用。該實現沒有使用平臺相關的特性,因而可以不加修改或只進行少量修改即可移植到支持socketpair調用的POSIX系統中去。

本文源碼下載: dpopen.zip


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章