[TOC]

Linux Namespaces (kernel/nsproxy.c, kernel/user_namespace.c, etc.) 内核隔离与虚拟化的基石

历史与背景

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

Linux Namespaces(命名空间)是为了在单一内核上实现**操作系统级虚拟化(OS-level Virtualization)而诞生的。其核心目标是隔离(Isolate)虚拟化(Virtualize)**全局的系统资源,使得一个进程组(A group of processes)能看到一套独立的系统资源,仿佛它们运行在一个独立的操作系统上,而实际上它们与其他进程组共享同一个内核。

这项技术解决了以下关键问题:

  • 轻量级隔离:在虚拟机(VM)技术出现之前或并行发展中,业界需要一种比完整虚拟化(模拟整个硬件)开销小得多的隔离方案。Namespaces通过隔离内核数据结构而非模拟硬件,实现了极低的性能开销。
  • 资源划分与视图分离:Unix的传统设计中,许多资源是全局唯一的,例如进程ID(PID)1是init进程,只有一个主机名,一套网络接口等。Namespaces允许创建这些资源的多个“视图”或“实例”,每个实例对其中的进程来说都是全局唯一的。
  • 安全沙箱(Sandboxing):通过将进程限制在一个独立的命名空间中,可以限制其可见性和能力,从而创建一个安全沙箱,防止其影响或窥探系统中的其他进程。
  • 应用打包与依赖管理:为应用程序及其依赖创建一个隔离的环境,使得应用可以在一个干净、可预测的环境中运行,而不会与宿主机或其他应用发生冲突。这就是现代容器技术的基础。

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

Linux Namespaces不是一次性完成的,而是随着内核的发展逐步添加的,每个都专注于隔离一种特定资源:

  • Mount Namespace (mnt):最早实现的命名空间(始于内核2.4.19)。它隔离了文件系统的挂载点列表,允许进程拥有独立的根目录(/)和挂载布局。这是chroot的超级增强版。
  • UTS Namespace:隔离了主机名和域名(通过unamesethostname系统调用)。
  • IPC Namespace:隔离了System V IPC对象和POSIX消息队列。
  • PID Namespace (pid):隔离了进程ID。在一个新的PID命名空间中,进程可以从PID 1开始,并拥有自己独立的进程树。
  • Network Namespace (net):隔离了网络设备、IP地址、路由表、端口号等网络栈。这使得每个网络命名空间都可以拥有独立的、虚拟的网络环境。
  • User Namespace (user):这是一个里程碑式的进展(在内核3.8中成熟)。它隔离了用户和组ID。最重要的是,它允许一个非特权的普通用户创建和拥有自己的一套命名空间。在自己的用户命名空间内,该用户可以被映射为root(UID 0),从而拥有执行特权操作(如创建其他类型的命名空间)的能力,但这种能力仅限于其拥有的命名空间内,对宿主机系统仍然是普通用户。这是现代无根容器(rootless container)技术的关键。

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

Linux Namespaces是Linux内核中一个极其核心、成熟且稳定的功能。它不再是一个实验性特性,而是现代Linux系统的基石之一。

  • 核心应用 - 容器技术:Namespaces是所有现代Linux容器技术(如Docker, containerd, Podman, Kubernetes)的绝对核心基础。容器的隔离性正是通过组合使用上述多种命名空间来实现的。
  • 系统服务systemd等现代init系统也利用命名空间来隔离和保护系统服务。
  • 应用沙箱:一些应用程序,如Google Chrome浏览器,也使用命名空间(特别是用户和PID命名空间)来沙箱化其子进程,增强安全性。

核心原理与设计

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

Linux Namespaces的核心原理是在内核数据结构中添加一层间接性,使得对全局资源的访问变成了对特定命名空间内资源的访问。

  1. 核心数据结构 (struct nsproxy):每个进程的描述符(task_struct)中都有一个指向struct nsproxy的指针。这个nsproxy结构体则包含了一系列指向该进程所属的各种具体命名空间(如mnt_ns, uts_ns, pid_ns_for_children等)的指针。当一个进程需要访问某个全局资源时,内核会通过其nsproxy找到对应的命名空间,并在这个命名空间的上下文中执行操作。
  2. 创建与加入 (clone, unshare, setns)
    • clone():在创建新进程时,可以通过传递一系列CLONE_NEW*标志(如CLONE_NEWPID)来为新进程创建并进入一个新的命名空间。
    • unshare():允许当前进程“脱离”其当前的命名空间,并为自己创建一个新的命名空间。
    • setns():允许当前进程加入一个已经存在的命名空间(通过该命名空间的文件描述符)。
  3. 用户命名空间与权能(User Namespace & Capabilities):用户命名空间是关键。当一个进程创建一个新的用户命名空间时,它在新空间内会被授予一个完整的权能集合(Capabilities)。这意味着它在新空间内是“root”,可以执行特权操作,比如创建新的网络或PID命名空间。但这些权能仅在该用户命名空间及其子命名空间内有效。同时,通过UID/GID映射文件(/proc/[pid]/uid_map),可以将外部的非特权UID映射为新空间内的UID 0,从而实现了安全的权限提升。

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

  • 轻量级:与完整的虚拟机相比,命名空间几乎没有性能开销,因为所有进程共享同一个宿主机内核。
  • 资源高效:无需为每个“容器”分配一套完整的操作系统资源,内存和CPU的利用率非常高。
  • 启动快速:创建一个新的命名空间几乎是瞬时的,因为它只是创建了一些内核数据结构,而不需要引导一个完整的操作系统。
  • 灵活性:可以根据需要组合使用不同的命名空间,实现不同程度的隔离。

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

  • 共享内核带来的安全风险:这是其最大也是最本质的“劣势”。所有容器共享同一个内核,因此,如果内核本身存在一个可被利用的漏洞,攻击者就可能从一个容器中“逃逸”出来,直接攻击宿主机和其他容器。这使得容器的隔离性在理论上弱于使用独立内核的虚拟机。
  • 隔离性并非完美:并非所有的系统资源都被命名空间化了。例如,一些/proc/sys下的信息、系统时间、内核日志(dmesg)、物理设备(/dev)等仍然是全局共享的。需要配合其他安全机制(如Seccomp, AppArmor, SELinux)来提供更强的安全性。
  • 复杂性:命名空间之间的交互,特别是涉及用户和PID命名空间的层次结构时,其行为可能非常复杂和微妙,给开发者和系统管理员带来挑战。

使用场景

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

  • 容器化部署 (Docker/Kubernetes):这是命名空间最主要的应用场景。通过组合使用PID、网络、挂载、UTS等命名空间,为每个应用创建一个隔离的运行环境。例如,两个运行在同一主机上的Web服务器容器,可以各自监听80端口而不会冲突,因为它们位于不同的网络命名空间中。
  • 应用沙箱:Web浏览器或文档查看器可以使用命名空间来隔离处理不可信内容的进程。即使渲染恶意网页的代码存在漏洞,它也只能在被严格限制的沙箱命名空间内造成破坏。
  • 构建与测试环境:开发者可以利用命名空间快速创建一个干净、隔离的环境来编译或测试软件,避免与主机系统上的库和工具发生版本冲突。

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

  • 需要运行不同操作系统的场景:命名空间是Linux内核的一个特性,所有“容器”都共享宿主机的Linux内核。因此,无法使用它在Linux主机上运行一个Windows或macOS的“容器”。这种场景必须使用虚拟机技术(如KVM, VMware)。
  • 需要极强安全隔离的多租户环境:在公有云等环境中,如果租户之间完全不信任,并且需要运行任意代码,那么基于硬件虚拟化的虚拟机(VMs)通常是更安全的选择。因为VM提供了由硬件(CPU虚拟化指令)强制执行的、更强的隔离边界。

对比分析

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

Namespaces (容器) vs. 全虚拟化 (虚拟机)

特性 Namespaces (容器) 全虚拟化 (虚拟机, VM)
隔离层次 操作系统内核层。进程间隔离。 硬件层。通过Hypervisor模拟完整的硬件。
资源开销 。共享宿主机内核,只有应用本身的开销。 。每个VM都有自己的完整操作系统内核和分配的内存/CPU。
性能 接近原生。几乎没有性能损失。 有性能损失,因为存在硬件模拟和Hypervisor的开销。
启动速度 (毫秒级)。 (秒级到分钟级),需要引导整个操作系统。
镜像/磁盘占用 。镜像只包含应用及其库,不包含内核。 。镜像是完整的操作系统磁盘映像。

Namespaces vs. chroot

特性 Namespaces (特别是Mount Namespace) chroot
隔离范围 全面。隔离挂载点、进程树、网络、用户等。 仅文件系统。只隔离了根目录的视图。
安全性 更高chroot的进程仍然可以看到外部的进程树和网络,并且一个特权进程很容易“逃逸”出chroot环境。 。逃逸chroot jail相对容易。
功能 是一个完整的虚拟化和隔离框架。 只是一个改变进程根目录的简单工具。

include/linux/user_namespace.h

get_user_ns 获取用户命名空间

1
2
3
4
static inline struct user_namespace *get_user_ns(struct user_namespace *ns)
{
return &init_user_ns;
}

ucount_type: 用户命名空间资源计数类型枚举

此代码片段定义了一个名为ucount_type的C语言枚举(enum)。它的核心作用是为内核中需要按用户(或更准确地说是按用户命名空间)进行追踪和限制的各种资源, 提供一组有意义的、易于理解的符号常量

这个枚举是Linux内核用户命名空间(User Namespace)资源管理机制的基础。当内核需要增加或减少某个用户所消耗的某种资源(例如, 该用户又创建了一个新的PID命名空间)时, 它不会使用魔法数字(magic number), 而是使用这个枚举中定义的符号常量(如UCOUNT_PID_NAMESPACES)作为索引, 来更新该用户对应的资源账户数组。


代码逐行解析

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
/*
* 定义一个名为 ucount_type 的枚举.
* 枚举为一组整数常量提供了符号名称.
*/
enum ucount_type {
/*
* UCOUNT_USER_NAMESPACES: 代表一个用户拥有的用户命名空间数量.
* 当一个用户创建一个新的用户命名空间时, 这个计数会增加.
*/
UCOUNT_USER_NAMESPACES,
/*
* UCOUNT_PID_NAMESPACES: 代表一个用户拥有的PID(进程ID)命名空间数量.
*/
UCOUNT_PID_NAMESPACES,
/*
* UCOUNT_UTS_NAMESPACES: 代表一个用户拥有的UTS(主机名和域名)命名空间数量.
*/
UCOUNT_UTS_NAMESPACES,
/*
* UCOUNT_IPC_NAMESPACES: 代表一个用户拥有的IPC(进程间通信)命名空间数量.
*/
UCOUNT_IPC_NAMESPACES,
/*
* UCOUNT_NET_NAMESPACES: 代表一个用户拥有的网络命名空间数量.
*/
UCOUNT_NET_NAMESPACES,
/*
* UCOUNT_MNT_NAMESPACES: 代表一个用户拥有的挂载命名空间数量.
*/
UCOUNT_MNT_NAMESPACES,
/*
* UCOUNT_CGROUP_NAMESPACES: 代表一个用户拥有的cgroup命名空间数量.
*/
UCOUNT_CGROUP_NAMESPACES,
/*
* UCOUNT_TIME_NAMESPACES: 代表一个用户拥有的时间命名空间数量.
*/
UCOUNT_TIME_NAMESPACES,

/*
* 预处理器指令: 下面的枚举成员只有在内核配置了 CONFIG_INOTIFY_USER 时才会被编译.
* inotify 是一个用于监控文件系统事件的内核子系统.
*/
#ifdef CONFIG_INOTIFY_USER
/*
* UCOUNT_INOTIFY_INSTANCES: 代表一个用户拥有的inotify实例数量.
*/
UCOUNT_INOTIFY_INSTANCES,
/*
* UCOUNT_INOTIFY_WATCHES: 代表一个用户拥有的inotify监视点(watch)数量.
*/
UCOUNT_INOTIFY_WATCHES,
#endif

/*
* 预处理器指令: 下面的枚举成员只有在内核配置了 CONFIG_FANOTIFY 时才会被编译.
* fanotify 是另一个用于监控文件系统访问的机制.
*/
#ifdef CONFIG_FANOTIFY
/*
* UCOUNT_FANOTIFY_GROUPS: 代表一个用户拥有的fanotify组数量.
*/
UCOUNT_FANOTIFY_GROUPS,
/*
* UCOUNT_FANOTIFY_MARKS: 代表一个用户拥有的fanotify标记(mark)数量.
*/
UCOUNT_FANOTIFY_MARKS,
#endif

/*
* UCOUNT_COUNTS: 这是一个特殊的"哨兵"成员.
* C语言的枚举会自动从0开始为成员赋值 (UCOUNT_USER_NAMESPACES=0, UCOUNT_PID_NAMESPACES=1, ...).
* 因此, UCOUNT_COUNTS 的值将恰好等于它前面所有成员的总数.
* 这是一种非常常见的C语言编程技巧, 用于在编译时自动计算枚举的大小,
* 从而可以方便地定义大小正确的数组, 例如: int counter_array[UCOUNT_COUNTS];
*/
UCOUNT_COUNTS,
};

kernel/user.c

init_user_ns

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
/*
* 用户命名空间计数为 1 对于 root 用户,1 对于 init_uts_ns,
* 以及 1 对于...?
*/
struct user_namespace init_user_ns = {
.uid_map = {
{
.extent[0] = {
.first = 0,
.lower_first = 0,
.count = 4294967295U,
},
.nr_extents = 1,
},
},
.gid_map = {
{
.extent[0] = {
.first = 0,
.lower_first = 0,
.count = 4294967295U,
},
.nr_extents = 1,
},
},
.projid_map = {
{
.extent[0] = {
.first = 0,
.lower_first = 0,
.count = 4294967295U,
},
.nr_extents = 1,
},
},
.ns.count = REFCOUNT_INIT(3),
.owner = GLOBAL_ROOT_UID,
.group = GLOBAL_ROOT_GID,
.ns.inum = PROC_USER_INIT_INO,
.flags = USERNS_INIT_FLAGS,
};
EXPORT_SYMBOL_GPL(init_user_ns);

用户ID缓存: 数据结构与哈希操作

此代码片段是Linux内核中用于用户ID (UID) 缓存机制的核心组成部分。它定义了用于存储和快速查找struct user_struct对象的哈希表数据结构, 以及操作该哈希表的一些内联辅助函数。这个缓存的设计目标是在诸如setuid()这样的系统调用发生时, 能够高效地获取与特定UID相关联的struct user_struct对象。

从数据结构和算法设计的角度来看, 整个代码片段展现了对性能和内存效率的精细权衡:

  1. 哈希表大小的动态调整: 通过UIDHASH_BITS宏, 哈希表的大小(UIDHASH_SZ)可以在编译时根据内核配置(CONFIG_BASE_SMALL)进行调整。
    • CONFIG_BASE_SMALL: 通常在资源极度受限的嵌入式系统中启用。在这种情况下, 为了节省内存, 哈希表的大小会被设置为1 << 3 = 8
    • 在更”完整”的系统中, 哈希表的大小会被设置为1 << 7 = 128
  2. 哈希函数 (__uidhashfn): 此函数负责将一个UID值映射到哈希表的索引。
    • 它的实现非常简单, 但却巧妙地结合了位移和加法操作: (((uid >> UIDHASH_BITS) + uid) & UIDHASH_MASK)。这种组合通常能够提供相对较好的散列效果, 同时又避免了复杂的乘除法运算, 从而保证了极高的执行效率。
  3. 自旋锁 (uidhash_lock): 这是保证哈希表并发安全的关键。由于对哈希表的修改(如插入和删除)不是原子操作, 必须使用锁来保护, 以防止多个进程或中断同时修改哈希表而导致数据损坏。
    • 重要考虑: 代码注释明确指出, 这个锁需要在进程上下文、软中断/tasklet上下文(在task-struct被RCU释放时), 甚至在禁用本地中断的情况下被获取。这要求锁机制必须同时满足以下两个条件:
      1. 能够防止抢占(进程上下文之间)。
      2. 能够防止中断(包括软中断)的干扰。
        自旋锁(加上_irqsave后缀)是满足这些需求的最佳选择。在单核系统中, 它通过禁用内核抢占和本地中断来保证临界区的原子性。
  4. 全局根用户实例 (root_user): 确保有一个预先存在的、静态的root_user实例, 避免了在需要频繁访问根用户时(如在系统初始化过程中)进行重复的内存分配和初始化操作。

在STM32H750上, 由于资源非常有限, 启用CONFIG_BASE_SMALL的可能性很高。这意味着哈希表会被限制在只有8个桶的大小, 从而增加了哈希冲突的概率, 降低了查找效率。然而, 在实际的嵌入式系统中, 通常只有少数几个用户(甚至只有一个root用户), 因此这种妥协带来的性能影响可能并不明显。即使是单核系统, uidhash_lock的存在也并非多余, 它可以防止哈希表被中断处理程序破坏。

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
/*
* UID 任务计数缓存, 用于在更改用户ID时(例如 setuid() 及其类似函数)
* 在 "alloc_uid" 中快速查找用户.
*/

/*
* UIDHASH_BITS: 用于UID哈希表的位数.
* 如果启用了 CONFIG_BASE_SMALL (针对小型系统优化), 则使用3位 (8个桶).
* 否则使用7位 (128个桶).
*/
#define UIDHASH_BITS (IS_ENABLED(CONFIG_BASE_SMALL) ? 3 : 7)
/* UIDHASH_SZ: UID哈希表的大小, 是2的UIDHASH_BITS次方. */
#define UIDHASH_SZ (1 << UIDHASH_BITS)
/* UIDHASH_MASK: 用于UID哈希的掩码, 等于哈希表大小减1. */
#define UIDHASH_MASK (UIDHASH_SZ - 1)
/*
* __uidhashfn: UID哈希函数.
* 将一个UID值映射到哈希表中的一个索引.
* 它首先将UID右移 UIDHASH_BITS 位, 然后加上原始UID, 最后与 UIDHASH_MASK 进行 AND 运算.
* 这种组合通常能提供较好的散列效果.
*/
#define __uidhashfn(uid) (((uid >> UIDHASH_BITS) + uid) & UIDHASH_MASK)
/* uidhashentry: 获取给定UID的哈希表条目的地址. */
#define uidhashentry(uid) (uidhash_table + __uidhashfn((__kuid_val(uid))))

/* uid_cachep: 指向kmem_cache的指针, 用于分配 struct user_struct 对象. */
static struct kmem_cache *uid_cachep;
/* uidhash_table: UID哈希表, 是一个 hlist_head 数组. */
static struct hlist_head uidhash_table[UIDHASH_SZ];

/*
* uidhash_lock: 用于保护 uidhash_table 的自旋锁.
* 必须在禁用中断的情况下安全使用.
*/
static DEFINE_SPINLOCK(uidhash_lock);

/*
* root_user: 全局的 struct user_struct 实例, 代表root用户 (UID 0).
* .__count 初始化为 1, 因为init进程以root身份运行.
*/
struct user_struct root_user = {
.__count = REFCOUNT_INIT(1),
.uid = GLOBAL_ROOT_UID,
.ratelimit = RATELIMIT_STATE_INIT(root_user.ratelimit, 0, 0),
};

/*
* 这些例程必须在持有 uidhash 自旋锁的情况下调用!
*/
/* uid_hash_insert: 将一个 user_struct 对象插入到哈希表中. */
static void uid_hash_insert(struct user_struct *up, struct hlist_head *hashent)
{
/* 将 user_struct 的 uidhash_node 成员添加到指定的哈希链表头. */
hlist_add_head(&up->uidhash_node, hashent);
}

uid_cache_init: 初始化用户ID缓存

此函数的核心作用是在Linux内核启动的早期阶段, 初始化一个用于高效管理和查找struct user_struct对象的高速缓存系统struct user_struct是内核中用来追踪每个独立用户(由UID标识)所拥有的资源(如打开的文件数量、正在运行的进程数等)的核心数据结构。

这个初始化过程的根本原理是结合使用两种经典的高性能数据结构——SLAB缓存和哈希表——来构建一个专门为user_struct优化的对象分配与查找机制

工作流程:

  1. 创建SLAB缓存 (kmem_cache_create):

    • 这是初始化的第一步。函数调用kmem_cache_create来创建一个名为"uid_cache"SLAB缓存池
    • 原理: SLAB是Linux内核中用于管理同尺寸小对象的高效内存分配器。通过为struct user_struct创建一个专属的kmem_cache, 内核可以在需要一个新的user_struct时(例如, 当一个新用户首次登录时), 快速地从这个预先分配好的池中获取一个对象, 而不是每次都调用通用的kmalloc。这避免了内存碎片, 并提高了分配和释放的效率。
    • SLAB_HWCACHE_ALIGN: 这个标志确保了分配出的每个user_struct对象的起始地址都与CPU的硬件缓存行对齐, 这可以减少缓存伪共享(false sharing), 提高多核性能。
    • SLAB_PANIC: 这个标志意味着如果SLAB缓存创建失败, 内核将立即停止运行(panic)。这表明该缓存对于系统的正常运行是至关重要的, 如果无法创建, 系统也无法继续。
  2. 初始化哈希表 (INIT_HLIST_HEAD):

    • 函数接着遍历一个名为uidhash_table的数组, 并将数组中的每一个元素初始化为一个哈希链表的头。
    • 原理: uidhash_table是一个哈希表, 它是实现快速查找的关键。当需要查找一个特定UID对应的user_struct时, 内核会首先根据UID计算出一个哈希值, 这个哈希值就是该哈希表数组的索引。然后, 内核只需遍历该索引位置上的短链表, 就可以快速找到目标对象, 而无需线性扫描所有存在的user_struct。这个操作的平均时间复杂度是O(1)。
  3. 初始化并插入根用户 (root_user):

    • 系统必须有一个”根用户”(root, UID=0)的实例。root_user是一个静态定义的全局变量。
    • 函数首先为root_user分配其私有的epoll资源。
    • 然后, 它获取一个自旋锁uidhash_lock来保护哈希表的并发访问。
    • 最后, 它调用uid_hash_insert, 将这个全局的root_user实例插入到哈希表的第0个桶(bucket)中。
  4. 注册为早期初始化调用 (subsys_initcall):

    • subsys_initcall是一个内核初始化宏, 它确保uid_cache_init函数在内核启动的一个非常早的阶段被调用。这非常重要, 因为在后续的许多内核服务(如创建init进程)初始化之前, root_user的存在和UID缓存系统的就绪是必需的前提条件。
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
/*
* uid_cache_init: 用户ID缓存的初始化函数.
*/
static int __init uid_cache_init(void)
{
int n;

/*
* 步骤1: 创建一个名为 "uid_cache" 的kmem_cache.
* 这个缓存专门用于快速分配和释放大小为 sizeof(struct user_struct) 的对象.
* SLAB_HWCACHE_ALIGN: 确保分配的对象地址与CPU缓存行对齐, 提高性能.
* SLAB_PANIC: 如果创建失败, 内核将panic, 因为这个缓存是系统运行的必要条件.
*/
uid_cachep = kmem_cache_create("uid_cache", sizeof(struct user_struct),
0, SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL);

/*
* 步骤2: 初始化哈希表.
* uidhash_table 是一个hlist_head数组. UIDHASH_SZ是哈希表的大小.
* 这个循环将数组的每个元素都初始化为一个空的哈希链表头.
*/
for(n = 0; n < UIDHASH_SZ; ++n)
INIT_HLIST_HEAD(uidhash_table + n);

/*
* 步骤3a: 为全局的 root_user 实例分配其每CPU的epoll计数器资源.
*/
if (user_epoll_alloc(&root_user))
panic("root_user epoll percpu counter alloc failed");

/*
* 步骤3b: 将 root_user 插入到哈希表中.
* 获取 uidhash_lock 自旋锁以保护哈希表的并发访问.
*/
spin_lock_irq(&uidhash_lock);
/*
* uidhashentry(GLOBAL_ROOT_UID) 计算出UID 0对应的哈希表桶(bucket)的地址.
* uid_hash_insert 将 &root_user 添加到该桶的链表中.
*/
uid_hash_insert(&root_user, uidhashentry(GLOBAL_ROOT_UID));
/* 释放自旋锁. */
spin_unlock_irq(&uidhash_lock);

/* 初始化成功, 返回0. */
return 0;
}
/*
* subsys_initcall 是一个内核初始化宏.
* 它确保 uid_cache_init 函数在内核启动的一个非常早的阶段(在大多数驱动和服务之前)被调用.
*/
subsys_initcall(uid_cache_init);

kernel/ucount.c

用户命名空间 Sysctl 接口的实现

此代码片段是Linux内核用户命名空间(User Namespace)与sysctl接口集成的核心实现。它的根本原理是为每一个用户命名空间动态地创建和注册一套独立的sysctl条目, 这些条目允许在运行时查看和修改该特定命名空间内的资源限制

这套机制通过一系列精巧的回调函数和每个命名空间私有的数据结构, 实现了以下关键功能:

  1. 上下文感知: 当一个进程访问/proc/sys/user/下的文件时, 内核能够通过set_lookup回调函数, 自动识别出该进程属于哪个用户命名空间, 并将其导向该命名空间私有的sysctl数据集。
  2. 动态权限控制: 对这些文件的访问权限不是静态的, 而是通过set_permissions回调函数动态计算的。它会检查当前进程在其所属的用户命名空间内是否具有CAP_SYS_RESOURCE权限。这使得一个在命名空间内拥有”root”权限的进程可以修改其资源限制, 而其他进程则最多只能读取, 提供了细粒度的、符合命名空间隔离原则的安全性。
  3. 资源隔离: setup_userns_sysctls函数的核心是为每个新的用户命名空间复制一个sysctl表模板(user_table), 然后将这个副本中的数据指针(data)精确地指向该命名空间自己的资源限制数组(ns->ucount_max)。这确保了当管理员修改/proc/sys/user/max_user_namespaces时, 修改的是当前进程所属命名空间的限制, 而不会影响到其他命名空间。

代码逐行解析

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
107
108
109
110
111
112
113
114
115
116
117
118
/*
* set_lookup: 一个回调函数, 用于根据当前上下文查找正确的sysctl"集合".
* @root: 指向sysctl树的根.
* @return: 指向当前进程所属用户命名空间的sysctl集合(&current_user_ns()->set).
* 原理: 当进程访问/proc/sys时, 内核调用此函数, 它返回当前进程的用户命名空间
* 私有的ctl_table_set, 从而将访问导向正确的数据集.
*/
static struct ctl_table_set *
set_lookup(struct ctl_table_root *root)
{
return &current_user_ns()->set;
}

/*
* set_is_seen: 一个回调函数, 判断一个给定的sysctl集合是否对当前进程可见.
* @set: 被检查的sysctl集合.
* @return: 只有当被检查的集合就是当前进程自己的集合时,才返回true.
*/
static int set_is_seen(struct ctl_table_set *set)
{
return &current_user_ns()->set == set;
}

/*
* set_permissions: 一个回调函数, 动态计算sysctl文件的访问权限.
* @head: 指向包含该条目的表头.
* @table: 指向具体的sysctl条目.
* @return: 计算后的文件模式 (mode).
* 原理: 它检查当前进程在条目所属的用户命名空间(user_ns)内是否拥有CAP_SYS_RESOURCE
* 权限. 如果有, 就赋予该条目定义的读写权限; 如果没有, 则最多只赋予读权限.
* 这是实现基于命名空间上下文的动态权限控制的核心.
*/
static int set_permissions(struct ctl_table_header *head,
const struct ctl_table *table)
{
struct user_namespace *user_ns =
container_of(head->set, struct user_namespace, set);
int mode;

if (ns_capable(user_ns, CAP_SYS_RESOURCE))
mode = (table->mode & S_IRWXU) >> 6;
else
mode = table->mode & S_IROTH;
return (mode << 6) | (mode << 3) | mode;
}

/*
* set_root: 一个ctl_table_root结构体, 它将上述的回调函数打包在一起,
* 定义了所有用户命名空间sysctl树的通用行为.
*/
static struct ctl_table_root set_root = {
.lookup = set_lookup,
.permissions = set_permissions,
};

/* 定义两个全局变量, 作为proc_doulongvec_minmax处理函数的最小/最大值参数. */
static long ue_zero = 0;
static long ue_int_max = INT_MAX;

/*
* UCOUNT_ENTRY: 一个宏, 用于简化定义一个ucounts sysctl条目.
* 它预先填充了大部分字段, 如文件名、最大长度、权限和处理函数.
* 这是一种"代码即数据"的模板化方法, 使代码更简洁, 减少重复.
*/
#define UCOUNT_ENTRY(name) \
{ \
.procname = name, \
.maxlen = sizeof(long), \
.mode = 0644, \
.proc_handler = proc_doulongvec_minmax, \
.extra1 = &ue_zero, \
.extra2 = &ue_int_max, \
}
/*
* user_table: 一个静态常量数组, 作为创建sysctl文件的模板.
* 它使用UCOUNT_ENTRY宏定义了所有与用户命名空间资源限制相关的条目.
*/
static const struct ctl_table user_table[] = {
UCOUNT_ENTRY("max_user_namespaces"),
UCOUNT_ENTRY("max_pid_namespaces"),
/* ... 其他命名空间类型 ... */
};

/*
* setup_userns_sysctls: 为一个特定的用户命名空间 ns 设置其sysctl接口.
* @ns: 指向要被设置的用户命名空间结构体.
* @return: 成功时返回true, 失败时返回false.
*/
bool setup_userns_sysctls(struct user_namespace *ns)
{

struct ctl_table *tbl;

/* 编译时断言, 确保模板数组的大小与定义的计数常量一致, 防止不匹配. */
BUILD_BUG_ON(ARRAY_SIZE(user_table) != UCOUNT_COUNTS);
/* 初始化ns->set, 将其与set_root中定义的回调函数关联起来. */
setup_sysctl_set(&ns->set, &set_root, set_is_seen);
/* 关键: 复制全局的user_table模板. 每个命名空间都有自己独立的表副本. */
tbl = kmemdup(user_table, sizeof(user_table), GFP_KERNEL);
if (tbl) {
int i;
/* 关键: 遍历副本, 将每个条目的.data指针指向该命名空间私有的资源限制数组ns->ucount_max. */
for (i = 0; i < UCOUNT_COUNTS; i++) {
tbl[i].data = &ns->ucount_max[i];
}
/* 注册这个为ns量身定制的、已绑定数据的表. */
ns->sysctls = __register_sysctl_table(&ns->set, "user", tbl,
ARRAY_SIZE(user_table));
}
/* 错误处理: 如果注册失败, 释放所有已分配的资源. */
if (!ns->sysctls) {
kfree(tbl);
retire_sysctl_set(&ns->set);
return false;
}

return true;
}

hlist_add_ucounts: 将用户资源账户添加到全局哈希表

此函数是Linux内核用户命名空间资源管理子系统的一个内部函数。它的核心作用是将一个特定用户的资源记账本(struct ucounts)添加到一个全局的、可供快速查找的哈希表(hash table)中

当内核首次需要为一个用户(由其UID和所属的用户命名空间唯一确定)记录资源消耗时, 它会先为该用户创建一个ucounts结构体, 然后调用此函数将其”挂号”到全局系统中。完成此操作后, 内核在后续任何时候需要查找该用户的记账本时, 都可以通过同一个哈希算法快速地定位到它。


代码逐行解析

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
/*
* 静态函数声明: hlist_add_ucounts
* 将一个 ucounts 结构体添加到一个哈希链表中.
* @ucounts: 指向要被添加的 ucounts 结构体的指针.
*/
static void hlist_add_ucounts(struct ucounts *ucounts)
{
/*
* 步骤1: 计算哈希桶(bucket).
* 调用 ucounts_hashentry 辅助函数, 它内部实现了一个哈希算法.
* 该算法以用户命名空间(ucounts->ns)和用户ID(ucounts->uid)作为输入,
* 计算出一个哈希值, 并根据此哈希值从一个全局的哈希表中找到对应的链表头(哈希桶).
* hashent 指向了这个链表头.
*/
struct hlist_nulls_head *hashent = ucounts_hashentry(ucounts->ns, ucounts->uid);

/*
* 步骤2: 获取自旋锁并禁用中断.
* ucounts_lock 是一个全局的自旋锁, 用于保护整个 ucounts 哈希表.
* spin_lock_irq 会:
* a) 在单核抢占式内核上, 禁用内核抢占, 防止当前任务被切换出去.
* b) 禁用当前CPU核心的本地中断.
* 这两项措施确保了接下来的链表插入操作是原子的, 不会被其他任务或中断处理程序干扰,
* 从而保证了全局哈希表数据结构的完整性.
*/
spin_lock_irq(&ucounts_lock);

/*
* 步骤3: 执行链表头插法.
* hlist_nulls_add_head_rcu 是一个经过特殊优化的链表插入函数.
* - hlist_..._add_head: 将 ucounts 结构体中的链表节点(ucounts->node)插入到
* 哈希桶(hashent)的头部. 头插法是一个O(1)复杂度的操作, 非常快速.
* - _rcu: 表明此操作是RCU(Read-Copy-Update)安全的. 这意味着在写操作(由锁保护)
* 进行的同时, 其他代码可以无锁地、安全地读取这个哈希表.
*/
hlist_nulls_add_head_rcu(&ucounts->node, hashent);

/*
* 步骤4: 释放锁并恢复中断.
* spin_unlock_irq 会释放自旋锁, 并恢复在加锁前保存的中断状态.
*/
spin_unlock_irq(&ucounts_lock);
}

用户命名空间资源限制的层级式增加与读取

此代码片段包含两个协同工作的函数, 它们是Linux内核用户命名空间(User Namespace)资源限制机制的核心实现。其根本原理是实现一个层级式的资源记账系统: 当一个用户在某个嵌套的命名空间中消耗一项资源时, 这个消耗量不仅会在当前命名空间的账户上累加, 还会递归地、原子地累加到所有父级命名空间的账户上, 直至根命名空间。在每一级累加时, 都会检查是否超过了该级命名空间设定的资源上限。

这个机制确保了资源限制在整个命名空间层次结构中都是有效和一致的, 防止了任何一个子命名空间(即使它内部有自己的”root”用户)能够绕过其父级或系统全局的资源限制。


代码逐行解析

get_userns_rlimit_max: 安全地读取资源限制最大值

这是一个简单的内联辅助函数, 用于安全地读取一个命名空间中特定资源类型的最大限制值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
* 静态内联函数: get_userns_rlimit_max
* @ns: 指向要查询的用户命名空间.
* @type: 要查询的资源限制类型.
* @return: 返回该资源类型的最大限制值.
*/
static inline long get_userns_rlimit_max(struct user_namespace *ns, enum rlimit_type type)
{
/*
* READ_ONCE: 这是一个内存屏障宏, 用于保证原子读取.
* 它确保了对 ns->rlimit_max[type] 的读取是一次完整的、不可分割的操作.
* 这可以防止编译器进行不安全的优化(如将值缓存到寄存器中), 并防止在多核系统上发生"撕裂读"
* (reading a partially-updated value). 在单核系统上, 它主要防止编译器优化问题.
*/
return READ_ONCE(ns->rlimit_max[type]);
}

inc_rlimit_ucounts: 层级式地增加资源计数

这是执行层级式资源记账的核心函数。

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
/*
* inc_rlimit_ucounts: 增加一个资源限制的计数.
* @ucounts: 要开始增加计数的最低层级的资源账户.
* @type: 要增加的资源类型.
* @v: 要增加的值 (可以为负数, 表示减少).
* @return: 如果成功, 返回最低层级的新计数值; 如果在任何层级超出限制, 返回 LONG_MAX.
*/
long inc_rlimit_ucounts(struct ucounts *ucounts, enum rlimit_type type, long v)
{
/* iter: 用于遍历命名空间层级的迭代器. */
struct ucounts *iter;
/* max: 当前层级允许的最大值, 由其父级决定. 初始化为最大值以通过第一级检查. */
long max = LONG_MAX;
/* ret: 用于保存最终返回值的变量. */
long ret = 0;

/*
* 核心循环: 从当前账户(ucounts)开始, 沿着命名空间层级向上遍历, 直到根(iter->ns->ucounts 为 NULL).
*/
for (iter = ucounts; iter; iter = iter->ns->ucounts) {
/*
* 原子地将 v 增加到当前层级的资源计数器上, 并返回新值.
* atomic_long_add_return 保证了即使有多个进程同时操作, 计数也不会损坏.
*/
long new = atomic_long_add_return(v, &iter->rlimit[type]);

/*
* 检查新值是否合法: 不能为负, 也不能超过从上一级(父级)获取到的最大值 max.
*/
if (new < 0 || new > max)
/* 如果超出限制, 将返回值设置为 LONG_MAX, 作为一个错误标志. */
ret = LONG_MAX;
else if (iter == ucounts)
/*
* 仅在第一次循环(即操作最低层级的账户)时, 将成功的新计数值保存到 ret 中.
* 这是最终要返回给调用者的值.
*/
ret = new;
/*
* 在处理完当前层级后, 从当前层级获取其最大限制值.
* 这个值将作为下一轮循环中, 检查其父级账户时的 max.
*/
max = get_userns_rlimit_max(iter->ns, type);
}
/*
* 返回结果.
*/
return ret;
}

user_namespace_sysctl_init: 初始化用户命名空间的sysctl接口与资源计数

此函数是Linux内核在启动过程中的一个初始化函数, 其核心作用是为用户命名空间(User Namespace)这一特性建立必要的基础设施, 主要包含两个方面:

  1. 创建Sysctl接口: 如果内核配置了sysctl支持, 它会在/proc/sys/目录下创建一个名为user的子目录 (/proc/sys/user/)。这个目录将作为根目录, 用于未来挂载所有与用户命名空间相关的可调参数(例如, max_user_namespaces等), 使得系统管理员可以在运行时查看和修改这些限制。
  2. 初始化资源计数: 它为系统中的第一个、也是根用户命名空间(init_user_ns)初始化资源记账本(ucounts)。用户命名空间允许非特权用户创建独立的”用户环境”, 为防止滥用(例如, 一个用户创建过多的进程或命名空间耗尽系统资源), 内核必须对每个用户(或用户命名空间集合)所使用的资源进行追踪和限制。此函数执行了最原始的记账操作: 将init进程(PID 1)本身计入到根用户命名空间的进程数(UCOUNT_RLIMIT_NPROC)限制中。

代码逐行解析

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
/*
* 静态的、在初始化阶段执行的函数, 返回一个整型值.
* static: 此函数仅在当前文件中可见.
* __init: 这是一个宏, 告诉编译器将此函数和其使用的数据放入特殊的 ".init" 段.
* 内核在启动过程完成后, 会释放这部分内存, 以节省RAM.
*/
static __init int user_namespace_sysctl_init(void)
{
/*
* 这是一个预处理器指令, 表示从这里到 #endif 的代码只有在内核编译时
* 定义了 CONFIG_SYSCTL 宏的情况下才会被包含进来.
*/
#ifdef CONFIG_SYSCTL
/*
* 定义一个静态的指向 ctl_table_header 的指针 user_header.
* static: 使得该变量的生命周期贯穿整个内核运行期间, 并且仅在函数内部可见(但其指向的内容是全局的).
* 它将用于保存注册 sysctl 目录后返回的句柄.
*/
static struct ctl_table_header *user_header;
/*
* 定义一个只包含一个空元素的 ctl_table 数组.
* 这是为了满足 register_sysctl_sz 函数的参数要求, 当我们只想创建一个空目录时,
* 就需要传递一个空的定义表.
*/
static struct ctl_table empty[1];
/*
* 注释解释: 必须在默认的命名空间集合中注册 "user" 目录,
* 这样后续在子命名空间集合中的注册才能正常工作.
*/
/*
* 调用 register_sysctl_sz 函数.
* - "user": 要创建的目录名, 即 /proc/sys/user.
* - empty: 描述该目录下文件的空数组.
* - 0: 数组大小.
* 作用: 在 /proc/sys/ 目录下创建一个名为 "user" 的空目录, 并返回其句柄.
*/
user_header = register_sysctl_sz("user", empty, 0);
/*
* 调用 kmemleak_ignore, 告知内核的内存泄漏检测工具, user_header
* 指向的内存是一个全局的、预期不会被释放的单例对象, 不应将其报告为内存泄漏.
*/
kmemleak_ignore(user_header);
/*
* BUG_ON 是一个强力断言宏. 如果括号内的条件为真(即 user_header 为NULL),
* 内核会立即崩溃(panic). 这表明 "user" sysctl 目录被认为是系统正常运行的必要条件.
*/
BUG_ON(!user_header);
/*
* 调用 setup_userns_sysctls, 为初始用户命名空间(&init_user_ns)设置具体的sysctl条目
* (例如 max_user_namespaces). BUG_ON确保这个设置过程必须成功.
*/
BUG_ON(!setup_userns_sysctls(&init_user_ns));
#endif
/*
* 调用 hlist_add_ucounts, 将初始用户命名空间的资源计数结构体(&init_ucounts)
* 添加到一个全局的哈希链表中, 使其成为内核可追踪的资源账户.
*/
hlist_add_ucounts(&init_ucounts);
/*
* 调用 inc_rlimit_ucounts, 为指定的资源限制类型增加计数.
* - &init_ucounts: 要操作的资源账户.
* - UCOUNT_RLIMIT_NPROC: 要增加计数的资源类型, 这里是 "进程数量".
* - 1: 增加的数量.
* 作用: 将初始用户命名空间的进程数加1, 这代表了系统中的第一个进程(init进程, PID 1)
* 已经被创建, 并计入资源消耗.
*/
inc_rlimit_ucounts(&init_ucounts, UCOUNT_RLIMIT_NPROC, 1);
/*
* 函数成功完成, 返回 0.
*/
return 0;
}
/*
* subsys_initcall() 是一个宏, 它将 user_namespace_sysctl_init 函数注册为一个
* 在内核启动过程中、在相当早的"子系统"级别就要被调用的初始化函数.
* 这确保了用户命名空间的基础设施在任何可能需要它的服务(如模块加载)启动之前就已经准备就绪.
*/
subsys_initcall(user_namespace_sysctl_init);

fs/namespace.c

alloc_mnt_ns 分配和初始化挂载命名空间(mnt_namespace

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
/*
* 分配一个序列号,以便检测尝试将旧挂载命名空间的引用绑定到当前挂载命名空间时,
* 防止引用计数循环。一个以10GHz递增的64位数字需要12,427年才能回绕,
* 这实际上是永远不会发生的,因此我们可以忽略这种可能性。
*/
static atomic64_t mnt_ns_seq = ATOMIC64_INIT(1);

static struct mnt_namespace *alloc_mnt_ns(struct user_namespace *user_ns, bool anon)
{
struct mnt_namespace *new_ns;
struct ucounts *ucounts;
int ret;
/* 增加用户命名空间(user_namespace)的挂载命名空间计数 */
ucounts = inc_mnt_namespaces(user_ns);
if (!ucounts)
return ERR_PTR(-ENOSPC);

new_ns = kzalloc(sizeof(struct mnt_namespace), GFP_KERNEL_ACCOUNT);
if (!new_ns) {
dec_mnt_namespaces(ucounts);
return ERR_PTR(-ENOMEM);
}
/* 命名空间不是匿名的(anon 为 false) */
if (!anon) {
/* 调用 ns_alloc_inum 为命名空间分配唯一编号 */
ret = ns_alloc_inum(&new_ns->ns);
if (ret) {
kfree(new_ns);
dec_mnt_namespaces(ucounts);
return ERR_PTR(ret);
}
}
new_ns->ns.ops = &mntns_operations;
if (!anon)
new_ns->seq = atomic64_inc_return(&mnt_ns_seq);
refcount_set(&new_ns->ns.count, 1);
refcount_set(&new_ns->passive, 1);
new_ns->mounts = RB_ROOT;
INIT_LIST_HEAD(&new_ns->mnt_ns_list);
RB_CLEAR_NODE(&new_ns->mnt_ns_tree_node);
init_waitqueue_head(&new_ns->poll);
new_ns->user_ns = get_user_ns(user_ns);
new_ns->ucounts = ucounts;
return new_ns;
}

mnt_add_to_ns 将挂载点添加到挂载命名空间

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
static void mnt_add_to_ns(struct mnt_namespace *ns, struct mount *mnt)
{
struct rb_node **link = &ns->mounts.rb_node;
struct rb_node *parent = NULL;
bool mnt_first_node = true, mnt_last_node = true;

WARN_ON(mnt_ns_attached(mnt));
mnt->mnt_ns = ns;
/* 遍历红黑树,根据挂载点的唯一 ID(mnt_id_unique)找到插入位置。
如果 ID 小于当前节点的 ID,移动到左子树;否则,移动到右子树。
更新 mnt_first_node 和 mnt_last_node 标志,记录是否是树中的第一个或最后一个节点 */
while (*link) {
parent = *link;
if (mnt->mnt_id_unique < node_to_mount(parent)->mnt_id_unique) {
link = &parent->rb_left;
mnt_last_node = false;
} else {
link = &parent->rb_right;
mnt_first_node = false;
}
}

if (mnt_last_node)
ns->mnt_last_node = &mnt->mnt_node;
if (mnt_first_node)
ns->mnt_first_node = &mnt->mnt_node;
rb_link_node(&mnt->mnt_node, parent, link);
rb_insert_color(&mnt->mnt_node, &ns->mounts);

// mnt_notify_add(mnt);
}

init_mount_tree 设置文件系统的根挂载点(rootfs)以及相关的挂载命名空间(mnt_namespace)

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 void __init init_mount_tree(void)
{
struct vfsmount *mnt;
struct mount *m;
struct mnt_namespace *ns;
struct path root;

mnt = vfs_kern_mount(&rootfs_fs_type, 0, "rootfs", NULL);
if (IS_ERR(mnt))
panic("Can't create rootfs");
/* 调用 alloc_mnt_ns 为根文件系统分配一个挂载命名空间(mnt_namespace */
ns = alloc_mnt_ns(&init_user_ns, false);
if (IS_ERR(ns))
panic("Can't allocate initial namespace");
/* 获取挂载点的实际结构(mount */
m = real_mount(mnt);
ns->root = m;
ns->nr_mounts = 1;
/* 将挂载点添加到命名空间 */
mnt_add_to_ns(ns, m);
init_task.nsproxy->mnt_ns = ns;
get_mnt_ns(ns);

root.mnt = mnt;
root.dentry = mnt->mnt_root;
/* 挂载点标记为锁定状态(MNT_LOCKED),防止其被卸载 */
mnt->mnt_flags |= MNT_LOCKED;

/* 设置当前进程的工作目录和根目录 */
set_fs_pwd(current->fs, &root);
set_fs_root(current->fs, &root);
/* 调用 mnt_ns_tree_add 将命名空间添加到挂载树中 */
mnt_ns_tree_add(ns);
}

mnt_init 设置与挂载点管理相关的缓存、哈希表和文件系统的核心数据结构

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
void __init mnt_init(void)
{
int err;

mnt_cache = kmem_cache_create("mnt_cache", sizeof(struct mount),
0, SLAB_HWCACHE_ALIGN|SLAB_PANIC|SLAB_ACCOUNT, NULL);

mount_hashtable = alloc_large_system_hash("Mount-cache",
sizeof(struct hlist_head),
mhash_entries, 19,
HASH_ZERO,
&m_hash_shift, &m_hash_mask, 0, 0);
mountpoint_hashtable = alloc_large_system_hash("Mountpoint-cache",
sizeof(struct hlist_head),
mphash_entries, 19,
HASH_ZERO,
&mp_hash_shift, &mp_hash_mask, 0, 0);

if (!mount_hashtable || !mountpoint_hashtable)
panic("Failed to allocate mount hash table\n");
/* 初始化内核文件系统 */
kernfs_init();
/* 初始化 sysfs 文件系统 */
err = sysfs_init();
if (err)
printk(KERN_WARNING "%s: sysfs_init error: %d\n",
__func__, err);
/* 创建文件系统对象 */
fs_kobj = kobject_create_and_add("fs", NULL);
if (!fs_kobj)
printk(KERN_WARNING "%s: kobj create error\n", __func__);
/* 初始化共享内存文件系统 */
shmem_init();
/* 初始化根文件系统 */
/* 未使能 IS_ENABLED(CONFIG_TMPFS) */
init_rootfs();
/* 始化挂载点树结构 */
init_mount_tree();
}

vfs_parse_monolithic_sep 解析 key[=val][,key[=val]]* 挂载数据

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
/**
* vfs_parse_monolithic_sep - 解析 key[=val][,key[=val]]* 挂载数据
* @fc: 要填充的超级块配置。
* @data: 要解析的数据
* @sep: 用于分隔下一个选项的回调函数
*
* 解析以 key[=val][,key[=val]]* 形式的数据块,使用自定义选项分隔回调函数。
*
* 成功时返回 0,失败时返回由 ->parse_option() fs_context 操作返回的错误。
*/
int vfs_parse_monolithic_sep(struct fs_context *fc, void *data,
char *(*sep)(char **))
{
char *options = data, *key;
int ret = 0;

if (!options)
return 0;

/* return 0; */
ret = security_sb_eat_lsm_opts(options, &fc->security);
if (ret)
return ret;

while ((key = sep(&options)) != NULL) {
if (*key) {
size_t v_len = 0;
char *value = strchr(key, '=');

if (value) {
if (unlikely(value == key))
continue;
*value++ = 0;
v_len = strlen(value);
}
ret = vfs_parse_fs_string(fc, key, value, v_len);
if (ret < 0)
break;
}
}

return ret;
}
EXPORT_SYMBOL(vfs_parse_monolithic_sep);

generic_parse_monolithic 解析 key[=val][,key[=val]]* 挂载数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static char *vfs_parse_comma_sep(char **s)
{
return strsep(s, ",");
}

/**
* generic_parse_monolithic - 解析 key[=val][,key[=val]]* 挂载数据
* @fc: 要填充的超级块配置。
* @data: 要解析的数据

解析以 key[=val][,key[=val]]* 形式存在的数据块。可以从 ->monolithic_mount_data() fs_context 操作中调用此函数。

成功时返回 0,失败时返回 ->parse_option() fs_context 操作返回的错误。
*/
int generic_parse_monolithic(struct fs_context *fc, void *data)
{
return vfs_parse_monolithic_sep(fc, data, vfs_parse_comma_sep);
}
EXPORT_SYMBOL(generic_parse_monolithic);

parse_monolithic_mount_data 解析单一挂载数据

1
2
3
4
5
6
7
8
9
10
int parse_monolithic_mount_data(struct fs_context *fc, void *data)
{
int (*monolithic_mount_data)(struct fs_context *, void *);

monolithic_mount_data = fc->ops->parse_monolithic;
if (!monolithic_mount_data)
monolithic_mount_data = generic_parse_monolithic;

return monolithic_mount_data(fc, data);
}

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);

vfs_kern_mount 用于内核挂载文件系统

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
struct vfsmount *vfs_kern_mount(struct file_system_type *type,
int flags, const char *name,
void *data)
{
struct fs_context *fc;
struct vfsmount *mnt;
int ret = 0;

if (!type)
return ERR_PTR(-EINVAL);

fc = fs_context_for_mount(type, flags);
if (IS_ERR(fc))
return ERR_CAST(fc);

if (name)
/* 如果提供了源名称(name),将其解析为挂载上下文中的参数 */
ret = vfs_parse_fs_string(fc, "source",
name, strlen(name));
if (!ret)
/* 源名称解析成功,进一步解析挂载数据(data),将其添加到挂载上下文中
挂载数据通常包含文件系统的配置选项。 */
ret = parse_monolithic_mount_data(fc, data);
if (!ret)
/* 执行挂载操作,返回挂载点 */
mnt = fc_mount(fc);
else
mnt = ERR_PTR(ret);
/* 调用 put_fs_context 释放挂载上下文,避免内存泄漏 */
put_fs_context(fc);
return mnt;
}
EXPORT_SYMBOL_GPL(vfs_kern_mount);

kern_mount 用于内核挂载文件系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct vfsmount *kern_mount(struct file_system_type *type)
{
struct vfsmount *mnt;
/* SB_KERNMOUNT:标志,表示这是一个内核挂载操作
NULL:挂载选项为空 */
mnt = vfs_kern_mount(type, SB_KERNMOUNT, type->name, NULL);
if (!IS_ERR(mnt)) {
/*
* 这是一个长期挂载,在文件系统注销之前卸载之前
* 不要释放 mnt
*/
/* 如果挂载成功,将其命名空间(mnt_ns)设置为 MNT_NS_INTERNAL。
MNT_NS_INTERNAL 表示这是一个内部挂载点,与用户空间的挂载命名空间隔离 */
real_mount(mnt)->mnt_ns = MNT_NS_INTERNAL;
}
return mnt;
}
EXPORT_SYMBOL_GPL(kern_mount);

mnt_alloc_id 为挂载点分配唯一的 ID

1
2
3
4
5
6
7
8
9
10
11
static int mnt_alloc_id(struct mount *mnt)
{
int res;

xa_lock(&mnt_id_xa);
res = __xa_alloc(&mnt_id_xa, &mnt->mnt_id, mnt, XA_LIMIT(1, INT_MAX), GFP_KERNEL);
if (!res)
mnt->mnt_id_unique = ++mnt_id_ctr;
xa_unlock(&mnt_id_xa);
return res;
}

alloc_vfsmnt 为挂载点分配内存并初始化

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
static struct mount *alloc_vfsmnt(const char *name)
{
struct mount *mnt = kmem_cache_zalloc(mnt_cache, GFP_KERNEL);
if (mnt) {
int err;

err = mnt_alloc_id(mnt);
if (err)
goto out_free_cache;

if (name)
mnt->mnt_devname = kstrdup_const(name,
GFP_KERNEL_ACCOUNT);
else
mnt->mnt_devname = "none";
if (!mnt->mnt_devname)
goto out_free_id;

#ifdef CONFIG_SMP
mnt->mnt_pcp = alloc_percpu(struct mnt_pcp);
if (!mnt->mnt_pcp)
goto out_free_devname;

this_cpu_add(mnt->mnt_pcp->mnt_count, 1);
#else
mnt->mnt_count = 1;
mnt->mnt_writers = 0;
#endif

INIT_HLIST_NODE(&mnt->mnt_hash);
INIT_LIST_HEAD(&mnt->mnt_child);
INIT_LIST_HEAD(&mnt->mnt_mounts);
INIT_LIST_HEAD(&mnt->mnt_list);
INIT_LIST_HEAD(&mnt->mnt_expire);
INIT_LIST_HEAD(&mnt->mnt_share);
INIT_LIST_HEAD(&mnt->mnt_slave_list);
INIT_LIST_HEAD(&mnt->mnt_slave);
INIT_HLIST_NODE(&mnt->mnt_mp_list);
INIT_LIST_HEAD(&mnt->mnt_umounting);
INIT_HLIST_HEAD(&mnt->mnt_stuck_children);
RB_CLEAR_NODE(&mnt->mnt_node);
mnt->mnt.mnt_idmap = &nop_mnt_idmap;
}
return mnt;

#ifdef CONFIG_SMP
out_free_devname:
kfree_const(mnt->mnt_devname);
#endif
out_free_id:
mnt_free_id(mnt);
out_free_cache:
kmem_cache_free(mnt_cache, mnt);
return NULL;
}

lock_mount_hash unlock_mount_hash 用于锁定和解锁挂载哈希表

1
2
3
4
5
6
7
8
9
10
static inline void lock_mount_hash(void)
{
write_seqlock(&mount_lock);
}

static inline void unlock_mount_hash(void)
{
write_sequnlock(&mount_lock);
}

vfs_create_mount 为已配置的超级块创建挂载点

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
/**
* vfs_create_mount - 为已配置的超级块创建挂载点
* @fc: 附加了超级块的配置上下文
*
* 为已配置的超级块创建挂载点。如有必要,调用者应在调用此函数之前调用 vfs_get_tree()。
*
* 请注意,这不会将挂载点附加到任何内容。
*/
struct vfsmount *vfs_create_mount(struct fs_context *fc)
{
struct mount *mnt;

if (!fc->root)
return ERR_PTR(-EINVAL);
/* 分配挂载点(vfsmount)的内存,并初始化其基本字段。*/
mnt = alloc_vfsmnt(fc->source);
if (!mnt)
return ERR_PTR(-ENOMEM);
/* 将挂载点标记为内部挂载(MNT_INTERNAL),表示该挂载点仅供内核使用 */
if (fc->sb_flags & SB_KERNMOUNT)
mnt->mnt.mnt_flags = MNT_INTERNAL;

atomic_inc(&fc->root->d_sb->s_active);
/* 挂载点的超级块(mnt_sb)、根目录(mnt_root)、挂载点(mnt_mountpoint)和父挂载点(mnt_parent) */
mnt->mnt.mnt_sb = fc->root->d_sb;
mnt->mnt.mnt_root = dget(fc->root);
mnt->mnt_mountpoint = mnt->mnt.mnt_root;
mnt->mnt_parent = mnt;

lock_mount_hash();
list_add_tail(&mnt->mnt_instance, &mnt->mnt.mnt_sb->s_mounts);
unlock_mount_hash();
return &mnt->mnt;
}
EXPORT_SYMBOL(vfs_create_mount);

fc_mount 完成文件系统的挂载操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct vfsmount *fc_mount(struct fs_context *fc)
{
/* 获取文件系统的树结构 */
int err = vfs_get_tree(fc);
if (!err) {
/* 树获取成功,释放超级块(superblock)的挂载锁(s_umount) */
up_write(&fc->root->d_sb->s_umount);
/* 创建挂载点(vfsmount)。
挂载点是文件系统在虚拟文件系统(VFS)中的表示,
用于连接文件系统树到挂载路径 */
return vfs_create_mount(fc);
}
return ERR_PTR(err);
}
EXPORT_SYMBOL(fc_mount);

copy_mnt_ns 复制挂载命名空间

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
__latent_entropy
struct mnt_namespace *copy_mnt_ns(unsigned long flags, struct mnt_namespace *ns,
struct user_namespace *user_ns, struct fs_struct *new_fs)
{
struct mnt_namespace *new_ns;
struct vfsmount *rootmnt = NULL, *pwdmnt = NULL;
struct mount *p, *q;
struct mount *old;
struct mount *new;
int copy_flags;

BUG_ON(!ns);
/* 未设置 CLONE_NEWNS 标志,表示新进程希望与父进程共享挂载命名空间 */
if (likely(!(flags & CLONE_NEWNS))) {
get_mnt_ns(ns);
return ns;
}

old = ns->root;

new_ns = alloc_mnt_ns(user_ns, false);
if (IS_ERR(new_ns))
return new_ns;

namespace_lock();
/* 初步:复制树拓扑结构 */
copy_flags = CL_COPY_UNBINDABLE | CL_EXPIRE;
/* 如果用户命名空间不同,设置额外的标志(CL_SHARED_TO_SLAVE)以处理挂载点的共享关系 */
if (user_ns != ns->user_ns)
copy_flags |= CL_SHARED_TO_SLAVE;
new = copy_tree(old, old->mnt.mnt_root, copy_flags);
if (IS_ERR(new)) {
namespace_unlock();
ns_free_inum(&new_ns->ns);
dec_mnt_namespaces(new_ns->ucounts);
mnt_ns_release(new_ns);
return ERR_CAST(new);
}
/* 如果用户命名空间不同,更新挂载树的锁状态以确保一致性 */
if (user_ns != ns->user_ns) {
lock_mount_hash();
lock_mnt_tree(new);
unlock_mount_hash();
}
new_ns->root = new;

/*
* 第二步:切换 tsk->fs->* 元素,并将新的 vfsmounts 标记为属于新的命名空间。
* 我们已经获取了一个私有的 fs_struct,因此不需要 tsk->fs->lock。
*/
p = old;
q = new;
/* 遍历旧挂载树和新挂载树,更新新命名空间的挂载点信息 */
while (p) {
mnt_add_to_ns(new_ns, q);
new_ns->nr_mounts++;
/* 如果提供了新的文件系统结构(new_fs),更新其根目录和当前工作目录的挂载点 */
if (new_fs) {
if (&p->mnt == new_fs->root.mnt) {
new_fs->root.mnt = mntget(&q->mnt);
rootmnt = &p->mnt;
}
if (&p->mnt == new_fs->pwd.mnt) {
new_fs->pwd.mnt = mntget(&q->mnt);
pwdmnt = &p->mnt;
}
}
p = next_mnt(p, old);
q = next_mnt(q, new);
if (!q)
break;
// 我们跳过的一个mntns绑定?
while (p->mnt.mnt_root != q->mnt.mnt_root)
p = next_mnt(skip_mnt_tree(p), old);
}
namespace_unlock();

/* 释放临时引用的挂载点资源,避免资源泄漏 */
if (rootmnt)
mntput(rootmnt);
if (pwdmnt)
mntput(pwdmnt);
/* 将新创建的命名空间添加到命名空间树中,并返回新命名空间 */
mnt_ns_tree_add(new_ns);
return new_ns;
}

Sysctl接口:配置挂载命名空间的最大挂载点数量

本代码片段的核心功能是向Linux内核的sysctl机制注册一个可配置的参数:mount-max。这个参数允许系统管理员在运行时通过/proc/sys/fs/mount-max这个虚拟文件来查看和修改单个挂载命名空间(mount namespace)中所允许的最大挂载点数量。这是一种内核参数动态调整机制,用于限制资源使用,防止潜在的滥用或错误导致系统挂载点数量失控。

实现原理分析

该功能完全构建在Linux的sysctl框架之上,该框架旨在将内核内部的变量安全地暴露给用户空间,以供查询和修改。其实现原理可分解为以下步骤:

  1. 变量定义: 内核中定义一个全局静态变量sysctl_mount_max,并赋予其一个默认值(100,000)。__read_mostly是一个编译器优化属性,它告知编译器此变量被读取的频率远高于写入,因此应将其放置在更容易被CPU缓存的内存区域,以提升性能。
  2. Sysctl表定义: 创建一个ctl_table结构体数组fs_namespace_sysctls。这个表是sysctl接口的核心,它描述了暴露给用户空间的参数的全部属性:
    • .procname: 指定在/proc/sys/fs/目录下创建的文件名,即mount-max
    • .data: 将此文件与内核变量sysctl_mount_max的地址进行绑定。这是实现文件读写与变量读写联动的关键。
    • .mode: 定义文件的访问权限(0644,即拥有者可读写,其他用户只读)。
    • .proc_handler: 指定一个处理函数proc_dointvec_minmax,这是一个内核提供的标准处理程序,用于读写整数值,并能进行范围检查(例如,确保设置的值不小于某个最小值)。
  3. 注册: 定义一个初始化函数init_fs_namespace_sysctls,并使用fs_initcall宏将其注册为文件系统子系统的初始化回调。当内核启动并初始化文件系统时,此函数会被调用。
  4. 激活: 初始化函数通过调用register_sysctl_init("fs", ...),将前面定义的fs_namespace_sysctls表注册到sysctl核心中。内核会据此在/proc/sys/下创建fs目录(如果尚不存在),并在其中创建mount-max文件,至此该参数便对用户空间可见并可操作。

代码分析

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
/* 定义一个挂载命名空间中允许的最大挂载点数量 */
// __read_mostly 是一个性能优化提示,告诉编译器这个变量大部分时间是只读的。
static unsigned int sysctl_mount_max __read_mostly = 100000;

// 仅当内核配置了CONFIG_SYSCTL时,以下代码才会被编译。
#ifdef CONFIG_SYSCTL
// 定义一个sysctl表,用于描述/proc/sys/fs/下的参数。
static const struct ctl_table fs_namespace_sysctls[] = {
{
// .procname: 在procfs中显示的文件名。最终路径为 /proc/sys/fs/mount-max。
.procname = "mount-max",
// .data: 指向内核中实际存储该值的变量的指针。
.data = &sysctl_mount_max,
// .maxlen: 变量的最大长度,此处为一个无符号整数的大小。
.maxlen = sizeof(unsigned int),
// .mode: 文件权限,0644表示所有者可读写,组用户和其他用户只读。
.mode = 0644,
// .proc_handler: 处理用户读写该文件的内核函数。
// proc_dointvec_minmax 是一个标准处理整数的函数,并支持最小值/最大值检查。
.proc_handler = proc_dointvec_minmax,
// .extra1: 传递给proc_handler的额外参数。SYSCTL_ONE通常表示允许的最小值为1。
.extra1 = SYSCTL_ONE,
},
};

// init_fs_namespace_sysctls: 初始化函数,用于注册上述sysctl表。
// __init 标记表示该函数仅在内核初始化期间使用,之后其占用的内存可以被回收。
static int __init init_fs_namespace_sysctls(void)
{
// 调用内核函数,将fs_namespace_sysctls表注册到/proc/sys/fs/目录下。
register_sysctl_init("fs", fs_namespace_sysctls);
return 0;
}
// fs_initcall: 一个宏,用于将init_fs_namespace_sysctls函数注册为文件系统初始化阶段的回调。
// 这确保了在procfs准备好之后再进行注册。
fs_initcall(init_fs_namespace_sysctls);

#endif /* CONFIG_SYSCTL */

VFS写访问控制:确保文件系统状态变更的安全性

本代码片段是Linux VFS层中负责管理对文件系统写操作权限的核心机制。它的主要功能不是简单地检查文件系统是否为只读,而是实现一个复杂而健壮的同步协议。这个协议确保了当一个任务(例如,用户执行remount,ro命令或fsfreeze)试图将文件系统变为只读或“冻结”状态时,不会与正在进行或即将开始的写操作(如write(), unlink())发生竞争,从而避免数据损坏。可以把它想象成是文件系统写操作的“空中交通管制”,在改变“跑道”(文件系统状态)之前,必须确保所有“飞机”(写操作)都已安全落地或暂停起飞。

实现原理分析

这个机制的核心思想是一个多阶段的“握手”协议,它使用原子计数器、特殊标志位和内存屏障来协调两类活动:

  1. 写者(Writers): 任何想要向文件系统写入数据的内核路径(例如,处理write()系统调用)。
  2. 状态改变者(State Changers): 想要将文件系统变为只读或冻结状态的内核路径(例如,处理mount(MS_REMOUNT | MS_RDONLY))。

这个握手协议的流程如下:

对于一个“写者” (mnt_want_write):

  1. 第一道门:冻结保护 (sb_start_write): 写者首先通知超级块(superblock),它想要开始写操作。如果文件系统当前处于“冻结”(frozen)状态,sb_start_write将会阻塞,直到文件系统被“解冻”。这是第一层保护。
  2. 第二道门:只读挂载保护 (mnt_get_write_access): 这是更复杂的一步。
    a. 宣告意图: 写者首先原子地增加一个“写者计数器”(mnt_inc_writers)。这就像是举手说:“我正准备开始写!”。
    b. 检查“暂停”标志: 写者接着检查一个名为MNT_WRITE_HOLD的标志。当一个“状态改变者”想要将文件系统变为只读时,它会先设置这个标志,意为:“所有新的写者请在此暂停!”。如果写者看到这个标志被设置,它就会在一个循环中等待,直到标志被清除。
    c. 最终确认: 当MNT_WRITE_HOLD标志被清除后(意味着状态改变已经完成),写者并不会立即开始写。它会最后再调用一次mnt_is_readonly来检查文件系统现在是否已经是只读的。因为可能就在它等待的期间,文件系统已经成功地被设置为只读了。
    d. 成功或放弃: 如果此时文件系统是只读的,写者就必须放弃,它会递减之前增加的写者计数器,并返回-EROFS错误。如果不是只读的,它就成功获得了写权限,函数返回0,写操作可以继续。
  3. 完成写入: 写操作完成后,写者必须调用mnt_drop_write(或其底层函数mnt_put_write_accesssb_end_write),它们会递减写者计数器和超级块的写计数。

内存屏障 (smp_mb, smp_rmb) 的关键作用:

在多核处理器上,一个CPU对内存的写入操作不会立即对所有其他CPU可见。内存屏障就是用来强制实施一个“可见性”顺序的指令。

  • smp_mb() (写者端): 保证“我增加了写者计数器”这个事实,必须在“我检查MNT_WRITE_HOLD标志”这个动作之前,被所有其他CPU看到。这防止了状态改变者错误地认为没有写者而继续操作。
  • smp_rmb() (写者端): 保证“我看到了MNT_WRITE_HOLD标志已被清除”这个事实之后,我才能去读取最终的只读标志。这确保了我看到的是状态改变完成之后的最终结果。

代码分析

mnt_is_readonly 函数

这是一个经过精心设计的只读状态检查函数,它不仅仅是读取一个标志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static int mnt_is_readonly(struct vfsmount *mnt)
{
// 步骤1: 快速检查。s_readonly_remount是remount过程开始时设置的标志。
// READ_ONCE确保我们从内存真实地读取该值,而不是使用寄存器中可能过时的值。
if (READ_ONCE(mnt->mnt_sb->s_readonly_remount))
return 1; // 如果remount正在进行中,就直接认为它是只读的。

/*
* 步骤2: 内存屏障。这是为了解决竞争条件的关键。
* 它确保了:如果我们在上面没有看到s_readonly_remount被设置,那么我们也
* 不会看到remount操作稍后对MNT_READONLY等标志的修改。
* 这就强制了一个“观察顺序”,防止我们看到一个不一致的、半完成的状态。
*/
smp_rmb();

// 步骤3: 调用内部函数,检查最终的只读标志(MNT_READONLY和SB_RDONLY)。
return __mnt_is_readonly(mnt);
}

mnt_get_write_access 函数

这是专门处理与remount,ro竞争的核心函数。

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
int mnt_get_write_access(struct vfsmount *m)
{
struct mount *mnt = real_mount(m); // 获取内部的mount结构体。
int ret = 0;

preempt_disable(); // 禁用内核抢占,进入一个微小的临界区。
mnt_inc_writers(mnt); // 原子地增加写者计数,宣告“我打算写”。

/*
* 步骤A: 全功能内存屏障。
* 确保上面对写者计数的增加,对所有其他CPU都是可见的,
* 然后我们才能继续去检查MNT_WRITE_HOLD标志。
*/
smp_mb();

might_lock(&mount_lock.lock); // 调试辅助,告诉锁检查器我们可能会获取这个锁。

// 步骤B: 等待循环。如果remount操作设置了MNT_WRITE_HOLD,就在此等待。
while (READ_ONCE(mnt->mnt.mnt_flags) & MNT_WRITE_HOLD) {
if (!IS_ENABLED(CONFIG_PREEMPT_RT)) {
cpu_relax(); // 在非实时内核中,只是一个CPU提示,表示在忙等。
} else {
// 在实时内核中,这是一个更复杂的机制,用于防止优先级反转。
// 它会短暂地重新启用抢占,尝试获取并释放一个全局锁来主动让出CPU。
/* 我们并不需要这个锁来保护任何数据,我们的目的只是为了“排队”。所以,我们立即调用unlock_mount_hash()将其释放,以免阻塞其他真正需要这个锁的任务。 */
preempt_enable();
lock_mount_hash();
unlock_mount_hash();
preempt_disable();
}
}

/*
* 步骤C: 读内存屏障。
* 确保:既然我们已经看到MNT_WRITE_HOLD被清除了,那么我们也一定能看到
* remount操作对s_readonly_remount或最终只读标志的修改。
* 这关闭了竞争的窗口。
*/
smp_rmb();

// 步骤D: 最终检查。在所有的同步和等待之后,最后再确认一次文件系统是否真的只读。
if (mnt_is_readonly(m)) {
mnt_dec_writers(mnt); // 如果是只读,就撤销之前的写意图。
ret = -EROFS; // 设置返回值为“只读文件系统”。
}
preempt_enable(); // 退出临界区,允许内核抢占。

return ret;
}
EXPORT_SYMBOL_GPL(mnt_get_write_access);

mnt_want_write 函数

这是提供给内核大部分代码使用的顶层API,它同时处理“冻结”和“只读”两种状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int mnt_want_write(struct vfsmount *m)
{
int ret;

// 第一道关卡:处理文件系统冻结(freeze)。
// 如果文件系统被冻结,这个函数会等待直到解冻。
sb_start_write(m->mnt_sb);

// 第二道关卡:处理只读挂载(remount,ro)。
ret = mnt_get_write_access(m);

// 如果第二道关卡失败了(返回错误),我们必须撤销第一道关卡的操作。
if (ret)
sb_end_write(m->mnt_sb);

return ret;
}
EXPORT_SYMBOL_GPL(mnt_want_write);