管道是UNIX系统IPC的最早的形式,并被所有UNIX系统提供。管道有两个限制。
1、历史上,它们是半双工的(也就是说,数据只往一个方向流)。一些系统现在提供全双工管道,但是为了最大的可移植性,我们不应该假定这种情况。
2、管道只能用在有共同祖先的进程之间。通常,一个管道被一个进程创建,这个进程调用fork,管道在父进程和子进程之间使用。
我们将看到FIFO(15.5节)处理了第二个限制,UNIX域套接字(17.3节)和命名基于STREAMS的管道(17.2.2节)处理了两个限制。
尽管这些限制,半双工管道仍是最普遍使用的IPC的形式。每次你在一个管道输入一个命令序列让外壳来执行时,外壳为每个命令创建独立的进程并把标准输出和后一个使用管道的标准输入链到一起。
一个管道通过调用pipe函数被创建。
- #include <unistd.h>
- int pipe(int filedes[2]);
- 成功返回0,错误返回-1。
两个文件描述符通过filedes参数被返回:filedes[0]被打开来读,filedes[1]被打开来写。filedes[1]的输出是filedes[0]的输入。
在4.3BSD、4.4BSD和Mac OS X 10.3里管道使用UNIX域套接字实现。即使UNIX域套接字默认是全双工,然而这些操作系统阻碍了管道使用的套接字,以便它们只以半双工模式操作。
POISIX.1允许一个实现支持全双工管道。对于这些实现,filedes[0]和filedes[1]都被打开来读和写。
一个半双工管道可以用两个角度来看,一个是管道两端在单个进程里相连接。另一个角度是强调管道的数据通过内核流动。
fstat函数(4.2节)为一个管道某端的文件描述符返回一个FIFO的文件类型。我们可以用S_ISFIFO宏测试一个管道。
POSIX.1指出stat结构体的st_size成员为于管道是无定义的。但是当fstat函数应用于管道的读端的文件描述符时,许多系统在st_size里存储管道里可读的字节。尽管如此,这不是可移植的。
单个进程里的管道是没什么用的。通常,调用pipe的进程接着会调用fork,创建一个父进程到子进程或相反方向的IPC渠道。下图显示了这个场景:
在fork之后发生了什么取决于我们想要数据流的哪个方向。对于一个从父进程到子进程的管道,父进程关闭管道的读端(fd[0]),而子进程关闭写端(fd[1])。下图显示了最终的描述符排列:
当一个进程的某端被关闭,以下的两条规则被应用:
1、
如果我们从一个写端被关闭的管道里read,那么read在所有数据被读之后返回0来指明一个文件末尾。(技术上,我们应该说这个文件末尾直到没有更多的
这个管道的写者时才产生。复制一个管道描述符以便多个进程打开这个管道来写是可能的。尽管如此,通常一个管道有单个读者和单个写者。当我们在下节讨论
FIFO时,我们将看到单个FIFO经常有多个写者。)
2、如果我们向一个读端被关闭的管道里write,那么信号SIGPIPE被产生。如果忽略这个信号或捕获它并从信号处理机返回,那么write返回-1,errno设置为EPIPE。
当
我们向一个管道(或FIFO)写时,常量PIPE_BUF指明内核的管道缓冲尺寸。一个PIPE_BUF字节或更少的write将不会和其它进程对相同管
道(或FIFO)的write交叉。但是如果多个进程正向一个管道(或FIFO)写,而我们write与PIPE_BUF字节更多,数据可能会和其它写者
的数据交叉。我们可以用pathconf或fpathconf来决定PIPE_BUF的值。
下面的代码展示了产生一个父子进程间的管道,并向管道发送数据。
- #include <unistd.h>
- #define MAXLINE 4096
- int
- main(void)
- {
- int n;
- int fd[2];
- pid_t pid;
- char line[MAXLINE];
- 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]);
- write(fd[1], "hello world\n", 12);
- } else { /* child */
- close(fd[1]);
- n = read(fd[0], line, MAXLINE);
- write(STDOUT_FILENO, line, n);
- }
- exit(0);
- }
在前一个例子里,我们直接在管道描述符上调用read和write。更有趣的是把管道描述符复制到标准输入和标准输出 。子进程经常接着运行一些其它程序,那个程序既可以从标准输入(我们创建的管道)里读,也可以向标准输出(管道)里写。
考
虑一个显示它创建的一些输出的程序,一次一页。我们想调用用户最感兴趣的换页器,而不是重新发明一些UNIX系统工具完成的页码。为了阻止向一个临时文件
写入所有的数据并调用system来显示这个文件,我们想把输出直接通过管道传到换页器。为了达到这个目的,我们创建一个管道,fork一个子进程,设置
子进程的标准入到管道的读端,并exec用户的换页程序。下面的代码展示了如何做这件事。(例子接受命令行参数来指定要显示的文件的名字。一个这样类型的
程序经常已经在内存里有了要显示在终端的数据了。)
- #include <unistd.h>
- #include <stdio.h>
- #define DEF_PAGER "/bin/more" /* default pager program */
- #define MAXLINE 4096
- int
- main(int argc, char *argv[])
- {
- int n;
- int fd[2];
- pid_t pid;
- char *pager, *argv0;
- char line[MAXLINE];
- FILE *fp;
- if (argc != 2) {
- printf("usage: a.out \n");
- exit(1);
- }
- if ((fp = fopen(argv[1], "r")) == NULL) {
- printf("can't open %s", argv[1]);
- exit(1);
- }
- 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 end */
-
- /* parent copies argv[1] to pipe */
- while (fgets(line, MAXLINE, fp) != NULL) {
- n = strlen(line);
- if (write(fd[1], line, n) != n) {
- printf("write error to pipe\n");
- exit(1);
- }
- }
- if (ferror(fp)) {
- printf("fgets error\n");
- exit(1);
- }
- close(fd[1]); /* close write end of pipe for reader */
- if (waitpid(pid, NULL, 0) < 0) {
- printf("waitpid error\n");
- exit(1);
- }
- exit(0);
- } else { /* child */
- close(fd[1]); /* close write end */
- if (fd[0] != STDIN_FILENO) {
- if (dup2(fd[0], STDIN_FILENO) != STDIN_FILENO) {
- printf("dup2 error to stdin");
- exit(1);
- }
- close(fd[0]); /* don't need this after dup2 */
- }
- /* get arguments for execl() */
- if ((pager = getenv("PAGER")) == NULL)
- pager = DEF_PAGER;
- if ((argv0 = strrchr(pager, '/')) != NULL)
- argv0++; /* step past rightmost slash */
- else
- argv0 = pager; /* no slash in pager */
- if (execl(pager, argv0, (char *)0) < 0) {
- printf("execl error for %s", pager);
- exit(1);
- }
- }
- exit(0);
- }
在调用fork之前,我们创建一个管道。在fork后,父进程关闭它的读端,而子进程关闭它的写端。然后子进程调用dup2来设置它的标准输入为管道的读端。当换页器程序被执行时,它的标准输入将是管道的读端。
当
我们复制一个描述符到另一个上时(子进程里fd[0]到标准输入),我们必须小心描述符不是已经有所需的值。如果描述符已经有所需的值而我们调用dup2
并close,那么描述符的单个拷贝会被关闭。(回想当两个参数相同时的dup2操作,3.12节)。在这个程序里,如果标准输入没有被一个外壳打开,开
头的fopen会使用描述符0,最小的未使用的描述符,所以fd[0]绝对不应该等于标准输入。尽管如此,每当我们调用dup2和close来复制一个描
述符到另一个上时,我们将总是先比较描述符,作为一个健壮编程的态度。
注意现在我们尝试使用环境变量PAGER来得到用户分页器程序。如果这不工作,我们使用默认的。这是环境变量的一个普遍用法。
回想8.9节的五个函数TELL_WAIT、TELL_PARENT、TELL_CHILD、WAIT_PARENT和WAIT_CHILD。在第10章,我们展示了使用信号的一个实现。下面的代码展示一个使用管道的实现。
- #include <unistd.h>
- static int pfd1[2], pfd2[2];
- void
- TELL_WAIT(void)
- {
- if (pipe(pfd1) < 0 || pipe(pfd2) < 0) {
- printf("pipe error\n");
- exit(1);
- }
- }
- void
- TELL_PARENT(pid_t pid)
- {
- if (write(pfd2[1], "c", 1) != 1) {
- printf("write error\n");
- exit(1);
- }
- }
- void
- WAIT_PARENT(void)
- {
- char c;
-
- if (read(pfd1[0], &c, 1) != 1) {
- printf("read error\n");
- exit(1);
- }
- if (c != 'p') {
- printf("WAIT_PARENT: incorrect data\n");
- exit(1);
- }
- }
- void
- TELL_CHILD(pid_t pid)
- {
- if (write(pfd1[1], "p", 1) != 1) {
- printf("write error\n");
- exit(1);
- }
- }
- void
- WAIT_CHILD(void)
- {
- char c;
-
- if (read(pfd2[0], &c, 1) != 1) {
- printf("read error\n");
- exit(1);
- }
- if (c != 'c') {
- printf("WAIT_CHILD: incorrect data\n");
- exit(1);
- }
- }
我们在fork前创建两个管道。父进程在调用TELL_CHILD时向第一个管道写字符“p”,而子进程在调用TELL_PARENT时向第二个管道写字符“c”。对应的WAIT_XXX函数为这个单个字符执行一个阻塞的read。
注意每个管道有一个额外的读者,这没有关系。也就是说,除了子进程从pfd1[0]读,父进程也将第一个管道的这一端打开来读。这不会影响我们,因为父进程不会从这个管道尝试去读。