[toc]

fs/nsfs.c 命名空间文件系统(Namespace Filesystem) 内核命名空间的句柄化接口

历史与背景

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

nsfs(Namespace Filesystem)是一个内核中的“伪文件系统”(Pseudo-filesystem),它的诞生是为了解决一个核心问题:如何让用户空间的进程能够安全、稳定地引用(Reference)和操纵内核中的命名空间(Namespace)对象

nsfs和其配套的setns(2)系统调用出现之前,进程与命名空间的交互是有限且单向的:

  1. 缺乏进入(Entering)机制:一个已经存在的进程,没有一个标准的方法可以“进入”到另一个已经存在的命名空间中。进程只能在创建时(通过clone())进入新的命名-空间,或者通过unshare()将自己与父进程的命名空间分离开来,进入一个新建的命名空间。
  2. 缺乏持久化(Persistence)机制:一个命名空间的生命周期通常与其内部的进程绑定。当一个命名空间中最后一个进程退出时,这个命名空间就会被销毁。这使得创建“空的”、可供将来使用的命名空间变得非常困难。
  3. 缺乏管理句柄:系统管理员和工具(如容器管理器)需要一种方法来“抓住”一个命名空间,以便后续对其进行操作(例如,将一个新进程加入其中)。

nsfs通过将抽象的、存在于内核内存中的命名空间对象具象化为一个可以被open()的文件,从而完美地解决了以上所有问题。这个文件就是一个稳定的句柄(Handle)

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

  • 命名空间的引入:首先,内核必须先有命名空间的概念(最早是Mount Namespace)。
  • /proc/[pid]/ns/接口的创建:这是决定性的里程碑。内核在procfs文件系统中为每个进程创建了一个/proc/[pid]/ns/目录,其中包含了指向该进程所属的各种命名空间(net, mnt, user等)的特殊文件。nsfs就是实现这些特殊文件的后端文件系统。
  • setns(2)系统调用的引入:与nsfs相辅相成,setns(2)系统调用被引入。它接收一个通过nsfs打开的文件描述符,并将调用进程迁移到该文件描述符所代表的命名空间中。这两者共同构成了现代Linux命名空间管理的核心。

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

nsfs是现代Linux容器化和虚拟化技术的绝对基石

  • 主流应用
    • 所有容器运行时:Docker, Podman, containerd等在执行docker execpodman exec时,都会利用nsfssetns将新进程加入到目标容器的命名空间中。
    • 网络虚拟化工具ip netns命令能够创建持久化的网络命名空间,其原理就是通过bind mount将一个nsfs文件(如/proc/self/ns/net)保存到/var/run/netns/目录下,从而即使没有进程在其中,也能保持该网络命名空间不被销毁。
    • 系统诊断工具nsenter命令允许管理员进入任意进程的任意命名空间,来执行调试命令,这完全依赖于nsfs

核心原理与设计

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

nsfs是一个“魔法”文件系统,它不存储在任何物理磁盘上。它的所有文件和目录都是在被访问时由内核动态生成的。

  1. inode与内核对象的链接:当procfs创建一个/proc/[pid]/ns/net这样的文件时,它并不会创建一个普通的inode,而是会调用nsfs的内部函数(如ns_get_inode)。这个函数会创建一个特殊的、属于nsfs的inode。最关键的一步是,这个inode的私有数据指针(inode->i_private)会直接指向内核中代表该命名空间的核心数据结构(例如,对于网络命名空间,就是struct net)。
  2. 打开文件即获取句柄:当一个用户空间程序调用open("/proc/123/ns/net")时,VFS层最终会调用nsfs的操作函数。nsfs会:
    • 验证调用者是否有权限访问这个命名空间。
    • 创建一个struct file对象。
    • file->private_data指针也设置为指向那个内核命名空间对象(struct net)。
    • 向用户空间返回一个文件描述符(FD)。
  3. 句柄的使用:这个返回的FD现在就是一个指向特定内核命名空间的强引用句柄。
    • setns(fd, ...): 当这个FD被传递给setns()系统调用时,内核可以轻易地从FD反查到struct file,再从file->private_data中找到内核命名空间对象的地址,然后执行命名空间切换操作。
    • 保持存活: 只要这个FD被某个进程持有,或者这个nsfs文件被bind mount到了文件系统的其他地方,内核就会保持对应的命名空间对象存活,即使该命名空间中已没有任何进程。

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

  • 优雅的抽象:复用了VFS和文件描述符这一成熟、强大的机制,来管理一种完全不同类型的内核资源。
  • 强大的功能性:使得跨命名空间操作、命名空间持久化和复杂的容器管理工具成为可能。
  • 安全性:对/proc/[pid]/ns/文件的访问遵循标准的Linux文件权限和能力(Capability)检查,提供了良好的安全边界。

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

  • 专用性nsfs是一个高度专用的文件系统。你不能对它进行readwrite(会返回错误),也不能mmap。它的文件是“不透明的句柄”,唯一有意义的操作就是opencloseioctl和将其传递给setns
  • 概念复杂性nsfs本身很简单,但它所暴露的命名空间、特别是用户命名空间的安全模型,对于用户来说是复杂的,容易产生配置错误。

使用场景

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

nsfs是任何需要在已存在进程间进行命名空间操纵的场景下的唯一且标准的解决方案。

  • 进入容器执行命令docker exec -it my_container bash。Docker守护进程会找到my_container中主进程的PID,然后打开/proc/[pid]/ns/下的多个命名空间文件,接着调用setns进入这些命名空间,最后执行bash
  • 创建持久化网络命名空间
    1. ip netns add test_net
    2. 这个命令会在后台创建一个新进程,该进程unshare一个新的网络命名空间。
    3. 然后它打开自己的/proc/self/ns/net文件,并将其bind mount到/var/run/netns/test_net
    4. 即使这个辅助进程退出,由于存在bind mount,test_net命名空间依然存活。
  • 跨网络命名空间调试:管理员在宿主机上执行nsenter --net=/var/run/netns/test_net ip addrnsenter工具打开指定路径的nsfs文件,setns进入后执行ip addr,从而查看到test_net命名空间内部的网络配置。

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

  • 任何非命名空间管理的操作:不推荐。nsfs不是为了数据传输(应使用管道/套接字)、也不是为了文件存储(应使用ext4/tmpfs等)而设计的。它有且只有一个目的:提供对内核命名空间对象的句柄。

对比分析

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

nsfs作为一个伪文件系统,最好的对比对象是其他伪文件系统,以凸显它们各自不同的目的。

特性 nsfs sysfs procfs debugfs
核心功能 提供对内核命名空间对象不透明句柄 (Handle) 提供对内核设备模型 (kobject)结构化视图 提供对进程信息和杂项系统信息的文本化视图 提供一个无规则的、临时的接口供内核开发者调试
文件内容 无内容。读写会报错。文件本身是个句柄。 通常是单个文本值(“一文件一值”),可读/可写。 通常是格式化的文本,可能包含多行多列信息。 任意,由开发者决定。
主要用途 传递给setns(),用于命名空间操作和持久化。 查看/修改设备状态和驱动参数。 查看进程状态(如status, maps)和系统信息(如meminfo)。 内核调试,例如导出内部数据结构。
ABI稳定性 稳定。是容器和系统工具的核心ABI。 稳定。是用户空间与内核驱动交互的稳定ABI。 部分稳定/proc/[pid]部分稳定,其他部分可能变化。 不稳定。没有任何稳定性保证,随时可变。
典型例子 /proc/123/ns/net /sys/class/leds/led0/brightness /proc/1/status, /proc/cpuinfo /sys/kernel/debug/dri/0/i915_dmc_info

fs/nsfs.c

nsfs_init_fs_context 用于初始化 nsfs 文件系统的上下文

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
static const struct stashed_operations nsfs_stashed_ops = {
.init_inode = nsfs_init_inode,
.put_data = nsfs_put_data,
};

/*
* Common helper for pseudo-filesystems (sockfs, pipefs, bdev - stuff that
* will never be mountable)
*/
struct pseudo_fs_context *init_pseudo(struct fs_context *fc,
unsigned long magic)
{
struct pseudo_fs_context *ctx;

ctx = kzalloc(sizeof(struct pseudo_fs_context), GFP_KERNEL);
if (likely(ctx)) {
ctx->magic = magic;
fc->fs_private = ctx;
fc->ops = &pseudo_fs_context_ops;
fc->sb_flags |= SB_NOUSER;
fc->global = true;
}
return ctx;
}
EXPORT_SYMBOL(init_pseudo);

static int nsfs_init_fs_context(struct fs_context *fc)
{
struct pseudo_fs_context *ctx = init_pseudo(fc, NSFS_MAGIC);
if (!ctx)
return -ENOMEM;
ctx->ops = &nsfs_ops;
ctx->dops = &ns_dentry_operations;
fc->s_fs_info = (void *)&nsfs_stashed_ops;
return 0;
}

nsfs_init 用于初始化 nsfs 文件系统

1
2
3
4
5
6
7
8
9
10
11
12
13
static struct file_system_type nsfs = {
.name = "nsfs",
.init_fs_context = nsfs_init_fs_context,
.kill_sb = kill_anon_super,
};

void __init nsfs_init(void)
{
nsfs_mnt = kern_mount(&nsfs);
if (IS_ERR(nsfs_mnt))
panic("can't set nsfs up\n");
nsfs_mnt->mnt_sb->s_flags &= ~SB_NOUSER;
}