Steve Kleiman 在 1986 年撰写了《Vnodes: An Architecture for Multiple File System Types in Sun UNIX》一文。这篇论文幅较短,大部分内容是数据结构的列举,以及 C 语言结构之间相互指向的图表。
Steve Kleiman是分布式文件系统领域的专家,在 Sun Microsystem 工作了多年,曾参与开发 Sun Network File System(NFS)等项目,为分布式文件系统领域做出了重要贡献。
Kleiman 希望在 Unix 中能够拥有多个文件系统,并希望这些文件系统能够共享接口和内存。具体而言,他希望设计一个能够提供以下功能的架构:
- 一个可以支持多个实现的通用接口;
- 支持 BSD FFS,以及两个远程文件系统 NFS 和 RFS,还有特定的非 Unix 文件系统,如MS-DOS;
- 接口定义的操作需要是原子性的。
并且,能够在不影响性能的情况下动态地处理内存和数据结构,支持重入(reentrant) 和多核,并且具有一定面向对象进行编程的特性。
重入(reentrant) 是指程序或子程序在尚未完成上一次调用之前,可以再次被调用且不会出错或发生冲突。
两个抽象概念
Steven 研究了文件系统的各种操作,决定将他们抽象为两个概念:
- vfs,虚拟文件系统,代表文件系统
- vnode,虚拟 inode,代表文件
vfs,虚拟文件系统,它提供统一的接口,使操作系统可以以一致的方式访问不同的文件系统,无论是本地文件系统还是网络文件系统。
vnode,虚拟 inode, 表示一个文件,每个文件都有一个相关联的索引节点,其中包含了文件的元数据(如文件权限、所有者、大小等)以及指向文件数据存储位置的指针。
采用了 C++风格(实际使用 C 语言),每一个类型会匹配一个虚函数表,通过虚函数表,系统在运行时根据对象的实际类型来调用适当的虚函数,实现动态绑定:
- 对于 vfs 类型,其虚函数表 struct vfsops,包含了一系列的函数指针,用来执行诸如 mount、unmount、sync 和 vget 等操作。在论文的后面,会解释这些函数的原型和功能;
- 对于 vnode 类型也是类似的,其虚函数表 struct vnodeops,包含 open、rdwr 和 close 等函数,还有create、unlink 和 rename 等函数。一些函数是针对特定的文件类型的,比如 readlink、mkdir、readdir 和 rmdir。
通过 vfs 对象来进行跟踪实际的挂载,其虚函数表 struct vfsops 指向适用于该特定子树的文件系统操作。
类似地,vnode 实例用来进行跟踪打开的文件。它包含 struct *vnodeops
指针,作为 vfs 的一部分,有指针 struct *vfs
指向文件系统实例。
vfs 和 vnode 这两个结构体都需要一些用于存储特定实现数据的字段(如“子类私有字段”)。他们都以 caddr_t ...data
指针结尾。这些私有数据并不是 vfs 和 vnode 的一部分,而是位于其他位置,并通过指针进行引用。
Vnodes 实操
在论文中,有一整页的内容专门用于展示各种相互指向的结构。乍一看可能会感到困惑,但一旦追踪下来,就会发现它非常直观和优雅。
Kleiman 详细解释了如何使用 lookuppn() 函数来解释事物的工作原理,该函数替代了传统 Unix 中的 namei() 函数。类似于 namei() ,这个函数接受一个路径,并返回表示该路径所代表的 vnode 的 struct vnode 指针。
路径遍历始于根 vnode 或当前进程的当前目录 vnode,具体取决于路径的第一个字符是否为/。
然后,这个函数会依次取出路径的每一个子项,并调用当前 vnode 的 lookup 函数,它接受一个路径子项和一个假设是目录的当前 vnode,并返回代表那个子项的 vnode。
当一个目录是个挂载点,它的 vfsmountedhere 会被设置为一个指向 struct vfs 的指针。lookuppn 函数会跟随这个指针,并调用 vfs 的根函数,以获取该文件系统的根 vnode,替换当前正在处理的 vnode。
反过来也是可能的:当解析父目录(".. ")时,如果当前 vnode 的 "flags" 字段中设置了根标志,我们会跟随 vfsmountedhere 指针从当前 vnode 到 vfs。然后,我们可以使用该 vfs 中的 vnodecovered 字段来获取上层文件系统的 vnode。
无论如何,在成功完成后,会返回一个 struct vnode 指针,即所使用的路径。
新增的系统调用
为了使系统高效地运行,需要添加一些新的系统调用来完善接口。
在 Unix 的历史中,我们看到引入了 statsfs 和 fstatsfs ,通过这两个函数可以获得与用户空间中的文件系统进行交互的接口。getdirentries 函数可以让用户一次性获取多个目录条目(取决于提供的缓冲区大小),这大大加快了远程文件系统的目录读取速度。
在 Linux 系统中
通过查看 Linux 内核源代码,我们可以找到 Kleiman 设计的总体结构,尽管 Linux 内核的复杂性和丰富性掩盖了其中大部分内容。Linux 内核拥有丰富的文件系统类型,并且还添加了许多在 40 年前的 BSD 中不存在的功能。因此,我们可以找到更多的数据结构和系统调用,它们被用于实现命名空间、配额、属性、只读模式、目录名称缓存等功能。
文件
如果你仔细观察,原始的结构仍然可以找到:Linux 内存中的文件相关结构分为两部分,一个是已打开的文件,它是一个带有当前位置的 inode;另一个是 inode,它代表整个文件。
我们可以在此处找到文件对象,struct file 的实例。在文件的所有其他内容中,最值得注意的是一个字段 loff_t f_pos
,它表示文件当前位置距离文件起始位置的偏移量(以字节为单位)。
文件的类是通过一个虚函数表来定义。我们可以找到一个指针 struct file_operations *f_op
。它展示了文件可以执行的所有操作,其中最常见的是打开(open)、关闭(close)、定位(lseek)、读取(read)和写入(write)。
文件还包含指向 inode 的指针,即 struct inode *f_inode
。
索引节点
对于不需要偏移量的文件操作,它们是针对整个文件进行的,定义为 struct inode *
。
查看此处的定义。我们可以看到这里还有其他的定义,40 年前的 BSD 中没有类似的定义,比如 ACL(访问控制列表)和属性(attributes)。
我们发现 inode 的类通过虚函数表来定义,即 struct inode_operations *i_op
。同样的,这其中很多函数涉及新特性,比如 ACL(访问控制列表)和扩展属性,但我们也会找到我们期望的功能,比如链接(link)、删除(unlink)、重命名(rename)等。
Inode 还包含一个指向文件系统的指针,即 struct super_block *i_sb
。
超级块
挂载点用 struct super_block
来表示,在此处查看其定义。同样地,它有 struct super_operations *s_op
定义的各个操作,在此处查看其定义。
支持的文件系统不再有限,可以通过内核模块动态地添加新的文件系统,通过数据结构 struct file_system_type
来表示,它只有一个用于创建 superblock 的工厂函数 mount。
小结
Unix 发生了变化。它的运行时变得更加复杂,增加了许多新的功能,并增加了系统调用。系统变得更有结构。
但是,由 Steve Kleiman 和 Bill Joy(BSD 操作系统的共同创始人之一) 构思的原始设计和数据结构仍然存在,在当前的 Linux 系统中仍然可以找到,虽然已经过去了40年。
回顾