歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux綜合 >> Linux資訊 >> 更多Linux

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

問題和現有方法
  Linux提供了popen和pclose函數 ,用於創建和關閉管道與另外一個進程進行通信。其接口如下:
  
  FILE *popen(const char *command, const char *mode);
  int pclose(FILE *stream);
  
  遺憾的是,popen創建的管道只能是單向的--mode只能是 "r" 或 "w" 而不能是某種組合--用戶只能選擇要麼往裡寫,要麼從中讀,而不能同時在一個管道中進行讀寫。實際應用中,經常會有同時進行讀寫的要求,比如,我們可能希望把文本數據送往sort工具排序後再取回結果。此時popen就無法用上了。我們需要尋找其它的解決方案。
  
  有一種解決方案是使用pipe函數 創建兩個單向管道。沒有錯誤檢測的代碼示意如下:
  
  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調用 ,可以實現上述在同一個文件描述符中進行讀寫的功能(該調用目前也是POSIX規范的一部分 )。該系統調用能創建一對已連接的(UNIX族)無名socket。在Linux中,完全可以把這一對socket當成pipe返回的文件描述符一樣使用,唯一的區別就是這一對文件描述符中的任何一個都可讀和可寫。
  
  這已經接近了我們想要的目標了。但仍然存在一個問題,阻礙我們使用socketpair實現一個管道與子進程通信:為了解決我前面的提出的使用sort的應用問題,我們需要關閉子進程的標准輸入通知子進程數據已經發送完畢,而後從子進程的標准輸出中讀取數據直到遇到EOF。使用兩個單向管道的話每個管道可以單獨關閉,因而不存在此問題;而在使用雙向管道時,如果不關閉管道就無法通知對端數據已經發送完畢,但關閉了管道又無法從中讀取結果數據。--這一問題不解決的話,使用socketpair的設想就變得毫無意義。
  
  在查找和試驗之後,我發現shutdown調用 可解決此問題。畢竟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文檔注釋 。我只略作幾點說明:
  
  本實現使用了一個鏈表來記錄所有dpopen打開的文件指針和子進程ID的對應關系,因此,在同時用dpopen打開的管道的多的時候,dpclose(需要搜索鏈表)的速度會稍慢一點。我認為在通常使用過程中這不會產生什麼問題。如果在某些特殊情況下這會是一個問題的話,可考慮更改dpopen的返回值類型和dpclose的傳入參數類型(不太方便使用,但實現簡單),或者使用哈希表/平衡樹來代替目前使用的鏈表以加速查找(接口不變,但實現較復雜)。
  
  當編譯時在gcc中使用了"-pthread"命令行參數時,本實現會啟用POSIX線程支持,使用互斥量保護對鏈表的訪問。因此本實現可以安全地用於POSIX多線程環境之中。
  
  與popen類似 ,dpopen會在fork產生的子進程中關閉以前用dpopen打開的管道。
  
  如果傳給dpclose的參數不是以前用dpopen返回的非NULL值,當前實現除返回-1表示錯誤外,還會把errno設為EBADF。對於pclose而言,這種情況在POSIX規范中被視為不確定(unspecified)行為 。
  
  實現中沒有使用任何平台相關特性,以方便移植到其它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系統中去。





Copyright © Linux教程網 All Rights Reserved