[TOC]

在这里插入图片描述

kernel/utsname_sysctl.c UTS命名空间与Sysctl接口 管理和暴露系统标识信息

历史与背景

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

这项技术是为了提供一个标准化的、可在运行时动态查询和修改系统核心标识信息的机制而诞生的。这些标识信息,统称为UTS(UNIX Time-sharing System)名称,包括:

  • 主机名 (Hostname)域名 (Domainname):在网络中唯一标识一台机器。
  • 操作系统发布版本 (OS Release)版本号 (Version):允许软件检查其运行的内核版本,以确定兼容性或是否存在特定功能/错误。
  • 硬件架构 (Machine):指明系统运行的CPU架构(如 x86_64, aarch64)。

utsname_sysctl.c 的核心任务是:

  1. 提供访问接口:允许用户空间程序和管理员通过 uname(2) 系统调用和 /proc/sys/kernel/ 文件系统接口来读取这些信息。
  2. 提供配置能力:尤其是对于主机名和域名,系统必须提供一种在运行时进行修改的方法,而无需重启。sysctl 接口(即/proc/sys/下的文件)正是为此而生。

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

utsname 结构和 uname() 系统调用是源自经典UNIX的标准。然而,kernel/utsname_sysctl.c 的实现逻辑经历了一个重大的演进:

  • 全局单实例时代:在早期内核中,整个系统只有一个全局的 utsname 实例。设置主机名会影响整个操作系统。
  • UTS命名空间的引入 (Major Milestone):Linux 2.6.19引入了UTS命名空间(UTS Namespaces)。这是一个革命性的变化,它允许系统上存在多个隔离的 utsname 实例。这意味着,每个容器都可以拥有自己独立的主机名和域名,而不会与宿主机或其他容器冲突。utsname_sysctl.c 的代码逻辑必须从操作全局变量,演变为操作与当前进程相关联的特定UTS命名空间。这是该文件现代实现的核心。

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

该技术是Linux内核中一个非常基础且稳定的部分。它不是一个经常变动的功能,但却是所有现代Linux系统不可或缺的组成部分。其主流应用包括:

  • 系统基础工具hostnameunamedomainname 等命令行工具直接依赖此机制。
  • 容器化:这是UTS命名空间最广泛的应用。Docker、Kubernetes、Podman等所有容器技术都利用此机制为每个容器提供隔离的主机名。
  • 系统初始化:在系统启动过程中,初始化脚本(如systemd)会通过写入 /proc/sys/kernel/hostname 来设置系统的主机名。

核心原理与设计

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

kernel/utsname_sysctl.c 的核心是作为UTS命名空间数据sysctl文件系统接口之间的桥梁。

  1. 数据存储:内核为每个UTS命名空间维护一个 struct uts_namespace 结构体。这个结构体中包含了 nodename (主机名)、domainname 等字符串数组。系统中的每个进程都通过其 nsproxy 指针关联到一个特定的 uts_namespace
  2. Sysctl注册:该文件定义了一个 ctl_table(控制表)。这个表将 /proc/sys/kernel/ 目录下的特定文件名(如hostnamedomainnameosrelease)映射到该文件中实现的特定处理函数(handler functions)。
  3. 处理请求
    • 读操作:当用户读取 /proc/sys/kernel/hostname 时,内核的VFS(虚拟文件系统)层会调用在 ctl_table 中注册的 handler。该 handler 会找到当前进程所属的 uts_namespace,从中读取 nodename 字符串,并将其复制到用户空间。
    • 写操作:当用户(需要CAP_SYS_ADMIN权限)写入 /proc/sys/kernel/hostname 时,handler 会执行权限检查,然后将来自用户空间的新主机名复制到当前进程所属的 uts_namespace 结构中,从而完成修改。
  4. 命名空间隔离:关键在于,所有的操作都是针对 current->nsproxy->uts_ns,即当前进程的命名空间。因此,在一个容器内修改主机名,只会影响该容器的 uts_namespace,而不会影响宿主机或其他容器。

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

  • 灵活性与动态性:允许在系统运行时轻松更改关键的系统标识符。
  • 命名空间隔离:为容器化等虚拟化技术提供了基础,使得主机名和域名可以在每个容器中独立存在。
  • 标准化的文件接口/proc/sys/ 提供了一个简单、易于脚本操作的文本文件接口,与传统的 sethostname(2) 系统调用互为补充。
  • 一致性:确保通过 sysctl 接口所做的更改能立即通过 uname(2) 系统调用反映出来,保证了数据的一致性。

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

  • 固定长度限制:UTS名称的长度受到内核中 __NEW_UTS_LEN 宏(通常为64)的硬编码限制。
  • sysctl 接口的局限性:对于简单的字符串读写,sysctl 足够简单。但对于更复杂的、结构化的数据配置,netlinkconfigfs 等接口通常被认为是更现代、更强大的选择。
  • 权限模型:修改主机名等操作需要 CAP_SYS_ADMIN 权能,这是一个非常强大的权能。虽然这是必要的,但也意味着任何需要此操作的进程都需要较高的权限。

使用场景

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

  • 系统初始化配置主机名:在系统首次启动或通过自动化工具(如Ansible, Puppet)配置时,最直接的方式就是写入 /proc/sys/kernel/hostname
    • 示例:echo "web-server-01" > /proc/sys/kernel/hostname
  • 容器运行时:当启动一个新容器时,容器运行时(如Docker)会为该容器创建一个新的UTS命名空间,并通过此机制设置容器内部的主机名。
    • 示例:docker run --hostname my-container-name -it ubuntu,Docker在后台为容器设置了主机名。
  • 监控和资产管理:监控脚本或代理需要获取内核版本号来上报或判断兼容性。最简单的方式是读取 /proc/sys/kernel/osrelease
    • 示例:kernel_version=$(cat /proc/sys/kernel/osrelease)
  • 临时修改主机名进行测试:在不永久更改配置文件的情况下,临时修改主机名进行网络测试。

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

该技术的功能非常专一,因此不存在“不推荐”的场景,只有“不适用”的场景。例如,配置网络IP地址、路由表或防火墙规则等,这些都属于网络子系统的范畴,需要使用 iproute2(netlink)、nftables 等专用工具,而与 utsname 无关。

对比分析

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

utsname_sysctl.c 提供的 sysctl 文件接口,最直接的对比对象是 sethostname(2)gethostname(2) 系统调用。

特性 Sysctl 接口 (/proc/sys/kernel/...) sethostname(2) / gethostname(2) 系统调用
功能概述 通过读写文件来查询和修改UTS名称。 通过函数调用来查询和修改UTS名称。
实现方式 VFS层将文件操作映射到内核中的ctl_table处理器。 标准的系统调用接口,通过软件中断进入内核执行相应函数。
底层逻辑 共享sysctl的写操作处理函数最终会调用与sethostname系统调用相同的内部内核函数(如set_uts_name)来修改uts_namespace结构体。它们是同一功能的两个不同入口。 共享。直接调用内核的内部函数来完成操作。
易用性 对脚本友好。在shell脚本或简单的自动化任务中非常易于使用,无需编写C代码。 对程序友好。在C/C++等编译型语言编写的应用程序中,是标准的、可移植的(POSIX)方式。
性能开销 略高。涉及VFS路径查找、文件打开/关闭等上下文切换开销。 略低。更直接的系统调用路径,开销更小。
主要用途 系统管理、自动化脚本、临时配置。 hostname等系统命令的底层实现、需要设置主机名的应用程序。

与Netlink/ConfigFS对比

  • Sysctl:适用于简单的、扁平化的键值对(如一个字符串、一个整数)。
  • Netlink:基于套接字的异步消息传递机制,适用于复杂的、事务性的配置,是现代网络配置(iproute2)的标准。
  • ConfigFS/Sysfs:基于文件系统的对象模型,适用于表示和配置具有复杂层次结构和属性的内核对象。

对于UTS名称这种简单的字符串属性,sysctl 接口虽然古老,但因其简单直观,仍然是完全适用且高效的解决方案。

utsname_sysctl_init: 内核身份标识 sysctl 接口初始化

本代码片段负责在 Linux 内核的 sysctl 文件系统中,于 /proc/sys/kernel/ 目录下,创建一系列用于展示和(部分)修改系统身份标识信息的文件。这些信息与用户空间 uname 命令所展示的内容相对应,例如主机名、操作系统版本、硬件架构等。其核心功能是将内核内部的初始 UTS 命名空间 (init_uts_ns) 中的数据结构成员直接暴露给用户空间,并为其中可变的项目(如主机名)提供了异步变更通知机制。

实现原理分析

该机制是内核 sysctl 框架的典型应用,通过一个静态定义的 ctl_table 结构体数组,将内核数据与 VFS 文件系统接口进行绑定。

  1. 直接数据绑定:

    • uts_kern_table 数组是此功能的核心。它的每个条目都定义了一个 sysctl 文件。关键在于 .data 成员,它被直接设置为指向内核全局变量 init_uts_ns.name 结构体中某个字段的指针(例如,.data = init_uts_ns.name.nodename)。
    • 这意味着当用户空间读写这些 /proc 文件时,sysctl 的处理函数 (proc_do_uts_string) 将直接操作内核核心数据结构所在的内存,而非通过一个中间缓冲区。这种设计效率很高,但要求处理函数必须正确地处理并发访问(例如,通过锁)。
  2. 异步变更通知机制 (poll):

    • 对于可被用户修改的条目,如 hostnamedomainnamesysctl 表中额外指定了一个 .poll 字段。
    • DEFINE_CTL_TABLE_POLL 宏为每个条目创建了一个 ctl_table_poll 结构体,其内部包含一个等待队列(wait queue)。
    • 当用户空间的应用程序对 /proc/sys/kernel/hostname 的文件描述符使用 poll(), select()epoll() 等系统调用时,内核会将该进程放入 hostname_poll 对应的等待队列中并使其睡眠。
    • 当内核的其他部分(例如,通过 sethostname() 系统调用)修改了主机名后,会调用 uts_proc_notify 函数。此函数通过 proc_sys_poll_notify 唤醒所有在该等待队列上睡眠的进程。
    • 这套机制实现了一个高效的事件驱动模型,允许用户空间程序异步地、低开销地监控系统关键配置的变化。
  3. 初始化与注册:

    • utsname_sysctl_init 函数调用 register_sysctl,将 uts_kern_table 注册到内核的 sysctl 树的 kernel 目录下。
    • device_initcall 宏将 utsname_sysctl_init 函数注册为一个内核启动回调,确保在内核初始化过程中的适当阶段自动完成 sysctl 接口的创建。

代码分析

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
/**
* @brief 为 hostname 和 domainname sysctl 条目定义 poll 结构体。
*
* DEFINE_CTL_TABLE_POLL 宏创建了一个 ctl_table_poll 类型的静态变量,
* 该变量内部包含一个等待队列头 (wait_queue_head_t),用于支持 poll 系统调用。
*/
static DEFINE_CTL_TABLE_POLL(hostname_poll);
static DEFINE_CTL_TABLE_POLL(domainname_poll);

/**
* @var uts_kern_table
* @brief 定义 /proc/sys/kernel/ 下与 UTS (uname) 信息相关的 sysctl 文件。
*
* 注意:对该表的任何修改都必须同步更新内核头文件中的 'enum uts_proc'。
*/
static const struct ctl_table uts_kern_table[] = {
{
.procname = "arch", /**< 文件名: architecture */
.data = init_uts_ns.name.machine, /**< 直接指向内核初始UTS命名空间中的 machine 字段 */
.maxlen = sizeof(init_uts_ns.name.machine), /**< 最大长度,用于边界检查 */
.mode = 0444, /**< 权限:全局只读 */
.proc_handler = proc_do_uts_string, /**< 指定专用的读写处理函数 */
},
{
.procname = "ostype", /**< 文件名: operating system type */
.data = init_uts_ns.name.sysname,
.maxlen = sizeof(init_uts_ns.name.sysname),
.mode = 0444,
.proc_handler = proc_do_uts_string,
},
{
.procname = "osrelease", /**< 文件名: operating system release */
.data = init_uts_ns.name.release,
.maxlen = sizeof(init_uts_ns.name.release),
.mode = 0444,
.proc_handler = proc_do_uts_string,
},
{
.procname = "version", /**< 文件名: kernel version */
.data = init_uts_ns.name.version,
.maxlen = sizeof(init_uts_ns.name.version),
.mode = 0444,
.proc_handler = proc_do_uts_string,
},
{
.procname = "hostname", /**< 文件名: host name */
.data = init_uts_ns.name.nodename,
.maxlen = sizeof(init_uts_ns.name.nodename),
.mode = 0644, /**< 权限:root 可读写,其他用户只读 */
.proc_handler = proc_do_uts_string,
.poll = &hostname_poll, /**< 关联 poll 结构体,以支持变更通知 */
},
{
.procname = "domainname", /**< 文件名: domain name */
.data = init_uts_ns.name.domainname,
.maxlen = sizeof(init_uts_ns.name.domainname),
.mode = 0644,
.proc_handler = proc_do_uts_string,
.poll = &domainname_poll, /**< 关联 poll 结构体 */
},
};

#ifdef CONFIG_PROC_SYSCTL
/**
* @brief 通知用户空间 uts_kern_table 中的某个条目发生了变化。
* @param proc 枚举值,作为 uts_kern_table 数组的索引,标识哪个条目已更改。
*/
void uts_proc_notify(enum uts_proc proc)
{
// 根据索引获取对应的 ctl_table 条目。
const struct ctl_table *table = &uts_kern_table[proc];

// 唤醒所有在该条目的 poll 等待队列上睡眠的进程。
proc_sys_poll_notify(table->poll);
}
#endif

/**
* @brief UTS sysctl 接口的初始化函数。
* @return 总是返回 0 表示成功。
*
* 此函数被标记为 __init,其代码段在内核启动后会被释放。
*/
static int __init utsname_sysctl_init(void)
{
// 将 uts_kern_table 注册到 "kernel" 目录下。
register_sysctl("kernel", uts_kern_table);
return 0;
}

/**
* @brief 使用 device_initcall 宏来注册初始化函数。
*
* 确保 utsname_sysctl_init 在内核启动的 device 初始化阶段被调用。
*/
device_initcall(utsname_sysctl_init);

proc_do_uts_string: 命名空间感知且锁安全的UTS字符串处理

本代码片段是 sysctl 框架中一个至关重要的、专用的处理函数,用于安全地读写与 UTS 命名空间相关的字符串,如主机名和域名。其核心功能是作为一个桥梁,在遵循内核严格的并发控制规则(不能在持有锁时睡眠)的前提下,实现对当前进程所属 UTS 命名空间内数据的读写,并确保变更可以被其他应用程序异步感知。

实现原理分析

此函数的设计是解决内核中一个经典并发问题的范例:如何安全地操作一个受锁保护且可能需要与用户空间进行可睡眠操作(如 copy_from_user)的数据。它采用了一种“拷贝-修改-写回”(Copy-Modify-Writeback)的缓冲策略。

  1. 命名空间感知地址解析 (get_uts):

    • 此函数是实现命名空间隔离的关键。sysctl 表中定义的 .data 指针(table->data)指向的是初始命名空间init_uts_ns)中的字段地址。
    • get_uts 函数首先获取当前进程所属的 UTS 命名空间(current->nsproxy->uts_ns)。
    • 然后,通过指针算术 (which - (char *)&init_uts_ns) 计算出目标字段(如 nodename)相对于 init_uts_ns 结构体起始地址的偏移量
    • 最后,将这个偏移量加到当前进程的 UTS 命名空间基地址上,从而精确地定位到当前进程应该看到和修改的数据的实际内存地址。
  2. 两阶段加锁与临时缓冲:

    • 核心问题: proc_dostring 函数内部可能会因为调用 copy_from_user/copy_to_user 而导致进程睡眠。在 Linux 内核中,持有信号量(uts_sem)或自旋锁时睡眠是严格禁止的,因为它可能导致死锁。
    • 解决方案:
      a. 创建临时副本: 函数在栈上创建了一个临时缓冲区 tmp_data,并创建了一个 ctl_table 的临时副本 uts_table,将其 .data 指针指向这个安全的栈上缓冲区。
      b. 第一阶段:带锁读取: 它获取一个读信号量down_read(&uts_sem)),这允许多个并发的读操作。在这个短暂的临界区内,它将受保护的真实 UTS 数据从当前命名空间拷贝到 tmp_data 缓冲区,然后立即释放锁up_read(&uts_sem))。
      c. 无锁操作: 接下来,它调用 proc_dostring。此时,proc_dostring 的所有读写操作都只针对 tmp_data 这个位于栈上的、不受保护的局部变量。即使 proc_dostring 睡眠,也不会有任何锁被持有,从而避免了死锁。
      d. 第二阶段:带锁写回 (仅限写操作): 如果是写操作,在 proc_dostring 返回后(此时 tmp_data 已包含用户写入的新数据),它获取一个写信号量down_write(&uts_sem)),这是排他性的。在临界区内,它将 tmp_data 的内容写回到真实的 UTS 命名空间内存中,然后立即释放锁up_write(&uts_sem))。
  3. 熵贡献与变更通知:

    • add_device_randomness: 在写回新值之前,将用户提供的新主机名/域名作为随机源,贡献给内核的熵池,以提高系统随机数的质量。
    • proc_sys_poll_notify: 写操作成功后,调用此函数来唤醒任何正在 poll 相应 sysctl 文件的进程,实现了对配置变更的异步通知。

代码分析

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
/**
* @brief 获取当前进程UTS命名空间中特定字段的地址。
* @param table 指向 ctl_table 的指针,其 .data 字段用作计算偏移量的模板。
* @return 指向当前命名空间中目标字段的 void 指针。
*/
static void *get_uts(const struct ctl_table *table)
{
// which 最初指向 init_uts_ns 中的字段(模板地址)。
char *which = table->data;
struct uts_namespace *uts_ns;

// 获取当前进程关联的 UTS 命名空间。
uts_ns = current->nsproxy->uts_ns;
// 计算目标字段在结构体内的偏移量,并将其加到当前命名空间的基地址上,
// 从而得到当前上下文下正确的字段地址。
which = (which - (char *)&init_uts_ns) + (char *)uts_ns;

return which;
}

/**
* @brief UTS 结构体专用的 sysctl 字符串处理函数。
*
* 这是一个 proc_dostring 的特例,因为它需要处理锁。
* 它通过一个临时缓冲区来避免在持有锁时调用可能睡眠的函数。
*/
static int proc_do_uts_string(const struct ctl_table *table, int write,
void *buffer, size_t *lenp, loff_t *ppos)
{
struct ctl_table uts_table;
int r;
// 在栈上创建一个临时缓冲区,用于与 proc_dostring 交互。
char tmp_data[__NEW_UTS_LEN + 1];

// 复制一份 ctl_table,以便我们可以安全地修改其 .data 指针。
memcpy(&uts_table, table, sizeof(uts_table));
// 将副本的 .data 指针指向我们的临时栈缓冲区。
uts_table.data = tmp_data;

/*
* 将值缓冲到 tmp_data 中,这样就可以在不持有任何锁的情况下调用 proc_dostring()。
* 在 write==1 的情况下,我们也需要读取原始值以支持部分写入。
*/
down_read(&uts_sem); // 获取读锁
memcpy(tmp_data, get_uts(table), sizeof(tmp_data)); // 从真实位置拷贝到临时缓冲
up_read(&uts_sem); // 立即释放读锁

// 现在,在不持有任何锁的情况下,对临时缓冲区执行标准的字符串读写操作。
r = proc_dostring(&uts_table, write, buffer, lenp, ppos);

if (write) {
/*
* 将新值写回。
* 注意:由于我们已释放 uts_sem,如果存在两个对同一 sysctl 在非零偏移量
* 上的并行写入,理论上结果可能不正确。这是一个已知的权衡。
*/
// 将用户输入的数据贡献给内核的熵池。
add_device_randomness(tmp_data, sizeof(tmp_data));
down_write(&uts_sem); // 获取排他性的写锁
memcpy(get_uts(table), tmp_data, sizeof(tmp_data)); // 从临时缓冲写回到真实位置
up_write(&uts_sem); // 释放写锁

// 通知任何正在 poll 此文件的用户空间进程,数据已发生变更。
proc_sys_poll_notify(table->poll);
}

return r;
}

init/version-timestamp.c

init_uts_ns 与 linux_banner: 内核身份标识的静态定义

本代码片段展示了 Linux 内核在编译时如何静态定义其核心身份标识。它初始化了两个关键的全局变量:init_uts_ns,即初始 UTS 命名空间,它以结构化形式存储了系统名、主机名、内核版本等信息;以及 linux_banner,一个包含了部分上述信息的、用于在启动时显示的、人类可读的常量字符串。

实现原理分析

此代码是内核自描述能力的基础,其原理基于 C 语言的静态初始化和构建系统的宏替换。

  1. 静态初始化:

    • init_uts_nslinux_banner 都是全局变量,在内核加载到内存时,它们就已经存在,并且其内容由编译器在生成内核映像时直接填充。它们不依赖于任何运行时的初始化代码来创建。
    • init_uts_ns 被初始化为一个 struct uts_namespace 实例。其 .name 成员是一个嵌套的结构体,包含了所有 uname 系统调用所需返回的信息。
  2. 编译时宏定义:

    • 初始化的值,如 UTS_SYSNAME, UTS_RELEASE, LINUX_COMPILE_BY 等,并非硬编码的字符串。它们是 C 预处理器宏。
    • 这些宏的值是在内核的编译过程中,由 Kconfig 配置系统和顶层 Makefile 文件根据当前的配置、编译器版本、编译主机名、编译者等环境信息动态生成的。这些信息最终通过 -D 编译器标志传递给 C 编译器。
    • 这种机制确保了每一个编译出的内核二进制文件都精确地内嵌了其自身的元数据,包括版本号、构建环境等,这对于调试、版本跟踪和系统识别至关重要。
  3. 命名空间根节点:

    • init_uts_ns 不仅仅是一个数据容器,它还是内核中所有 UTS 命名空间的。当系统启动时,init 进程就属于这个命名空间。
    • 如果系统后续创建了新的 UTS 命名空间(例如,用于容器),新命名空间的初始内容通常会从其父命名空间复制而来,最终可以追溯到 init_uts_ns
    • .ns.__ns_ref = REFCOUNT_INIT(2) 初始化了该命名空间的引用计数为2。一个引用由命名空间子系统本身持有,另一个由初始任务(init)的 nsproxy 持有。

代码分析

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
/**
* @var init_uts_ns
* @brief 内核的初始 UTS (UNIX Time-sharing System) 命名空间。
*
* 这是一个全局变量,在内核编译时被静态初始化。它包含了系统的核心身份信息,
* 并且是系统中所有后续创建的 UTS 命名空间的根。
*/
struct uts_namespace init_uts_ns = {
/**
* @var ns.ns_type
* @brief 命名空间的类型,对于UTS ns,此宏返回一个固定的类型标识。
*/
.ns.ns_type = ns_common_type(&init_uts_ns),
/**
* @var ns.__ns_ref
* @brief 命名空间的引用计数,初始为2。
* (一个用于nsfs,一个用于init_nsproxy)
*/
.ns.__ns_ref = REFCOUNT_INIT(2),
/**
* @var name
* @brief 包含所有 uname 信息的结构体。
*/
.name = {
/**
* @var sysname
* @brief 操作系统名称 (例如 "Linux")。值由 UTS_SYSNAME 宏在编译时确定。
*/
.sysname = UTS_SYSNAME,
/**
* @var nodename
* @brief 节点名/主机名 (例如 "localhost")。值由 UTS_NODENAME 宏在编译时确定。
*/
.nodename = UTS_NODENAME,
/**
* @var release
* @brief 内核发布版本 (例如 "5.10.0")。值由 UTS_RELEASE 宏在编译时确定。
*/
.release = UTS_RELEASE,
/**
* @var version
* @brief 内核详细版本字符串 (例如 "#1 SMP PREEMPT ...")。值由 UTS_VERSION 宏在编译时确定。
*/
.version = UTS_VERSION,
/**
* @var machine
* @brief 硬件架构名称 (例如 "armv7l")。值由 UTS_MACHINE 宏在编译时确定。
*/
.machine = UTS_MACHINE,
/**
* @var domainname
* @brief NIS/YP 域名。值由 UTS_DOMAINNAME 宏在编译时确定。
*/
.domainname = UTS_DOMAINNAME,
},
/**
* @var user_ns
* @brief 指向此命名空间所属的用户命名空间,初始时指向初始用户命名空间。
*/
.user_ns = &init_user_ns,
/**
* @var ns.inum
* @brief 此命名空间在 nsfs 文件系统中的 inode 号。
*/
.ns.inum = ns_init_inum(&init_uts_ns),
#ifdef CONFIG_UTS_NS
/**
* @var ns.ops
* @brief 指向UTS命名空间操作函数的指针表。
*/
.ns.ops = &utsns_operations,
#endif
};

/* 固定的字符串!请勿修改! */
/**
* @var linux_banner
* @brief 内核启动时打印的横幅信息。
*
* 这是一个静态常量字符串,其内容在内核编译时由多个宏拼接而成,
* 包含了版本、编译器、编译主机等信息。它通常通过 /proc/version 文件暴露给用户空间。
*/
const char linux_banner[] =
"Linux version " UTS_RELEASE " (" LINUX_COMPILE_BY "@"
LINUX_COMPILE_HOST ") (" LINUX_COMPILER ") " UTS_VERSION "\n";

kernel/user.c

init_user_ns: 初始用户命名空间的静态定义

本代码片段定义并初始化了 init_user_ns,即内核的初始用户命名空间(Root User Namespace)。这是 Linux 内核中用户和组身份标识体系的基石。其核心功能是为整个系统建立一个初始的、1:1 的身份映射,并作为所有进程默认的用户命名空间,以及未来创建任何新用户命名空间的最终父节点。

实现原理分析

init_user_ns 的实现依赖于 C 语言的静态初始化,并为内核的用户隔离机制(用户命名空间)提供了基础配置。

  1. 静态初始化:

    • init_user_ns 是一个在编译时就被完全初始化的全局变量。它不依赖于任何运行时的代码来创建,而是作为内核 .data 段的一部分,在内核映像加载到内存时就已存在。
  2. 身份映射 (Identity Mapping):

    • 用户命名空间的核心功能是建立一套 UID/GID 的映射规则。init_user_nsuid_mapgid_mapprojid_map 被配置为最基础的身份映射
    • .first = 0: 映射范围从命名空间内部的 ID 0 开始。
    • .lower_first = 0: 内部 ID 0 映射到父命名空间(对于根命名空间,即物理系统)的 ID 0。
    • .count = 4294967295U: 映射范围覆盖了整个 32 位 ID 空间。
    • 综合来看,这套配置意味着在初始用户命名空间中,任意一个 UID/GID X 都等同于物理系统中的 UID/GID X。这是所有后续嵌套命名空间进行ID转换的参照系原点。
  3. 引用计数 (REFCOUNT_INIT(3)):

    • init_user_ns 的引用计数被初始化为 3。这是一个关键的细节,确保了根命名空间在系统运行期间不会被意外释放。这三个引用通常来源于:
      1. 用户命名空间子系统本身持有的一个引用,作为其根节点。
      2. 初始任务(init_task,即 pid 0 的 swapper 进程)的凭证(cred)结构持有的一个引用。
      3. 初始任务代理(init_nsproxy)通过其包含的其他命名空间(如 init_uts_ns,它本身需要关联一个用户命名空间)间接持有的一个引用。
    • 代码注释中的 ? 暗示了第三个引用的来源不那么直观,但它确保了所有初始内核子系统都能安全地关联到这个根用户命名空间。
  4. 符号导出 (EXPORT_SYMBOL_GPL):

    • 此宏将 init_user_ns 变量的地址导出到内核的符号表中。这使得遵循 GPL 许可证的可加载内核模块(LKMs)能够在运行时查找到并使用这个变量,例如,用于与用户身份相关的操作。

代码分析

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
/*
* 用户命名空间引用计数:1个来自 root 用户,1个来自 init_uts_ns,
* 还有1个来自...?(实际上是来自内核子系统和初始任务凭证)
*/
/**
* @var init_user_ns
* @brief 内核的初始用户命名空间 (Root User Namespace)。
*
* 这是系统中所有用户/组身份的根。它定义了一个覆盖所有ID的 1:1 映射,
* 并作为所有初始进程和未来创建的新用户命名空间的最终父节点。
*/
struct user_namespace init_user_ns = {
/** @brief UID 映射表 */
.uid_map = { {
.extent[0] = {
.first = 0, /**< 命名空间内的起始 UID */
.lower_first = 0, /**< 映射到父命名空间中的起始 UID */
.count = 4294967295U, /**< 映射范围覆盖整个32位UID空间 */
},
.nr_extents = 1, /**< 只有一个映射范围 */
}, },
/** @brief GID 映射表,结构同 UID 映射 */
.gid_map = { {
.extent[0] = {
.first = 0,
.lower_first = 0,
.count = 4294967295U,
},
.nr_extents = 1,
}, },
/** @brief Project ID 映射表,结构同 UID 映射 */
.projid_map = { {
.extent[0] = {
.first = 0,
.lower_first = 0,
.count = 4294967295U,
},
.nr_extents = 1,
}, },
/** @brief 命名空间的通用类型信息 */
.ns.ns_type = ns_common_type(&init_user_ns),
/** @brief 命名空间的引用计数,初始为3 */
.ns.__ns_ref = REFCOUNT_INIT(3),
/** @brief 此命名空间的创建者UID,对于初始命名空间为全局 root */
.owner = GLOBAL_ROOT_UID,
/** @brief 此命名空间的创建者GID,对于初始命名空间为全局 root */
.group = GLOBAL_ROOT_GID,
/** @brief 此命名空间在 nsfs 文件系统中的 inode 号 */
.ns.inum = ns_init_inum(&init_user_ns),
#ifdef CONFIG_USER_NS
/** @brief 指向用户命名空间操作函数的指针表 */
.ns.ops = &userns_operations,
#endif
/** @brief 命名空间的标志位 */
.flags = USERNS_INIT_FLAGS,
#ifdef CONFIG_KEYS
/** @brief 内核密钥环相关的链表头 */
.keyring_name_list = LIST_HEAD_INIT(init_user_ns.keyring_name_list),
/** @brief 内核密钥环相关的读写信号量 */
.keyring_sem = __RWSEM_INITIALIZER(init_user_ns.keyring_sem),
#endif
#if IS_ENABLED(CONFIG_BINFMT_MISC)
/** @brief 指向 binfmt_misc 数据结构的指针 */
.binfmt_misc = &init_binfmt_misc,
#endif
};
/**
* @brief 将 init_user_ns 符号导出,使其对 GPL 兼容的内核模块可见。
*/
EXPORT_SYMBOL_GPL(init_user_ns);