一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第3天,点击查看活动详情。
原文链接 xv6-riscv文档
1.2 I/O与文件描述符
文件描述符是代表内核管理的可能会被进程读写的对象的整数。进程可以通过打开文件、目录、设备、创建管道、复制一个存在的文件描述符等方式获取文件描述符。简单的说我们可以把文件描述符指向的是对象是文件,文件描述符抽离了文件、管道、设备之间的区别,使他们看起来都像字节流。我们将输入和输出称为I/O。
xv6内核使用文件描述符作为每一个进程表的属性,所以每一个进程都有一个自0开始的文件描述符空间。相应的,进程从文件描述符0开始读取,输出标准输出到文件描述符1,输出错误信息到文件描述符2。正如我们看到的一样,shell使用这个特性来实现I/O重定向以及管道。shell确保它总是有三个打开着的默认是控制台文件描述符的文件描述符。
read和write系统调用接口从文件描述符描述的打开文件读取和写入字节。read(fd, buf, n)从文件描述符fd处读取最多n个字节,并且将他们复制到buf,同时返回读取的字节数。每一个文件描述符都有一个和它相关的偏移量。Read从当前的偏移量处读取数据并且根据读取字节数更新偏移量;之后的read将会跟着前面的read的返回做读取。当没有更多的数据读取时,read返回0表示文件结束了。
write(fd, buf, n)则从buf处往fd写入n个字节的数据并且返回写入的字节数,只有当少于n个字节被写入时才会报错。像read一样,write向文件的偏移处写入数据并且根据写入字节数更新该偏移量,每个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(1);
}
if(write(1, buf, n) != n){
fprintf(2, "write error\n");
exit(1);
}
}
代码中重要的一点是cat并不知道它是从文件、控制台还是管道读入的数据,同样它也不知道它是往控制台、文件还是其他什么做输出。文件描述符0输入文件描述符1做输出的特性和使用成就了cat的简单实现。
close系统调用释放文件描述符使得他可以被下一个open、pipe或dup调用。一个新分配的文件描述符总是是当前进程未使用描述符的最低序号。
文件描述符和fork交互简化了I/O重定向的实现。fork复制父进程的文件描述符和相关的内存,这样子进程可以立刻从同样的文件开始。系统调用exec替换调用进程的内存但是保留它的文件描述符。这个行为允许shell可以通过fork来实现I/O重定向:在子进程中重新打开关闭的文件,然后调用exec运行新程序,下面是一个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被调用使用这个文件描述符打开input.txt,cat将文件描述符0指向input.txt,父进程的文件描述符不会因为这个而改变,因为这只修改了子进程的文件描述符。
xv6的shell的I/O重定向就以这样的方式实现,请记得shell已经fork出了一个子shell进程而且runcmd将会在子shell中载入新程序。
今天主要介绍的是IO相关的一些系统调用,篇幅较长且时间较短,仅介绍了前一半,还有一半明天再见。
感谢阅读。