3. 超级块(super_block)深度解析

超级块是VFS中最核心的数据结构之一,它代表一个已挂载的文件系统实例。每个文件系统在被挂载时,都会在内存中创建一个对应的超级块对象,用于描述该文件系统的全局元数据信息。本章将深入剖析super_block结构体的定义、关键字段、操作函数表以及挂载与卸载的完整流程。

3.1 super_block 结构体定义与关键字段

super_block结构体定义在include/linux/fs.h中,是VFS层与具体文件系统实现之间的桥梁。以下列出其核心字段及其含义:

字段名 类型 含义说明
s_list struct list_head 全局超级块链表节点,用于链接所有已挂载文件系统的超级块
s_dev dev_t 该文件系统所挂载块设备的设备号
s_blocksize unsigned int 文件系统的逻辑块大小(字节),通常为1024、2048或4096
s_blocksize_bits unsigned char s_blocksize对应的位数,即log2(s_blocksize)
s_maxbytes loff_t 文件系统支持的最大文件大小
s_type struct file_system_type * 指向该文件系统类型的指针,标识文件系统驱动
s_op struct super_operations * 超级块操作函数表,提供对超级块和inode的核心操作
s_root struct dentry * 文件系统根目录的dentry对象,是路径查找的起点
s_dirty struct list_head 脏inode链表,记录需要写回磁盘的inode
s_inodes struct list_head 该超级块下所有inode的链表头
s_dentry_lru struct list_head dentry LRU链表,用于dentry缓存回收
s_fs_info void * 指向文件系统私有数据的指针,用于存储具体文件系统的超级块信息
s_flags unsigned long 挂载标志位,如MS_RDONLY(只读)、MS_NOSUID等
s_time_gran u32 文件时间戳的粒度(纳秒)
关键理解:s_fs_info是VFS超级块与具体文件系统之间的适配层。例如,对于ext4文件系统,s_fs_info指向一个struct ext4_sb_info,其中包含了ext4特有的超级块扩展信息。这种设计使得VFS层无需关心底层文件系统的具体实现细节。

3.2 超级块操作函数表 (super_operations)

super_operations结构体定义了VFS层可以调用的、与超级块和inode管理相关的一组回调函数。每个文件系统驱动必须实现这些函数中的核心部分,以完成挂载、卸载、inode分配与回收等操作。

struct super_operations {
    struct inode *(*alloc_inode)(struct super_block *sb);
    void (*destroy_inode)(struct inode *inode);
    void (*free_inode)(struct inode *inode);
    void (*dirty_inode)(struct inode *inode, int flags);
    int (*write_inode)(struct inode *inode, struct writeback_control *wbc);
    int (*drop_inode)(struct inode *inode);
    void (*evict_inode)(struct inode *inode);
    void (*put_super)(struct super_block *sb);
    int (*sync_fs)(struct super_block *sb, int wait);
    int (*freeze_super)(struct super_block *sb);
    int (*unfreeze_super)(struct super_block *sb);
    int (*statfs)(struct dentry *dentry, struct kstatfs *buf);
    int (*remount_fs)(struct super_block *sb, int *flags, char *data);
    void (*umount_begin)(struct super_block *sb);
    int (*show_options)(struct seq_file *m, struct dentry *root);
    int (*show_devname)(struct seq_file *m, struct dentry *root);
    int (*show_path)(struct seq_file *m, struct dentry *root);
    ssize_t (*quota_read)(struct super_block *sb, int type, char *data, size_t len, loff_t off);
    ssize_t (*quota_write)(struct super_block *sb, int type, const char *data, size_t len, loff_t off);
    int (*bdev_try_to_free_page)(struct super_block *sb, struct page *page, gfp_t gfp);
};
回调函数 触发时机 典型实现行为
alloc_inode 创建新inode时 分配struct inode及文件系统私有inode结构体
destroy_inode inode引用计数降为0时 释放inode占用的内存资源
dirty_inode inode被标记为脏时 将inode加入超级块的脏inode链表
write_inode 回写脏inode时 将inode元数据写入磁盘
evict_inode 从inode缓存中驱逐时 清理inode关联的数据页,释放磁盘块
put_super 卸载文件系统时 释放超级块私有数据,关闭底层块设备
sync_fs 同步文件系统时 将超级块和所有脏数据写回磁盘
statfs 调用statfs系统调用时 返回文件系统的统计信息(总块数、可用块数等)
remount_fs 重新挂载时 修改挂载参数(如从读写切换为只读)
适配要点:文件系统驱动在实现super_operations时,必须确保alloc_inode和destroy_inode的配对使用。许多文件系统通过alloc_inode分配一个包含struct inode的更大的结构体(如ext4的struct ext4_inode_info),并在destroy_inode中释放。这种嵌入式结构体设计是VFS适配的常见模式。

3.3 挂载流程深度解析

文件系统的挂载流程涉及从用户态mount系统调用到VFS层、再到具体文件系统驱动的完整调用链。以下流程图展示了核心步骤:

文件系统挂载流程 用户态 mount 系统调用 VFS: path_mount() → do_new_mount() 查找 file_system_type → 调用 mount 回调 具体文件系统: fill_super() → 读取磁盘超级块 → 填充 super_block 创建根 dentry → 设置 s_root → 挂载到目录树 挂载完成 → 返回新挂载点 dentry 关键点:fill_super() 是文件系统驱动的入口,负责从磁盘读取超级块并初始化 VFS super_block

挂载流程的详细步骤分解如下:

  1. 用户态触发:用户程序调用mount()系统调用,传入设备名、挂载点路径、文件系统类型和挂载标志。
  2. VFS路径解析:内核进入path_mount()函数,解析挂载点路径,获取目标目录的dentry和vfsmount。
  3. 文件系统类型匹配:do_new_mount()根据传入的文件系统名称,在全局的file_systems链表中查找对应的file_system_type结构体。
  4. 调用mount回调:VFS调用file_system_type->mount回调函数。对于基于块设备的文件系统,通常调用mount_bdev()通用辅助函数。
  5. 读取磁盘超级块:mount_bdev()打开块设备,调用文件系统驱动的fill_super()函数。该函数从磁盘读取原始超级块数据,填充super_block结构体,并设置s_fs_info指向文件系统私有数据。
  6. 初始化根inode和dentry:文件系统驱动创建根目录的inode和dentry,并将super_block->s_root指向根dentry。
  7. 挂载到全局目录树:VFS将新创建的vfsmount和根dentry挂接到挂载点的dentry上,完成挂载操作。
常见适配错误:在实现fill_super()时,如果未能正确设置s_op指针,或者s_root为NULL,将导致挂载失败并返回-ENOMEM或-EINVAL。务必在fill_super()返回前验证这两个字段的有效性。

3.4 卸载流程深度解析

卸载流程是挂载的逆过程,其核心目标是安全地释放超级块及其关联的所有资源。以下流程图展示了卸载的关键路径:

文件系统卸载流程 用户态 umount 系统调用 VFS: path_umount() → do_umount() 检查引用计数 → 同步脏数据 → 调用 umount_begin 具体文件系统: put_super() → 释放私有数据 → 关闭块设备 销毁根 dentry → 释放 super_block → 从全局链表移除 卸载完成 → 返回 0

卸载流程的详细步骤分解如下:

  1. 用户态触发:用户程序调用umount()系统调用,传入挂载点路径或设备名。
  2. VFS路径解析与验证:path_umount()解析挂载点,获取对应的vfsmount和超级块。VFS检查该挂载点是否被其他进程使用(通过引用计数)。
  3. 同步与预卸载:do_umount()调用sync_filesystem()将所有脏数据写回磁盘,然后调用super_operations->umount_begin()通知文件系统驱动即将卸载。
  4. 调用put_super:VFS调用super_operations->put_super(),文件系统驱动在此函数中释放s_fs_info指向的私有数据,关闭底层块设备。
  5. 清理VFS对象:VFS销毁根dentry(s_root),释放super_block结构体本身,并将其从全局超级块链表中移除。
  6. 完成卸载:VFS更新挂载树结构,释放vfsmount对象,卸载操作完成。
卸载失败场景:如果文件系统上有文件被打开或进程的当前工作目录位于该文件系统内,引用计数将不为0,卸载操作会返回-EBUSY。内核提供了MNT_FORCE标志(对应umount -l的延迟卸载),允许在引用计数不为0时强制卸载,但可能导致后续访问该文件系统的进程收到-EIO错误。

3.5 超级块生命周期管理

超级块的生命周期从alloc_super()函数分配结构体开始,到deactivate_super()或kill_super()释放结束。理解其生命周期对于文件系统驱动的正确实现至关重要。

阶段 关键函数 状态描述
分配 alloc_super() 分配struct super_block内存,初始化链表头和锁,设置默认值
填充 fill_super() 文件系统驱动从磁盘读取元数据,填充超级块字段,设置s_op和s_root
激活 grab_super() 超级块被挂载到目录树,引用计数增加,进入活跃状态
同步 sync_filesystem() 将超级块和所有脏inode写回磁盘,确保数据一致性
冻结 freeze_super() 暂停文件系统写入操作,用于快照或在线检查
卸载 put_super() 文件系统驱动释放私有资源,关闭块设备
销毁 deactivate_super() 引用计数降为0,释放super_block结构体内存
性能优化提示:在实现put_super()时,应避免在持有超级块锁的情况下执行耗时操作(如大量磁盘I/O)。建议在put_super()中仅完成必要的资源释放,将复杂的清理工作放在evict_inode()阶段完成。这样可以减少卸载过程中的锁竞争,提升系统整体响应性。