[toc]

fs/ VFS - 虚拟文件系统(Virtual Filesystem) 内核统一的文件系统抽象层

历史与背景

这项技术是为了解决什么特定问题而诞生的?

虚拟文件系统(Virtual Filesystem Switch, VFS)是Linux内核最核心、最强大的子系统之一。它的诞生是为了解决一个根本性的问题:如何让应用程序以一种统一的方式来访问各种不同类型的文件系统

在VFS出现之前,操作系统如果想支持一种新的文件系统(例如,从Minix文件系统切换到ext文件系统),可能需要重写大量与文件操作相关的代码。应用程序也可能会与特定的文件系统实现产生耦合。VFS通过创建一个通用的抽象层来解决这个问题:

  1. 对应用程序的统一接口:无论底层是ext4、XFS、Btrfs、NFS(网络文件系统),还是一个USB U盘上的FAT32,应用程序都使用同样标准的系统调用(open, read, write, close, stat等)来操作文件。应用程序完全不需要知道底层文件系统的具体类型和实现细节。
  2. 对文件系统驱动的统一接口: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的核心是通过面向对象的思想,使用上面提到的四个核心对象来抽象所有文件系统的共同特征。

  1. 超级块对象 (struct super_block):当一个文件系统被mount时,内核会读取该文件系统在磁盘上的“超级块”,并在内存中创建一个super_block对象。这个对象包含了该文件系统的元信息(如块大小、总空间等)和一组操作函数指针(struct super_operations),例如,如何分配一个新的inode或如何将整个文件系统同步到磁盘。它代表一个已挂载的文件系统
  2. 索引节点对象 (struct inode):当一个文件第一次被访问时,内核会读取它在磁盘上的“inode”,并在内存中创建一个inode对象。这个对象包含了文件的元数据(权限、大小、所有者、时间戳等)和两组关键的操作函数指针:struct inode_operations(例如,如何创建一个新文件、如何创建一个符号链接)和struct file_operations(例如,如何读/写这个文件)。它代表一个文件实体
  3. 目录项对象 (struct dentry):当内核解析一个路径(如/home/user/file.txt)时,它会为路径的每一个组成部分(home, user, file.txt)创建一个dentry对象。dentry的核心作用是将一个文件名与一个inode链接起来,并维护目录的父子关系。这些dentry对象被缓存在dcache中。它代表一个路径组件
  4. 文件对象 (struct file):当一个进程调用open()时,内核会创建一个file对象。这个对象代表一个打开的文件实例。它最重要的成员是f_pos(当前读写位置),以及一个指向对应inodef_opfile_operations)的指针。不同的进程打开同一个文件会得到不同的file对象,但它们都指向同一个inode对象。

调用流程示例:read(fd, ...)

  1. 内核通过文件描述符fd在当前进程的打开文件表中找到对应的struct file对象。
  2. 通过file对象找到其f_op指针,即file_operations
  3. 调用f_op中的.read.read_iter函数,并将file对象(其中包含了f_pos)作为参数传入。
  4. 这个.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
2
3
4
5
6
7
8
9
static struct file_system_type **find_filesystem(const char *name, unsigned len)
{
struct file_system_type **p;
for (p = &file_systems; *p; p = &(*p)->next)
if (strncmp((*p)->name, name, len) == 0 &&
!(*p)->name[len])
break;
return p;
}

register_filesystem 注册新的文件系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* register_filesystem - 注册新的文件系统
* @fs:文件系统结构
*
* 将传递给内核可识别的文件系统列表的文件系统添加 mount 和其他 syscall。成功时返回 0,错误时返回负 errno 代码。
*
* 传递的 &struct file_system_type 链接到内核结构中,在取消注册文件系统之前不得释放。
*/
int register_filesystem(struct file_system_type * fs)
{
int res = 0;
struct file_system_type ** p;
/* 如果文件系统结构包含参数(fs->parameters),调用 fs_validate_description 验证参数的合法性 */
if (fs->parameters &&
!fs_validate_description(fs->name, fs->parameters))
return -EINVAL;
/* 检查文件系统名称中是否包含非法字符(如 .) */
BUG_ON(strchr(fs->name, '.'));
/* 如果文件系统结构的 next 字段非空,表示该文件系统已经被注册过,返回 -EBUSY */
if (fs->next)
return -EBUSY;
write_lock(&file_systems_lock);
/* 调用 find_filesystem 查找文件系统名称是否已存在 */
p = find_filesystem(fs->name, strlen(fs->name));
if (*p)
res = -EBUSY;
else
/* 将file_systems->next 挂载为 fs */
*p = fs;
write_unlock(&file_systems_lock);
return res;
}

get_filesystem 获取文件系统的引用

1
2
3
4
5
6
/* WARNING: This can be used only if we _already_ own a reference */
struct file_system_type *get_filesystem(struct file_system_type *fs)
{
__module_get(fs->owner);
return fs;
}