nsproxy

nsproxy 简单来说nsproxyNamespace 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。