1.3: Pipes
简要介绍
管道是一种用于进程间通信的小型内核缓冲区。它由一对文件描述符表示——一个用于读取,一个用于写入。向管道的一端写入数据后,这些数据会从管道的另一端供读取使用。
主要特性
管道为进程之间提供了一种简单的通信机制。
如果没有数据可读,管道的读取操作会阻塞,直到发生以下情况之一:
管道中有数据被写入。
管道写端的所有文件描述符都已关闭,此时读取操作会返回 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。
管道可以传递任意长的数据流,而文件重定向需要磁盘上有足够的空间来存储所有数据。
管道允许管道阶段的并行执行,而文件方法需要第一个程序完成后才能启动第二个程序。