[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 :隔离了主机名和域名(通过uname
和sethostname
系统调用)。
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的核心原理是在内核数据结构中添加一层间接性,使得对全局资源的访问变成了对特定命名空间内资源的访问。
核心数据结构 (struct nsproxy
) :每个进程的描述符(task_struct
)中都有一个指向struct nsproxy
的指针。这个nsproxy
结构体则包含了一系列指向该进程所属的各种具体命名空间(如mnt_ns
, uts_ns
, pid_ns_for_children
等)的指针。当一个进程需要访问某个全局资源时,内核会通过其nsproxy
找到对应的命名空间,并在这个命名空间的上下文中执行操作。
创建与加入 (clone
, unshare
, setns
) :
clone()
:在创建新进程时,可以通过传递一系列CLONE_NEW*
标志(如CLONE_NEWPID
)来为新进程创建并进入一个新的命名空间。
unshare()
:允许当前进程“脱离”其当前的命名空间,并为自己创建一个新的命名空间。
setns()
:允许当前进程加入一个已经存在的命名空间(通过该命名空间的文件描述符)。
用户命名空间与权能(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 enum ucount_type { UCOUNT_USER_NAMESPACES, UCOUNT_PID_NAMESPACES, UCOUNT_UTS_NAMESPACES, UCOUNT_IPC_NAMESPACES, UCOUNT_NET_NAMESPACES, UCOUNT_MNT_NAMESPACES, UCOUNT_CGROUP_NAMESPACES, UCOUNT_TIME_NAMESPACES, #ifdef CONFIG_INOTIFY_USER UCOUNT_INOTIFY_INSTANCES, UCOUNT_INOTIFY_WATCHES, #endif #ifdef CONFIG_FANOTIFY UCOUNT_FANOTIFY_GROUPS, UCOUNT_FANOTIFY_MARKS, #endif 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 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
对象。
从数据结构和算法设计的角度来看, 整个代码片段展现了对性能和内存效率的精细权衡:
哈希表大小的动态调整 : 通过UIDHASH_BITS
宏, 哈希表的大小(UIDHASH_SZ
)可以在编译时根据内核配置(CONFIG_BASE_SMALL
)进行调整。
CONFIG_BASE_SMALL
: 通常在资源极度受限的嵌入式系统中启用。在这种情况下, 为了节省内存, 哈希表的大小会被设置为1 << 3 = 8
。
在更”完整”的系统中, 哈希表的大小会被设置为1 << 7 = 128
。
哈希函数 (__uidhashfn
) : 此函数负责将一个UID值映射到哈希表的索引。
它的实现非常简单, 但却巧妙地结合了位移和加法操作: (((uid >> UIDHASH_BITS) + uid) & UIDHASH_MASK)
。这种组合通常能够提供相对较好的散列效果, 同时又避免了复杂的乘除法运算, 从而保证了极高的执行效率。
自旋锁 (uidhash_lock
) : 这是保证哈希表并发安全的关键。由于对哈希表的修改(如插入和删除)不是原子操作, 必须使用锁来保护, 以防止多个进程或中断同时修改哈希表而导致数据损坏。
重要考虑 : 代码注释明确指出, 这个锁需要在进程上下文、软中断/tasklet上下文(在task-struct被RCU释放时), 甚至在禁用本地中断的情况下被获取。这要求锁机制必须同时满足以下两个条件:
能够防止抢占(进程上下文之间)。
能够防止中断(包括软中断)的干扰。 自旋锁(加上_irqsave
后缀)是满足这些需求的最佳选择。在单核系统中, 它通过禁用内核抢占和本地中断来保证临界区的原子性。
全局根用户实例 (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 #define UIDHASH_BITS (IS_ENABLED(CONFIG_BASE_SMALL) ? 3 : 7) #define UIDHASH_SZ (1 << UIDHASH_BITS) #define UIDHASH_MASK (UIDHASH_SZ - 1) #define __uidhashfn(uid) (((uid >> UIDHASH_BITS) + uid) & UIDHASH_MASK) #define uidhashentry(uid) (uidhash_table + __uidhashfn((__kuid_val(uid)))) static struct kmem_cache *uid_cachep ;static struct hlist_head uidhash_table [UIDHASH_SZ ];static DEFINE_SPINLOCK (uidhash_lock) ;struct user_struct root_user = { .__count = REFCOUNT_INIT(1 ), .uid = GLOBAL_ROOT_UID, .ratelimit = RATELIMIT_STATE_INIT(root_user.ratelimit, 0 , 0 ), }; static void uid_hash_insert (struct user_struct *up, struct hlist_head *hashent) { hlist_add_head(&up->uidhash_node, hashent); }
uid_cache_init: 初始化用户ID缓存 此函数的核心作用是在Linux内核启动的早期阶段, 初始化一个用于高效管理和查找struct user_struct
对象的高速缓存系统 。struct user_struct
是内核中用来追踪每个独立用户(由UID标识)所拥有的资源(如打开的文件数量、正在运行的进程数等)的核心数据结构。
这个初始化过程的根本原理是结合使用两种经典的高性能数据结构——SLAB缓存和哈希表——来构建一个专门为user_struct
优化的对象分配与查找机制 。
工作流程:
创建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
)。这表明该缓存对于系统的正常运行是至关重要的 , 如果无法创建, 系统也无法继续。
初始化哈希表 (INIT_HLIST_HEAD
) :
函数接着遍历一个名为uidhash_table
的数组, 并将数组中的每一个元素初始化为一个哈希链表的头。
原理 : uidhash_table
是一个哈希表 , 它是实现快速查找的关键。当需要查找一个特定UID对应的user_struct
时, 内核会首先根据UID计算出一个哈希值, 这个哈希值就是该哈希表数组的索引。然后, 内核只需遍历该索引位置上的短链表, 就可以快速找到目标对象, 而无需线性扫描所有存在的user_struct
。这个操作的平均时间复杂度是O(1)。
初始化并插入根用户 (root_user
) :
系统必须有一个”根用户”(root, UID=0)的实例。root_user
是一个静态定义的全局变量。
函数首先为root_user
分配其私有的epoll
资源。
然后, 它获取一个自旋锁uidhash_lock
来保护哈希表的并发访问。
最后, 它调用uid_hash_insert
, 将这个全局的root_user
实例插入到哈希表的第0个桶(bucket)中。
注册为早期初始化调用 (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 static int __init uid_cache_init (void ) { int n; uid_cachep = kmem_cache_create("uid_cache" , sizeof (struct user_struct), 0 , SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL ); for (n = 0 ; n < UIDHASH_SZ; ++n) INIT_HLIST_HEAD(uidhash_table + n); if (user_epoll_alloc(&root_user)) panic("root_user epoll percpu counter alloc failed" ); spin_lock_irq(&uidhash_lock); uid_hash_insert(&root_user, uidhashentry(GLOBAL_ROOT_UID)); spin_unlock_irq(&uidhash_lock); return 0 ; } subsys_initcall(uid_cache_init);
kernel/ucount.c 用户命名空间 Sysctl 接口的实现 此代码片段是Linux内核用户命名空间(User Namespace)与sysctl
接口集成的核心实现 。它的根本原理是为每一个用户命名空间动态地创建和注册一套独立的sysctl
条目, 这些条目允许在运行时查看和修改该特定命名空间内的资源限制 。
这套机制通过一系列精巧的回调函数和每个命名空间私有的数据结构, 实现了以下关键功能:
上下文感知 : 当一个进程访问/proc/sys/user/
下的文件时, 内核能够通过set_lookup
回调函数, 自动识别出该进程属于哪个用户命名空间, 并将其导向该命名空间私有的sysctl
数据集。
动态权限控制 : 对这些文件的访问权限不是静态的, 而是通过set_permissions
回调函数动态计算的。它会检查当前进程在其所属的用户命名空间内 是否具有CAP_SYS_RESOURCE
权限。这使得一个在命名空间内拥有”root”权限的进程可以修改其资源限制, 而其他进程则最多只能读取, 提供了细粒度的、符合命名空间隔离原则的安全性。
资源隔离 : 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 static struct ctl_table_set *set_lookup (struct ctl_table_root *root) { return ¤t_user_ns()->set ; } static int set_is_seen (struct ctl_table_set *set ) { return ¤t_user_ns()->set == set ; } 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; } static struct ctl_table_root set_root = { .lookup = set_lookup, .permissions = set_permissions, }; static long ue_zero = 0 ;static long ue_int_max = INT_MAX;#define UCOUNT_ENTRY(name) \ { \ .procname = name, \ .maxlen = sizeof(long), \ .mode = 0644, \ .proc_handler = proc_doulongvec_minmax, \ .extra1 = &ue_zero, \ .extra2 = &ue_int_max, \ } static const struct ctl_table user_table [] = { UCOUNT_ENTRY("max_user_namespaces" ), UCOUNT_ENTRY("max_pid_namespaces" ), }; bool setup_userns_sysctls (struct user_namespace *ns) { struct ctl_table *tbl ; BUILD_BUG_ON(ARRAY_SIZE(user_table) != UCOUNT_COUNTS); setup_sysctl_set(&ns->set , &set_root, set_is_seen); tbl = kmemdup(user_table, sizeof (user_table), GFP_KERNEL); if (tbl) { int i; for (i = 0 ; i < UCOUNT_COUNTS; i++) { tbl[i].data = &ns->ucount_max[i]; } 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 static void hlist_add_ucounts (struct ucounts *ucounts) { struct hlist_nulls_head *hashent = ucounts_hashentry(ucounts->ns, ucounts->uid); spin_lock_irq(&ucounts_lock); hlist_nulls_add_head_rcu(&ucounts->node, hashent); 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 static inline long get_userns_rlimit_max (struct user_namespace *ns, enum rlimit_type type) { 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 long inc_rlimit_ucounts (struct ucounts *ucounts, enum rlimit_type type, long v) { struct ucounts *iter ; long max = LONG_MAX; long ret = 0 ; for (iter = ucounts; iter; iter = iter->ns->ucounts) { long new = atomic_long_add_return(v, &iter->rlimit[type]); if (new < 0 || new > max) ret = LONG_MAX; else if (iter == ucounts) ret = new; max = get_userns_rlimit_max(iter->ns, type); } return ret; }
user_namespace_sysctl_init: 初始化用户命名空间的sysctl接口与资源计数 此函数是Linux内核在启动过程中的一个初始化函数, 其核心作用是为用户命名空间(User Namespace)这一特性建立必要的基础设施 , 主要包含两个方面:
创建Sysctl接口 : 如果内核配置了sysctl
支持, 它会在/proc/sys/
目录下创建一个名为user
的子目录 (/proc/sys/user/
)。这个目录将作为根目录, 用于未来挂载所有与用户命名空间相关的可调参数(例如, max_user_namespaces
等), 使得系统管理员可以在运行时查看和修改这些限制。
初始化资源计数 : 它为系统中的第一个、也是根用户命名空间(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 int user_namespace_sysctl_init (void ) { #ifdef CONFIG_SYSCTL static struct ctl_table_header *user_header ; static struct ctl_table empty [1]; user_header = register_sysctl_sz("user" , empty, 0 ); kmemleak_ignore(user_header); BUG_ON(!user_header); BUG_ON(!setup_userns_sysctls(&init_user_ns)); #endif hlist_add_ucounts(&init_ucounts); inc_rlimit_ucounts(&init_ucounts, UCOUNT_RLIMIT_NPROC, 1 ); return 0 ; } 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 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; 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); } if (!anon) { 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; 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); }
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" ); ns = alloc_mnt_ns(&init_user_ns, false ); if (IS_ERR(ns)) panic("Can't allocate initial namespace" ); 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->mnt_flags |= MNT_LOCKED; set_fs_pwd(current->fs, &root); set_fs_root(current->fs, &root); 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(); 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(); 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 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 ; 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, "," ); } 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 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) ret = vfs_parse_fs_string(fc, "source" , name, strlen (name)); if (!ret) ret = parse_monolithic_mount_data(fc, data); if (!ret) mnt = fc_mount(fc); else mnt = ERR_PTR(ret); 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 ; mnt = vfs_kern_mount(type, SB_KERNMOUNT, type->name, NULL ); if (!IS_ERR(mnt)) { 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 struct vfsmount *vfs_create_mount (struct fs_context *fc) { struct mount *mnt ; if (!fc->root) return ERR_PTR(-EINVAL); mnt = alloc_vfsmnt(fc->source); if (!mnt) return ERR_PTR(-ENOMEM); if (fc->sb_flags & SB_KERNMOUNT) mnt->mnt.mnt_flags = MNT_INTERNAL; atomic_inc (&fc->root->d_sb->s_active); 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) { up_write(&fc->root->d_sb->s_umount); 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); 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; 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; p = old; q = new; while (p) { mnt_add_to_ns(new_ns, q); new_ns->nr_mounts++; 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 ; 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
框架之上,该框架旨在将内核内部的变量安全地暴露给用户空间,以供查询和修改。其实现原理可分解为以下步骤:
变量定义 : 内核中定义一个全局静态变量sysctl_mount_max
,并赋予其一个默认值(100,000)。__read_mostly
是一个编译器优化属性,它告知编译器此变量被读取的频率远高于写入,因此应将其放置在更容易被CPU缓存的内存区域,以提升性能。
Sysctl表定义 : 创建一个ctl_table
结构体数组fs_namespace_sysctls
。这个表是sysctl
接口的核心,它描述了暴露给用户空间的参数的全部属性:
.procname
: 指定在/proc/sys/fs/
目录下创建的文件名,即mount-max
。
.data
: 将此文件与内核变量sysctl_mount_max
的地址进行绑定。这是实现文件读写与变量读写联动的关键。
.mode
: 定义文件的访问权限(0644,即拥有者可读写,其他用户只读)。
.proc_handler
: 指定一个处理函数proc_dointvec_minmax
,这是一个内核提供的标准处理程序,用于读写整数值,并能进行范围检查(例如,确保设置的值不小于某个最小值)。
注册 : 定义一个初始化函数init_fs_namespace_sysctls
,并使用fs_initcall
宏将其注册为文件系统子系统的初始化回调。当内核启动并初始化文件系统时,此函数会被调用。
激活 : 初始化函数通过调用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 static unsigned int sysctl_mount_max __read_mostly = 100000 ;#ifdef CONFIG_SYSCTL static const struct ctl_table fs_namespace_sysctls [] = { { .procname = "mount-max" , .data = &sysctl_mount_max, .maxlen = sizeof (unsigned int ), .mode = 0644 , .proc_handler = proc_dointvec_minmax, .extra1 = SYSCTL_ONE, }, }; static int __init init_fs_namespace_sysctls (void ) { register_sysctl_init("fs" , fs_namespace_sysctls); return 0 ; } fs_initcall(init_fs_namespace_sysctls); #endif
VFS写访问控制:确保文件系统状态变更的安全性 本代码片段是Linux VFS层中负责管理对文件系统写操作权限的核心机制。它的主要功能不是简单地检查文件系统是否为只读,而是实现一个复杂而健壮的同步协议。这个协议确保了当一个任务(例如,用户执行remount,ro
命令或fsfreeze
)试图将文件系统变为只读或“冻结”状态时,不会与正在进行或即将开始的写操作(如write()
, unlink()
)发生竞争,从而避免数据损坏。可以把它想象成是文件系统写操作的“空中交通管制”,在改变“跑道”(文件系统状态)之前,必须确保所有“飞机”(写操作)都已安全落地或暂停起飞。
实现原理分析 这个机制的核心思想是一个多阶段的“握手”协议,它使用原子计数器、特殊标志位和内存屏障来协调两类活动:
写者(Writers) : 任何想要向文件系统写入数据的内核路径(例如,处理write()
系统调用)。
状态改变者(State Changers) : 想要将文件系统变为只读或冻结状态的内核路径(例如,处理mount(MS_REMOUNT | MS_RDONLY)
)。
这个握手协议的流程如下:
对于一个“写者” (mnt_want_write
) :
第一道门:冻结保护 (sb_start_write
) : 写者首先通知超级块(superblock),它想要开始写操作。如果文件系统当前处于“冻结”(frozen)状态,sb_start_write
将会阻塞,直到文件系统被“解冻”。这是第一层保护。
第二道门:只读挂载保护 (mnt_get_write_access
) : 这是更复杂的一步。 a. 宣告意图 : 写者首先原子地增加一个“写者计数器”(mnt_inc_writers
)。这就像是举手说:“我正准备开始写!”。 b. 检查“暂停”标志 : 写者接着检查一个名为MNT_WRITE_HOLD
的标志。当一个“状态改变者”想要将文件系统变为只读时,它会先设置这个标志,意为:“所有新的写者请在此暂停!”。如果写者看到这个标志被设置,它就会在一个循环中等待,直到标志被清除。 c. 最终确认 : 当MNT_WRITE_HOLD
标志被清除后(意味着状态改变已经完成),写者并不会立即开始写。它会最后再调用一次mnt_is_readonly
来检查文件系统现在 是否已经是只读的。因为可能就在它等待的期间,文件系统已经成功地被设置为只读了。 d. 成功或放弃 : 如果此时文件系统是只读的,写者就必须放弃,它会递减之前增加的写者计数器,并返回-EROFS
错误。如果不是只读的,它就成功获得了写权限,函数返回0,写操作可以继续。
完成写入 : 写操作完成后,写者必须调用mnt_drop_write
(或其底层函数mnt_put_write_access
和sb_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) { if (READ_ONCE(mnt->mnt_sb->s_readonly_remount)) return 1 ; smp_rmb(); 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); int ret = 0 ; preempt_disable(); mnt_inc_writers(mnt); smp_mb(); might_lock(&mount_lock.lock); while (READ_ONCE(mnt->mnt.mnt_flags) & MNT_WRITE_HOLD) { if (!IS_ENABLED(CONFIG_PREEMPT_RT)) { cpu_relax(); } else { preempt_enable(); lock_mount_hash(); unlock_mount_hash(); preempt_disable(); } } smp_rmb(); 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; sb_start_write(m->mnt_sb); ret = mnt_get_write_access(m); if (ret) sb_end_write(m->mnt_sb); return ret; } EXPORT_SYMBOL_GPL(mnt_want_write);