# 1.3 管道

管道是作为一对文件描述符公开给进程的小型内核缓冲区，一个用于读取，一个用于写入。将数据写入管道的一端使得这些数据可以从管道的另一端读取。管道为进程提供了一种通信方式。

下面的示例代码使用连接到管道读端的标准输入来运行程序`wc`。

```c
int p[2];
char *argv[2];
argv[0] = "wc";
argv[1] = 0;
pipe(p);
if (fork() == 0) {
    close(0);
    dup(p[0]);
    close(p[0]);
    close(p[1]);
    exec("/bin/wc", argv);
} else {
    close(p[0]);
    write(p[1], "hello world\n", 12);
    close(p[1]);
}
```

程序调用`pipe`，创建一个新的管道，并在数组p中记录读写文件描述符。在`fork`之后，父子进程都有指向管道的文件描述符。子进程调用`close`和`dup`使文件描述符0指向管道的读取端（前面说过优先分配最小的未使用的描述符），然后关闭p中所存的文件描述符，并调用`exec`运行`wc`。当`wc`从它的标准输入读取时，就是从管道读取。父进程关闭管道的读取端，写入管道，然后关闭写入端。

如果没有可用的数据，则管道上的`read`操作将会进入等待，直到有新数据写入或所有指向写入端的文件描述符都被关闭，在后一种情况下，`read`将返回0，就像到达数据文件的末尾一样。事实上，`read`在新数据不可能到达前会一直阻塞，这是子进程在执行上面的`wc`之前关闭管道的写入端非常重要的一个原因：如果wc的文件描述符之一指向管道的写入端，wc将永远看不到文件的结束。

Xv6 shell以类似于上面代码(***user/sh.c***:100)的方式实现了诸如`grep fork sh.c | wc -l`之类的管道。子进程创建一个管道将管道的左端和右端连接起来。然后对管道的左端调用`fork`和`runcmd`，对管道的右端调用`fork`和`runcmd`，并等待两者都完成。管道的右端可能是一个命令，该命令本身包含一个管道(例如，`a | b | c`)，该管道本身`fork`为两个新的子进程(一个用于b，一个用于c)。因此，shell可以创建一个进程树。这个树的叶子是命令，内部节点是等待左右两个子进程完成的进程。

原则上，可以让内部节点在管道的左端运行，但是正确地这样做会使实现复杂化。考虑进行以下修改：将***sh.c***更改为不对`p->left`进行`fork`，并在内部进程中运行`runcmd(p->left)`。然后，例如，`echo hi | wc`将不会产生输出，因为当`echo hi`在`runcmd`中退出时，内部进程将退出，而不会调用`fork`来运行管道的右端。这个不正确的行为可以通过不调用内部进程的`runcmd`中的`exit`来修复，但是这个修复使代码复杂化：现在`runcmd`需要知道它是否是一个内部进程。同样的，当没有对`(p->right)`执行`fork`时也会更加复杂。例如，只需进行上述的修改，`sleep 10 | echo hi`将立即打印“hi”，而不是在10秒后，因为`echo`将立即运行并退出，而不是等待`sleep`完成。因为***sh.c***的目标是尽可能的简单，所以它不会试图避免创建内部进程。

管道看起来并不比临时文件更强大：下面的管道命令行

```bash
echo hello world | wc
```

可以不通过管道实现，如下

```bash
echo hello world > /tmp/xyz; wc < /tmp/xyz
```

在这种情况下，管道相比临时文件至少有四个优势

- 首先，管道会自动清理自己；在文件重定向时，shell使用完`/tmp/xyz`后必须小心删除

- 其次，管道可以任意传递长的数据流，而文件重定向需要磁盘上足够的空闲空间来存储所有的数据。

- 第三，管道允许并行执行管道阶段，而文件方法要求第一个程序在第二个程序启动之前完成。

- 第四，如果实现进程间通讯，管道的**阻塞**式读写比文件的非阻塞语义更高效。