[TOC]
fs/super.c 超级块管理(Superblock Management) VFS与具体文件系统的桥梁
历史与背景
这项技术是为了解决什么特定问题而诞生的?
这项技术是为了解决操作系统中一个根本性的抽象问题:如何让内核以一种统一、标准化的方式来管理和操作各种各样、格式迥异的文件系统。
在fs/super.c
所代表的VFS(虚拟文件系统)层出现之前,操作系统内核如果要支持一种新的文件系统(例如,从minix fs切换到ext2),可能需要对内核的大量代码进行修改。fs/super.c
及其相关代码的诞生就是为了:
- 提供统一接口:为用户空间程序(如
mount
命令)和内核其他部分提供一个稳定的、与具体文件系统无关的接口。无论底层是ext4, XFS, Btrfs还是NFS,上层都使用同样的方式来挂载、卸载和获取文件系统信息。 - 解耦通用逻辑与特定实现:将所有文件系统都共有的逻辑(如管理挂载点列表、维护缓存、权限检查的通用部分)从特定文件系统的实现(如如何从磁盘块中读取inode、如何分配数据块)中分离出来。
- 实现文件系统的可插拔性:创建一个框架,使得添加一个新的文件系统就像编写一个符合特定规范的“驱动程序”模块一样,而无需改动内核核心代码。
super.c
就是这个框架中负责管理“文件系统实例”(即一个挂载了的文件系统)的部分。
它的发展经历了哪些重要的里程碑或版本迭代?
fs/super.c
是Linux内核最古老、最核心的文件之一,其基本思想源于Unix。它的发展与整个VFS层的演进同步进行,主要里程碑体现在struct super_block
和struct super_operations
这两个核心数据结构的扩展上,以支持不断增长的文件系统新特性:
- 基本功能:早期版本定义了超级块的基本概念,管理一个已挂载文件系统的根目录、设备信息等。
- 日志/事务支持:为了支持像ext3这样的日志文件系统,在
super_operations
中加入了与事务和日志相关的钩子函数,如.sync_fs()
。 - 配额(Quota)支持:加入了对用户和组磁盘配额管理的支持。
- 扩展属性(xattr)和ACL支持:
super_block
中增加了标志位和配置选项,以启用对扩展属性和访问控制列表的支持。 - 安全模块(LSM)集成:为了支持像SELinux和AppArmor这样的安全框架,加入了安全相关的钩子,允许LSM在文件系统挂载和操作时进行策略控制。
- 现代特性集成:近年来,为了支持像ID-mapped Mounts这样的容器化新技术,
super_block
结构中也加入了与ID映射相关的字段。
目前该技术的社区活跃度和主流应用情况如何?
fs/super.c
是内核VFS层最稳定的基石。它的代码库极少发生颠覆性的改变,因为任何改动都可能影响到系统中所有的文件系统。社区的“活跃度”主要体现在:
- 为新VFS特性提供支持:当VFS层引入新功能(如近期的folio转换)时,
super.c
中的通用函数会随之更新。 - 新文件系统的接入点:任何新的文件系统都必须通过
register_filesystem
注册,并实现super.c
所要求的接口来与VFS交互。 - 重构和优化:内核开发者会不时地对其进行微小的性能优化和代码清理。
它被Linux内核中每一个被挂载的文件系统实例所使用,是操作系统正常运转不可或缺的一部分。
核心原理与设计
它的核心工作原理是什么?
fs/super.c
的核心是管理struct super_block
对象的生命周期,并作为VFS通用调用与具体文件系统实现之间的分派中心。struct super_block
是内核中代表一个已挂载文件系统实例的内存对象。
其工作流程(以mount
为例):
- 注册:在内核启动或模块加载时,每个文件系统(如ext4)都会调用
register_filesystem()
函数,将自身的信息(名字、mount
回调函数等)注册到一个全局的链表中。 - 挂载请求:当用户执行
mount
系统调用时,VFS层的代码会根据用户指定的类型(如”ext4”)在全局链表中找到对应的文件系统。 - 调用特定文件系统的
mount
:VFS会调用该文件系统注册的.mount
回调函数(例如ext4_mount()
)。 - 读取物理超级块:
ext4_mount()
函数会执行ext4特有的操作,即从块设备上读取ext4格式的超级块,并校验其有效性。 - 分配和填充VFS超级块:
ext4_mount()
会调用fs/super.c
中提供的辅助函数(如sget()
)来分配一个VFS层通用的struct super_block
对象。然后,它会用从磁盘读取到的信息(如块大小、inode总数等)以及ext4自身的操作函数表(ext4_sops
)来填充这个super_block
对象。 - 建立连接:
ext4_sops
被赋值给super_block->s_op
。这个s_op
是一个struct super_operations
类型的指针,它包含了statfs
,sync_fs
等一系列函数指针。 - 分派:挂载完成后,当VFS需要对这个文件系统执行一个通用操作时(例如,用户运行
statfs
命令),VFS会通过super_block->s_op->statfs
这个函数指针,调用到ext4提供的具体实现(ext4_statfs()
)。
fs/super.c
负责维护所有这些super_block
对象的全局列表,处理它们的引用计数,以及在umount
时执行通用的清理工作并调用特定文件系统的.put_super
来释放资源。
它的主要优势体現在哪些方面?
- 高度抽象:完美地将“什么是文件系统”的通用概念与“一个ext4文件系统是什么样的”的具体实现分离开来。
- 模块化:使得文件系统可以作为独立的模块被开发、加载和卸载。
- 代码复用:所有文件系统共享
fs/super.c
中提供的通用管理逻辑。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- VFS模型的限制:它定义了一套所有文件系统都必须遵循的“公约”。如果某个文件系统有一些非常独特的、无法用标准VFS模型(inode, dentry, file, superblock)描述的特性,那么想把这些特性完全暴露给上层可能会非常困难。
- 性能开销:函数指针的间接调用会带来微小的性能开销,但这在宏观的文件系统操作中完全可以忽略不计。
- 复杂性:VFS层的抽象和多层回调使得跟踪一个文件系统操作的完整代码路径变得相对复杂。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?
它是Linux内核中实现文件系统的唯一且标准的解决方案。任何需要在Linux下被挂载和使用的文件系统,都必须与fs/super.c
提供的框架进行交互。
- 挂载根文件系统:内核启动的最后阶段,必须挂载一个根文件系统,这个过程就由
super.c
的框架管理。 - 所有
mount
命令:用户执行mount /dev/sda1 /mnt
,mount -t nfs server:/share /mnt
,mount -t proc none /proc
等所有命令,其内核侧的实现都深度依赖fs/super.c
。 - 自动挂载:U盘、移动硬盘等设备的自动挂载,也是通过用户空间的守护进程(如udisksd)调用
mount
系统调用来完成的。 - 容器和命名空间:创建新的挂载命名空间(mount namespace)时,内核需要复制和管理挂载点信息,这些信息都与
super_block
对象关联。
是否有不推荐使用该技术的场景?为什么?
这个问题的提法不太适用,因为它是一个必须使用的基础框架。更准确的说法是,什么场景不应该被实现为一个文件系统。
- 简单的键值对数据暴露:如果只是想向用户空间暴露一些简单的配置项或状态信息,实现一个完整的文件系统过于复杂。
sysfs
(用于设备属性)、procfs
(用于进程信息)或debugfs
(用于调试)提供了更轻量级的接口。 - 设备驱动:一个硬件设备(如串口、声卡)应该被实现为字符设备或块设备驱动,通过
/dev
下的设备节点来访问,而不是一个文件系统。
对比分析
请将其 与 其他相似技术 进行详细对比。
这里最合适的对比不是与其他技术,而是**VFS通用层(以fs/super.c
为代表)与具体文件系统实现(以fs/ext4/super.c
为例)**之间的职责划分。
特性 | fs/super.c (VFS通用层) |
fs/ext4/super.c (具体文件系统实现) |
---|---|---|
职责/角色 | 框架提供者:定义“什么是文件系统实例”,并管理所有实例。 | 框架实现者:实现一个具体的、符合VFS规范的文件系统。 |
知识领域 | 通用性:不关心任何具体的磁盘布局。只知道super_block 中通用的字段(如设备号、标志位)。 |
特异性:精确地知道ext4文件系统在磁盘上的每一个字节的含义(魔数、块组描述符等)。 |
核心数据结构 | 定义 struct super_block 和 struct super_operations 。 |
填充 struct super_block ,并提供一个静态的struct super_operations 实例(ext4_sops )。 |
操作 | 提供sget() , deactivate_super() 等通用辅助函数,供具体文件系统调用。 |
实现ext4_mount() , ext4_put_super() , ext4_statfs() 等具体的回调函数。 |
交互方向 | 向下调用:通过s_op 函数指针调用具体文件系统的实现。 |
向上调用:调用sget() 等VFS辅助函数来与通用框架交互。 |
生命周期 | 在内核的整个运行期间都存在。 | 仅在ext4模块被加载,并且有ext4文件系统被挂载时,其代码和数据才处于活跃状态。 |
include/linux/fs.h
SB_FREEZE_LEVELS
1 | /* 'frozen'字段的可能状态 */ |
fs/super.c
deactivate_super 删除对超级块的活动引用
1 | /** |
super_wake 唤醒等待者
1 | /* 唤醒等待者 */ |
vfs_get_tree 获取可挂载的根目录
1 | /** |
alloc_super 创建新的超级块
1 | /** |
sget_fc 查找或创建文件系统的超级块(super_block)
1 | /** |
set_anon_super_fc 设置匿名超级块
1 | static DEFINE_IDA(unnamed_dev_ida); |
get_tree_nodev 获取无设备的超级块
1 | static int vfs_get_super(struct fs_context *fc, |
超级块销毁:通过 RCU 和工作队列进行多阶段延迟拆解
本代码片段定义了 super_block
结构体生命周期的最后阶段——当其引用计数降为零时的销毁过程。这是一个精心设计的、分为三个阶段的**延迟释放(deferred-free)**机制。它首先使用 RCU (Read-Copy-Update) 来确保所有“读者”(正在遍历超级块列表的任务)都已完成,然后再将最终的、可能导致睡眠的清理工作推迟到一个工作队列中执行。这种设计的核心目标是在一个高度并发的内核中,安全、无竞争地销毁一个全局性的核心数据结构。
实现原理分析
super_block
的销毁是一个典型的展示内核如何处理复杂对象生命周期管理的例子。
第一阶段: 引用计数与逻辑移除 (
__put_super
)- 入口: 当内核某处代码(如
iterate_supers
)释放对super_block
的引用时,__put_super
被调用。调用者必须持有全局的sb_lock
。 - 核心逻辑:
if (!--s->s_count)
。函数原子地递减引用计数。只有当计数器恰好变为零时,销毁流程才会启动。 - 逻辑移除: 一旦确定这是最后一个引用,
list_del_init(&s->s_list)
会立即将该super_block
从全局的super_blocks
链表中移除。由于sb_lock
仍被持有,这个操作是安全的。从这一刻起,任何新的iterate_supers
调用都不会再看到这个垂死的super_block
。 - 延迟物理移除:
super_block
结构体本身的内存没有被释放。取而代之的是call_rcu(&s->rcu, destroy_super_rcu)
。 - 为什么用 RCU?:
iterate_supers
等函数采用了一种“解锁-回调”模式,它们在释放sb_lock
的情况下持有super_block
的指针。如果在__put_super
中立即kfree(s)
,那么那些已经“看到”s
但暂时释放了sb_lock
的任务,在重新获取锁后就会访问到一块已经被释放的内存(use-after-free),导致内核崩溃。call_rcu
保证了destroy_super_rcu
这个回调函数只有在一个RCU宽限期(grace period)之后才会被调用,届时可以确保所有在list_del_init
之前就已经在访问该super_block
的“读者”都已经完成了它们的临界区。
- 入口: 当内核某处代码(如
第二阶段: RCU 回调与上下文切换 (
destroy_super_rcu
)- 执行上下文: 这个函数在 RCU 的软中断(softirq)上下文中执行。这个上下文非常受限,绝对不能睡眠。
- 核心逻辑: 它并不执行真正的清理工作。相反,它初始化一个嵌入在
super_block
结构体中的work_struct
,然后通过schedule_work
将destroy_super_work
函数提交到内核的共享工作队列中。 - 为什么用工作队列?: 最终的清理函数
destroy_super_work
需要调用kfree()
等函数,而kfree()
在某些情况下(如内存紧张时)是可以睡眠的。为了避免在禁止睡眠的 RCU 回调中调用可能睡眠的函数,这里再次进行了一次“延迟”,将工作交给了可以安全睡眠的内核工作线程上下文。
第三阶段: 最终清理与内存释放 (
destroy_super_work
)- 执行上下文: 这个函数在内核工作线程(kworker)的进程上下文中执行,可以安全地睡眠。
- 核心逻辑: 到了这一步,可以保证没有任何代码再持有指向这个
super_block
的有效指针了。函数按部就班地调用各个子系统的清理函数,释放所有与该super_block
关联的资源:fsnotify_sb_free
: 清理文件系统通知。security_sb_free
: 清理LSM(如SELinux)相关的安全上下文。put_user_ns
: 释放用户命名空间的引用。percpu_free_rwsem
: 释放用于写者锁的per-CPU读写信号量。kfree(s)
: 最后一步,将super_block
结构体本身占用的内存返还给slab分配器。
代码分析
1 | // destroy_super_work: 最终的清理工作函数,在工作队列中执行。 |
超级块迭代器:遍历所有挂载文件系统的通用机制
本代码片段定义了Linux VFS层中一个基础的、通用的工具函数 iterate_supers
。其核心功能是提供一个安全、健壮的方式来遍历内核中所有当前已挂载的文件系统(由 struct super_block
表示),并对每一个文件系统执行一个由调用者提供的回调函数。这是许多全局性文件系统操作(如 sync
、umount -a
、以及我们之前分析的 drop_caches
)的基石。
实现原理分析
iterate_supers
的强大之处在于其核心实现 __iterate_supers
,它采用了一种非常复杂和健壮的**“解锁-回调-再锁定”(unlock-call-relock)**的并发策略,以实现高性能和安全性。
全局列表与锁: 内核维护一个全局的双向链表
super_blocks
,其中包含了所有活动的super_block
实例。这个列表由一个全局自旋锁sb_lock
保护。迭代器标志 (
super_iter_flags_t
):__iterate_supers
的行为可以通过标志进行定制:SUPER_ITER_REVERSE
: 允许反向遍历super_blocks
链表。SUPER_ITER_UNLOCKED
: 一个重要标志,它告诉迭代器,调用者(即回调函数f
)将自己负责处理单个super_block
的锁定。SUPER_ITER_EXCL
: 当迭代器负责锁定时,此标志要求获取排他性写锁,而不是默认的共享读锁。
核心遍历逻辑 (
__iterate_supers
):- 获取全局锁: 函数开始时,首先获取全局的
sb_lock
,以安全地开始遍历super_blocks
链表。 - 循环与引用计数: 在
for
循环中,它从链表中获取一个sb
。- 它会跳过正在被销毁的文件系统(
SB_DYING
)。 - 关键步骤 1:
sb->s_count++
。它原子地增加super_block
的引用计数。 - 关键步骤 2:
spin_unlock(&sb_lock)
。它立即释放了全局的sb_lock
。
- 它会跳过正在被销毁的文件系统(
- “解锁-回调”模式: 这两个步骤是该模式的核心。为什么这么做?
- 回调函数
f
(例如drop_pagecache_sb
)可能会执行非常耗时的操作,甚至可能睡眠(cond_resched
)。长时间持有全局自旋锁会严重损害系统性能,并可能导致死锁。 - 通过在调用
f
之前释放sb_lock
,避免了这个问题。而之前增加的引用计数(s_count++
)则像一个“护身符”,保证了即使在sb_lock
被释放的窗口期,其他CPU上的任务也无法卸载和销毁这个sb
,因为它的引用计数不为零。
- 回调函数
- 回调执行: 在全局锁被释放后,它安全地调用回调函数
f
。默认情况下,它还会为调用者锁定单个的super_block
本身(super_lock(sb, excl)
),除非指定了SUPER_ITER_UNLOCKED
。 - 重新获取锁并清理: 回调返回后,函数重新获取全局的
sb_lock
以安全地移动到链表中的下一个元素。同时,它使用__put_super(p)
来释放上一次循环中持有的super_block
的引用计数。
- 获取全局锁: 函数开始时,首先获取全局的
公共API (
iterate_supers
): 这是一个简化的包装函数,它使用默认标志(0)调用__iterate_supers
,提供了最常用、最安全的遍历方式(正向遍历,迭代器负责锁定)。
代码分析
1 | enum super_iter_flags_t { |
超级块锁:同步对文件系统生命周期的访问
本代码片段定义了 super_lock
函数,它是内核中用于安全访问一个 super_block
对象的核心同步原语。其主要功能并不仅仅是获取一个锁,而是实现了一个复杂的、两阶段的同步过程:首先,它会等待一个super_block
进入一个明确的、可用的状态(SB_BORN
)或者已死的状态(SB_DYING
);然后,如果super_block
是可用的,它才会去获取该super_block
内部的 s_umount
读写信号量。这个机制是确保所有对文件系统元数据(如inode
列表)的操作都能在一个稳定、已完全初始化的super_block
上进行的关键。
实现原理分析
super_lock
的实现逻辑非常精妙,它处理了super_block
生命周期中非常微妙的竞态条件。
底层锁 (
__super_lock
,super_unlock
):super_block
结构体内部包含一个名为s_umount
的读写信号量(rw_semaphore
)。这个信号量是保护super_block
免受并发修改(尤其是在卸载umount
过程中)的主要工具。__super_lock
和super_unlock
是简单的内联函数,它们根据excl
参数(表示是否需要排他性访问)来决定是调用down_read
/up_read
(获取共享读锁)还是down_write
/up_write
(获取排他写锁)。
核心同步逻辑 (
super_lock
):- 前提:
super_lock
的调用者必须已经通过sb->s_count++
获得了对sb
的一个临时引用。这保证了即使在等待期间,sb
对象本身也不会被释放。 - 第一阶段: 等待状态明确 (
wait_var_event
):super_block
在其生命周期中会经历几个状态,通过s_flags
中的标志位表示。SB_BORN
标志在文件系统成功挂载并完全初始化后设置。SB_DYING
标志在文件系统开始卸载时设置。wait_var_event(&sb->s_flags, ...)
是一个关键的等待原语。它会使当前任务睡眠,直到sb->s_flags
的值发生变化,并且super_flags(sb, SB_BORN | SB_DYING)
这个条件表达式为真。- 这解决了在
super_block
刚被分配、但尚未完全初始化(SB_BORN
还未设置)时,其他任务尝试访问它的竞态问题。
- 第二阶段: 获取
s_umount
锁:- 当
wait_var_event
返回后,可以确定sb
要么是“活的”,要么是“死的”。 if (super_flags(sb, SB_DYING)) return false;
: 首先检查是否是“死的”。如果是,就没必要再去获取一个垂死对象的锁了,直接返回失败。__super_lock(sb, excl);
: 如果是“活的”,现在才去获取s_umount
信号量。
- 当
- 双重检查 (Double-Check):
if (sb->s_flags & SB_DYING) { ... }
: 在成功获取s_umount
锁之后,必须再次检查SB_DYING
标志。- 为什么? 因为在
wait_var_event
返回和__super_lock
成功获取锁之间的这个微小时间窗口内,另一个CPU上的任务可能已经开始卸载这个文件系统并设置了SB_DYING
标志。这个双重检查确保了我们不会在一个刚刚开始死亡的文件系统上继续进行操作。如果检查到SB_DYING
,函数会立即释放刚刚获取的锁,并返回失败。
- 前提:
内存屏障 (
super_flags
):smp_load_acquire
是一个带有“acquire”语义的内存读取操作。它创建了一个内存屏障,确保在读取s_flags
之后的所有代码(包括对super_block
其他成员的访问),在CPU层面不会被乱序执行到读取s_flags
之前。这保证了当我们看到SB_BORN
标志时,我们也一定能看到由文件系统挂载过程完成的所有初始化写入。
代码分析
1 | // __super_lock: 获取s_umount读写信号量的底层实现。 |