3. 传统初始化流程:Linux内核启动时VFS模块的静态注册与链接

在Linux内核的传统启动路径中,VFS(Virtual File System)层及其下属的文件系统驱动采用静态注册与链接机制。这意味着所有被编译进内核的文件系统模块,在start_kernel()之后的vfs_caches_init()阶段被一次性、顺序地初始化并注册到全局链表file_systems中。这种设计虽然简单可靠,但在嵌入式场景下带来了显著的启动时延和内存开销。

3.1 传统VFS初始化流程全景

下图展示了从内核入口到VFS初始化完成的完整调用链,以及文件系统驱动在其中被静态注册的位置:

传统VFS初始化流程:静态注册与链接 传统VFS初始化流程:静态注册与链接 start_kernel() vfs_caches_init() mnt_init() init_rootfs() → init_mount_tree() register_filesystem() 循环 (ext4、btrfs、fat、ntfs、proc、sysfs……) 全局链表 file_systems 传统流程:所有模块在VFS初始化阶段被一次性静态注册,无论后续是否被使用

3.2 静态注册的核心机制

传统初始化中,每个文件系统驱动通过register_filesystem()函数将自己注册到VFS核心。该函数将驱动对应的file_system_type结构体插入全局链表file_systems。关键数据结构如下:

// include/linux/fs.h
struct file_system_type {
    const char *name;               // 文件系统名称(如 "ext4")
    int fs_flags;                   // 文件系统标志
    struct dentry *(*mount) (struct file_system_type *, int,
                             const char *, void *);
    void (*kill_sb) (struct super_block *);
    struct module *owner;           // 模块指针(内置为 NULL)
    struct file_system_type *next;  // 链表下一项
    struct hlist_head fs_supers;    // 超级块链表头
    // ...
};

// 全局链表头
extern struct file_system_type *file_systems;

对于静态编译的驱动,owner字段为NULL,表示该驱动不可卸载。注册过程在vfs_caches_init() → mnt_init() → init_rootfs()路径中完成,所有驱动被顺序遍历并调用register_filesystem()。

3.3 静态注册与动态加载的对比

对比维度 传统静态注册 按需动态加载
注册时机 内核启动早期,vfs_caches_init()阶段 用户空间首次挂载或访问时
模块存在形式 静态编译进内核镜像(built-in) 独立.ko文件,通过request_module()加载
内存占用 所有驱动代码和数据常驻内存 仅加载被使用的驱动,节省内存
启动时间 所有驱动依次初始化,增加启动延迟 仅初始化根文件系统驱动,启动更快
依赖关系 编译时确定,链接器静态解析 运行时通过modprobe自动处理依赖
卸载能力 不可卸载(built-in) 可通过rmmod卸载释放资源

3.4 静态注册的典型调用序列

以下是从start_kernel()到文件系统注册完成的典型调用序列,展示了每个阶段注册的文件系统类型:

  1. start_kernel() — 内核入口,初始化基本子系统。
  2. vfs_caches_init() — 初始化VFS缓存(dentry、inode、mnt等)。
  3. mnt_init() — 初始化挂载子系统,调用init_rootfs()。
  4. init_rootfs() — 注册rootfs(基于ramfs/tmpfs的初始根文件系统)。
  5. init_mount_tree() — 挂载rootfs,创建初始命名空间。
  6. register_filesystem() 循环 — 注册所有静态编译的文件系统驱动:
    • ext4_fs_type — ext4文件系统
    • btrfs_fs_type — Btrfs文件系统
    • fat_fs_type — FAT/VFAT文件系统
    • ntfs_fs_type — NTFS文件系统
    • proc_fs_type — proc文件系统
    • sysfs_fs_type — sysfs文件系统
    • devtmpfs_fs_type — devtmpfs文件系统
    • ……(取决于内核配置)
关键观察: 在传统流程中,即使嵌入式设备只使用squashfs作为根文件系统,内核仍然会注册所有静态编译的文件系统驱动(如ext4、btrfs、NTFS等)。这些驱动占用的代码段、数据段和初始化时间在启动阶段被完全浪费。

3.5 静态注册的链接过程

静态注册的底层依赖链接器的__initcall机制。每个文件系统驱动通过module_init()宏(对于built-in模块)将其初始化函数放入特定的初始化段:

// 示例:ext4文件系统的静态注册
// fs/ext4/super.c
static struct file_system_type ext4_fs_type = {
    .owner      = THIS_MODULE,
    .name       = "ext4",
    .mount      = ext4_mount,
    .kill_sb    = ext4_kill_sb,
    .fs_flags   = FS_REQUIRES_DEV,
    // ...
};

// module_init 将 ext4_init_fs 放入 .initcall6.init 段
static int __init ext4_init_fs(void)
{
    return register_filesystem(&ext4_fs_type);
}
module_init(ext4_init_fs);

链接阶段,所有module_init()定义的函数指针被收集到.initcall段中。内核启动时,do_initcalls()函数按优先级顺序遍历该段,依次调用每个初始化函数。对于文件系统驱动,这等价于在VFS初始化阶段执行register_filesystem()。

嵌入式痛点: 静态链接意味着所有被编译进内核的文件系统驱动都会在启动时被注册,即使它们从未被使用。在资源受限的嵌入式系统中,这会导致:
• 内核镜像体积膨胀(代码段+数据段)
• 启动时间增加(每个驱动执行初始化代码)
• 内存浪费(驱动数据结构常驻内存)
• 无法按需卸载(built-in模块不可卸载)

3.6 传统流程的局限性总结

传统静态注册与链接机制在通用Linux发行版中工作良好,但在嵌入式场景下暴露了以下核心问题:

  • 启动时延不可控: 每个静态注册的驱动都需要执行register_filesystem(),包括内存分配、链表操作和可能的硬件探测。随着内核配置中文件系统数量的增加,启动时间线性增长。
  • 内存碎片化: 所有驱动的file_system_type结构体、mount函数指针、kill_sb函数指针等数据常驻内存,无法回收。
  • 缺乏灵活性: 静态编译的驱动无法在运行时卸载,即使系统明确知道某些文件系统永远不会被使用(如只读设备上的NTFS驱动)。
  • 初始化顺序耦合: 所有驱动在同一个初始化阶段注册,无法根据实际挂载需求延迟注册,导致VFS初始化阶段成为启动瓶颈。

这些局限性直接催生了文件系统驱动按需加载的需求——将文件系统驱动的注册时机从启动阶段推迟到首次挂载时,从而减少VFS初始化阶段的模块数量,优化嵌入式系统的启动性能和内存占用。