xv6:一个简单的类Unix教学操作系统--操作系统接口--管道

1.3 管道

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

接下来的示例代码运行了程序wc,它的标准输出绑定到了一个管道的读端口。

1

int p[2];

2

char *argv[2];

3

argv[0] = "wc";

4

argv[1] = 0;

5

pipe(p);

6

if(fork() == 0) {

7

    close(0);

8

    dup(p[0]);

9

    close(p[0]);

10

    close(p[1]);

11

    exec("/bin/wc", argv);

12

} else {

13

    close(p[0]);

14

    write(p[1], "hello world\n", 12);

15

    close(p[1]);

16

}

pipe系统调用,创建了一个新的管道并把读取和写入文件描述符保存在数组p里。fork之后,父子都有了代表管道的文件描述符。子进程把读取端复制到文件描述符0,关闭p中的文件描述符,并执行wc。当wc从自己的标准输入读取时,实际是从管道读取。父关闭了管道的读取端,向管道写入,然后关闭了写入端。

xv6:一个简单的类Unix教学操作系统--操作系统接口--管道

图1 第5行,父进程创建管道

xv6:一个简单的类Unix教学操作系统--操作系统接口--管道

图2 第6行,父进程创建子进程成功后

xv6:一个简单的类Unix教学操作系统--操作系统接口--管道

图3 第7、8、9、10、13行执行后,父进程可以写管道,wc从管道读出

xv6:一个简单的类Unix教学操作系统--操作系统接口--管道

程序运行结果如下。

xv6:一个简单的类Unix教学操作系统--操作系统接口--管道

如果没有数据可用,那么对管道执行的read会一直等待,直到有数据了或者其他绑定在这个管道写端口的描述符都已经关闭了。在后一种情况下,read将返回0,就像读到数据文件的结尾一样。事实上,在新数据无法到达之前读取数据块是子进程在执行上面的wc之前关闭管道写入端的一个重要原因:如果wc的一个文件描述符引用了管道的写入端,wc将永远看不到文件结尾。

xv6 shell用了跟上面代码类似的方式(user/sh.c:100)实现了像grep fork sh.c | wc -l这样的管道。子进程创建了一个管道并把管道的左右两端连接起来。然后它分别为左右两端调用了fork和runcmd,并等待完成。管道的右端也许自己就包含一个管道(比如a | b | c),它自己fork了两个新的子线程(一个给a一个给b)。因此,shell 可能会创建一棵进程树。树的叶子是命令,内部节点是等待左右孩子结束的进程。原则上,您可以让内部节点运行管道的左端,但是如果按这种正确的方法去做会使实现复杂化。

xv6:一个简单的类Unix教学操作系统--操作系统接口--管道

user/sh.c

管道看起来可能并不比临时文件强大,管道:

echo hello world | wc

不用管道也能实现:

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

在这种情况下,管道至少比临时文件有四个优点。首先,管道是自清洁的;因为文件重定向,结束时shell必须很小心删除/tmp/xyz。其次,管道可以传递任意长的数据流,而文件重定向需要磁盘上足够的可用空间来存储所有数据。第三,管道允许管道各阶段并行执行,然而文件处理需要等到第一个程序结束第二个程序才能开始。第四,如果要实现进程间通信,管道的阻塞读写比文件的非阻塞语义更有效。