[toc]
fs/ VFS - 虚拟文件系统(Virtual Filesystem) 内核统一的文件系统抽象层
历史与背景
这项技术是为了解决什么特定问题而诞生的?
虚拟文件系统(Virtual Filesystem Switch, VFS)是Linux内核最核心、最强大的子系统之一。它的诞生是为了解决一个根本性的问题:如何让应用程序以一种统一的方式来访问各种不同类型的文件系统。
在VFS出现之前,操作系统如果想支持一种新的文件系统(例如,从Minix文件系统切换到ext文件系统),可能需要重写大量与文件操作相关的代码。应用程序也可能会与特定的文件系统实现产生耦合。VFS通过创建一个通用的抽象层来解决这个问题:
- 对应用程序的统一接口:无论底层是ext4、XFS、Btrfs、NFS(网络文件系统),还是一个USB U盘上的FAT32,应用程序都使用同样标准的系统调用(
open,read,write,close,stat等)来操作文件。应用程序完全不需要知道底层文件系统的具体类型和实现细节。 - 对文件系统驱动的统一接口:VFS定义了一套标准的“插件”接口。任何想要被Linux内核支持的文件系统,只需要实现这套接口(即实现一系列定义好的回调函数),就可以无缝地“挂载”到VFS中,从而被所有应用程序使用。
简单来说,VFS就像一个“转换插头”,它将各种不同形状的“插座”(具体的文件系统实现)转换成一个标准的“插口”,供所有“电器”(应用程序)使用。
它的发展经历了哪些重要的里程碑或版本迭代?
VFS的概念源于早期的Unix系统,Linux继承并极大地发展了这一思想。
- 四大核心对象的建立:VFS的核心设计围绕四个主要的对象(数据结构)展开,这些对象的稳定和完善是VFS发展的基石:
- 超级块(Superblock): 代表一个已挂载的文件系统实例。
- 索引节点(Inode): 代表一个具体的文件或目录。
- 目录项(Dentry): 代表一个目录中的一个条目(即文件名与其inode的链接),是路径的组成部分。
- 文件(File): 代表一个进程打开的文件实例(由
open()调用创建)。
- dcache(目录项缓存)的引入:这是Linux VFS一个巨大的性能优化里程碑。每次查找文件都需要从根目录开始逐级解析路径,这是一个非常耗时的操作。dcache将路径名到inode的解析结果缓存起来,极大地加速了后续对同一路径的访问。
- 与Page Cache的深度融合:VFS与内存管理子系统中的页缓存(Page Cache)紧密协作。对文件的读写操作实际上是在操作内存中的页缓存,由内核的后台线程负责将“脏”页写回底层存储设备,这极大地提高了I/O性能。
目前该技术的社区活跃度和主流应用情况如何?
VFS是Linux内核的绝对核心,其稳定性和性能对整个系统至关重要。它不是一个经常添加新奇功能的领域,而是作为所有I/O操作的基石被持续地进行性能优化和精细维护。
- 主流应用:
- 所有文件操作:系统中的每一次文件读、写、创建、删除,都必须经过VFS层。
- 支持多样化的文件系统:从传统的磁盘文件系统(ext4, XFS, Btrfs),到网络文件系统(NFS, CIFS),再到各种伪文件系统(procfs, sysfs, tmpfs),它们能够共存并协同工作,完全得益于VFS的抽象。
核心原理与设计
它的核心工作原理是什么?
VFS的核心是通过面向对象的思想,使用上面提到的四个核心对象来抽象所有文件系统的共同特征。
- 超级块对象 (
struct super_block):当一个文件系统被mount时,内核会读取该文件系统在磁盘上的“超级块”,并在内存中创建一个super_block对象。这个对象包含了该文件系统的元信息(如块大小、总空间等)和一组操作函数指针(struct super_operations),例如,如何分配一个新的inode或如何将整个文件系统同步到磁盘。它代表一个已挂载的文件系统。 - 索引节点对象 (
struct inode):当一个文件第一次被访问时,内核会读取它在磁盘上的“inode”,并在内存中创建一个inode对象。这个对象包含了文件的元数据(权限、大小、所有者、时间戳等)和两组关键的操作函数指针:struct inode_operations(例如,如何创建一个新文件、如何创建一个符号链接)和struct file_operations(例如,如何读/写这个文件)。它代表一个文件实体。 - 目录项对象 (
struct dentry):当内核解析一个路径(如/home/user/file.txt)时,它会为路径的每一个组成部分(home,user,file.txt)创建一个dentry对象。dentry的核心作用是将一个文件名与一个inode链接起来,并维护目录的父子关系。这些dentry对象被缓存在dcache中。它代表一个路径组件。 - 文件对象 (
struct file):当一个进程调用open()时,内核会创建一个file对象。这个对象代表一个打开的文件实例。它最重要的成员是f_pos(当前读写位置),以及一个指向对应inode的f_op(file_operations)的指针。不同的进程打开同一个文件会得到不同的file对象,但它们都指向同一个inode对象。
调用流程示例:read(fd, ...)
- 内核通过文件描述符
fd在当前进程的打开文件表中找到对应的struct file对象。 - 通过
file对象找到其f_op指针,即file_operations。 - 调用
f_op中的.read或.read_iter函数,并将file对象(其中包含了f_pos)作为参数传入。 - 这个
.read函数是由具体的文件系统驱动(如ext4)实现的。ext4的read函数会根据f_pos和要读取的长度,计算出需要读取哪些磁盘块,然后通过块设备层去读取数据。
它的主要优势体现在哪些方面?
- 极高的抽象性和可移植性:应用程序和大部分内核代码都无需关心底层文件系统的细节。
- 代码复用:通用的逻辑(如权限检查、路径解析)都在VFS层实现,避免了每个文件系统驱动重复实现。
- 性能优化:通过dcache和inode缓存,极大地减少了对物理设备的访问次数。
- 灵活性:能够轻松地支持各种非传统的文件系统,甚至是用户空间的文件系统(FUSE)。
它存在哪些已知的劣s势、局限性或在特定场景下的不适用性?
- 性能开销:VFS的抽象层会带来一些微小的性能开销。
- 功能集的限制(“最小公分母”):VFS的标准接口只能支持大多数文件系统都具备的通用功能。对于某些高级文件系统(如Btrfs, ZFS)提供的独特功能(如子卷、快照),应用程序需要通过特定的
ioctl系统调用来访问,这在一定程度上绕过了VFS的统一抽象。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?
这不是一个可选的方案,而是Linux/Unix系统中进行任何文件操作的唯一框架。
- 所有常规文件操作:
ls,cp,mv,rm, 文本编辑,编译代码等。 - 挂载不同介质:挂载硬盘、U盘、光盘、网络共享目录等。
- 与内核交互:通过
/proc和/sys等伪文件系统,以读写文件的方式来查看和修改内核状态。
是否有不推荐使用该技术的场景?为什么?
不能说“不推荐使用VFS”,但存在一些场景需要绕过VFS的文件系统层。
- 裸设备访问(Raw Device Access):一些高性能的数据库(如Oracle)为了实现自己的缓存和I/O调度策略,会选择直接打开块设备文件(如
/dev/sda1),绕过文件系统的逻辑,直接对磁盘块进行读写。但这仍然需要经过VFS的块设备处理部分。
对比分析
请将其 与 其他相似技术 进行详细对比。
最好的对比是理解VFS在整个Linux I/O栈中所处的位置。
| 层次 | 名称 | 核心功能 | 典型数据单元 |
|---|---|---|---|
| 应用层 | 用户程序 (e.g., cp, bash) |
发起逻辑I/O请求。 | 用户缓冲区 (char array)。 |
| 系统调用接口 | Glibc / Syscall Interface | 将用户请求转换为内核请求。 | open(), read(), write()。 |
| VFS (虚拟文件系统) | VFS Core (fs/) | 提供统一的文件系统抽象。管理inode, dentry, file对象。 |
struct inode, struct dentry, struct file。 |
| 具体文件系统层 | Concrete FS (e.g., ext4, xfs) | 实现VFS接口。管理自己的元数据(如块位图),将文件逻辑块号映射为物理块号。 | 文件系统特定的元数据(如ext4_extent)。 |
| 块设备层 | Block Layer (block/) | 提供对块设备的通用访问接口,进行I/O调度和合并。 | struct bio (Block I/O request)。 |
| 设备驱动层 | Device Driver (drivers/scsi, drivers/nvme) | 实现块层接口。知道如何与具体的硬件控制器(如SATA, NVMe)通信。 | 硬件特定的命令 (如SCSI CDB)。 |
| 硬件层 | 物理设备 | 物理上执行读写操作。 | 磁盘扇区。 |
fs/filesystems.c
find_filesystem 查找文件系统
1 | static struct file_system_type **find_filesystem(const char *name, unsigned len) |
register_filesystem 注册新的文件系统
1 | /** |
get_filesystem 获取文件系统的引用
1 | /* WARNING: This can be used only if we _already_ own a reference */ |
/proc/filesystems 接口创建与展示 proc_filesystems_init
本代码片段展示了内核如何在 /proc 虚拟文件系统中创建一个名为 filesystems 的文件,并通过这个文件向用户空间展示当前内核支持的所有已注册文件系统类型。它由两部分组成:proc_filesystems_init 在内核初始化时创建 /proc/filesystems 文件,并将其与一个回调函数 filesystems_proc_show 关联;当用户读取该文件时,filesystems_proc_show 函数被调用,遍历内核中维护的文件系统链表,并将每个文件系统的名称及其一个重要属性打印出来。
实现原理分析
此机制是内核通过 procfs 向用户空间提供信息的一个典型范例。其核心原理是利用 VFS(虚拟文件系统)层维护的一个全局链表和 procfs 的回调机制。
全局文件系统链表 (
file_systems):- 内核中有一个全局的、以
NULL结尾的单向链表,其头指针为file_systems。每当一个文件系统模块被加载(通过register_filesystem),一个新的file_system_type结构体就会被添加到这个链表的头部。 - 这个
file_system_type结构体包含了文件系统的元数据,如名称 (name) 和一系列标志位 (fs_flags)。
- 内核中有一个全局的、以
Procfs 文件创建 (
proc_create_single):proc_filesystems_init函数在内核启动时被module_init机制调用。- 它调用
proc_create_single,这是一个简化的procfs接口,用于创建一个只读的、内容由单个回调函数生成的/proc文件。 它将文件名 “filesystems” 与filesystems_proc_show函数绑定。当有进程尝试读取/proc/filesystems时,内核会调用filesystems_proc_show。
信息展示 (
filesystems_proc_show):- 安全遍历: 此函数首先调用
read_lock(&file_systems_lock)来获取一个读锁。file_systems_lock是一个读写锁(rw_lock),用于保护file_systems链表。获取读锁允许多个读者同时安全地遍历链表,但会阻止任何写者(例如,正在加载或卸载文件系统模块的任务)修改链表,从而防止了竞态条件。 - 信息提取与格式化: 函数通过一个
while循环遍历链表。对于每一个file_system_type节点:
a. 它检查tmp->fs_flags & FS_REQUIRES_DEV。FS_REQUIRES_DEV是一个标志位,用于标识该文件系统是否需要一个块设备作为其物理存储后端(如 ext4、FAT32)。 如果该文件系统不需要块设备(例如,像procfs,sysfs这样的虚拟文件系统),条件为假,函数会打印 “nodev”。
b. 它打印文件系统的注册名称tmp->name。 - 释放锁: 遍历完成后,调用
read_unlock(&file_systems_lock)释放读锁,允许写者继续操作。
- 安全遍历: 此函数首先调用
代码分析
1 | /** |










