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 接口调用:
- 分配与初始化:文件系统注册时,VFS 调用
alloc_super()分配struct super_block内存,并初始化默认字段(如链表头、信号量)。随后调用文件系统的fill_super()回调,从磁盘读取超级块数据填充内存结构。 - 挂载激活:填充完成后,VFS 将超级块加入全局超级块链表
super_blocks,并设置s_root指向根 dentry。此时文件系统对用户可见。 - 运行与同步:在文件系统使用期间,VFS 通过
s_op->sync_fs()定期将内存中的脏数据写回磁盘。超级块本身也可能被标记为脏(s_dirt),需要同步。 - 销毁与释放:卸载文件系统时,VFS 调用
kill_sb()或deactivate_super(),释放所有关联的 inode、dentry,最后调用put_super()释放超级块内存。
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)。
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;
}