文件系统驱动按需加载 减少VFS初始化阶段的模块数量 - 第2节
2. VFS架构概览
虚拟文件系统(VFS,Virtual File System)是 Linux 内核中最核心的抽象层之一。它的存在使得用户空间程序可以统一使用 open()、read()、write() 等系统调用,而无需关心底层文件系统是 ext4、Btrfs、NFS 还是 FUSE。VFS 通过一套标准的数据结构和操作函数指针表,将具体文件系统的差异封装在驱动层之下。
核心设计目标: 提供 POSIX 兼容的文件操作接口,屏蔽不同文件系统在存储布局、命名规则、权限模型上的差异,同时支持动态加载和卸载文件系统驱动。
VFS 的分层架构从上到下依次为:
- 系统调用层(Syscall Layer): 用户态通过
sys_open、sys_read等入口进入内核。 - VFS 通用层(Generic VFS Layer): 处理路径解析、权限检查、inode/dentry 缓存管理,不依赖具体文件系统。
- 具体文件系统层(Specific FS Layer): 如 ext4、XFS、F2FS 等,实现各自的 inode 读写、块分配策略。
- 块设备层(Block Layer): 提供统一的块 I/O 请求队列和调度机制。
关键理解: VFS 通用层本身不包含任何存储介质相关的代码,它只维护 dentry 缓存、inode 缓存和挂载点树。具体文件系统的驱动代码(如 ext4 的 .ko 模块)在 VFS 初始化阶段并不需要全部加载,只有根文件系统对应的驱动必须提前就绪。
2.2 核心数据结构:超级块、inode、dentry、文件
VFS 定义了四种核心对象,它们构成了文件系统操作的基本单元。每个对象都关联一个操作函数表(operations),具体文件系统通过填充这些函数表来注册自己的实现。
| 数据结构 | 内核结构体 | 主要作用 | 生命周期 |
|---|---|---|---|
| 超级块 | struct super_block |
描述整个文件系统的元信息(块大小、挂载选项、根 inode 编号) | 从挂载到卸载 |
| inode | struct inode |
描述一个文件或目录的元数据(权限、大小、时间戳、数据块指针) | 从首次访问到 evict |
| dentry | struct dentry |
描述路径名与 inode 的映射关系,维护目录树结构 | 从路径解析到 dentry 回收 |
| 文件 | struct file |
描述一个打开的文件实例(当前读写位置、标志、私有数据) | 从 open 到 close |
初始化阶段的关键约束: 在 VFS 初始化时,至少需要创建一个
super_block 实例(根文件系统)和一个根 dentry。如果根文件系统驱动尚未加载,VFS 无法完成挂载,系统将无法进入用户态。这正是“按需加载”策略需要解决的核心矛盾——既要延迟加载非必要驱动,又要保证根文件系统驱动在 VFS 初始化前就绪。
2.3 操作函数表:多态的实现机制
VFS 通过操作函数表实现面向对象风格的多态。每个核心数据结构都包含一个指向操作函数表的指针,具体文件系统在注册时填充这些函数指针。
// 超级块操作函数表
struct super_operations {
struct inode *(*alloc_inode)(struct super_block *sb);
void (*destroy_inode)(struct inode *inode);
void (*dirty_inode)(struct inode *inode, int flags);
int (*write_inode)(struct inode *inode, struct writeback_control *wbc);
void (*evict_inode)(struct inode *inode);
int (*sync_fs)(struct super_block *sb, int wait);
// ...
};
// inode 操作函数表
struct inode_operations {
struct dentry *(*lookup)(struct inode *dir, struct dentry *dentry,
unsigned int flags);
int (*create)(struct mnt_idmap *idmap, struct inode *dir,
struct dentry *dentry, umode_t mode, bool excl);
int (*link)(struct dentry *old_dentry, struct inode *dir,
struct dentry *new_dentry);
int (*unlink)(struct inode *dir, struct dentry *dentry);
int (*mkdir)(struct mnt_idmap *idmap, struct inode *dir,
struct dentry *dentry, umode_t mode);
// ...
};
// 文件操作函数表
struct file_operations {
loff_t (*llseek)(struct file *file, loff_t offset, int whence);
ssize_t (*read)(struct file *file, char __user *buf,
size_t count, loff_t *pos);
ssize_t (*write)(struct file *file, const char __user *buf,
size_t count, loff_t *pos);
int (*mmap)(struct file *file, struct vm_area_struct *vma);
int (*open)(struct inode *inode, struct file *file);
int (*release)(struct inode *inode, struct file *file);
// ...
};
按需加载的切入点: 每个具体文件系统在注册时,会将自己的
file_system_type 结构体链入全局链表。VFS 初始化阶段只需要遍历该链表,但不需要调用每个文件系统的 mount() 方法。只有根文件系统对应的驱动需要立即执行挂载,其余驱动可以延迟到用户空间首次访问对应挂载点时再加载。
2.4 挂载点树与命名空间
VFS 维护一棵全局的挂载点树,每个挂载点对应一个 struct mount 实例。挂载点树的结构决定了路径解析的遍历路径。
| 概念 | 数据结构 | 说明 |
|---|---|---|
| 挂载描述符 | struct mount |
包含挂载点 dentry、被挂载的 super_block、挂载标志 |
| 挂载点 | mnt_mountpoint |
指向父文件系统中的一个 dentry,表示挂载位置 |
| 挂载子节点 | mnt_children |
链表,连接该挂载点下的子挂载 |
| 命名空间 | struct mnt_namespace |
每个进程所属的挂载视图,支持容器隔离 |
初始化阶段的优化策略: 在 VFS 初始化时,只注册根文件系统(通常为 initramfs 或 initrd)对应的驱动。其他文件系统驱动对应的
file_system_type 虽然已链入全局列表,但对应的内核模块可以延迟加载。当用户空间执行 mount -t ext4 时,VFS 发现 ext4 的 mount() 函数指针为空,自动触发 request_module("ext4") 加载模块。
2.5 VFS 初始化流程与模块加载时机
VFS 的初始化流程在 start_kernel() 中按严格顺序执行。理解这个顺序对于设计按需加载策略至关重要。
// VFS 初始化核心路径(简化)
start_kernel()
├── vfs_caches_init() // 初始化 dentry/inode 缓存、挂载点树
│ ├── dcache_init() // dentry 哈希表与 LRU 链表
│ ├── inode_init() // inode 哈希表与未使用链表
│ ├── files_init() // 文件表(struct file 分配器)
│ └── mnt_init() // 初始化挂载点树、注册 rootfs
├── init_rootfs() // 注册 rootfs 文件系统类型
├── init_mount() // 挂载根文件系统(调用 rootfs 的 mount)
└── ... // 后续 init 进程启动
| 初始化阶段 | 加载的模块 | 是否可延迟 | 说明 |
|---|---|---|---|
| vfs_caches_init | 无(纯内存结构) | 不可延迟 | 必须在内核启动时完成 |
| init_rootfs | rootfs(内置) | 不可延迟 | rootfs 编译进内核,非模块 |
| init_mount | 根文件系统驱动 | 不可延迟 | 若根 fs 为 ext4,则 ext4 必须提前加载 |
| init 进程启动后 | 其他文件系统驱动 | 可延迟 | 按需加载策略的主要优化区间 |
实践陷阱: 如果根文件系统驱动被编译为模块(.ko),且 initramfs 中没有包含该模块,VFS 在
init_mount() 阶段将无法挂载根文件系统,导致 kernel panic。因此,按需加载策略必须保证根文件系统驱动要么编译进内核,要么在 initramfs 中提前就绪。非根文件系统驱动则可以安全地延迟到用户空间首次访问时加载。