简单来说:nsproxy(Namespace Proxy)是 Linux 内核中用来“打包和管理”一个进程所拥有的所有 Namespace 的结构体。如果把进程比作一个“自然人”,那么nsproxy就是这个人的“身份证明大礼包”,里面装了他属于哪个国家(网络)、哪个家族(PID)、哪个共享文件区(Mount)的所有证明。
1. 为什么内核需要nsproxy?
在 Linux 内核中,每一个进程都由一个巨大的结构体task_struct(也就是常说的进程控制块 / PCB)来表示。
早期 Linux 的 Namespace 种类很少,内核直接把 Namespace 指针塞在task_struct里面。但随着 Linux 支持的 Namespace 越来越多(目前有 PID、Network、Mount、IPC、UTS、User、Cgroup、Time 等 8 种以上),如果每个进程的task_struct都直接挂载 8 个不同的指针,会导致两个严重问题:
- 结构体过度臃肿:占用过多的内核内存。
- 复制和共享极其低效:在 Linux 中,很多父子进程或同一容器内的进程是共享某些 Namespace 的(比如同一个 Pod 内的容器共享 Network Namespace)。
为了解决这个问题,内核开发者做了一层抽象:把所有 Namespace 的指针收纳到一个独立的结构体中,这个结构体就叫struct nsproxy。
2.nsproxy的内部结构
在 Linux 内核源码(include/linux/nsproxy.h)中,nsproxy的定义大致如下(简化版):
structnsproxy{atomic_tcount;// 引用计数,记录有多少个进程正在共享这个 nsproxystructuts_namespace*uts_ns;// 主机名与域名空间structipc_namespace*ipc_ns;// 进程间通信空间structmnt_namespace*mnt_ns;// 文件系统挂载点空间structpid_namespace*pid_ns_for_children;// 子进程的 PID 空间structnet_namespace*net_ns;// 网络协议栈空间structcgroup_namespace*cgroup_ns;// cgroup 拓扑空间structtime_namespace*time_ns;// 系统时间空间};它的工作原理:
- 解耦与共享:
task_struct内部现在只需要保留一个指针指向nsproxy。如果两个进程属于同一个 Docker 容器,它们的task_struct->nsproxy指针就会指向同一个nsproxy实例。 - 引用计数(
count):当一个新进程加入这些命名空间时,count加 1;当进程退出时,count减 1。只有当count减到 0 时,内核才会真正销毁这个nsproxy及其包含的各种 Namespace。
3.nsproxy在系统调用中的体现
你在使用 Docker 或进行容器底层开发时,常用的三个 Linux 系统调用,在内核里全是在对nsproxy进行增删改查:
1.clone()—— 创建新进程
当你启动一个新容器时,Docker 会调用clone()并传入控制参数(如CLONE_NEWNET | CLONE_NEWPID)。
- 内核行为:内核发现你需要新的空间,于是会复制(Copy)父进程的
nsproxy,创建一个新的nsproxy实例,并把 Network 和 PID 空间替换为新创建的,最后让新进程指向这个新的nsproxy。
2.unshare()—— 孤立当前进程
允许当前进程主动脱离当前的某个 Namespace。
- 内核行为:创建一个新的
nsproxy,把指定的 Namespace 剥离出来替换掉,然后更新当前进程的指针。
3.setns()—— 加入已有空间
这就是docker exec(进入一个正在运行的容器)的底层原理。
- 内核行为:找到目标容器进程的
nsproxy,然后把当前bash进程的task_struct->nsproxy指针直接指向目标容器的nsproxy(或者其中的某几个 Namespace)。此时,你的bash就瞬间“穿越”到了容器内部。
总结
如果把 Linux 容器比作一栋大楼:
- Namespace是大楼里划分出的一个个独立房间。
task_struct是在房间里工作的人。nsproxy就是钥匙串。每个人手里不需要抓着一堆单独的房门钥匙,只需要拿着nsproxy这串钥匙,就能决定他能进入哪间办公室(Network)、哪间档案室(Mount)和哪间会议室(PID)。