APUE协同进程的讨论

来源:互联网 发布:淘宝人工服务号码多少 编辑:程序博客网 时间:2024/05/22 12:57

什么是过滤程序(filter)?

也许很多人和我一样,天天都在使用过滤程序,却没有去了解过到底什么是过滤程序? 特地wikipedia了下(http://en.wikipedia.org/wiki/Filter_(software)#Unix):

filter is a computer program or subroutine to process a stream, producing another stream. While a single filter can be used individually, they are frequently strung together to form a pipeline.

In Unix and Unix-like operating systems, a filter is a program that gets most of its data from its standard input (the main input stream) and writes its main results to its standard output (the main output stream). Auxiliary input may come from command line flags or configuration files, while auxiliary output may go to standard error. The command syntax for getting data from a device or file other than standard input is the input operator (<). Similarly, to send data to a device or file other than standard output is the output operator (>). To append data lines to an existing output file, one can use the append operator (>>). Filters may be strung together into a pipeline with the pipe operator ("|"). This operator signifies that the main output of the command to the left is passed as main input to the command on the right.


简而言之,过滤程序是流(stream)处理程序,它们从标准输入读取流数据,对其进行适当处理后写到标准输出。标准输入默认来自键盘,不过可以重定向到一个文件或者来自其它程序的输出。标准输出默认是显示器,当然也可以重定向到一个文件,也可以是其它设备(例如打印机),或者重定向到管道作为其他程序的输入等。通常情况下,几个过滤程序在SHELL管道命令行(|)中线性地连接。
最常用的过滤程序有grep,cat,awk,head,sed,less,more等。

什么是协同进程(coprocess)?

了解了什么是过滤程序后,再来理解什么是协同进程就很简单了!
关于协同进程的定义,APUE2上是这样定义的:当一个程序产生某个过滤程序的输入,同时又读取该过滤程序的输出时,则该过滤程序就成为协同进程(coprocess)。可见,协同进程首先得是个过滤程序,其次它的标准输入和标准输出都不是默认的了 -- 通过管道来重定向。

协同进程实例

我们这里结合APUE2上的配图15-8来综述下如果一个进程需要协同进程处理数据的步骤如下:


第一步, 创建两个管道,fd1[2]和fd2[2](父进程在fd1[1]写入数据,再从fd2[0]读出数据)。
第二步, 调用fork()创建子进程。
第三步, 在子进程中,关闭fd1[1], fd2[0],并调用dup2使协同进程的标准输入连接到fd1[0], 标准输出连接到fd2[1],这样就将两个管道连接起来了。再在子进程中调用execl调用编写的协同处理程序(这里的协同程序做为一般的程序编写即可,从标准输入读入数据,处理后输出到标准输出)。
第四步, 在父进程中,关闭fd1[0], fd2[1],将协同进程需要处理的数据写入fd1[1], 再从fd2[0]读出协同进程的输出即可。

程序如下:

#include <stdio.h>#include <stdlib.h>#include <signal.h>#include <string.h>#include <unistd.h>#include <stdarg.h>#include <sys/types.h>#include <sys/wait.h>/** *parentchild *->  +---------------+ -> *fd1[1]||fd1[0] *+---------------+ * *  <-  +---------------+ <- *  fd2[0]||fd2[1] *+---------------+ */#define MAXLINE(4096)static void err_sys(const char *err_msg){perror(err_msg);exit(EXIT_FAILURE);}static void sig_pipe(int signo){printf("parent: SIGPIPE caught\n");exit(-1);}int main(int argc, const char *argv[]){int fd1[2], fd2[2];pid_t pid;size_t nwrite;ssize_t nread;char line[MAXLINE];if (signal(SIGPIPE, sig_pipe) == SIG_ERR) {err_sys("signal error");}if (pipe(fd1) == -1 || pipe(fd2) == -1) {err_sys("pipe error");}if ((pid = fork()) == -1) {/* error */err_sys("fork error");} else if (pid > 0) {/* parent */close(fd1[0]);close(fd2[1]);while (fgets(line, MAXLINE, stdin) != NULL) {if (line[0] == '\n')continue;nwrite = strlen(line);if (write(fd1[1], line, nwrite) != nwrite) {err_sys("write error");}if ((nread = read(fd2[0], line, MAXLINE)) < 0) {err_sys("read error");}if (nread == 0) {fprintf(stderr, "child closed pipe\n");break;}line[nread] = '\0';/* null terminate */if (fputs(line, stdout) == EOF) {err_sys("fputs error");}}if (ferror(stdin)) {err_sys("fgets error");}if (feof(stdin)) {printf("EOF\n");}close(fd1[1]);/* child will see EOF */close(fd2[0]);waitpid(pid, NULL, 0);exit(0);} else {/* child */close(fd1[1]);close(fd2[0]);/* stdin -> fd1[0]*/if (fd1[0] != STDIN_FILENO) {if (dup2(fd1[0], STDIN_FILENO) != STDIN_FILENO) {err_sys("dup2 error to stdin");}close(fd1[0]);}/* stdout -> fd2[0]*/if (fd2[1] != STDOUT_FILENO) {if (dup2(fd2[1], STDOUT_FILENO) != STDOUT_FILENO) {err_sys("dup2 error to stdout");}close(fd2[1]);}if (execl("./add2", "add2", (char *)NULL) < 0) {err_sys("execl error");}}}

#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <string.h>#include <signal.h>#define MAXLINE(4096)static void err_sys(const char *err_msg){perror(err_msg);exit(EXIT_FAILURE);}static void sig_pipe(int signo){fprintf(stderr, "child: SIGPIPE caught\n");exit(-1);}int main(int argc, const char *argv[]){int int1, int2;char line[MAXLINE];ssize_t nr;size_t nw;if (signal(SIGPIPE, sig_pipe) == SIG_ERR) {err_sys("signal error: ");}while ((nr = read(STDIN_FILENO, line, MAXLINE)) > 0) {if (line[0] == '\n')continue;line[nr]= '\0';if (sscanf(line, "%d %d", &int1, &int2) == 2) {sprintf(line, "%d\n", int1 + int2);nw = strlen(line);if (write(STDOUT_FILENO, line, nw) != nw) {err_sys("write error: ");}} else {if (write(STDOUT_FILENO, "invalid args\n", 13) != 13) {err_sys("write error: ");}}}if (nr == 0) {fprintf(stderr, "parent close the pipe\n");exit(-1);}exit(EXIT_SUCCESS);}


程序较APUE2上的例子有些许改动。关于程序这里想讨论如下几个小问题:

1.  什么时候父进程会收到SIGPIPE信号,什么时候子进程会收到SIGPIPE信号?

首先要知道,当所有的读者都关闭了管道的读取端时,如果此时再试图写入管道,则内核会发送信号SIGPIPE给进程。
这里因为只有父进程和一个子进程,所以很简单,只要其中一方关闭读取端就可以使对方收到SIGPIPE信号。具体操作如下:
父进程收到SIGPIPE信号:
在程序等待输入时,使用kill -9杀死add2协同进程,然后输入两个数,回车。
子进程收到SIGPIPE信号:
在程序往fd1[1]成功写入两个数字后,父进程调用exit退出即可,这样在子进程往fd2[1]写计算的结果时会收到信号。

2.  APUE2上阐述的产生死锁的条件正确吗?

APUE2上给出了使用标准I/O的add2程序如下:

#include <stdio.h>#include <stdlib.h>#include <signal.h>#define MAXLINE(4096)static void err_sys(const char *err_msg){perror(err_msg);exit(EXIT_FAILURE);}static void sig_pipe(int signo){fprintf(stderr, "child: SIGPIPE caught\n");exit(-1);}int main(int argc, const char *argv[]){int int1, int2;char line[MAXLINE];if (signal(SIGPIPE, sig_pipe) == SIG_ERR) {err_sys("signal error: ");}while (fgets(line, MAXLINE + 1, stdin) != NULL) {if (line[0] == '\n')continue;if (sscanf(line, "%d %d", &int1, &int2) == 2) {printf("%d\n", int1 + int2);} else {printf("invalid args\n");}}if (ferror(stdin)) {err_sys("fgets error");exit(-1);}if (feof(stdin)) {fprintf(stderr, "parent close the pipe\n");exit(-1);}exit(EXIT_SUCCESS);}

 但是书上描述的产生死锁的条件其实是错误的,为什么这么说呢?
fgets从标准输入读取并不会发生阻塞,因为父进程确确实实将数据写入管道了。add2计算两数之和后调用printf写入结果,问题就出在这了 -- 因为操作管道的标准I/O使用的是全缓冲,所以printf之后stdio缓冲区并没有满,数据依然停留在stdio缓冲区,并未写入管道,这就导致父进程的read会一直阻塞,而子进程接下来的fgets也会一直阻塞,这才是产生死锁的真正条件!
那么什么时候会出现书上描述的死锁情况呢 -- 很简单,只有在父进程也是使用的标准I/O的情况下才会出现,这样子进程fgets读取不到数据而阻塞。
因此,除了书上提到的使用setvbuf改变缓冲类型为行缓冲的方法外,还可以在子进程的两个printf之后显示地调用fflush(stdout),这样也是可以解决问题的。

0 0