管道
管道有两种局限性:
- 历史上,管道是半双工的(即数据只能在一个方向上流动,现在某些系统提供全双工管道)。
- 管道只能在具有公共祖先的两个进程之间使用。(通常一个管道由一个进程创建,在进程调用 fork 之后,这个管道就能在父进程和子进程之间使用)。
管道通过调用 pipe 函数创建:
#include <unistd.h>
int pipe(int fd[2]);
返回值:
- 成功,返回 0
- 失败,返回 -1
经由参数 fd 返回两个文件描述符:fd[0]为读而打开,fd[1]为写而打开。fd[1]的输出是fd[0]的输入。
通常,进程会调用 pipe,接着调用 fork,从而创建从父进程到子进程的 IPC 通道,反之亦然。
fork之后做什么取决于我们想要的数据流方向。对于从父进程到子进程的管道,父进程关闭管道的读端(fd[0]),子进程关闭写端(fd[1])。
当管道的一端被关闭后,下列两条规则起作用:
- 当读(read)一个写端已被关闭的管道时,在所有数据都被读取后,read 返回 0,表示文件结束。
- 如果写(write)一个读端已被关闭的管道,则产生信号SIGPIP。如果忽略该信号或者捕捉该信号并从其处理程序返回,则write返回-1,errno 设置为 EPIPE 。
实例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void) {
int n;
int fd[2];
pid_t pid;
char line[2048];
if (pipe(fd) < 0) {
printf("pipe error\n");
exit(-1);
}
if ((pid = fork()) < 0) {
printf("fork error\n");
exit(-1);
} else if (pid > 0) {
// parent
close(fd[0]); // close read
write(fd[1], "hello world\n", 12);
} else {
// child
close(fd[1]); // close write
n = read(fd[0], line, 2048);
write(STDOUT_FILENO, line, n);
}
exit(0);
}
上面的例子中,直接对管道描述符调用了 read 和 write,更有趣的是将管道描述符复制到了标准输入或标准输出上。
协同进程
UNIX 系统过滤程序从标准输入读取数据,向标准输出写数据。几个过滤程序通常在 shell 管道中线性连接。当一个过滤程序即产生某个过滤程序的输入,又读取该过滤程序的输出时,它就变成了协同进程(coprocess)。
协同程序通常在 shell 的后台运行,其标准输入和标准输出通过管道连接到另一个程序。
协同进程有连接到另一个进程的两个单向管道:一个接到其标准输入,另一个则来自其标准输出。我们想将数据写到其标准输入,将其处理后,再从其标准输出读取数据。
实例
该实例是一个简单地协同进程,它从其它标准输入读取两个数,计算它们的和,然后将和写至其标准输出。
// add.c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.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;
if (sscanf(line, "%d%d", &int1, &int2) == 2) {
sprintf(line, "%d\n", int1 + int2);
n = strlen(line);
if (write(STDOUT_FILENO, line, n) != n) {
perror("write error");
exit(1);
}
} else {
if (write(STDOUT_FILENO, "invalid args\n", 13) != 13) {
perror("write error");
exit(1);
}
}
}
exit(0);
}
对此程序进行编译,将其可执行目标代码存入名为 add2 的文件。
下面的程序从其标准输入读取两个数之后调用 add2 协同进程,并将协同进程送来的值写到其标准输出。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define MAXLINE 4096
static void sig_pipe(int);
int main(void) {
int n, fd1[2], fd2[2];
pid_t pid;
char line[MAXLINE];
if (signal(SIGPIPE, sig_pipe) == SIG_ERR) {
perror("signal error\n");
exit(1);
}
if (pipe(fd1) < 0 || pipe(fd2) < 0) {
perror("pipe error\n");
exit(1);
}
if ((pid = fork()) < 0) {
perror("fork error\n");
exit(1);
} else if (pid > 0) {
// parent
close(fd1[0]); // 关闭 fd1 读(read)
close(fd2[1]); // 关闭 fd2 写(write)
while (fgets(line, MAXLINE, stdin) != NULL) {
n = strlen(line);
printf("pid %d write to child: %s\n", pid, line);
if (write(fd1[1], line, n) != n) {
perror("write error to pipe\n");
exit(1);
}
if ((n = read(fd2[0], line, MAXLINE)) < 0) {
perror("read error from pipe");
exit(1);
}
if (n == 0) {
perror("child closed pipe\n");
break;
}
line[n] = 0;
if (fputs(line, stdout) == EOF) {
perror("fputs error");
exit(1);
}
}
if (ferror(stdin)) {
perror("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) {
perror("dup2 error to stdin\n");
exit(1);
}
close(fd1[0]);
}
if (fd2[1] != STDOUT_FILENO) {
if (dup2(fd2[1], STDOUT_FILENO) != STDOUT_FILENO) {
perror("dup2 error to stdout");
exit(1);
}
close(fd2[1]);
}
if (execl("./add2", "add2", (char *) 0) < 0) {
perror("execl error\n");
exit(1);
}
}
exit(0);
}
static void sig_pipe(int signo) {
printf("SIGPIPE caught\n");
exit(1);
}
这个程序创建了两个管道,父进程、子进程各自关闭它们不需使用的管道端,必须使用两个管道:一个用作协同进程的标准输入,另一个则用作它的标准输出。然后,子进程调用 dup2 使管道描述符转移至其标准输入和输出,最后调用 execl 。
参考
《UNIX 环境高级编程》
进程间的通信方式——pipe(管道)
进程间8种通信方式详解
Linux 进程间通信