一个UNIX系统过滤器是一个从标准输入读并写到标准输出的程序。过滤器通常在外壳管道里线性连接。一个过滤器变为一个协进程,当同一个的程序产生过滤器的输入并读取过滤器的输出。
Korn
外壳提供了协进程。Bourne、Bourne-again和C外壳都没有提供一种把进程作为协进程连接在一起的方法。一个协进程通常从一个外壳运行到后
台,它的标准输入和标准输出使用一个管道被连接到另一个程序。尽管初始化一个协进程并连接它的输入输出到另一个程序所需的外壳语法相当绕,但是协进程在C
程序里也有用。
popen给了我们一个写到另一个进程的标准输入或从它的标准输出读的单向管道,而使用协进程,我们有了到另一个进程的双向管道:一个向它的标准输入写,一个从它的标准输出读。我们想向它的标准输入写,让让操作数据,然后从它的标准输出读。
让我们在一个例子里看下协进程。进程创建两个管道:一个是协进程的标准输入,而另一个是协进程的标准输出。
下面的代码是一个简单的协进程,它从标准输入读取两个数,计算它们的和,并把和写到标准输出。(协进程通常做比我们这里演示的更有趣的工作。这个例子是公认地捏造的,以便我们可以学习连接进程所需的探索。)
- #include <unistd.h>
- #define MAXLINE 4096
- int main(void)
- {
- int n, int1, int2;
- char line[MAXLINE];
- while ((n = read(STDIN_FILENO, line, MAXLINE)) > 0) {
- line[n] = 0; /* null terminate */
- if (sscanf(line, "%d%d", &int1, &int2) == 2) {
- sprintf(line, "%d\n", int1 + int2);
- n = strlen(line);
- if (write(STDOUT_FILENO, line, n) != n) {
- printf("write error\n");
- exit(1);
- }
- } else {
- if (write(STDOUT_FILENO, "invalid args\n", 13) != 13) {
- printf("write error\n");
- exit(1);
- }
- }
- }
- exit(0);
- }
我们把这个程序编译为可执行程序filter_add_two_numbers。
下面的代码调用上面的协进程,在从标准输入读两个数后。协进程的值被写到它的标准输出。
- #include <unistd.h>
- #include <signal.h>
- #include <stdio.h>
- #define MAXLINE 4096
- static void sig_pipe(int); /* our signal handlers */
- int
- main(void)
- {
- int n, fd1[2], fd2[2];
- pid_t pid;
- char line[MAXLINE];
- if (signal(SIGPIPE, sig_pipe) == SIG_ERR) {
- printf("signal error\n");
- exit(1);
- }
- if (pipe(fd1) < 0 || pipe(fd2) < 0) {
- printf("pipe error\n");
- exit(1);
- }
- if ((pid = fork()) < 0) {
- printf("fork error\n");
- exit(1);
- } else if (pid > 0) { /* parent */
- close(fd1[0]);
- close(fd2[1]);
- while (fgets(line, MAXLINE, stdin) != NULL) {
- n = strlen(line);
- if (write(fd1[1], line, n) != n) {
- printf("write error to pipe\n");
- exit(1);
- }
- if ((n = read(fd2[0], line, MAXLINE)) < 0) {
- printf("read error from pipe\n");
- exit(1);
- }
- if (n == 0) {
- printf("child closed pipe\n");
- break;
- }
- line[n] = 0; /* null terminate */
- if (fputs(line, stdout) == EOF) {
- printf("fputs error\n");
- exit(1);
- }
- }
- if (ferror(stdin)) {
- printf("fgets error on stdin\n");
- exit(1);
- }
- exit(0);
- } else { /* child */
- close(fd1[1]);
- close(fd2[0]);
- if (fd1[0] != STDIN_FILENO) {
- if (dup2(fd1[0], STDIN_FILENO) != STDIN_FILENO) {
- printf("dup2 error to stdin\n");
- exit(1);
- }
- close(fd1[0]);
- }
-
- if (fd2[1] != STDOUT_FILENO) {
- if (dup2(fd2[1], STDOUT_FILENO) != STDOUT_FILENO) {
- printf("dup2 error to stdout\n");
- exit(1);
- }
- close(fd2[1]);
- }
- if (execl("./filter_add_two_numbers", "filter_add_two_numbers", (char *)0) < 0) {
- printf("execl error\n");
- exit(1);
- }
- }
- exit(0);
- }
- static void
- sig_pipe(int signo)
- {
- printf("SIGPIPE caught\n");
- exit(1);
- }
在协进程filter_add_two_numbers里,我们有目的地使用了低级I/O(UNIX系统调用):read和write。如果使用标准I/O来重写这个协进程会如何呢?下面的代码展示了新版本。
- #include <stdio.h>
- #define MAXLINE 4096
- int
- main(void)
- {
- int int1, int2;
- char line[MAXLINE];
- while (fgets(line, MAXLINE, stdin) != NULL) {
- if (sscanf(line, "%d%d", &int1, &int2) == 2) {
- if (printf("%d\n", int1 + int2) == EOF) {
- fprintf(stderr, "printf error\n");
- exit(1);
- }
- } else {
- if (printf("invalid args\n") == EOF) {
- fprintf(stderr, "printf error\n");
- exit(1);
- }
- }
- }
- exit(0);
- }
如果我们在之前的程序里调用这个新的协进程,那么它不再工作。问题是默认的标准I/O缓冲。当我们调用上面的代码时,第一个标准输入上
的fgets导致标准I/O库分配一个缓冲并选择缓冲的类型。因为标准输入是一个管道,所以标准I/O库默认为完全缓冲的。相同的事情在标准输出里也会发
生。当add2被阻塞在从它的标准输入里读时,协进程正被阻塞在从管道的读。我们有一个死锁。
这里,我们有对正被运行的协进程的控制。我们可以改变上面的代码,在while循环前加入以下几行:
if (setvbuf(stdin, NULL, _IOLBF, 0) != 0) {
fprintf(stderr, "setvbuf error\n");
exit(1);
}
if (setvbuf(stdout, NULL, _IOLBF, 0) != 0) {
fprintf(stderr, "setvbuf error\n");
exit(1);
}
这几行导致fgets在一行可用时返回,并导致printf当输出一个换行符时执行一个fflush(5.4节,标准I/O缓冲)。执行这此显示的setvbuf调用修复了前面的代码。
如果我们不能修改我们为其建立管道输出的程序,那么需要其它技术。例如,如果我们使用awk作为我们程序的一个协进程(而不是filter_add_two_numbers程序),下面的命令不会工作:
#! /usr/bin/awk -f
{ print $1 + $2 }
不工作的原因再次是标准I/O缓冲。但是在这种情况睛, 我们不能改变awk工作的方式(除非我们有它的源码)。我们不能以任何方式修改awk的可执行程序来改变标准I/O缓冲被处理的方式。
这个通用问题的解决方案是让被调用的协进程(这个情况下是awk)认为它的标准输入和标准输出被连接到一个终端。那导致协进程进程里的标准I/O例程来行缓冲这两个I/O流,和我们之前显示地调用setvbuf相似。我们在19章使用伪终端来做这个。
阅读(1143) | 评论(0) | 转发(0) |