vx6是MIT开发的教学用的小型操作系统,是Unix Version 6(v6)的重新实现,这个笔记会摘抄xv6中文文档(以下简称“文档”)的内容,同时结合一些自己的看法,文章遵循文档中的顺序。
1)在文档中,这样定义操作系统的作用:
操作系统的工作是(1)将计算机的资源在多个程序间共享,并且给程序提供一系列比硬件本身更有用的服务。(2)管理并抽象底层硬件,举例来说,一个文字处理软件(比如 word)不用去关心自己使用的是何种硬盘。(3)多路复用硬件,使得多个程序可以(至少看起来是)同时运行的。(4)最后,给程序间提供一种受控的交互方式,使得程序之间可以共享数据、共同工作。
2)关于文件描述符:
文件描述符是一个整数,它代表了一个进程可以读写的被内核管理的对象。进程可以通过多种方式获得一个文件描述符,如打开文件、目录、设备,或者创建一个管道(pipe),或者复制已经存在的文件描述符。简单起见,我们常常把文件描述符指向的对象称为“文件”。文件描述符的接口是对文件、管道、设备等的抽象,这种抽象使得它们看上去就是字节流。
文件描述符提供了文件之上的一层巧妙的抽象,有了文件描述符,只需要关心文件的作用(如标准输入输入),而不需要关心具体是什么文件,这样就为重定向等功能提供了方便。
3)什么是链接(link)
系统调用
link
创建另一个文件系统的名称,它指向同一个inode
。下面的代码创建了一个既叫做a
又叫做b
的新文件。open("a", O_CREATE|O_WRONGLY); link("a", "b");
读写 a 就相当于读写 b。
也就是说,链接是一个起别名的过程,当一个文件被链接时,nlink数会增加,其中nlink是描述文件信息的结构体的一个成员,结构体定义如下:
struct stat {
short type; // Type of file
int dev; // File system’s disk device
uint ino; // Inode number
short nlink; // Number of links to file
uint size; // Size of file in bytes
};
同样,unlink函数从文件系统移除一个文件名,nlink减1。当一个文件的nlink为0时,该文件被从磁盘删除。否则即使执行unlink("a"),还是可以通过b访问该文件。
4)管道
在文档中对管道的解释十分清楚,在此直接摘抄:
管道是一个小的内核缓冲区,它以文件描述符对的形式提供给进程,一个用于写操作,一个用于读操作。从管道的一端写的数据可以从管道的另一端读取。管道提供了一种进程间交互的方式。
接下来的示例代码运行了程序
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); } else { write(p[1], "hello world\n", 12); close(p[0]); close(p[1]); }
这段程序调用
pipe
,创建一个新的管道并且将读写描述符记录在数组p
中。在fork
之后,父进程和子进程都有了指向管道的文件描述符。子进程将管道的读端口拷贝在描述符0上,关闭p
中的描述符,然后执行wc
。当wc
从标准输入读取时,它实际上是从管道读取的。父进程向管道的写端口写入然后关闭它的两个文件描述符。如果数据没有准备好,那么对管道执行的
read
会一直等待,直到有数据了或者其他绑定在这个管道写端口的描述符都已经关闭了。在后一种情况中,read
会返回 0,就像是一份文件读到了最后。读操作会一直阻塞直到不可能再有新数据到来了,这就是为什么我们在执行wc
之前要关闭子进程的写端口。如果wc
指向了一个管道的写端口,那么wc
就永远看不到 eof 了。xv6 shell 对管道的实现(比如
fork sh.c | wc -l
)和上面的描述是类似的(7950行)。子进程创建一个管道连接管道的左右两端。然后它为管道左右两端都调用runcmd
,然后通过两次wait
等待左右两端结束。管道右端可能也是一个带有管道的指令,如a | b | c
, 它fork
两个新的子进程(一个b
一个c
),因此,shell 可能创建出一颗进程树。树的叶子节点是命令,中间节点是进程,它们会等待左子和右子执行结束。理论上,你可以让中间节点都运行在管道的左端,但做的如此精确会使得实现变得复杂。pipe 可能看上去和临时文件没有什么两样:命令
echo hello world | wc
可以用无管道的方式实现:
echo hello world > /tmp/xyz; wc < /tmp/xyz
但管道和临时文件起码有三个关键的不同点。首先,管道会进行自我清扫,如果是 shell 重定向的话,我们必须要在任务完成后删除
/tmp/xyz
。第二,管道可以传输任意长度的数据。第三,管道允许同步:两个进程可以使用一对管道来进行二者之间的信息传递,每一个读操作都阻塞调用进程,直到另一个进程用write
完成数据的发送。
5)关于shell的运行机制:
shell在死循环中调用getcmd()来获取用户输入的命令,读取到命令之后,fork一个子进程,并在子进程中运行runcmd(),runcmd()根据传入的参数用exec()函数在该子进程中加载命令对应的程序,待执行结束后子进程退出,期间父进程一直等待(父进程执行wait()),直到子进程结束。
然而cd函数不能用这种机制执行,原因如下:
cd 必须改变 shell 自身的当前工作目录。如果 cd 作为一个普通命令执行,那么 shell 就会 fork 一个子进程,而子进程会运行 cd,cd 只会改变子进程的当前工作目录。父进程的工作目录保持原样。
因此cd在父进程中实现,而非子进程。
参考文献
1. xv6 中文文档