1.3 Pipes
1.4 File System
1.5 Real World
xv6 是 MIT 开发的一个教学用的完整的类 Unix 操作系统,并且在 MIT 的操作系统课程 6.828 中使用。通过阅读并理解 xv6 的代码,可以清楚地了解操作系统中众多核心的概念,对操作系统感兴趣的同学十分推荐一读!这份文档是中文翻译的 MIT xv6 文档,是阅读代码过程中非常好的参考资料。
1.3 Pipes 管道是一个小的内核缓冲区,作为一对文件描述符暴露给进程,一个用于读,一个用于写。将数据写入管道的一端就可以从管道的另一端读取数据。管道为进程提供了一种通信方式。 下面的示例代码运行程序wc,标准输入连接到管道的读取端。 int p[2]; char *argv[2]; argv[0] = "wc"; argv[1] = 0; pipe(p); if (fork() == 0) { close(0); // 释放文件描述符0 dup(p[0]); // 复制一个p[0](管道读端),此时文件描述符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中,经过fork后,父进程和子进程的文件描述符都指向管道。子进程调用close和dup使文件描述符0引用管道的读端,并关闭p中的文件描述符,并调用exec运行wc。当wc从其标准输入端读取时,它将从管道中读取。父进程关闭管道的读端,向管道写入,然后关闭写端。
如果没有数据可用,管道上的read会等待数据被写入,或者等待所有指向写端的文件描述符被关闭;在后一种情况下,读将返回0,就像数据文件的结束一样。事实上,如果没有数据写入,读会无限阻塞,直到新数据不可能到达为止(写端被关闭),这也是子进程在执行上面的wc之前关闭管道的写端很重要的一个原因:如果wc的一个文件描述符仍然引用了管道的写端,那么wc将永远看不到文件的关闭(被自己阻塞)。
xv6的shell实现了管道,如grep fork sh.c | wc -l,shell的实现类似于上面的代码(user/sh.c:100)。执行shell的子进程创建一个管道来连接管道的左端和右端(去看源码,不看难懂)。然后,它在管道左端(写入端)调用fork和runcmd,在右端(读取端)调用fork和runcmd,并等待两者的完成 。管道的右端(读取端)可以是一个命令,也可以是包含管道的多个命令(例如,a | b | c),它又会分叉为两个新的子进程(一个是b,一个是c)。因此,shell可以创建一棵进程树。这棵树的叶子是命令,内部(非叶子)节点是等待左右子进程完成的进程。 原则上,我们可以让内部节点(非叶节点)运行管道的左端,但这样的实现会更加复杂。考虑只做以下修改:修改sh.c,使其不为runcmd(p->left) fork进程,直接递归运行runcmd(p->left)。像这样,echo hi | wc不会产生输出,因为当echo hi在runcmd中退出时,内部进程会退出,而不会调用fork来运行管道的右端。这种不正确的行为可以通过不在runcmd中为内部进程调用exit来修正,但是这种修正会使代码变得复杂:runcmd需要知道该进程是否是内部进程(非叶节点)。当不为runcmd(p->right) fork进程时,也会出现复杂的情况。像这样的修改,sleep 10 | echo hi就会立即打印出hi,而不是10秒后,因为echo会立即运行并退出,而不是等待sleep结束。由于sh.c的目标是尽可能的简单,所以它并没有试图避免创建内部进程。 管道似乎没有比临时文件拥有更多的功能:echo hello world | wc
不使用管道:
echo hello world >/tmp/xyz; wc
在这种情况下,管道比临时文件至少有四个优势。首先,管道会自动清理自己;如果是文件重定向,shell在完成后必须小心翼翼地删除/tmp/xyz。第二,管道可以传递任意长的数据流,而文件重定向则需要磁盘上有足够的空闲空间来存储所有数据。第三,管道可以分阶段的并行执行,而文件方式则需要在第二个程序开始之前完成第一个程序。第四,如果你要实现进程间的通信,管道阻塞读写比文件的非阻塞语义更有效率。
1.4 File systemxv6 文件系统包含了数据文件(拥有字节数组)和目录(拥有对数据文件和其他目录的命名引用)。这些目录形成一棵树,从一个被称为根目录的特殊目录开始。像/a/b/c这样的路径指的是根目录/中的a目录中的b目录中的名为c的文件或目录。不以/开头的路径是相对于调用进程的当前目录进行计算其绝对位置的,可以通过chdir系统调用来改变进程的当前目录。下面两个open打开了同一个文件(假设所有涉及的目录都存在)。
chdir("/a"); chdir("b"); open("c", O_RDONLY); open("/a/b/c", O_RDONLY);
前两行将进程的当前目录改为/a/b;后面两行既不引用也不改变进程的当前目录。
有一些系统调用来可以创建新的文件和目录:mkdir创建一个新的目录,用O_CREATE标志创建并打开一个新的数据文件,以及mknod创建一个新的设备文件。这个例子说明了这两个系统调用的使用。
mkdir("/dir"); fd = open("/dir/file", O_CREATE | O_WRONLY); close(fd); mknod("/console", 1, 1);
mknod创建了一个引用设备的特殊文件。与设备文件相关联的是主要设备号和次要设备号(mknod的两个参数),它们唯一地标识一个内核设备。当一个进程打开设备文件后,内核会将系统的读写调用转移到内核设备实现上,而不是将它们传递给文件系统。
文件名称与文件是不同的;底层文件(非磁盘上的文件)被称为inode,一个inode可以有多个名称,称为链接。每个链接由目录中的一个项组成;该项包含一个文件名和对inode的引用。inode保存着一个文件的metadata(元数据),包括它的类型(文件或目录或设备),它的长度,文件内容在磁盘上的位置,以及文件的链接数量。
fstat系统调用从文件描述符引用的inode中检索信息。它定义在stat.h (kernel/stat.h)的 stat 结构中:
#define T_DIR 1 // Directory#define T_FILE 2 // File#define T_DEVICE 3 // Device struct stat { int dev; // File system’s disk device uint ino; // Inode number short type; // Type of file short nlink; // Number of links to file uint64 size; // Size of file in bytes };
link系统调用创建了一个引用了同一个inode的文件(文件名)。下面的片段创建了引用了同一个inode两个文件a和b。
open("a", O_CREATE | O_WRONLY); link("a", "b");
读写a与读写b是一样的,每个inode都有一个唯一的inode号来标识。经过上面的代码序列后,可以通过检查fstat的结果来确定a和b指的是同一个底层内容:两者将返回相同的inode号(ino),并且nlink计数为2。
unlink系统调用会从文件系统中删除一个文件名。只有当文件的链接数为零且没有文件描述符引用它时,文件的inode和存放其内容的磁盘空间才会被释放。
unlink("a");
上面这行代码会删除a,此时只有b会引用inode。
fd = open("/tmp/xyz", O_CREATE | O_RDWR); unlink("/tmp/xyz");
这段代码是创建一个临时文件的一种惯用方式,它创建了一个无名称inode,故会在进程关闭fd或者退出时删除文件。
Unix提供了shell可调用的文件操作程序,作为用户级程序,例如mkdir、ln和rm。这种设计允许任何人通过添加新的用户级程序来扩展命令行接口。现在看来,这个设计似乎是显而易见的,但在Unix时期设计的其他系统通常将这类命令内置到shell中(并将shell内置到内核中)。
1.5 Real worldUnix将标准文件描述符、管道和方便的shell语法结合起来进行操作,是编写通用可重用程序的一大进步。这个想法引发了一种软件工具文化,这也是Unix强大和流行的主要原因,而shell是第一种所谓的脚本语言。Unix系统调用接口今天仍然存在于BSD、Linux和Mac OS X等系统中。
Xv6 并不符合 POSIX 标准:它缺少许多系统调用(包括基本的系统调用,如 lseek),而且它提供的许多系统调用与标准不同。我们对xv6的主要目标是简单明了,同时提供一个简单的类似UNIX的系统调用接口。一些人已经添加了一些系统调用和一个简单的C库扩展了xv6,以便运行基本的Unix程序。然而,现代内核比xv6提供了更多的系统调用和更多种类的内核服务。例如,它们支持网络、窗口系统、用户级线程、许多设备的驱动程序等等。现代内核不断快速发展,并提供了许多超越POSIX的功能。
Unix用一套文件名和文件描述符接口统一了对多种类型资源(文件、目录和设备)的访问。这个思想可以扩展到更多种类的资源,一个很好的例子是Plan 9[13],它把资源就是文件的概念应用到网络、图形等方面。然而,大多数Unix衍生的操作系统都没有遵循这一路线。
文件系统和文件描述符已经是强大的抽象。即便如此,操作系统接口还有其他模式。Multics是Unix的前身,它以一种使文件存储看起来像内存的方式抽象了文件存储,产生了一种截然不同的接口。Multics设计的复杂性直接影响了Unix的设计者,他们试图建立一些更简单的东西。
Xv6没有用户系统;用Unix的术语来说,所有的xv6进程都以root身份运行。
本书研究的是xv6如何实现其类似Unix的接口,但其思想和概念不仅仅适用于Unix。任何操作系统都必须将进程复用到底层硬件上,将进程相互隔离,并提供受控进程间通信的机制。在学习了xv6之后,您应该能够研究其他更复杂的操作系统,并在这些系统中看到xv6的基本概念。
[1]读取端会因为管道无数据且输入端未关闭而阻塞,即只能等待左边的命令执行完才能执行右边的命令,写入端需要将自己的输出写入到管道,或者关闭管道,右边的命令读取管道并将其作为自己的输入(可以没有参数),
[2]Inode是linux和类unix操作系统用来储存除了文件名和实际数据的数据结构,它是用来连接实际数据和文件名的。
未完待续关注CFC Studio 公众号订阅后续文章
扫码关注更多精彩