1.2 I/O与文件描述符 —— XV6操作系统学习

张开发
2026/4/18 2:50:23 15 分钟阅读

分享文章

1.2 I/O与文件描述符 —— XV6操作系统学习
原文链接https://pdos.csail.mit.edu/6.828/2021/xv6/book-riscv-rev2.pdf原文标题XV6一种简单、类Unix的教学操作系统文件描述符是一个小整数它代表内核管理的对象内核可以从中读取或写入。进程可以通过打开文件、目录或者设备或者创建管道或者复制已存在的文件描述符来获取文件描述符。简单起见我们后面提到的对象的文件描述符指的是”文件“文件描述符接口把文件管道和设备的区别抽象的隐藏让它们看起来都像字节流。我们将用I/O指代输入和输出。在内部xv6内核使用文件描述符作为预处理表的索引所以每一个进程都拥有以0为开始的文件描述符的私有空间。习惯上进程从文件描述符0处标准输入读取向文件描述符1处标准输出写入在文件描述符2处标准错误写入错误信息。就像我们将要看到的那样shell利用惯例进行I/O重定向和管道。shell确保有3个文件描述符一直处于打开状态user/sh.c:151)这3个是控制台的缺省文件描述符。read和write系统调用从文件描述符命名的已打开文件中读取和写入字节流。read(fd, buf, n)调用从文件描述符fd处读取最多n个字节复制到buf中返回读取的比特数。每一个指向文件的文件描述符都有一个与它相关联的偏移量。Read从当前文件的偏移量中读取数据然后通过读取的字节数向前移动偏移量。当读取完毕后read返回 0来标记文件的 结束。writefd, buf, n)调用从buf中向文件描述符fd写入n个字节并返回写入的字节数。只有当错误发生时写入的字节数才会不足 n 个。像 read 一样write 在当前的文件偏移量写入数据然后根据字节数向前移动偏移量。每一个 write 调用都从上一个结束的地方开始。下面的程序段 本质上是cat 调用从标准输入复制数据到标准输出。如果有错误发生就会在标准错误中写入信息。char buf[512]......在这个代码段中需要注意的是 cat 不知道它究竟是从文件控制台还是从管道中读取数据。而且 cat 也不知道它是输出到控制台文件还是到其它地方。文件描述符的使用以及0为输入1为输出的习惯让 cat 的执行变得简单。close 系统调用给出一个文件描述符将来 open pipe 或者 dup 系统调用 (见下都可以再利用。一个新分配的文件描述符总是使用当前进程中尚未使用的最小值。文件描述符与 fork 交互让 I/O重定向变得容易执行。 Fork 在复制父进程的内存的同时也复制父进程的文件描述符表所以子进程会从与父进程完全相同的打开文件处开始。系统调用 exec 替换调用进程的内存但是会保留它的文件描述符表。这样的行为允许 shell 通过 fork、再次 open 子进程中所选的文件描述符来执行 I/O 重定向然后调用 exec 来运行新程序。下面是一个shell 运行 catinput.txt命令的代码的简化版本。char *argv[2];argv[0] cat;argv[1] 0;iffork() 0) {close(0);open(input.txt, O_RDONLY);exec(cat, argv);}在子进程关闭文件描述符0后可以确保 open 为新打开的 input.txt 使用这个文件描述符 0将是最小的可用描述符。在这个过程中父进程的文件描述符没有变化只更改了子进程的描述符。xv6 shell的 I/O重定向代码完全按照这样的方式工作user/sh.c:82)。回溯一下在代码的这位置shell 已经 fork 子进程的shellruncmd 将调用 exec 来加载新程序。open的第二个参数包含一套标识通过比特流来表达控制 open作什么。在文件控制头(kernel/fcntl.h: 1-5)里面定义了可能的值O_RDONLYO_WRONL, O_REWR, O_CREATE 和 O_TRUNC它们指示 open打开文件用于读取写入既读取又写入如果不存在就创建文件截短文件至0长度。为什么 fork 和 exec 是各自独立的调用呢现在应该很清楚了在这两个调用之间shell 就有机会在不打扰主shell I/O设置的情况下重定向子进程的I/O。也许人们可以假设有一个合并的 forkexec 系统调用但是用这样的调用来执行 I/O 重定向的选项看起来很可怕。在调用 forkexec前shell可以修改它自己的 I/O设置然后撤回这些修改; 或者 forkexec 可以把I/O重定向的指令作为参数; 或者(不那么有吸引力地让每一个像 cat 一样的程序作自己的 I/O重定向。尽管fork 复制文件描述符表每一个潜在的文件文件偏移在父进程与子进程之间共享。考虑一下这个例子if(fork() 0) {write(1, hello, 6);exit(0);} else {wait(0);write(1, world\n, 6);}在这个代码段的最后附在文件描述符1上的文件将包含数据 hello world。父进程(因为有wait, 所以仅在子进程结束后运行的 write 拾取 子进程 write结束的地方。这样的行为有助于依据 shell 命令的顺序依次产生输出比如(echo hello; echo world) output.txt。dup 系统调用复制一个已有的文件描述符返回一个新的指向同样的潜在的 I/O对象。正如fork 复制文件描述符那样两个文件描述符的偏移量相同。下面是向文件中写入hello world的另一种方式fd dup(1);write(1, hello , 6);write(fd, world\n. 6;如果两个文件描述符通过一系列的fork和 dup 的调用而继承自同样的原始文件描述符则它们的偏移量相同。否则的话即使 open从同一个文件调用两个文件描述符的偏移量也不同。Dup 允许 shell 像这样来执行命令 ls existing-file non-existing-file tep1 21。21 让 shell 给出命令文件描述符2是文件描述符1的副本。现有文件的名称和不存在文件的错误信息都会在文件 tmp1 中显示。 xv6 shell 不支持错误文件描述符的 I/O 重定向不过你已经知道该如何解决了。文件描述符是一个强有力的抽象因为它们隐藏了与之相联系的对象的细节一个写入文件描述符1的进程可以是写入到一个文件写入到一个像控制台那样的设备或写入到一个管道。

更多文章