Appearance
虚拟文件系统
2026 大纲 四(三)3 虚拟文件系统:前两条讲一个文件系统内部怎么组织,这一条回答多个不同文件系统怎么共用同一套系统调用。
一台机器上不止一个文件系统
前面所有内容讲的都是一个文件系统内部怎么组织。可现实里, 一台机器上同时挂着好几个:系统盘是 ext4,U 盘是 FAT32, 挂载的网络目录是 NFS,光盘是 ISO 9660。
而用户程序读它们用的是同一个 read()。
这件事没有听起来那么理所当然,因为这几种文件系统的差别一直深到底层。 FAT32 用显式链接的 FAT 表,ext4 用混合索引 inode; FAT32 连 inode 号和硬链接这两个概念都没有;NFS 的数据根本不在本机。 "第
所以必须有一层挡在中间:对上提供统一接口,对下适配各家实现。 这就是虚拟文件系统(VFS)。
实现手段说穿了很简单——函数指针。每个对象上挂一组接口函数指针, VFS 只写 file->f_op->read(...),它不认识任何具体文件系统, 只知道"这里应该有个能读的函数"。谁填进去的、怎么实现的,它一概不管。
这就是 C 语言里手工实现的虚函数表与动态绑定:接口类型相当于抽象基类, 各文件系统填的那组实例相当于子类,f_op 就是 vtable。 于是"接入一个新文件系统"这件事被简化成:实现 VFS 规定的那组接口、 把指针填进去,VFS 本身一行都不用改。
这一节剩下的是 VFS 定义的四个核心对象,以及它们之间那三条"一对多"—— 硬链接为什么不能跨文件系统、一个文件为什么能有多个名字、 删一个硬链接为什么不释放数据,全都从那三条里直接读出来。
一、函数指针:C 语言里的面向对象
VFS 靠什么让同一句 read 走到不同的代码上?答案很朴素:每个对象里挂一组函数指针,VFS 只管调用指针,不管指针指向谁。
c
// VFS 规定的接口(每个文件系统都要提供一份)
struct file_operations {
ssize_t (*read) (struct file *, char *, size_t, loff_t *);
ssize_t (*write)(struct file *, const char *, size_t, loff_t *);
int (*open) (struct inode *, struct file *);
int (*release)(struct inode *, struct file *);
};
// ext4 填一份
struct file_operations ext4_fops = { .read = ext4_read, .write = ext4_write, ... };
// FAT32 填另一份
struct file_operations fat_fops = { .read = fat_read, .write = fat_write, ... };
// VFS 自己只写这一句,不认识 ext4 也不认识 FAT32
return file->f_op->read(file, buf, n, &file->f_pos);把这段和面向对象语言对照,是一一对应的:
| 面向对象里的概念 | VFS 里的实现 |
|---|---|
| 抽象基类 / 接口 | struct file_operations 这组函数指针的类型定义 |
| 子类 | ext4、FAT32、NFS 各自填出来的那份实例 |
| 虚函数表(vtable) | 对象里那个 f_op 指针指向的结构体 |
| 动态绑定 / 多态 | file->f_op->read(...) 这一句在运行时才确定跳到哪 |
| 子类必须实现基类的纯虚函数 | 接入一个新文件系统 = 实现 VFS 规定的那组接口并把指针填进去 |
vnode 的内部形态正是这套设计的样子——通用部分自己存,私有部分挂指针:
vnode
├── 文件系统类型
├── 通用属性(大小、权限、时间戳...) ← 所有文件系统都有的
├── 操作函数指针 → { .read = ext4_read, .write = ext4_write, ... }
└── 具体 inode 指针 → ext4_inode_info ← 只有 ext4 认识的部分二、四个核心对象:一条从"整个文件系统"到"一次打开"的收窄链
⚠️ VFS 用的是 Linux 的对象名:读写位置记在
file对象里。而通行教学口径把"读写指针"放在进程打开文件表中。两者不矛盾,分歧只在哪一级表叫什么名字——完整说明见文件的操作第五节。
三、dentry 为什么要单独成一个对象
这是四个对象里唯一在磁盘上没有直接对应物的一个——超级块、inode、file 都能在前面几篇里找到对应结构,只有 dentry 是 VFS 自己造出来的。三条理由,每一条都不能靠把信息塞进 inode 来解决:
- 名字与文件本来就是多对一的。 一个文件可以有多个硬链接。若把路径分量的信息放进 inode 对象,就得在 inode 里维护一张名字列表,且每建/删一个硬链接都要改它——这正是 inode 里不放文件名的同一个理由在内存对象这一层重演。把名字单独做成对象,inode 就永远不需要知道自己叫什么。
- 要缓存的是"这一级路径解析的结果",不是文件本身。 路径解析真正昂贵的是"在某个目录里按名字找到子项",要缓存的正是
(父目录, 分量名) → 子项 inode这条映射,而这个键里的两样东西都不属于 inode,只能另立一个对象来装。 - 目录本身也需要被表示成路径中的一环。
/home/alice/a.txt里home、alice是目录、a.txt是普通文件,但它们在路径解析里扮演的角色完全相同:都是"父级里的一个具名入口"。dentry 把目录和文件在这一层抹平了,于是路径解析可以写成一个统一的循环。
dentry 一旦成为独立对象,把它们缓存起来就是自然的事——这就是目录项缓存(dentry cache)。命中一级就省掉那一级的 2 次磁盘 I/O,路径全部命中时整条解析不碰磁盘;量化的收益估算见文件系统的全局结构第三节。
四、VFS 的工作流程
以 read(fd, buf, n) 为例:通过 fd 找到进程打开文件表中的 file 对象 → file 对象含读写位置并指向 vnode → vnode 中那组函数指针里的 read 指向具体文件系统的读操作 → 调用它,由具体文件系统完成"逻辑块 → 物理块 → 读盘"的全部工作。
整个过程对用户完全透明。VFS 自己一行读盘代码都没有,它只做三件事:维护四个对象、按对象里的指针分发、以及在挂载点处切换到另一个超级块(见文件系统挂载)。
考点速记
- VFS 的目标:对上层提供统一的文件操作接口,对下层适配 ext4 / FAT32 / NFS / ISO 9660 等不同实现。⚠️它定义的是接口,不是一种运行在虚拟内存里的文件系统——"虚拟"指的是"不对应任何具体存储"。
- 实现手段就是函数指针:VFS 只写
file->f_op->read(...),它不认识任何具体文件系统。这是 C 语言里手工实现的虚函数表与动态绑定。接入新文件系统 = 实现那组接口并填指针,VFS 本身一行不改。 - ⚠️VFS 不加快访问速度。它多了一层间接调用,严格说还有极小的开销;它换来的是兼容性与可扩展性,不是性能。
- ⚠️VFS 不限于本地文件系统。NFS 这类网络文件系统正是靠 VFS 才能用同一套
read/write访问——"只能访问本地文件系统"说反了它存在的意义。 - 四个核心对象:超级块对象 = 一个已挂载的文件系统;索引节点对象 / vnode = 一个文件本体;目录项对象 dentry = 路径中的一个分量;文件对象 file = 一次已打开的文件(含读写位置)。
- 三条从属关系与多重性:superblock 1:n inode(inode 号只在本超级块内有效 ⇒ 硬链接不能跨文件系统);inode 1:n dentry(一个文件多个名字);dentry 1:n file(同一名字可被多次打开,各有读写位置)。
- 删一个硬链接为什么不释放数据:删掉一个 dentry 后,inode 上还挂着别的 dentry,引用它的边没有归零。
- 生命周期递减:superblock(挂载到卸载,最久)→ vnode → dentry 是纯缓存、可随时淘汰 → file(
open到close,最短)。 - ⚠️vnode 与具体文件系统的 inode 不能合成一个:vnode 只放各种文件系统都有的通用属性加一组函数指针,私有信息(ext4 的混合索引块指针、FAT32 的起始簇号)留在各自的结构里由 vnode 挂着。合成一个等于要求所有文件系统元数据格式一样。
这一节在真题里被考过的形式:
只出过一道题,四个选项恰好把四种典型误解摆齐——记住"它是接口层"这一句就够。
- 判断关于虚拟文件系统的四种说法(2025-28)。答VFS 定义了可访问不同文件系统的统一接口。⚠️ 另三项各错一处:"运行在虚拟内存的文件系统"——把"虚拟"望文生义成"在虚拟内存里",VFS 的"虚拟"指的是它不对应任何具体存储;"可以加快文件系统的访问速度"——它多了一层间接调用,换来的是兼容性不是性能;"只能访问本地文件系统,不能访问网络文件系统"——正好说反,NFS 这类网络文件系统恰恰是 VFS 最典型的适配对象。
复习优先级:按选择题准备,记住三句反例即可。 一是它是接口不是存储, 二是它不提速,三是它支持网络文件系统。速记第六条那三条"一对多"虽然没直接考过, 但"硬链接不能跨文件系统"这个结论会在硬链接和软链接 被用到,理解一遍就够。
易错:把"虚拟文件系统"理解成"运行在虚拟内存里的文件系统"。"虚拟"指它不对应任何具体存储。
易错:认为 VFS 能加快文件访问速度。它多了一层间接调用,换来的是兼容性与可扩展性。
易错:认为 VFS 只能访问本地文件系统。NFS 这类网络文件系统正是它最典型的适配对象。
易错:认为 VFS 的 vnode 就是 ext4 的 inode。vnode 只放通用属性 + 函数指针,私有信息由它挂着的结构存。
易错:认为接入新文件系统要改 VFS。只需实现那组接口并把函数指针填进去。
教材出处
- 孙钟秀、费翔林《操作系统教程》(第 6 版)印刷版 p254:虚拟文件系统层"建立并维持一个打开文件表,它的每一项通过 v 节点对应一个已打开的文件,用于指出文件是本地的还是远程的";"VFS 层中的每一个 v 节点最终都有一个指针指向客户机代码中的一个 r 节点,或本地操作系统中的 i 节点。因此,从 v 节点可以看出文件或目录是位于本地还是远程"——这正是本篇"vnode 用一个指针挂着具体文件系统的结构"的依据。
相关知识
外存空闲空间管理|文件系统挂载|文件的操作|文件元数据与索引节点|文件系统的全局结构