Linux 虚拟文件系统
文件系统是对一个存储设备上的数据和元数据进行组织的机制,由于定义如此宽泛,各个文件系统的实现也大不相同,其中常见的文件系统有 ext4、NFS、/proc 等。Linux 采用为分层的体系结构,将用户接口层、文件系统实现和存储设备的驱动程序分隔开,进而兼容不同的文件系统。
虚拟文件系统(Virtual File System, VFS)是 Linux 内核中的软件层,它在内核中提供了一组标准的、抽象的文件操作,允许不同的文件系统实现共存,并向用户空间程序提供统一的文件系统接口。下面这张图展示了 Linux 虚拟文件系统的整体结构:
从上图可以看出,用户空间的应用程序直接、或是通过编程语言提供的库函数间接调用内核提供的 System Call 接口(如open()
、write()
等)执行文件操作。System Call 接口再将应用程序的参数传递给虚拟文件系统进行处理。
每个文件系统都为 VFS 实现了一组通用接口,具体的文件系统根据自己对磁盘上数据的组织方式操作相应的数据。当应用程序操作某个文件时,VFS 会根据文件路径找到相应的挂载点,得到具体的文件系统信息,然后调用该文件系统的对应操作函数。
VFS 提供了两个针对文件系统对象的缓存 INode Cache 和 DEntry Cache,它们缓存最近使用过的文件系统对象,用来加快对 INode 和 DEntry 的访问。Linux 内核还提供了 Buffer Cache 缓冲区,用来缓存文件系统和相关块设备之间的请求,减少访问物理设备的次数,加快访问速度。Buffer Cache 以 LRU 列表的形式管理缓冲区。
VFS 的好处是实现了应用程序的文件操作与具体的文件系统的解耦,使得编程更加容易:
- 应用层程序只要使用 VFS 对外提供的
read()
、write()
等接口就可以执行文件操作,不需要关心底层文件系统的实现细节; - 文件系统只需要实现 VFS 接口就可以兼容 Linux,方便移植与维护;
- 无需关注具体的实现细节,就实现跨文件系统的文件操作。
了解 Linux 文件系统的整体结构后,下面主要分析 Linux VFS 的技术原理。由于文件系统与设备驱动的实现非常复杂,笔者也未接触过这方面的内容,因此文中不会涉及具体文件系统的实现。
VFS 结构
Linux 以一组通用对象的角度看待所有文件系统,每一级对象之间的关系如下图所示:
fd 与 file
每个进程都持有一个fd[]
数组,数组里面存放的是指向file
结构体的指针,同一进程的不同fd
可以指向同一个file
对象;
file
是内核中的数据结构,表示一个被进程打开的文件,和进程相关联。当应用程序调用open()
函数的时候,VFS 就会创建相应的file
对象。它会保存打开文件的状态,例如文件权限、路径、偏移量等等。
|
|
从上面的代码可以看出,文件的路径实际上是一个指向 DEntry 结构体的指针,VFS 通过 DEntry 索引到文件的位置。
除了文件偏移量f_pos
是进程私有的数据外,其他的数据都来自于 INode 和 DEntry,和所有进程共享。不同进程的file
对象可以指向同一个 DEntry 和 Inode,从而实现文件的共享。
DEntry 与 INode
Linux文件系统会为每个文件都分配两个数据结构,目录项(DEntry, Directory Entry)和索引节点(INode, Index Node)。
DEntry 用来保存文件路径和 INode 之间的映射,从而支持在文件系统中移动。DEntry 由 VFS 维护,所有文件系统共享,不和具体的进程关联。dentry
对象从根目录“/”开始,每个dentry
对象都会持有自己的子目录和文件,这样就形成了文件树。举例来说,如果要访问"/home/beihai/a.txt"文件并对他操作,系统会解析文件路径,首先从“/”根目录的dentry
对象开始访问,然后找到"home/“目录,其次是“beihai/”,最后找到“a.txt”的dentry
结构体,该结构体里面d_inode
字段就对应着该文件。
|
|
每一个dentry
对象都持有一个对应的inode
对象,表示 Linux 中一个具体的目录项或文件。INode 包含管理文件系统中的对象所需的所有元数据,以及可以在该文件对象上执行的操作。
|
|
虚拟文件系统维护了一个 DEntry Cache 缓存,用来保存最近使用的 DEntry,加速查询操作。当调用open()
函数打开一个文件时,内核会第一时间根据文件路径到 DEntry Cache 里面寻找相应的 DEntry,找到了就直接构造一个file
对象并返回。如果该文件不在缓存中,那么 VFS 会根据找到的最近目录一级一级地向下加载,直到找到相应的文件。期间 VFS 会缓存所有被加载生成的dentry
。
INode 存储的数据存放在磁盘上,由具体的文件系统进行组织,当需要访问一个 INode 时,会由文件系统从磁盘上加载相应的数据并构造 INode。一个 INode 可能被多个 DEntry 所关联,即相当于为某一文件创建了多个文件路径(通常是为文件建立硬链接)。
SuperBlock
SuperBlock 表示特定加载的文件系统,用于描述和维护文件系统的状态,由 VFS 定义,但里面的数据根据具体的文件系统填充。每个 SuperBlock 代表了一个具体的磁盘分区,里面包含了当前磁盘分区的信息,如文件系统类型、剩余空间等。SuperBlock 的一个重要成员是链表s_list
,包含所有修改过的 INode,使用该链表很容易区分出来哪个文件被修改过,并配合内核线程将数据写回磁盘。SuperBlock 的另一个重要成员是s_op
,定义了针对其 INode 的所有操作方法,例如标记、释放索引节点等一系列操作。
|
|
SuperBlock 是一个非常复杂的结构,通过 SuperBlock 我们可以将一个实体文件系统挂载到 Linux 上,或者对 INode 进行增删改查操作。所以一般文件系统都会在磁盘上存储多份 SuperBlock,防止数据意外损坏导致整个分区无法读取。
应用
procfs
/proc
目录是 Linux 提供的一个虚拟文件系统,存储的是当前内核运行状态的一系列特殊文件。用户可以通过这些文件查关系统硬件及当前正在运行进程的信息,甚至可以通过更改其中某些文件来改变内核的运行状态。
/proc
不是一个真正的文件系统,它并不占用存储空间,只是占用有限的内存。但 /proc
实现了虚拟文件系统的接口,使得我们可以像操作一个普通的文件那样操作/proc
目录下的内容:
|
|
有关/proc
更多的用法可以参考文档*proc(5) — Linux manual page*,在 Linux 系统中类似于 procfs 的伪文件系统还有 sysfs、tmpfs 等。
Linux 的一个重要概念就是「一切皆是文件」,从这里可以看出,不论是普通的文件,还是特殊的目录、设备等,只要实现了相关的接口,VFS 就可以将它们同等看待成文件,通过同一套文件操作方式来对它们进行操作。当我们打开文件时,VFS 会获取该文件对应的文件系统格式,当 VFS 把控制权传给实际的文件系统时,实际的文件系统再做出具体区分,对不同的文件类型执行不同的操作。
总结
虚拟文件系统是操作系统中非常重要的一层抽象,其主要作用在于让上层的软件,能够用统一的方式,与底层不同的文件系统沟通。在操作系统与底层的各类文件系统之间,虚拟文件系统提供了标准的操作接口,让操作系统能够很快地支持新的文件系统。也因为 VFS 的支持,众多不同的实际文件系统才能在 Linux 中共存,跨文件系统的操作才能实现。