文件系统驱动按需加载:减少VFS初始化阶段的模块数量 - 第2节

2.1 VFS 的设计目标与分层思想

虚拟文件系统(VFS,Virtual File System)是 Linux 内核中最核心的抽象层之一。它的存在使得用户空间程序可以统一使用 open()、read()、write() 等系统调用,而无需关心底层文件系统是 ext4、Btrfs、NFS 还是 FUSE。VFS 通过一套标准的数据结构和操作函数指针表,将具体文件系统的差异封装在驱动层之下。

核心设计目标: 提供 POSIX 兼容的文件操作接口,屏蔽不同文件系统在存储布局、命名规则、权限模型上的差异,同时支持动态加载和卸载文件系统驱动。

VFS 的分层架构从上到下依次为:

  1. 系统调用层(Syscall Layer): 用户态通过 sys_open、sys_read 等入口进入内核。
  2. VFS 通用层(Generic VFS Layer): 处理路径解析、权限检查、inode/dentry 缓存管理,不依赖具体文件系统。
  3. 具体文件系统层(Specific FS Layer): 如 ext4、XFS、F2FS 等,实现各自的 inode 读写、块分配策略。
  4. 块设备层(Block Layer): 提供统一的块 I/O 请求队列和调度机制。
VFS 分层架构图 VFS 分层架构 系统调用层 (sys_open / sys_read) VFS 通用层 (路径解析 / 缓存) 具体文件系统层 (ext4 / XFS / FUSE) 块设备层 (Block I/O 调度) VFS 通用层是初始化阶段最重的模块,按需加载策略主要优化该层以下驱动的加载时机
关键理解: 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 核心数据结构关系图 VFS 核心数据结构关系 super_block inode dentry file s_inodes 链表 s_root d_inode f_path.dentry 数据结构之间的引用关系 • super_block 通过 s_inodes 链表管理所有 inode • super_block 的 s_root 指向根目录的 dentry • dentry 通过 d_inode 指针关联到具体 inode • file 通过 f_path.dentry 找到对应的 dentry 和 inode
初始化阶段的关键约束: 在 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 每个进程所属的挂载视图,支持容器隔离
挂载点树结构 挂载点树结构 / (rootfs) /proc /sys /data (ext4) 按需加载在挂载点树中的体现 • 根挂载 (/) 必须在 VFS 初始化阶段完成,驱动必须提前加载 • /proc、/sys 等虚拟文件系统可在 init 进程启动后按需加载 • /data 等存储文件系统可延迟到用户空间首次访问时加载驱动
初始化阶段的优化策略: 在 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 中提前就绪。非根文件系统驱动则可以安全地延迟到用户空间首次访问时加载。
VFS 初始化时序图 VFS 初始化时序与模块加载窗口 时间 vfs_caches_init init_rootfs init_mount init 进程启动 必须加载根文件系统驱动(不可延迟) 按需加载窗口(可延迟) 按需加载策略的时序约束 1. vfs_caches_init 阶段:分配内存缓存,不涉及任何文件系统驱动 2. init_rootfs 阶段:注册 rootfs 类型(编译进内核,非模块) 3. init_mount 阶段:调用 rootfs 的 mount 函数,若根 fs 为 ext4 则 ext4 驱动必须已加载 4. init 进程启动后:用户空间执行 mount 命令时,VFS 通过 request_module 按需加载驱动 5. 优化目标:将非根文件系统驱动从 initramfs 中移除,减少 VFS 初始化阶段的模块数量 6. 前提条件:根文件系统驱动必须编译进内核或放在 initramfs 早期阶段 7. 收益:减少内核镜像大小、加快启动速度、降低内存占用

2.6 本章小结