文件描述符表 —— 进程的磁盘资源

如果说基于虚拟页表的虚拟内存空间是进程物理内存资源的抽象,那基于文件描述符表的文件系统就是进程磁盘资源的抽象。通过持有文件描述符,进程可以方便地对磁盘上的特定位置(文件)进行读写。
打个不太准确的比方。如果我们把磁盘也看作一个像物理内存一样的字节序列,那磁盘上的文件就像内存中的分页,文件描述符就像 PTE,文件描述符中的 offset 就像 VPN(PPN)。
本文就来解释进程中的文件描述符表的机制。

一、文件描述符表的内容

每个进程都有独立文件描述符表(File Description Table, FDT)来记录所有其打开的文件。每次打开文件时获得的 int 类型文件描述符(File Description, FD)其实更准确地说是文件描述符索引。根据文件描述符可以在文件描述符表中找到这个描述符的表项(File Description Table Entry, FDTE)。
操作系统有一个全局文件表(File Table)来记录所有被打开的文件,所有进程共享这个文件表,而每个进程的 FDTE 都指向一个文件表表项(File Table Entry, FTE)。FTE 里面记录了被打开文件的当前读写位置和被 FDTE 引用的次数(ref_count)。当一个 FTE 的引用计数为 0 时,也就是没有 FDTE 指向它时,FTE 就会被销毁
操作系统还有一个全局v-node 表用来记录所有被打开文件的元信息,比如文件大小、类型(文件/目录/网络套接字)、权限等等。所有进程共享 v-node 表,每个 FTE 里面记录着一个指向 v-node 的指针。

二、文件描述符的生命周期

FD 从哪里来呢?最直接的来源就是 open 函数调用。每次调用 open 函数,会在当前进程 FDT 最小可用位置上创建一个 FDTE,把它和 FTE 关联起来后返回它的 index。
当对 FD 执行 close 函数时,就会在 FDT 中删去对应的 FDTE。同时,对应的 FTE 引用计数也会减一。如果忘记对 FD 调用 close 函数,PDTE 就不会消失,FTE 和 v-node 也不会消失,直到进程结束释放整个 FDT。
FD 还有一个除了 open 函数以外的来源:通过 forkexecve 创建进程时,新的进程会继承当前进程的 FDT。fork 时由于 FDT 被复制了,所以每个 FTE 的引用计数也会增加。而 execve 没有复制 FDT,只是保留了原来的,所以引用计数不变。

三、使用文件描述符进行读写

文件读写的细节不在本文讨论范围之内,此处只关注的是如何操作文件描述符达到读写的目的:

  • 所有的程序以 FD 0 作为其标准输入,FD 1 作为其标准输出,FD 2 作为标准错误
  • 一个文件可以被多次打开,形成多个 FD -> FDTE -> FTE 对应关系。由于每个 FTE 有不同的读写位置,所以就能实现对同一个文件进行不同地方的读写。但是即使一个文件多次被打开,它也只有一个 v-node,这样保证任何进程中读到的文件元信息是一致的
  • 同进程中多个 FDTE 可以指向同一个 FTE。在进程中使用 int dup2(int oldfd, int newfd)oldfd 的 FDTE 指向 newfd 的 FDTE,此后通过 oldfd 进行的读写都会重定向到 newfd 上,而 oldfd 的 FDTE 的引用计数会减一
  • 新进程有原进程一样的 FDT,共享了文件的读写