1.3: Pipes

简要介绍

管道是一种用于进程间通信的小型内核缓冲区。它由一对文件描述符表示——一个用于读取,一个用于写入。向管道的一端写入数据后,这些数据会从管道的另一端供读取使用。

主要特性

  • 管道为进程之间提供了一种简单的通信机制。

  • 如果没有数据可读,管道的读取操作会阻塞,直到发生以下情况之一:

  1. 管道中有数据被写入。

  2. 管道写端的所有文件描述符都已关闭,此时读取操作会返回 0,就像到达文件末尾一样

通过管道连接子进程和 wc 程序 以下代码示例展示了如何创建一个管道,并运行wc程序,其中标准输入连接到了管道的读取端。

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); // 执行 wc 程序
} else {
    close(p[0]);       // 父进程关闭管道读取端
    write(p[1], "hello world\n", 12); // 向管道写入数据
    close(p[1]);       // 关闭管道写入端
}

解析

pipe(p):创建一个管道,返回的文件描述符存储在数组 p 中。

  • p[0]:管道的读取端。

  • p[1]:管道的写入端。

子进程:

  • 通过 dup(p[0]) 将标准输入重定向到管道的读取端。

  • 关闭管道的原始文件描述符,避免影响后续的 read 和 write。

  • 使用 exec 执行 wc 程序。

父进程:

  • 关闭管道的读取端,避免干扰。

  • 向管道写入数据,供子进程读取。

  • 写入完成后,关闭管道的写入端

阻塞行为与结束标志

  • 当管道的写端关闭后,read 操作会返回 0,表示数据已读完。

  • 子进程在执行 wc 前必须关闭管道的写入端,以确保 wc 能正确检测到文件结束符 (EOF)。

管道与 xv6 Shell 的实现

xv6 shell 以类似的方式实现了管道,比如 grep fork sh.c | wc -l(见 user/sh.c:101)。子进程创建一个管道,将管道左端和右端连接起来。然后调用 fork 和 runcmd

分别运行管道左端和右端,最后等待两者完成。管道右端可能是一个包含管道的命令(例如 a | b | c),此时 shell 将再创建两个新子进程(一个用于 b,一个用于 c)。因此,shell 可能会创建一个进程树。该树的叶节点是命令,内部节点则是等待左右子进程完成的进程。

管道看起来并不比临时文件更强大:例如,管道操作

echo hello world | wc

可以通过临时文件实现为:

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

在这种情况下,管道相对于临时文件至少有三个优势:

  • 管道会自动清理自身;而使用文件重定向,shell 需要小心地在完成后删除 /tmp/xyz。

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

  • 管道允许管道阶段的并行执行,而文件方法需要第一个程序完成后才能启动第二个程序。