1. 程式人生 > >程序間通訊--popen函式和pclose函式blog.chinaunix.net/xmlrpc.php?r=blog/article&uid=25940216&id=3206312

程序間通訊--popen函式和pclose函式blog.chinaunix.net/xmlrpc.php?r=blog/article&uid=25940216&id=3206312

因為一個普遍的操作是為另一個程序建立一個管道,或者讀它的輸出或向它傳送輸入,所以標準I/O庫歷史上提供了popen和pclose函式。這兩 個函式處理我們自己一直在做的髒活:建立一個管道、fork一個子程序、關閉管道無用的端,執行一個外殼來執行這個命令,等待命令終止。


  1. #include <stdio.h>
  2. FILE *popen(const char *cmdstring, const char *type);
  3. 成功返回檔案指標,錯誤返回NULL。
  4. int pclose(FILE *fp);
  5. 返回cmdstring的終止狀態,錯誤返回-1。

函式popen執行一個fork和exec來執行cmdstring,並返回一個標準I/O檔案指標。如果type是“r”,那麼檔案指標被連線到cmdstring的標準輸入。

如果type是“w”,那麼檔案指標被連線到cmdstring的標準輸入。

一種記住popen的最後一個引數的方法是:像fopen一樣,返回的檔案指標在“r”的type時是可讀的,或在“w”的type時是可寫的。

pclose函式關閉標準I/O流,等待命令的終止,返回外殼的終止狀態。(我們在8.6節描述過終止狀態。system函式,8.13節,也返回終止狀態。)如果外殼不能被執行,pclose返回的狀態就好像外殼執行了一個exit(127)。

cmdstring被Bourne shell,如
sh -c cmdstring

這意味著外殼展開了cmdstring裡的任何特殊字元。例如,這允許我們說:fp = popen("ls *.c", "r");或fp = popen("cmd 2>&1", "r");

讓我們用popen重新實現15.2節的第二個程式。

  1. #include <stdio.h>
  2. #define PAGER "${GAGER:-more}" /* environment variable, or default */
  3. #define MAXLINE 4096
  4. int
  5. main(int argc, char *argv[])
  6. {
  7.     char line[MAXLINE];
  8.     FILE *fpin, *fpout;
  9.     if (argc != 2) {
  10.         printf("usage: a.out \n");
  11.         exit(1)
    ;
  12.     }
  13.     if ((fpin = fopen(argv[1], "r")) == NULL) {
  14.         printf("can't open %s\n", argv[1]);
  15.         exit(1);
  16.     }
  17.     if ((fpout = popen(PAGER, "w")) == NULL) {
  18.         printf("popen error\n");
  19.         exit(1);
  20.     }
  21.     /* copy argv[1] to pager */
  22.     while (fgets(line, MAXLINE, fpin) != NULL) {
  23.         if (fputs(line, fpout) == EOF) {
  24.             printf("fputs error to pipe\n");
  25.             exit(1);
  26.         }
  27.     }
  28.     if (ferror(fpin)) {
  29.         printf("fgets error\n");
  30.         exit(1);
  31.     }
  32.     if (pclose(fpout) == -1) {
  33.         printf("pclose error\n");
  34.         exit(1);
  35.     }
  36.     exit(0);
  37. }

使用popen減少了我們必須寫的程式碼量。

外殼命令${PAGER:-more}說如果這個外殼變數PAGER被定義且非空則使用它,否則使用字串more。

下面的程式碼展示了popen和pclose的我們的版本。

  1. #include <errno.h>
  2. #include <fcntl.h>
  3. #include <stdio.h>
  4. #include <unistd.h>
  5. /*
  6.  * Pointer to array allocated at run-time.
  7.  */
  8. static pid_t *childpid = NULL;
  9. /*
  10.  * From our open_max(), Section 2.5.
  11.  */
  12. static int maxfd;
  13. FILE *
  14. popen(const char *cmdstring, const char *type)
  15. {
  16.     int i;
  17.     int pfd[2];
  18.     pid_t pid;
  19.     FILE *fp;
  20.     /* only allow "r" or "w" */
  21.     if ((type[0] != 'r' && type[0] != 'w') || type[1] != 0) {
  22.         errno = EINVAL; /* required by POSIX */
  23.         return(NULL);
  24.     }
  25.     if (childpid == NULL) { /* first time through */
  26.         /* allocate zeroed out array for child pids */
  27.         maxfd = open_max();
  28.         if ((childpid = calloc(maxfd, sizeof(pid_t))) == NULL)
  29.             return(NULL);
  30.     }
  31.     if (pipe(pfd) < 0)
  32.         return(NULL); /* errno set by pipe() */
  33.     if ((pid = fork()) < 0) {
  34.         return(NULL); /* errno set by fork() */
  35.     } else if (pid == 0) { /* child */
  36.         if (*type == 'r') {
  37.             close(pfd[0]);
  38.             if (pfd[1] != STDOUT_FILENO) {
  39.                 dup2(pfd[1], STDOUT_FILENO);
  40.                 close(pfd[1]);
  41.             }
  42.         } else {
  43.             close(pfd[1]);
  44.             if (pfd[0] != STDIN_FILENO) {
  45.                 dup2(pfd[0], STDIN_FILENO);
  46.                 close(pfd[0]);
  47.             }
  48.         }
  49.         /* close all descriptors in childpid[] */
  50.         for (= 0; i < maxfd; i++)
  51.             if (childpid[i] > 0)
  52.                 close(i);
  53.         execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
  54.         _exit(127);
  55.     }
  56.     /* parent continues... */
  57.     if (*type == 'r') {
  58.         close(pfd[1]);
  59.         if ((fp = fdopen(pfd[0], type)) == NULL)
  60.             return(NULL);
  61.     } else {
  62.         close(pfd[0]);
  63.         if ((fp = fdopen(pfd[1], type)) == NULL)
  64.             return(NULL);
  65.     }
  66.     childpid[fileno(fp)] = pid; /* remember child pid for this fd */
  67.     return(fp);
  68. }
  69. int
  70. pclose(FILE *fp)
  71. {
  72.     int fd, stat;
  73.     pid_t pid;
  74.     if (childpid == NULL) {
  75.         errno = EINVAL;
  76.         return(-1); /* popen() has never been called */
  77.     }
  78.     fd = fileno(fp);
  79.     if ((pid = childpid[fd]) == 0) {
  80.         errno = EINVAL;
  81.         return(-1); /* fp wasn't opened by popen() */
  82.     }
  83.     childpid[fd] = 0;
  84.     if (fclose(fp) == EOF)
  85.         return(-1);
  86.     while (waitpid(pid, &stat, 0) < 0)
  87.         if (errno != EINTR)
  88.             return(-1); /* error other than EINTR from waitpid() */
  89.     return(stat); /* return child's termination status */
  90. }

儘管popen的核心和我們在本章前面使用的程式碼相似,但是有許多我們需要小心的細節。首先,每個popen被呼叫時,我們必須記住我們建立 的子程序的程序ID和它的檔案描述符或FILE指標。我們選擇在childpid數組裡儲存子程序的ID,並索引它來得到檔案描述符。通過這種方法,當 pclose在用FILE指標作為引數被呼叫時我們呼叫標準I/O函式fileno來得到檔案描述符,然後把子程序ID用在waitpid呼叫裡。因為一 個組宣程序不只一次呼叫popen是可能的,所以我們動態分配childpid陣列(在第一次popen被呼叫時),它有足夠大的空間來容納和檔案描述符 數量相同的子程序。

呼叫pipe和fork然後為每個程序複製恰當的描述符和我們在本章前面做的事件相似。

POSIX.1要求popen關閉任何在子程序裡通過上次popen呼叫開啟的流。為了做到這個,我們遍歷子程序裡的childpid陣列,關掉任何仍然開啟的描述符。

如 果pclose呼叫者已為SIGCHLD設立一個訊號處理機會發生什麼?pclose裡的waitpid呼叫會返回EINTR的錯誤。因為呼叫者被允許捕 獲這個訊號(或任何可能中斷waitpid的其它訊號),所以我們簡單地再次呼叫waitpid,如果它被一個捕獲的訊號中斷。

注意如果應用呼叫waitpid並獲得popen建立的子程序的退出狀態,那麼我們將在應用呼叫pclose的時候呼叫waitpid,發現子程序不再存在,返回-1並設定errno為ECHILD。這是POSIX.1在這種情況所要求的行為。

pclose的早期版本返回一個EINTR的錯誤,如果一個訊號中斷了wait。同樣,一些早期版本的plose在wait期間阻塞或忽略訊號SIGINT、SIGQUIT和SIGHUP。這不被POSIX.1允許。

注 意popen決不應該被一個設定使用者ID或設定組ID程式呼叫。當它執行命令時,popen做等價於execl("/bin/sh", "sh", "-c", command, NULL);的事,它用從呼叫者繼承下來的環境執行外殼和command。一個惡意使用者可以操作環境,以便外殼執行不被期望的命令,使用從設定ID檔案模 式得到的許可權。

popen特別適合的事是執行簡單的過濾器來轉換執行的命令的輸入或輸出。這是一個命令想要建立自己的管道的情況。

考 慮一個向標準輸出寫一個提示並從標準輸入讀一任的應用。使用popen,我們可以在應用和它的輸入之間插入一個程式來轉換輸入。這些程序的排列為:父程序 建立一個子程序執行這個過濾器,並建立管道,使過濾器的標準輸出變為管道的寫端。父程序向用戶終端輸出提示,使用者通過終端向過濾器輸入,而過濾器的輸出通 過管道,被父程序讀取。

例如,這個轉換可以是路徑名擴充套件,或者提供一個歷史機制(記住前一個輸入的命令)。

下面的程式碼展示了一個簡單的過濾器來證明這個操作。這個過濾拷貝標準輸入到標準輸出,把任何大寫字元輪換為小寫。在寫一個換行符我們小心地ffush標準輸出的原因在下節談到協程序時討論。

  1. #include <stdio.h>
  2. int
  3. main(void)
  4. {
  5.     int c;
  6.     while ((= getchar()) != EOF) {
  7.         if (isupper(c))
  8.             c = tolower(c);
  9.         if (putchar(c) == EOF) {
  10.             printf("output error\n");
  11.             exit(1);
  12.         }
  13.         if (== '\n')
  14.             fflush(stdout);
  15.     }
  16.     exit(0);
  17. }

我們把這個過濾器編譯為可執行檔案filter_upper_to_lower,我們在下面程式碼裡使用popen呼叫它。


  1. #include <stdio.h>
  2. #define MAXLINE 4096
  3. int main(void)
  4. {
  5.     char line[MAXLINE];
  6.     FILE *fpin;
  7.     if ((fpin = popen("./filter_upper_to_lower", "r")) == NULL) {
  8.         printf("popen error\n");
  9.         exit(1);
  10.     }
  11.     for (;;) {
  12.         fputs("prompt> ", stdout);
  13.         fflush(stdout);
  14.         if (fgets(line, MAXLINE, fpin) == NULL) /* read from pipe */
  15.             break;
  16.         if (fputs(line, stdout) == EOF) {
  17.             printf("fputs error to pipe\n");
  18.             exit(1);
  19.         }
  20.     }
  21.     if (pclose(fpin) == -1) {
  22.         printf("pclose error\n");
  23.         exit(1);
  24.     }
  25.     putchar('\n');
  26.     exit(0);
  27. }


我們需要在寫提示後呼叫fflush,因為標準輸出通常是行緩衝的,而提示沒有包行一個換行符。