进程间通信--popen函数和pclose函数blog.chinaunix.net/xmlrpc.php?r=blog/article&uid=25940216&id=3206312

来源:互联网 发布:popcorn time mac 编辑:程序博客网 时间:2024/06/08 05:37

分类: 系统运维

进程间通信--popen函数和pclose函数

因为一个普遍的操作是为另一个进程创建一个管道,或者读它的输出或向它发送输入,所以标准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.     
  10.     if (argc != 2) {
  11.         printf("usage: a.out \n");
  12.         exit(1);
  13.     }
  14.     if ((fpin = fopen(argv[1], "r")) == NULL) {
  15.         printf("can't open %s\n", argv[1]);
  16.         exit(1);
  17.     }

  18.     if ((fpout = popen(PAGER, "w")) == NULL) {
  19.         printf("popen error\n");
  20.         exit(1);
  21.     }

  22.     /* copy argv[1] to pager */
  23.     while (fgets(line, MAXLINE, fpin) != NULL) {
  24.         if (fputs(line, fpout) == EOF) {
  25.             printf("fputs error to pipe\n");
  26.             exit(1);
  27.         }
  28.     }
  29.     if (ferror(fpin)) {
  30.         printf("fgets error\n");
  31.         exit(1);
  32.     }
  33.     if (pclose(fpout) == -1) {
  34.         printf("pclose error\n");
  35.         exit(1);
  36.     }
  37.     
  38.     exit(0);
  39. }

使用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.     
  50.         /* close all descriptors in childpid[] */
  51.         for (= 0; i < maxfd; i++)
  52.             if (childpid[i] > 0)
  53.                 close(i);

  54.         execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
  55.         _exit(127);
  56.     }

  57.     /* parent continues... */
  58.     if (*type == 'r') {
  59.         close(pfd[1]);
  60.         if ((fp = fdopen(pfd[0], type)) == NULL)
  61.             return(NULL);
  62.     } else {
  63.         close(pfd[0]);
  64.         if ((fp = fdopen(pfd[1], type)) == NULL)
  65.             return(NULL);
  66.     }

  67.     childpid[fileno(fp)] = pid; /* remember child pid for this fd */
  68.     return(fp);
  69. }

  70. int
  71. pclose(FILE *fp)
  72. {
  73.     int fd, stat;
  74.     pid_t pid;

  75.     if (childpid == NULL) {
  76.         errno = EINVAL;
  77.         return(-1); /* popen() has never been called */
  78.     }

  79.     fd = fileno(fp);
  80.     if ((pid = childpid[fd]) == 0) {
  81.         errno = EINVAL;
  82.         return(-1); /* fp wasn't opened by popen() */
  83.     }

  84.     childpid[fd] = 0;
  85.     if (fclose(fp) == EOF)
  86.         return(-1);
  87.     
  88.     while (waitpid(pid, &stat, 0) < 0)
  89.         if (errno != EINTR)
  90.             return(-1); /* error other than EINTR from waitpid() */

  91.     return(stat); /* return child's termination status */
  92. }

尽管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,因为标准输出通常是行缓冲的,而提示没有包行一个换行符。
0 0