2. VFS 核心数据结构(上):超级块对象(super_block)

超级块对象是 VFS 中第一个需要深入理解的核心数据结构。它描述了一个已挂载文件系统的全局元信息,包括块大小、最大文件大小、操作函数表以及根目录 dentry 等。在 VFS 层面,每个挂载的文件系统实例都对应一个 struct super_block,它是文件系统与 VFS 交互的“身份证”。

核心认知:超级块对象是 VFS 对底层文件系统“全局状态”的抽象。无论底层是 ext4、XFS 还是 tmpfs,VFS 都通过 super_block 统一访问其容量、操作接口和根节点。

2.1 超级块对象的定义与作用

超级块对象定义在 <linux/fs.h> 中,结构体名称是 struct super_block。它的主要作用可以归纳为以下三点:

  • 承载文件系统全局属性:记录块设备、块大小、最大文件大小、挂载选项等。
  • 提供操作函数表:通过 s_op 指针挂载 struct super_operations,VFS 通过该表调用文件系统特有的读写 inode、同步文件系统等方法。
  • 维护文件系统树根:通过 s_root 指向根目录的 dentry 对象,VFS 路径查找的起点即在此。
类比理解:如果把文件系统比作一个“国家”,超级块就是国家的“宪法+人口登记处”。宪法(s_op)规定了基本操作规则,人口登记处(s_root)记录了从哪里能找到所有居民(inode)。

2.2 关键字段解析

struct super_block 包含数十个字段,以下按功能分组列出最核心的字段:

分类 字段名 类型 说明
设备与块信息 s_dev dev_t 文件系统所在块设备的设备号
s_bdev struct block_device * 指向底层块设备描述符
s_blocksize unsigned int 文件系统逻辑块大小(字节),如 ext4 通常为 4096
操作函数表 s_op const struct super_operations * 超级块操作函数表,包含 alloc_inode、destroy_inode、sync_fs 等
s_d_op const struct dentry_operations * 该文件系统默认的 dentry 操作函数表
文件系统树根 s_root struct dentry * 根目录的 dentry 对象,路径查找起点
s_mounts struct list_head 该超级块下所有挂载点的链表头
链表与同步 s_inodes struct list_head 该文件系统所有 inode 的链表头
s_dirty struct list_head 脏 inode 链表,用于回写机制
挂载选项 s_flags unsigned long 挂载标志位,如 MS_RDONLY(只读)、MS_NOATIME 等
注意:s_blocksize 与磁盘物理扇区大小不同。逻辑块大小是文件系统分配空间的最小单位,而物理扇区是块设备 I/O 的最小单位。ext4 支持 1024、2048、4096 字节的逻辑块大小,默认 4096。

2.3 超级块对象的生命周期

超级块对象从创建到销毁经历四个阶段,每个阶段都有对应的 VFS 接口调用:

  1. 分配与初始化:文件系统注册时,VFS 调用 alloc_super() 分配 struct super_block 内存,并初始化默认字段(如链表头、信号量)。随后调用文件系统的 fill_super() 回调,从磁盘读取超级块数据填充内存结构。
  2. 挂载激活:填充完成后,VFS 将超级块加入全局超级块链表 super_blocks,并设置 s_root 指向根 dentry。此时文件系统对用户可见。
  3. 运行与同步:在文件系统使用期间,VFS 通过 s_op->sync_fs() 定期将内存中的脏数据写回磁盘。超级块本身也可能被标记为脏(s_dirt),需要同步。
  4. 销毁与释放:卸载文件系统时,VFS 调用 kill_sb() 或 deactivate_super(),释放所有关联的 inode、dentry,最后调用 put_super() 释放超级块内存。
超级块对象生命周期流程图 1. 分配 alloc_super() 2. 填充 fill_super() 3. 激活 挂载完成 4. 运行 sync_fs() 5. 销毁 kill_sb() 说明: • 分配阶段只创建空壳,磁盘数据尚未读入 • 填充阶段从磁盘读取超级块内容 • 销毁阶段释放所有关联资源

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

struct super_operations 是 VFS 与具体文件系统之间的关键接口。每个文件系统必须实现其中部分或全部回调。以下是最核心的五个回调:

回调函数 原型 调用时机 典型实现(ext4)
alloc_inode struct inode *(*alloc_inode)(struct super_block *) VFS 需要新建 inode 时 从 slab 缓存分配 struct ext4_inode_info
destroy_inode void (*destroy_inode)(struct inode *) inode 引用计数降为 0 时 释放 ext4 私有数据,归还 slab 缓存
sync_fs int (*sync_fs)(struct super_block *, int wait) 系统调用 sync() 或内存压力回写时 提交日志(JBD2),刷新块设备缓冲区
write_super int (*write_super)(struct super_block *) 超级块本身被标记为脏时 将内存超级块写入磁盘特定块号
statfs int (*statfs)(struct dentry *, struct kstatfs *) 系统调用 statfs() 时 计算并返回总块数、可用块数、inode 数等
关键设计:VFS 通过 s_op 调用文件系统实现时,不关心底层具体如何分配 inode 或同步数据。这种“面向接口编程”的设计使得 Linux 可以同时支持数十种文件系统,而 VFS 核心代码几乎不需要修改。

2.5 超级块与 VFS 全局链表的关系

所有已挂载文件系统的超级块对象通过全局链表串联在一起,VFS 通过遍历该链表执行全局操作(如 sync() 遍历所有超级块调用 sync_fs)。

超级块全局链表结构 super_blocks super_block (ext4 /dev/sda1) super_block (XFS /dev/sdb1) super_block (tmpfs) NULL 每个超级块通过 s_list 字段链接到全局链表。sync() 遍历此链表,对每个超级块调用 s_op->sync_fs()。

2.6 超级块在 ext4 中的具体实现

以 ext4 为例,磁盘上的超级块结构是 struct ext4_super_block,位于块组的第 0 号块(偏移 1024 字节处)。VFS 的 struct super_block 中的 s_fs_info 指针指向 ext4 的私有超级块信息 struct ext4_sb_info,其中包含了从磁盘读取的 ext4_super_block 副本以及其他运行时状态。

ext4 私有数据示例:ext4_sb_info 包含 s_es(指向磁盘超级块副本)、s_log_groups_per_flex(弹性块组大小)、s_mount_opt(挂载选项位图)等字段。VFS 超级块通过 s_fs_info 访问这些数据,实现了“VFS 通用层 + 文件系统私有层”的经典分层。

以下代码片段展示了 ext4 文件系统在 fill_super 回调中如何初始化 VFS 超级块:

static int ext4_fill_super(struct super_block *sb, void *data, int silent)
{
    struct ext4_sb_info *sbi;
    struct ext4_super_block *es;

    // 1. 分配 ext4 私有信息结构体
    sbi = kzalloc(sizeof(struct ext4_sb_info), GFP_KERNEL);
    sb->s_fs_info = sbi;

    // 2. 从块设备读取磁盘超级块
    es = ext4_read_super_block(sb, &silent);
    sbi->s_es = es;

    // 3. 设置 VFS 超级块字段
    sb->s_maxbytes = ext4_max_size(sb->s_blocksize_bits);
    sb->s_blocksize = le32_to_cpu(es->s_log_block_size) << 10;
    sb->s_blocksize_bits = blksize_bits(sb->s_blocksize);
    sb->s_op = &ext4_sops;  // 挂载 ext4 的操作函数表

    // 4. 读取根目录 inode 并设置 s_root
    root_inode = ext4_iget(sb, EXT4_ROOT_INO);
    sb->s_root = d_make_root(root_inode);

    return 0;
}