[toc]

fs/fs_context.c 文件系统创建上下文(Filesystem Creation Context)

历史与背景

这项技术是为了解决什么特定问题而诞生的?

fs_context 框架的诞生是为了彻底改革和现代化Linux内核中挂载文件系统的方式,以解决传统mount(2)系统调用长期以来存在的诸多根本性问题:

  • 混乱的参数传递mount(2)系统调用使用一个名为data的参数(void *data),它实际上被用作一个非结构化的、以逗号分隔的字符串来传递文件系统特定的挂载选项(如"rw,noatime,jounal_checksum")。这种方式有几个巨大的缺点:
    • 解析复杂且容易出错:每个文件系统都必须编写自己的、脆弱的字符串解析器来提取选项。
    • 缺乏类型安全:所有选项都被当作字符串,无法以原生方式传递整数、布尔标志或二进制数据(如安全密钥)。
    • 难以验证:内核很难在文件系统解析之前对选项进行统一的验证。
  • API不灵活且难以扩展mount(2)的参数是固定的。当出现新的挂载需求或新型文件系统(特别是那些没有传统块设备源的,如网络文件系统或虚拟文件系统)时,很难在不滥用现有参数或不增加新系统调用的情况下优雅地支持它们。
  • “源”的概念过于狭隘mount(2)的设计思想中心是“将一个设备节点挂载到一个目录上”。这对于NFS、Ceph等网络文件系统或完全在内存中操作的虚拟文件系统来说,模型并不完全匹配。

fs_context被设计为一个全新的、基于状态机的API,它将挂载操作从一个单一、庞大的系统调用分解为一系列清晰、独立的配置步骤,从而解决了上述所有问题。

它的发展经历了哪些重要的里程碑或版本迭代?

  • 概念提出与开发:由内核资深开发者David Howells主导设计和开发,旨在提供一个更干净、更可扩展的挂载API。
  • 首次合入内核主线:围绕fs_context的一系列新系统调用(fsopen, fsconfig, fsmount, fspick等)在Linux内核5.1版本中被正式合入。
  • 逐步采纳与转换:在新API引入后,内核社区开始逐步将现有的文件系统迁移到使用fs_context框架。同时,所有新开发的文件系统都被强烈推荐使用这个新框架。
  • 用户空间工具支持:核心的系统工具包util-linux(提供了mount, umount等命令)也进行了更新,使其能够利用新的fs_context系列系统调用,从而将新框架的优势带给最终用户。

目前该技术的社区活跃度和主流应用情况如何?

fs_context现在是Linux内核中推荐的、标准的挂载文件系统的方式。虽然为了向后兼容,旧的mount(2)系统调用仍然被支持,但新的开发和功能都围绕fs_context进行。

  • 它被视为所有未来文件系统挂载和配置工作的基础
  • 许多主流的文件系统,如Ext4, Btrfs, XFS, NFS, Ceph等,已经支持或正在迁移到fs_context框架。
  • 它是解决复杂挂载场景(例如,需要传递Kerberos安全票据来挂载网络文件系统)的首选方案。

核心原理与设计

它的核心工作原理是什么?

fs_context的核心思想是将挂载过程从一个原子操作转变为一个状态化的、分步构建的过程。这个过程由一个名为fs_context的内核对象来驱动,并通过一系列新的系统调用进行交互。

典型的挂载流程如下:

  1. 创建上下文 (fsopen): 用户空间首先调用fsopen(),并指定所需的文件系统类型(如 “ext4”)。内核会为这个请求创建一个fs_context对象,并返回一个指向该对象的文件描述符。这个上下文可以被看作是一个“挂载请求的草稿”。
  2. 配置上下文 (fsconfig): 用户空间随后可以多次调用fsconfig()来配置这个上下文。fsconfig()键值对的形式接收参数,取代了旧的逗号分隔字符串。例如:
    • fsconfig(fd, FSCONFIG_SET_STRING, "source", "/dev/sda1", 0): 设置挂载源。
    • fsconfig(fd, FSCONFIG_SET_FLAG, "ro", NULL, 0): 设置只读标志。
    • fsconfig(fd, FSCONFIG_SET_BINARY, "key", key_blob, 0): 传递二进制数据。
      每次调用,文件系统都可以立即解析和验证传入的参数,如果参数无效则立即返回错误。
  3. 创建/重用超级块 (fsmount, fspick): 当所有配置完成后,用户空间调用一个最终动作:
    • fsmount(): 指示内核根据配置好的上下文创建一个新的文件系统实例(即一个新的超级块super_block),并返回一个可用于挂载的文件描述符。
    • fspick(): 指示内核查找一个已存在的、与当前上下文配置相匹配的文件系统实例,并返回其文件描述符。

这个过程将复杂的挂载操作分解为一系列简单、明确的步骤,使得整个流程更加清晰、健壮和可扩展。

它的主要优势体现在哪些方面?

  • 清晰和结构化的API:用类型化的键值对取代了混乱的选项字符串,提高了代码的可读性和安全性。
  • 极高的可扩展性:文件系统可以轻松地定义和添加新的配置选项,而无需修改任何系统调用的签名。
  • 及早的参数验证:每个选项在通过fsconfig()设置时就可以被验证,用户可以得到更及时、更精确的错误反馈。
  • 对无源文件系统的原生支持:该模型不强制要求一个块设备源,非常适合网络或虚拟文件系统。它们可以定义自己的配置键(如 “server_addr”)来接收必要的信息。
  • 支持复杂数据:可以原生传递二进制数据,解决了之前需要通过临时文件等变通方法来传递密钥等信息的难题。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 对简单场景的开销:对于一个非常简单的挂载操作,执行一系列系统调用(fsopen, fsconfig, fsmount, mount_setattr)比执行单个mount(2)系统调用在代码上更繁琐,并且有更多的上下文切换开销。然而,这种开销在宏观上通常可以忽略不计,并且换来了巨大的灵活性和健壮性。
  • 过渡期的复杂性:在过渡期间,内核和用户空间工具需要同时支持新旧两套API,这会增加一定的维护负担。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。

fs_context是所有新的文件系统实现和新的挂载相关功能的首选解决方案。

  • 例一:挂载需要复杂认证的网络文件系统
    当挂载一个需要Kerberos认证的NFS或Ceph文件系统时,认证票据是一个二进制数据块。使用fs_context,用户空间程序可以直接通过fsconfig(fd, FSCONFIG_SET_BINARY, ...)将这个票据传递到内核,而无需将其写入临时文件。
  • 例二:实现具有大量可调参数的文件系统
    像Btrfs这样的现代文件系统有大量的挂载选项来控制其各种子功能(如压缩、配额组、子卷等)。通过fs_context,每个功能都可以被实现为一个独立的配置键,使得用户空间的管理工具可以清晰、独立地配置它们。
  • 例三:容器化环境中的挂载
    在容器运行时中,需要以编程方式创建和配置各种挂载点。fs_context提供的分步、可验证的API非常适合这种需要精确控制和错误处理的自动化场景。

是否有不推荐使用该技术的场景?为什么?

基本上没有不推荐使用该技术的新开发场景。它的设计目标就是成为未来的标准。唯一会使用旧mount(2)接口的场景是:

  • 维护遗留应用程序:为了保持与旧的、未经修改的应用程序的二进制兼容性,内核必须继续支持mount(2)。但任何对这些程序的新功能添加或重构都应该考虑迁移到新的API。

对比分析

请将其 与 其他相似技术 进行详细对比。

fs_context最直接的对比对象就是它所要取代的传统mount(2)系统调用

特性 fs_context API (fsopen, fsconfig, …) 传统 mount(2) 系统调用
API风格 状态化、多步构建。通过一个上下文对象逐步配置。 原子化、单次调用。所有参数一次性传入。
参数传递 结构化的键值对,支持字符串、标志、二进制等多种类型。 非结构化。依赖一个扁平的标志位掩码和一个混乱的、文件系统特定的选项字符串
可扩展性 非常高。添加新选项只需文件系统实现新的键,无需改变API。 。选项字符串的复杂性不断增加,难以管理。添加非字符串类型的参数几乎不可能。
参数验证 及早、分步验证。每个fsconfig调用都可以进行验证。 延迟、整体验证。只有在mount调用时,文件系统才会一次性解析和验证整个选项字符串。
对无源FS的支持 原生设计。上下文的配置不依赖于特定的源类型。 需要变通。通常将源参数设为某个占位符字符串,然后在选项字符串中传递真正的信息。
错误报告 精确。可以明确指出是哪个配置键或值出了问题。 模糊。通常只返回一个通用的错误码,难以定位问题源头。
用户空间复杂度 对于简单挂载,代码略显冗长(需要多次调用)。 对于简单挂载,代码非常简洁(一次调用)。

fs/fs_context.c

put_fs_context 处理超级块配置上下文

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
/**
* put_fs_context - 处理超级块配置上下文。
* @fc: 要处理的上下文。
*/
void put_fs_context(struct fs_context *fc)
{
struct super_block *sb;

if (fc->root) {
sb = fc->root->d_sb;
dput(fc->root);
fc->root = NULL;
deactivate_super(sb);
}

if (fc->need_free && fc->ops && fc->ops->free)
fc->ops->free(fc);

security_free_mnt_opts(&fc->security);
put_net(fc->net_ns);
put_user_ns(fc->user_ns);
put_cred(fc->cred);
put_fc_log(fc);
put_filesystem(fc->fs_type);
kfree(fc->source);
kfree(fc);
}
EXPORT_SYMBOL(put_fs_context);

alloc_fs_context 创建一个文件系统上下文

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
/**
* alloc_fs_context - 创建一个文件系统上下文。
* @fs_type: 文件系统类型。
* @reference: 派生自的 dentry(或 NULL)
* @sb_flags: 文件系统/超级块标志(SB_*)
* @sb_flags_mask: @sb_flags 中适用的成员
* @purpose: 此配置将要使用的目的。
*
* 打开一个文件系统并创建挂载上下文。挂载上下文会用提供的标志进行初始化,
* 如果提供了另一个超级块的子挂载/自动挂载(由 @reference 指定),
* 可能会从该超级块复制命名空间等参数。
*/
static struct fs_context *alloc_fs_context(struct file_system_type *fs_type,
struct dentry *reference,
unsigned int sb_flags,
unsigned int sb_flags_mask,
enum fs_context_purpose purpose)
{
int (*init_fs_context)(struct fs_context *);
struct fs_context *fc;
int ret = -ENOMEM;

fc = kzalloc(sizeof(struct fs_context), GFP_KERNEL_ACCOUNT);
if (!fc)
return ERR_PTR(-ENOMEM);

fc->purpose = purpose;
fc->sb_flags = sb_flags;
fc->sb_flags_mask = sb_flags_mask;
fc->fs_type = get_filesystem(fs_type);
/* 当前进程的凭据 */
fc->cred = get_current_cred();
/* 网络命名空间(net_ns) */
fc->net_ns = get_net(current->nsproxy->net_ns);
/* 初始化日志前缀 */
fc->log.prefix = fs_type->name;

mutex_init(&fc->uapi_mutex);

/* 根据上下文用途(purpose)设置用户命名空间(user_ns) */
switch (purpose) {
/* 从当前进程的凭据中获取命名空间 */
case FS_CONTEXT_FOR_MOUNT:
fc->user_ns = get_user_ns(fc->cred->user_ns);
break;
/* 从参考超级块(reference->d_sb)中继承命名空间 */
case FS_CONTEXT_FOR_SUBMOUNT:
fc->user_ns = get_user_ns(reference->d_sb->s_user_ns);
break;
/* 激活参考超级块并设置根目录 */
case FS_CONTEXT_FOR_RECONFIGURE:
atomic_inc(&reference->d_sb->s_active);
fc->user_ns = get_user_ns(reference->d_sb->s_user_ns);
fc->root = dget(reference);
break;
}

init_fs_context = fc->fs_type->init_fs_context;
if (!init_fs_context)
/* 文件系统不支持现代上下文初始化逻辑,
使用传统的初始化函数(legacy_init_fs_context) */
init_fs_context = legacy_init_fs_context;
/* 为挂载操作设置文件系统特定的参数 */
ret = init_fs_context(fc);
if (ret < 0)
goto err_fc;
fc->need_free = true;
return fc;

err_fc:
put_fs_context(fc);
return ERR_PTR(ret);
}

fs_context_for_mount 用于创建文件系统上下文

1
2
3
4
5
6
struct fs_context *fs_context_for_mount(struct file_system_type *fs_type,
unsigned int sb_flags)
{
return alloc_fs_context(fs_type, NULL, sb_flags, 0,
FS_CONTEXT_FOR_MOUNT);
}

vfs_parse_sb_flag 解析超级块标志

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
static const struct constant_table common_set_sb_flag[] = {
{ "dirsync", SB_DIRSYNC },
{ "lazytime", SB_LAZYTIME },
{ "mand", SB_MANDLOCK },
{ "ro", SB_RDONLY },
{ "sync", SB_SYNCHRONOUS },
{ },
};

static const struct constant_table common_clear_sb_flag[] = {
{ "async", SB_SYNCHRONOUS },
{ "nolazytime", SB_LAZYTIME },
{ "nomand", SB_MANDLOCK },
{ "rw", SB_RDONLY },
{ },
};

/*
* 检查一个常见的挂载选项,该选项会操作 s_flags。
*/
static int vfs_parse_sb_flag(struct fs_context *fc, const char *key)
{
unsigned int token;

token = lookup_constant(common_set_sb_flag, key, 0);
if (token) {
fc->sb_flags |= token;
fc->sb_flags_mask |= token;
return 0;
}

token = lookup_constant(common_clear_sb_flag, key, 0);
if (token) {
fc->sb_flags &= ~token;
fc->sb_flags_mask |= token;
return 0;
}

return -ENOPARAM;
}

vfs_parse_fs_param_source 用于处理文件系统上下文(fs_context)中的 “source” 参数

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
/**
* vfs_parse_fs_param_source - 通过参数处理设置“source”
* @fc: 要修改的文件系统上下文
* @param: 参数
*
* 这是一个简单的助手,用于文件系统验证它们接受的“source”是否合理。
*
* 成功时返回 0,如果不是“source”参数则返回 -ENOPARAM,否则返回 -EINVAL。在失败的情况下,会记录补充的错误信息。
*/
int vfs_parse_fs_param_source(struct fs_context *fc, struct fs_parameter *param)
{
if (strcmp(param->key, "source") != 0)
return -ENOPARAM;

if (param->type != fs_value_is_string)
return invalf(fc, "Non-string source");

if (fc->source)
return invalf(fc, "Multiple sources");

fc->source = param->string;
param->string = NULL;
return 0;
}
EXPORT_SYMBOL(vfs_parse_fs_param_source);

vfs_parse_fs_param 解析文件系统参数

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
/**
* vfs_parse_fs_param - 向超级块配置添加单个参数
* @fc: 要修改的文件系统上下文
* @param: 参数
*
* 以字符串形式的单个挂载选项会被应用到正在设置的文件系统上下文中。
* 某些标准选项(例如 "ro")会被转换为标志位,而不会传递给文件系统。
* 活跃的安全模块可以观察并拦截这些选项。其他选项则交由文件系统解析。
*
* 此函数可针对同一上下文多次调用。
*
* 成功时返回 0,失败时返回负的错误码。发生失败时,可能会设置补充的错误信息。
*/
int vfs_parse_fs_param(struct fs_context *fc, struct fs_parameter *param)
{
int ret;

if (!param->key)
/* 返回错误并记录相关信息到文件系统 */
return invalf(fc, "Unnamed parameter\n");

ret = vfs_parse_sb_flag(fc, param->key);
if (ret != -ENOPARAM)
return ret;

/* return -ENOPARAM; */
ret = security_fs_context_parse_param(fc, param);
if (ret != -ENOPARAM)
/* 参数属于LSM或被LSM禁止;
* 因此不要传递给文件系统。
*/
return ret;

if (fc->ops->parse_param) {
ret = fc->ops->parse_param(fc, param);
if (ret != -ENOPARAM)
return ret;
}

/* 如果文件系统不接受任何参数,则为其提供
* 默认的源处理。
*/
ret = vfs_parse_fs_param_source(fc, param);
if (ret != -ENOPARAM)
return ret;

return invalf(fc, "%s: Unknown parameter '%s'",
fc->fs_type->name, param->key);
}
EXPORT_SYMBOL(vfs_parse_fs_param);

vfs_parse_fs_string 仅用于解析字符串

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
/**
* vfs_parse_fs_string - 方便函数,仅用于解析字符串。
* @fc: 文件系统上下文。
* @key: 参数名称。
* @value: 默认值。
* @v_size: 值的最大字节数。
*/
int vfs_parse_fs_string(struct fs_context *fc, const char *key,
const char *value, size_t v_size)
{
int ret;

struct fs_parameter param = {
.key = key,
/* fs_value_is_flag,表示参数是一个标志 */
.type = fs_value_is_flag,
.size = v_size,
};

/* 供了参数值(value),使用 kmemdup_nul 分配内存并复制值,同时确保字符串以空字符(\0)结尾 */
if (value) {
param.string = kmemdup_nul(value, v_size, GFP_KERNEL);
if (!param.string)
return -ENOMEM;
/* 参数类型更新为 fs_value_is_string,表示参数是一个字符串 */
param.type = fs_value_is_string;
}

ret = vfs_parse_fs_param(fc, &param);
kfree(param.string);
return ret;
}
EXPORT_SYMBOL(vfs_parse_fs_string);