程序間通訊--popen函式和pclose函式blog.chinaunix.net/xmlrpc.php?r=blog/article&uid=25940216&id=3206312
因為一個普遍的操作是為另一個程序建立一個管道,或者讀它的輸出或向它傳送輸入,所以標準I/O庫歷史上提供了popen和pclose函式。這兩 個函式處理我們自己一直在做的髒活:建立一個管道、fork一個子程序、關閉管道無用的端,執行一個外殼來執行這個命令,等待命令終止。
-
#include <stdio.h>
-
FILE *popen(const char *cmdstring, const char *type);
-
成功返回檔案指標,錯誤返回NULL。
-
int pclose(FILE *fp);
- 返回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節的第二個程式。
-
#include <stdio.h>
-
#define PAGER "${GAGER:-more}" /* environment variable, or default */
-
#define MAXLINE 4096
-
int
-
main(int argc, char *argv[])
-
{
-
char line[MAXLINE];
-
FILE *fpin, *fpout;
-
if (argc != 2) {
-
printf("usage: a.out \n");
-
exit(1)
-
}
-
if ((fpin = fopen(argv[1], "r")) == NULL) {
-
printf("can't open %s\n", argv[1]);
-
exit(1);
-
}
-
if ((fpout = popen(PAGER, "w")) == NULL) {
-
printf("popen error\n");
-
exit(1);
-
}
-
/* copy argv[1] to pager */
-
while (fgets(line, MAXLINE, fpin) != NULL) {
-
if (fputs(line, fpout) == EOF) {
-
printf("fputs error to pipe\n");
-
exit(1);
-
}
-
}
-
if (ferror(fpin)) {
-
printf("fgets error\n");
-
exit(1);
-
}
-
if (pclose(fpout) == -1) {
-
printf("pclose error\n");
-
exit(1);
-
}
-
exit(0);
- }
使用popen減少了我們必須寫的程式碼量。
外殼命令${PAGER:-more}說如果這個外殼變數PAGER被定義且非空則使用它,否則使用字串more。
下面的程式碼展示了popen和pclose的我們的版本。
-
#include <errno.h>
-
#include <fcntl.h>
-
#include <stdio.h>
-
#include <unistd.h>
-
/*
-
* Pointer to array allocated at run-time.
-
*/
-
static pid_t *childpid = NULL;
-
/*
-
* From our open_max(), Section
2.5.
-
*/
-
static int maxfd;
-
FILE *
-
popen(const char *cmdstring, const char *type)
-
{
-
int i;
-
int pfd[2];
-
pid_t pid;
-
FILE *fp;
-
/* only allow "r" or "w" */
-
if ((type[0] != 'r' && type[0] != 'w') || type[1] != 0) {
-
errno = EINVAL; /* required
by POSIX */
-
return(NULL);
-
}
-
if (childpid == NULL) { /* first time through */
-
/* allocate zeroed out array for child
pids */
-
maxfd = open_max();
-
if ((childpid = calloc(maxfd, sizeof(pid_t))) == NULL)
-
return(NULL);
-
}
-
if (pipe(pfd) < 0)
-
return(NULL); /* errno set by
pipe() */
-
if ((pid = fork()) < 0) {
-
return(NULL); /* errno set by
fork() */
-
} else if (pid == 0) { /* child */
-
if (*type == 'r') {
-
close(pfd[0]);
-
if (pfd[1] != STDOUT_FILENO) {
-
dup2(pfd[1], STDOUT_FILENO);
-
close(pfd[1]);
-
}
-
} else {
-
close(pfd[1]);
-
if (pfd[0] != STDIN_FILENO) {
-
dup2(pfd[0], STDIN_FILENO);
-
close(pfd[0]);
-
}
-
}
-
/* close all descriptors in childpid[] */
-
for (i = 0; i < maxfd; i++)
-
if (childpid[i] > 0)
-
close(i);
-
execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
-
_exit(127);
-
}
-
/* parent continues... */
-
if (*type == 'r') {
-
close(pfd[1]);
-
if ((fp = fdopen(pfd[0], type)) == NULL)
-
return(NULL);
-
} else {
-
close(pfd[0]);
-
if ((fp = fdopen(pfd[1], type)) == NULL)
-
return(NULL);
-
}
-
childpid[fileno(fp)] = pid; /* remember
child pid for this fd */
-
return(fp);
-
}
-
int
-
pclose(FILE *fp)
-
{
-
int fd, stat;
-
pid_t pid;
-
if (childpid == NULL) {
-
errno = EINVAL;
-
return(-1); /* popen() has
never been called */
-
}
-
fd = fileno(fp);
-
if ((pid = childpid[fd]) == 0) {
-
errno = EINVAL;
-
return(-1); /* fp
wasn't opened by popen() */
-
}
-
childpid[fd] = 0;
-
if (fclose(fp) == EOF)
-
return(-1);
-
while (waitpid(pid, &stat, 0) < 0)
-
if (errno != EINTR)
-
return(-1); /* error other
than EINTR from waitpid() */
-
return(stat); /* return
child's termination status */
- }
儘管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標準輸出的原因在下節談到協程序時討論。
-
#include <stdio.h>
-
int
-
main(void)
-
{
-
int c;
-
while ((c = getchar()) != EOF) {
-
if (isupper(c))
-
c = tolower(c);
-
if (putchar(c) == EOF) {
-
printf("output error\n");
-
exit(1);
-
}
-
if (c == '\n')
-
fflush(stdout);
-
}
-
exit(0);
- }
我們把這個過濾器編譯為可執行檔案filter_upper_to_lower,我們在下面程式碼裡使用popen呼叫它。
-
#include <stdio.h>
-
#define MAXLINE 4096
-
int main(void)
-
{
-
char line[MAXLINE];
-
FILE *fpin;
-
if ((fpin = popen("./filter_upper_to_lower", "r")) == NULL) {
-
printf("popen error\n");
-
exit(1);
-
}
-
for (;;) {
-
fputs("prompt> ", stdout);
-
fflush(stdout);
-
if (fgets(line, MAXLINE, fpin) == NULL) /* read
from pipe */
-
break;
-
if (fputs(line, stdout) == EOF) {
-
printf("fputs error to pipe\n");
-
exit(1);
-
}
-
}
-
if (pclose(fpin) == -1) {
-
printf("pclose error\n");
-
exit(1);
-
}
-
putchar('\n');
-
exit(0);
- }
我們需要在寫提示後呼叫fflush,因為標準輸出通常是行緩衝的,而提示沒有包行一個換行符。