分类: 系统运维
因为一个普遍的操作是为另一个进程创建一个管道,或者读它的输出或向它发送输入,所以标准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,因为标准输出通常是行缓冲的,而提示没有包行一个换行符。