文件描述符是一个整数,它代表了一个进程可以读写的被内核管理的对象。进程可以通过多种方式获得一个文件描述符,如打开文件、目录、设备,或者创建一个管道(pipe),或者复制已经存在的文件描述符。简单起见,我们常常把文件描述符指向的对象称为“文件”。文件描述符的接口是对文件、管道、设备等的抽象,这种抽象使得它们看上去就是字节流。
每个进程都有一张表,而 xv6 内核就以文件描述符作为这张表的索引,所以每个进程都有一个从0开始的文件描述符空间。按照惯例,进程从文件描述符0读入(标准输入),从文件描述符1输出(标准输出),从文件描述符2输出错误(标准错误输出)。我们会看到 shell 正是利用了这种惯例来实现 I/O 重定向。shell 保证在任何时候都有3个打开的文件描述符(8007),他们是控制台(console)的默认文件描述符。
系统调用 read 和 write 从文件描述符所指的文件中读或者写 n 个字节。read(fd, buf, n) 从 fd 读最多 n 个字节(fd 可能没有 n 个字节),将它们拷贝到 buf 中,然后返回读出的字节数。每一个指向文件的文件描述符都和一个偏移关联。read 从当前文件偏移处读取数据,然后把偏移增加读出字节数。紧随其后的 read 会从新的起点开始读数据。当没有数据可读时,read 就会返回0,这就表示文件结束了。
write(fd, buf, n) 写 buf 中的 n 个字节到 fd 并且返回实际写出的字节数。如果返回值小于 n 那么只可能是发生了错误。就像 read 一样,write 也从当前文件的偏移处开始写,在写的过程中增加这个偏移。
下面这段程序(实际上就是 cat 的本质实现)将数据从标准输入复制到标准输出,如果遇到了错误,它会在标准错误输出输出一条信息。
char buf[512];
int n;
for(;;){
n = read(0, buf, sizeof buf);
if(n == 0)
break;
if(n < 0){
fprintf(2, "read error\n");
exit();
}
if(write(1, buf, n) != n){
fprintf(2, "write error\n");
exit();
}
}
这段代码中值得一提的是 cat 并不知道它是从文件、控制台或者管道中读取数据的。同样地 cat 也不知道它是写到文件、控制台或者别的什么地方。文件描述符的使用和一些惯例(如0是标准输入,1是标准输出)使得我们可以轻松实现 cat。
系统调用 close 会释放一个文件描述符,使得它未来可以被 open, pipe, dup 等调用重用。一个新分配的文件描述符永远都是当前进程的最小的未被使用的文件描述符。
文件描述符和 fork 的交叉使用使得 I/O 重定向能够轻易实现。fork 会复制父进程的文件描述符和内存,所以子进程和父进程的文件描述符一模一样。exec 会替换调用它的进程的内存但是会保留它的文件描述符表。这种行为使得 shell 可以这样实现重定向:fork 一个进程,重新打开指定文件的文件描述符,然后执行新的程序。下面是一个简化版的 shell 执行 cat<input.txt 的代码:
char *argv[2];
argv[0] = "cat";
### argv[1] = 0;
if(fork() == 0) {
close(0);
open("input.txt", O_RDONLY);
exec("cat", argv);
}
子进程关闭文件描述符0后,我们可以保证open 会使用0作为新打开的文件 input.txt的文件描述符(因为0是 open 执行时的最小可用文件描述符)。之后 cat 就会在标准输入指向 input.txt 的情况下运行。
xv6 的 shell 正是这样实现 I/O 重定位的(7930)。在 shell 的代码中,记得这时 fork 出了子进程,在子进程中 runcmd 会调用 exec 加载新的程序。现在你应该很清楚为何 fork 和 exec 是单独的两种系统调用了吧。这种区分使得 shell 可以在子进程执行指定程序之前对子进程进行修改。
虽然 fork 复制了文件描述符,但每一个文件当前的偏移仍然是在父子进程之间共享的,考虑下面这个例子:
if(fork() == 0) {
write(1, "hello ", 6);
exit();
} else {
wait();
write(1, "world\n", 6);
}
在这段代码的结尾,绑定在文件描述符1上的文件有数据"hello world",父进程的 write 会从子进程 write 结束的地方继续写 (因为 wait ,父进程只在子进程结束之后才运行 write)。这种行为有利于顺序执行的 shell 命令的顺序输出,例如 (echo hello; echo world)>output.txt。