[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_blockstruct 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为例):

  1. 注册:在内核启动或模块加载时,每个文件系统(如ext4)都会调用register_filesystem()函数,将自身的信息(名字、mount回调函数等)注册到一个全局的链表中。
  2. 挂载请求:当用户执行mount系统调用时,VFS层的代码会根据用户指定的类型(如”ext4”)在全局链表中找到对应的文件系统。
  3. 调用特定文件系统的mount:VFS会调用该文件系统注册的.mount回调函数(例如ext4_mount())。
  4. 读取物理超级块ext4_mount()函数会执行ext4特有的操作,即从块设备上读取ext4格式的超级块,并校验其有效性。
  5. 分配和填充VFS超级块ext4_mount()会调用fs/super.c中提供的辅助函数(如sget())来分配一个VFS层通用的struct super_block对象。然后,它会用从磁盘读取到的信息(如块大小、inode总数等)以及ext4自身的操作函数表(ext4_sops)来填充这个super_block对象。
  6. 建立连接ext4_sops被赋值给super_block->s_op。这个s_op是一个struct super_operations类型的指针,它包含了statfs, sync_fs等一系列函数指针。
  7. 分派:挂载完成后,当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 /mntmount -t nfs server:/share /mntmount -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_blockstruct 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
2
3
4
5
6
7
8
9
10
/* 'frozen'字段的可能状态 */
enum {
SB_UNFROZEN = 0, /* 文件系统未冻结 */
SB_FREEZE_WRITE = 1, /* 写操作、目录操作、ioctl已冻结 */
SB_FREEZE_PAGEFAULT = 2, /* 页面错误也已停止 */
SB_FREEZE_FS = 3, /* 内部文件系统使用(例如在需要时停止内部线程) */
SB_FREEZE_COMPLETE = 4, /* ->freeze_fs成功完成 */
};

#define SB_FREEZE_LEVELS (SB_FREEZE_COMPLETE - 1)

fs/super.c

deactivate_super 删除对超级块的活动引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* deactivate_super - 删除对超级块的活动引用
* @s: 要停用的超级块
*
* deactivate_locked_super() 的变体,除了超级块未被调用者锁定。
* 如果我们将要删除最后一个活动引用,则会在此之前获取锁。
*/
void deactivate_super(struct super_block *s)
{
if (!atomic_add_unless(&s->s_active, -1, 1)) {
__super_lock_excl(s);
deactivate_locked_super(s);
}
}

EXPORT_SYMBOL(deactivate_super);

super_wake 唤醒等待者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 唤醒等待者 */
#define SUPER_WAKE_FLAGS (SB_BORN | SB_DYING | SB_DEAD)
static void super_wake(struct super_block *sb, unsigned int flag)
{
WARN_ON_ONCE((flag & ~SUPER_WAKE_FLAGS));
WARN_ON_ONCE(hweight32(flag & SUPER_WAKE_FLAGS) > 1);

/*
* 与 super_lock() 中的 smp_load_acquire() 配对,
* 确保超级块中的所有初始化操作对看到 SB_BORN 的用户可见。
*/
smp_store_release(&sb->s_flags, sb->s_flags | flag);
/*
* 与 prepare_to_wait_event() 中的屏障配对,
* 确保 ___wait_var_event() 要么看到 SB_BORN 设置,
* 要么 wake_up_var() 中的 waitqueue_active() 检查看到等待者。
*/
smp_mb();
wake_up_var(&sb->s_flags);
}

vfs_get_tree 获取可挂载的根目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/**
* vfs_get_tree - 获取可挂载的根目录
* @fc: 超级块配置上下文。
*
* 文件系统被调用以获取或创建一个超级块,该超级块随后可用于挂载。
* 文件系统将用于挂载的根目录指针放置在 @fc->root 中。
*/
int vfs_get_tree(struct fs_context *fc)
{
struct super_block *sb;
int error;

if (fc->root)
return -EBUSY;

/* 在 fc->root 中获取可挂载的根目录,同时保留对根目录和超级块的引用。 */
error = fc->ops->get_tree(fc);
if (error < 0)
return error;

if (!fc->root) {
pr_err("Filesystem %s get_tree() didn't set fc->root, returned %i\n",
fc->fs_type->name, error);
/* We don't know what the locking state of the superblock is -
* if there is a superblock.
*/
BUG();
}

sb = fc->root->d_sb;
WARN_ON(!sb->s_bdi);

/*
* super_wake() 包含一个内存屏障,它也负责
* super_cache_count() 的排序。我们将其放在设置
* SB_BORN 之前,因为这两个函数之间的数据依赖关系是
* 我们刚刚设置的超级块结构内容,而不是
* SB_BORN 标志。
*/
super_wake(sb, SB_BORN);

/* return 0; */
error = security_sb_set_mnt_opts(sb, fc->security, 0, NULL);
if (unlikely(error)) {
fc_drop_locked(fc);
return error;
}

/*
* 文件系统不应将 s_maxbytes 设置为大于 MAX_LFS_FILESIZE 的值,
* 但 s_maxbytes 在多个版本中是一个无符号长长整型。发出此警告
* 一段时间,以尝试捕获违反此规则的文件系统。
*/
WARN((sb->s_maxbytes < 0), "%s set sb->s_maxbytes to "
"negative value (%lld)\n", fc->fs_type->name, sb->s_maxbytes);

return 0;
}
EXPORT_SYMBOL(vfs_get_tree);

alloc_super 创建新的超级块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/**
* alloc_super - 创建新的超级块
* @type: 超级块所属的文件系统类型
* @flags: 挂载标志
* @user_ns: 超级块的用户命名空间
*
* 分配并初始化一个新的 &struct super_block。alloc_super() 返回一个指向新超级块的指针,
* 如果分配失败,则返回 %NULL。
*/
static struct super_block *alloc_super(struct file_system_type *type, int flags,
struct user_namespace *user_ns)
{
struct super_block *s = kzalloc(sizeof(struct super_block), GFP_KERNEL);
static const struct super_operations default_op;
int i;

if (!s)
return NULL;

INIT_LIST_HEAD(&s->s_mounts);
s->s_user_ns = get_user_ns(user_ns);
init_rwsem(&s->s_umount);
lockdep_set_class(&s->s_umount, &type->s_umount_key);
/*
* sget() 可以具有 s_umount 的递归调用。
*
* 当它无法找到合适的 sb 时,它会分配一个新的
* sb(这个),然后再次尝试找到一个合适的旧的 sb。
*
* 如果成功,它将获取旧 sb 的 s_umount 锁。
* 由于这些显然是不同的锁,并且这个对象尚未暴露,
* 因此不存在死锁的风险。
*
* 通过将此锁放入不同的子类来进行注释。
*/
down_write_nested(&s->s_umount, SINGLE_DEPTH_NESTING);
/* return 0; */
if (security_sb_alloc(s))
goto fail;

for (i = 0; i < SB_FREEZE_LEVELS; i++) {
if (__percpu_init_rwsem(&s->s_writers.rw_sem[i],
sb_writers_name[i],
&type->s_writers_key[i]))
goto fail;
}
s->s_bdi = &noop_backing_dev_info;
s->s_flags = flags;
if (s->s_user_ns != &init_user_ns)
/* 设置 SB_I_NODEV 标志,表示禁止设备文件 */
s->s_iflags |= SB_I_NODEV;
INIT_HLIST_NODE(&s->s_instances);
INIT_HLIST_BL_HEAD(&s->s_roots);
mutex_init(&s->s_sync_lock);
INIT_LIST_HEAD(&s->s_inodes);
spin_lock_init(&s->s_inode_list_lock);
INIT_LIST_HEAD(&s->s_inodes_wb);
spin_lock_init(&s->s_inode_wblist_lock);

s->s_count = 1;
atomic_set(&s->s_active, 1);
mutex_init(&s->s_vfs_rename_mutex);
lockdep_set_class(&s->s_vfs_rename_mutex, &type->s_vfs_rename_key);
init_rwsem(&s->s_dquot.dqio_sem);
s->s_maxbytes = MAX_NON_LFS;
s->s_op = &default_op;
s->s_time_gran = 1000000000;
s->s_time_min = TIME64_MIN;
s->s_time_max = TIME64_MAX;
/* 分配并初始化超级块的内存管理器(shrinker),用于管理缓存对象 */
s->s_shrink = shrinker_alloc(SHRINKER_NUMA_AWARE | SHRINKER_MEMCG_AWARE,
"sb-%s", type->name);
if (!s->s_shrink)
goto fail;

s->s_shrink->scan_objects = super_cache_scan;
s->s_shrink->count_objects = super_cache_count;
s->s_shrink->batch = 1024;
s->s_shrink->private_data = s;
/* 超级块的 LRU 列表(s_dentry_lru 和 s_inode_lru),用于管理缓存的 dentry 和 inode */
if (list_lru_init_memcg(&s->s_dentry_lru, s->s_shrink))
goto fail;
if (list_lru_init_memcg(&s->s_inode_lru, s->s_shrink))
goto fail;
return s;

fail:
destroy_unused_super(s);
return NULL;
}

sget_fc 查找或创建文件系统的超级块(super_block)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
/**
* sget_fc - 查找或创建超级块
* @fc: 文件系统上下文。
* @test: 比较回调
* @set: 设置回调
*
* 创建一个新的超级块或查找现有的超级块。
*
* @test 回调用于查找匹配的现有超级块。
* 是否考虑 @fc 中请求的参数取决于所使用的 @test 回调。
* 它们甚至可能被完全忽略。
*
* 如果匹配到现有的超级块,将返回该超级块,除非:
*
* (1) 文件系统上下文 @fc 的命名空间与现有超级块的命名空间不同
*
* (2) 文件系统上下文 @fc 请求不允许重用现有的超级块
*
* 在这两种情况下,将返回 EBUSY。
*
* 如果没有匹配项,将分配一个新的超级块并执行基本初始化
* (将设置 s_type、s_fs_info 和 s_id,并调用 @set 回调),超级块将被...
* 已发布,并将以部分构造状态返回
* SB_BORN 和 SB_ACTIVE 尚未设置。
*
* 返回值:成功时,返回现有或新创建的超级块。
* 失败时,返回错误指针。
*/
struct super_block *sget_fc(struct fs_context *fc,
int (*test)(struct super_block *, struct fs_context *),
int (*set)(struct super_block *, struct fs_context *))
{
struct super_block *s = NULL;
struct super_block *old;
struct user_namespace *user_ns = fc->global ? &init_user_ns : fc->user_ns;
int err;

/*
* 当未设置 FS_USERNS_MOUNT 时,绝不允许 s_user_ns != &init_user_ns,
* 因为文件系统可能无法处理这种情况。
* 当从 init_user_ns 调用 fsconfig() 且 fs_fd 在另一个用户命名空间中打开时,
* 可能会发生这种情况。
*/
/* 如果文件系统不支持用户命名空间挂载(FS_USERNS_MOUNT),但尝试从非初始命名空间挂载,则返回权限错误(-EPERM) */
if (user_ns != &init_user_ns && !(fc->fs_type->fs_flags & FS_USERNS_MOUNT)) {
errorfc(fc, "VFS: Mounting from non-initial user namespace is not allowed");
return ERR_PTR(-EPERM);
}

retry:
spin_lock(&sb_lock);
if (test) {
/* 遍历文件系统类型的超级块列表(fs_supers),查找与当前文件系统上下文匹配的超级块 */
hlist_for_each_entry(old, &fc->fs_type->fs_supers, s_instances) {
if (test(old, fc))
goto share_extant_sb;
}
}
if (!s) {
spin_unlock(&sb_lock);
s = alloc_super(fc->fs_type, fc->sb_flags, user_ns);
if (!s)
return ERR_PTR(-ENOMEM);
goto retry;
}

s->s_fs_info = fc->s_fs_info;
/* 初始化超级块 */
err = set(s, fc);
if (err) {
s->s_fs_info = NULL;
spin_unlock(&sb_lock);
destroy_unused_super(s);
return ERR_PTR(err);
}
fc->s_fs_info = NULL;
s->s_type = fc->fs_type;
s->s_iflags |= fc->s_iflags;
strscpy(s->s_id, s->s_type->name, sizeof(s->s_id));
/*
* 使超级块在 @super_blocks 和 @fs_supers 上可见。
* 它处于初始状态,用户应等待设置 SB_BORN 或 SB_DYING。
*/
list_add_tail(&s->s_list, &super_blocks);
hlist_add_head(&s->s_instances, &s->s_type->fs_supers);
spin_unlock(&sb_lock);
get_filesystem(s->s_type);
shrinker_register(s->s_shrink);
return s;
/* 共享现有超级块 */
share_extant_sb:
if (user_ns != old->s_user_ns || fc->exclusive) {
spin_unlock(&sb_lock);
destroy_unused_super(s);
if (fc->exclusive)
warnfc(fc, "reusing existing filesystem not allowed");
else
warnfc(fc, "reusing existing filesystem in another namespace not allowed");
return ERR_PTR(-EBUSY);
}
if (!grab_super(old))
goto retry;
destroy_unused_super(s);
return old;
}
EXPORT_SYMBOL(sget_fc);

set_anon_super_fc 设置匿名超级块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
static DEFINE_IDA(unnamed_dev_ida);
/**
* get_anon_bdev - 为没有块设备的文件系统分配一个块设备。
* @p: 指向 dev_t 的指针。
*
* 不使用真实块设备的文件系统可以调用此函数来分配一个虚拟块设备。
*
* 上下文:任何上下文。通常在持有 sb_lock 时调用。
* 返回值:成功时返回 0;如果没有剩余的匿名块设备,则返回 -EMFILE;
* 如果内存分配失败,则返回 -ENOMEM。
*/
int get_anon_bdev(dev_t *p)
{
int dev;

/*
* 许多用户空间工具认为 FSID 为 0 是无效的。
* get_anon_bdev 始终至少返回 1。
*/
dev = ida_alloc_range(&unnamed_dev_ida, 1, (1 << MINORBITS) - 1,
GFP_ATOMIC);
if (dev == -ENOSPC)
dev = -EMFILE;
if (dev < 0)
return dev;

*p = MKDEV(0, dev);
return 0;
}
EXPORT_SYMBOL(get_anon_bdev);

int set_anon_super(struct super_block *s, void *data)
{
return get_anon_bdev(&s->s_dev);
}
EXPORT_SYMBOL(set_anon_super);

int set_anon_super_fc(struct super_block *sb, struct fs_context *fc)
{
return set_anon_super(sb, NULL);
}
EXPORT_SYMBOL(set_anon_super_fc);

get_tree_nodev 获取无设备的超级块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static int vfs_get_super(struct fs_context *fc,
int (*test)(struct super_block *, struct fs_context *),
int (*fill_super)(struct super_block *sb,
struct fs_context *fc))
{
struct super_block *sb;
int err;
/* 尝试获取超级块 */
sb = sget_fc(fc, test, set_anon_super_fc);
if (IS_ERR(sb))
return PTR_ERR(sb);
/* 。如果超级块不存在,则创建一个新的超级块 */
if (!sb->s_root) {
err = fill_super(sb, fc);
if (err)
goto error;
/* 超级块标记为活动状态(SB_ACTIVE) */
sb->s_flags |= SB_ACTIVE;
}
/* 增加根目录的引用计数,确保其不会被释放 */
fc->root = dget(sb->s_root);
return 0;

error:
deactivate_locked_super(sb);
return err;
}

int get_tree_nodev(struct fs_context *fc,
int (*fill_super)(struct super_block *sb,
struct fs_context *fc))
{
return vfs_get_super(fc, NULL, fill_super);
}

超级块销毁:通过 RCU 和工作队列进行多阶段延迟拆解

本代码片段定义了 super_block 结构体生命周期的最后阶段——当其引用计数降为零时的销毁过程。这是一个精心设计的、分为三个阶段的**延迟释放(deferred-free)**机制。它首先使用 RCU (Read-Copy-Update) 来确保所有“读者”(正在遍历超级块列表的任务)都已完成,然后再将最终的、可能导致睡眠的清理工作推迟到一个工作队列中执行。这种设计的核心目标是在一个高度并发的内核中,安全、无竞争地销毁一个全局性的核心数据结构。

实现原理分析

super_block 的销毁是一个典型的展示内核如何处理复杂对象生命周期管理的例子。

  1. 第一阶段: 引用计数与逻辑移除 (__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 的“读者”都已经完成了它们的临界区。
  2. 第二阶段: RCU 回调与上下文切换 (destroy_super_rcu)

    • 执行上下文: 这个函数在 RCU 的软中断(softirq)上下文中执行。这个上下文非常受限,绝对不能睡眠
    • 核心逻辑: 它并不执行真正的清理工作。相反,它初始化一个嵌入在 super_block 结构体中的 work_struct,然后通过 schedule_workdestroy_super_work 函数提交到内核的共享工作队列中。
    • 为什么用工作队列?: 最终的清理函数 destroy_super_work 需要调用 kfree() 等函数,而 kfree() 在某些情况下(如内存紧张时)是可以睡眠的。为了避免在禁止睡眠的 RCU 回调中调用可能睡眠的函数,这里再次进行了一次“延迟”,将工作交给了可以安全睡眠的内核工作线程上下文。
  3. 第三阶段: 最终清理与内存释放 (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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// destroy_super_work: 最终的清理工作函数,在工作队列中执行。
static void destroy_super_work(struct work_struct *work)
{
// 从work_struct中反向计算出super_block的地址。
struct super_block *s = container_of(work, struct super_block,
destroy_work);
// 依次调用各个子系统的清理函数。
fsnotify_sb_free(s);
security_sb_free(s);
put_user_ns(s->s_user_ns);
kfree(s->s_subtype);
// 释放所有写者锁相关的per-cpu读写信号量。
for (int i = 0; i < SB_FREEZE_LEVELS; i++)
percpu_free_rwsem(&s->s_writers.rw_sem[i]);
// 最终,释放super_block结构体本身占用的内存。
kfree(s);
}

// destroy_super_rcu: RCU回调函数,在一个RCU宽限期后被调用。
static void destroy_super_rcu(struct rcu_head *head)
{
struct super_block *s = container_of(head, struct super_block, rcu);
// 初始化work item,并将其提交到工作队列。
INIT_WORK(&s->destroy_work, destroy_super_work);
schedule_work(&s->destroy_work);
}

// __put_super: 递减super_block的引用计数。调用者必须持有sb_lock。
static void __put_super(struct super_block *s)
{
// 原子地递减引用计数,并检查是否变为0。
if (!--s->s_count) {
// 如果是最后一个引用,则从全局链表中移除。
list_del_init(&s->s_list);
// 一些健壮性检查,确保此时状态是干净的。
WARN_ON(s->s_dentry_lru.node);
WARN_ON(s->s_inode_lru.node);
WARN_ON(!list_empty(&s->s_mounts));
// 使用call_rcu来延迟调用destroy_super_rcu,以保护并发的读者。
call_rcu(&s->rcu, destroy_super_rcu);
}
}

超级块迭代器:遍历所有挂载文件系统的通用机制

本代码片段定义了Linux VFS层中一个基础的、通用的工具函数 iterate_supers。其核心功能是提供一个安全、健壮的方式来遍历内核中所有当前已挂载的文件系统(由 struct super_block 表示),并对每一个文件系统执行一个由调用者提供的回调函数。这是许多全局性文件系统操作(如 syncumount -a、以及我们之前分析的 drop_caches)的基石。

实现原理分析

iterate_supers 的强大之处在于其核心实现 __iterate_supers,它采用了一种非常复杂和健壮的**“解锁-回调-再锁定”(unlock-call-relock)**的并发策略,以实现高性能和安全性。

  1. 全局列表与锁: 内核维护一个全局的双向链表 super_blocks,其中包含了所有活动的 super_block 实例。这个列表由一个全局自旋锁 sb_lock 保护。

  2. 迭代器标志 (super_iter_flags_t): __iterate_supers 的行为可以通过标志进行定制:

    • SUPER_ITER_REVERSE: 允许反向遍历 super_blocks 链表。
    • SUPER_ITER_UNLOCKED: 一个重要标志,它告诉迭代器,调用者(即回调函数 f)将自己负责处理单个super_block的锁定。
    • SUPER_ITER_EXCL: 当迭代器负责锁定时,此标志要求获取排他性写锁,而不是默认的共享读锁。
  3. 核心遍历逻辑 (__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 的引用计数。
  4. 公共API (iterate_supers): 这是一个简化的包装函数,它使用默认标志(0)调用 __iterate_supers,提供了最常用、最安全的遍历方式(正向遍历,迭代器负责锁定)。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
enum super_iter_flags_t {
SUPER_ITER_EXCL = (1U << 0),
SUPER_ITER_UNLOCKED = (1U << 1),
SUPER_ITER_REVERSE = (1U << 2),
};

static inline struct super_block *first_super(enum super_iter_flags_t flags)
{
if (flags & SUPER_ITER_REVERSE)
return list_last_entry(&super_blocks, struct super_block, s_list);
return list_first_entry(&super_blocks, struct super_block, s_list);
}

static inline struct super_block *next_super(struct super_block *sb,
enum super_iter_flags_t flags)
{
if (flags & SUPER_ITER_REVERSE)
return list_prev_entry(sb, s_list);
return list_next_entry(sb, s_list);
}

// __iterate_supers: 遍历所有超级块的核心实现。
// @f: 要对每个super_block执行的回调函数。
// @arg: 传递给回调函数的参数。
// @flags: 控制遍历行为的标志。
static void __iterate_supers(void (*f)(struct super_block *, void *), void *arg,
enum super_iter_flags_t flags)
{
struct super_block *sb, *p = NULL;
bool excl = flags & SUPER_ITER_EXCL;

// 获取保护全局super_blocks链表的锁。
guard(spinlock)(&sb_lock);

// 遍历super_blocks链表。
for (sb = first_super(flags);
!list_entry_is_head(sb, &super_blocks, s_list);
sb = next_super(sb, flags)) {
// 跳过正在被销毁的文件系统。
if (super_flags(sb, SB_DYING))
continue;
// 关键点1: 增加引用计数,以保护sb在全局锁释放后不被销毁。
sb->s_count++;
// 关键点2: 释放全局锁,以避免长时间持有它。
spin_unlock(&sb_lock);

// 根据标志决定如何调用回调函数。
if (flags & SUPER_ITER_UNLOCKED) {
f(sb, arg); // 调用者负责锁定。
} else if (super_lock(sb, excl)) {
f(sb, arg); // 迭代器负责锁定单个sb。
super_unlock(sb, excl);
}

// 关键点3: 重新获取全局锁,以安全地移动到下一个元素。
spin_lock(&sb_lock);
// 释放上一个循环中持有的super_block的引用。
if (p)
__put_super(p);
p = sb; // 保存当前sb,以便在下一次循环中释放。
}
// 释放最后一个持有的super_block的引用。
if (p)
__put_super(p);
}

// iterate_supers: 公共API,使用默认标志调用核心实现。
void iterate_supers(void (*f)(struct super_block *, void *), void *arg)
{
__iterate_supers(f, arg, 0);
}

超级块锁:同步对文件系统生命周期的访问

本代码片段定义了 super_lock 函数,它是内核中用于安全访问一个 super_block 对象的核心同步原语。其主要功能并不仅仅是获取一个锁,而是实现了一个复杂的、两阶段的同步过程:首先,它会等待一个super_block进入一个明确的、可用的状态(SB_BORN)或者已死的状态(SB_DYING);然后,如果super_block是可用的,它才会去获取该super_block内部的 s_umount 读写信号量。这个机制是确保所有对文件系统元数据(如inode列表)的操作都能在一个稳定、已完全初始化的super_block上进行的关键。

实现原理分析

super_lock 的实现逻辑非常精妙,它处理了super_block生命周期中非常微妙的竞态条件。

  1. 底层锁 (__super_lock, super_unlock):

    • super_block 结构体内部包含一个名为 s_umount 的读写信号量(rw_semaphore)。这个信号量是保护super_block免受并发修改(尤其是在卸载umount过程中)的主要工具。
    • __super_locksuper_unlock 是简单的内联函数,它们根据 excl 参数(表示是否需要排他性访问)来决定是调用 down_read/up_read(获取共享读锁)还是 down_write/up_write(获取排他写锁)。
  2. 核心同步逻辑 (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,函数会立即释放刚刚获取的锁,并返回失败。
  3. 内存屏障 (super_flags):

    • smp_load_acquire 是一个带有“acquire”语义的内存读取操作。它创建了一个内存屏障,确保在读取s_flags之后的所有代码(包括对super_block其他成员的访问),在CPU层面不会被乱序执行到读取s_flags之前。这保证了当我们看到SB_BORN标志时,我们也一定能看到由文件系统挂载过程完成的所有初始化写入。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// __super_lock: 获取s_umount读写信号量的底层实现。
static inline void __super_lock(struct super_block *sb, bool excl)
{
if (excl)
down_write(&sb->s_umount); // 获取排他性写锁
else
down_read(&sb->s_umount); // 获取共享读锁
}

// super_unlock: 释放s_umount读写信号量的底层实现。
static inline void super_unlock(struct super_block *sb, bool excl)
{
if (excl)
up_write(&sb->s_umount);
else
up_read(&sb->s_umount);
}

// super_flags: 带内存屏障的标志位读取。
static bool super_flags(const struct super_block *sb, unsigned int flags)
{
// smp_load_acquire确保在读取s_flags之后的所有代码,
// 不会被重排到读取之前,保证了我们能看到所有相关的初始化。
return smp_load_acquire(&sb->s_flags) & flags;
}

/**
* super_lock - 等待超级块就绪并锁定它
* @sb: 要等待的超级块
* @excl: 是否需要排他性访问
* 返回值: 如果成功获取锁并且sb是BORN状态,返回true;如果sb是DYING状态,返回false。
*/
static __must_check bool super_lock(struct super_block *sb, bool excl)
{
lockdep_assert_not_held(&sb->s_umount);

// 阶段1: 等待sb的状态变为BORN(已出生)或DYING(将死)。
// 如果sb既不是BORN也不是DYING,当前任务会在此睡眠。
wait_var_event(&sb->s_flags, super_flags(sb, SB_BORN | SB_DYING));

// 检查等待结束后的状态。如果是DYING,则无需加锁,直接失败返回。
if (super_flags(sb, SB_DYING))
return false;

// 阶段2: 获取s_umount读写信号量。
__super_lock(sb, excl);

// 关键的双重检查:在获取锁之后,再次检查DYING标志。
// 这处理了在wait_var_event和__super_lock之间的竞态条件。
if (sb->s_flags & SB_DYING) {
super_unlock(sb, excl); // 释放刚刚获取的锁。
return false;
}

// 断言:此时sb必须是BORN状态,否则是逻辑错误。
WARN_ON_ONCE(!(sb->s_flags & SB_BORN));
return true;
}