容器 02:namespace 改变的是哪些资源视图
在一个终端里改了主机名,另一个终端为什么没变?关键不是 shell 的变量,而是两个进程引用的 UTS namespace 不同。沿01 的进程生命周期继续,把隔离看成“进程持有哪些内核对象”,而不是把容器想象成一台小型物理机。
比较视图,不比较字符串
clone 可让新进程进入新建的 namespace;unshare 使调用者与先前共享的指定 namespace 分离;setns 把调用者加入已有 namespace,但有具体的权限、类型和多线程前提。UTS 隔离 hostname 与 domainname,IPC 隔离相应的进程间通信对象;PID、mount、net 与 user 各有自己的资源集合。/proc/<pid>/ns/<type> 的链接目标包含可比对的 namespace inode。两个进程具有相同 UTS inode,表示指向同一 UTS namespace;两个进程恰好都输出 hostname=demo,却可能在不同 UTS 对象里。不能跨类型比较数字本身。
普通读者并不必在宿主得到所有 capability。unshare -Ur -u 先创建新的 user namespace 并把调用者映射为该命名空间中的 root,再建立 UTS 视图;此映射身份只在相应权限范围内有意义。能改私有 hostname,不能推出可改宿主 hostname、挂载任何目录或管理共享网络。namespace 也不限制 CPU/内存耗用,资源控制要另查 cgroup;cgroup namespace 只涉及可见性。
新进程、新调用者与加入已有对象
讨论 namespace 时先问“哪一个进程从何时开始使用新视图”。clone 配合相应标志创建子进程,使新子进程进入一个或多个新 namespace;调用者如果只是 fork 而不指定隔离标志,子进程通常继承父进程的 namespace 关联。unshare 让调用者不再与原进程共享指定视图,但涉及 PID namespace 时还有特殊时序:调用 unshare(CLONE_NEWPID) 的进程本身不会突然改成内部 PID 1,新 PID namespace 作用于后续创建的子进程。把“打开了新的 PID namespace”和“当前 shell 已成 PID 1”混为一谈,正是很多错误截图的来源。
setns 则试图通过已有 namespace 的 FD 加入那个对象,它既不自动更换 rootfs,也不因调用成功就为应用设置资源额度。调用它需要满足目标类型的权限前提,某些 namespace 的加入行为还有线程、当前用户 namespace 或父子关系限制;尤其不能让一个活着的进程直接把自身 PID 号换成目标视图里的新数字。设计实验时对照调用前后 /proc/self/ns/*、新 fork 的子进程以及配置的挂载,而不是只看 setns 的返回码。本文实际运行的是 util-linux unshare 命令,不声称在本机成功执行过 setns。
名称相似也可能属于完全不同的层。Linux namespace 隔离资源视图;containerd 的 namespace 用于组织其 API/内容对象;Kubernetes Namespace 用于 API 资源的命名和权限分组。三个地方都能出现“namespace=demo”,并不能因此以 Kubernetes Namespace 名推导 Linux netns inode。后续定位 Pod 网络时,必须拿到该 Pod sandbox 实际持有的 netns 对象,再和 Pod UID、容器 ID 关联。
如何从一侧观察另一侧
在本次可运行的正例中,外侧 shell 的 UTS 链接是 uts:[4026532463],内部创建的新 UTS 链接是 uts:[4026532573],在新视图中设置 hostname 后,仅内部读到 containers-own-test,外部随后仍读到原宿主名。真正能够证明区别的是同一类型的不同 inode 和各自可见值相互印证。若两边都设置成字符串 test,名称一样却仍可能是两个独立对象;若两边名称不同但同时读到了相同 inode,则应先质疑采样方法和代码输出,不能凭字符串硬判成功。
IPC 示例只读 namespace inode,并没有建 System V 共享内存段、消息队列或 POSIX mqueue 对照。它能说明进入了不同的 IPC 对象,不能自动说明某个具体 IPC 资源已被创建或彻底隔离。UTS namespace 限定 hostname 等视图,IPC namespace 限定对应的进程间通信对象;两者都没有在本机更换 PID 根文件系统或文件权限。一个子进程即使看见另一 hostname,仍可能访问同一用户允许访问的普通磁盘文件;namespace 类型必须和被保护的资源一一对应。
此外,观察位置决定 /proc 提供什么证据。宿主可以在权限允许时读取内部进程的 /proc/<pid>/ns/uts,即使内部看不到宿主的所有进程;namespace 提供的是有方向、有层级的可见性,不是“宿主和容器互相绝对不可见”。进程很快退出时 /proc/<pid> 会消失,PID 也会复用,记录 inode 必须和进程出生时间、实验命令及采样时刻一起保存,不能事后对另一 PID 读到不同对象就判定“隔离失效”。
UTS 与 IPC 的安全对照
素材目录的原始命令与结果保存了本环境真实输出和退出码。宿主 shell 的 UTS 为 uts:[4026532463],unshare -Ur -u sh -c 'readlink /proc/self/ns/uts; hostname containers-own-test; hostname' 内得到 uts:[4026532573] 和新名字;返回宿主再次 hostname 仍是原名。unshare -Ur -i ... 同样得到另一 IPC inode。这里没有创建宿主默认路由、veth 或 bridge,也没有持久资源需要从共享网络清理。复跑入口和源码在实验说明及源码包。
失败案例同样重要:unshare -Ur -p --fork --mount-proc ... 在当前环境以 mount /proc failed: Operation not permitted 返回 1。内核可能允许创建新 PID namespace,却不允许这个环境完成 /proc 的新挂载;因而不能借此声称已获得正确的容器内 /proc 视图。缺少可写 cgroup 和运行时的节点上,不把这种失败升级为“Linux 不支持 PID namespace”。
将失败拆开看更清楚:--fork 本想在目标 PID namespace 建立一个新的子进程,--mount-proc 还要求为进程信息视图处理挂载。当前云端主 cgroup 只读,外侧进程没有有效 capability,环境对嵌套挂载也可能加限制;单凭 Operation not permitted 不能判定到底是哪条宿主策略拒绝,但至少可证明这条复合命令没有完成“新 procfs 视图”目标。安全做法不是关闭宿主限制来让教程截图成功,而是在专用 VM 中分阶段单独记录 unshare、mountinfo、procfs 的可见 PID 集与清理结果。
另一个容易误读的情况是 unshare -Ur -n 显示新 net:[...],却只能在 /proc/net/dev 看到 lo;这不是“网络一定被正确配置”的证据,而是一个刚分离的网络视图可能尚未拥有与别处通信的链路。必须单独检查 lo 是否 up、veth 是否已接入、地址、默认路由与 socket 监听位置。22 的实验就从这一点继续。反过来,如果在 UTS 实验里父子两侧都能看到某个服务端口,也不能据此断定 UTS 没有隔离:UTS 根本不负责隔离网络端口,实验选择了错误的观察对象。
这样才能区分成功、失败和观察错误三类输出。成功是“为某种资源建立新 namespace,并在该视图观察到预期差异”,需要运行进程和内核对象两侧证据;失败是系统调用未达成设置目标,需保存 errno 和当前权限;观察错误则可能是读取了旧 procfs、从客户端节点而非 daemon 节点采样,或者用文字名代替对象 inode。面对任何“容器里还是旧的”现象,先列出要隔离的是哪种资源,再查它对应的 namespace 类型和采样位置,最后才讨论运行时是否遗漏了创建动作。
本次 UTS/IPC 命令没有在共享宿主改永久配置。unshare -Ur -u 创建的子进程改自己的 hostname,返回后普通宿主 shell 再读到原值;新 IPC 测试同样仅活在受控子进程中。这些“无残留”检查限定为 hostname 字符串与进程退出:没有对 IPC 对象执行完整清单/资源回收验证,也没有对 netns/veth 等尚未创建的对象声称清理成功。把清理对象限定为实验真正创建过的资源,比机械记录一行“清理完成”更诚实。
想补充 IPC 的功能案例,可以在专用 VM 只为实验创建一个已知键的 System V 队列:父子在同一个 IPC namespace 时以该键查找并交换一条有上限消息,进入新的 IPC namespace 后再对同一键查找,预期它们引用的不是原先同一个队列对象。执行时必须同时保存 readlink /proc/<pid>/ns/ipc、IPC 查询结果和显式删除队列的返回码。如果只得到查找失败却没有确认进程实际进入新 IPC namespace,也可能是程序权限或键值选错;如果查找成功却不核对对象 ID,也可能只是不同 namespace 中出现了同名键。这个例子还未在本机执行,留在练习的 VM 补跑入口,不把推理包装为观察结果。
类似地,判断两条进程树是否共享 UTS namespace,不是问它们“是否属于同一个容器名字”,而是沿实际进程读取同类型对象的 inode,再在受控子进程中改变 hostname,看另一侧是否可见。测试失败后检查命令究竟运行在父还是子进程,父进程是否在新 namespace 外等待,是否意外读了另一个 PID 的 /proc 文件。可迁移的判断顺序始终是“对象类型 → 进程关联 → 受控操作 → 两侧可见性 → 清理”,其他 namespace 的实验也沿这个顺序设计,但每一种资源还要补自己的权限和生命周期条件。
本机既没有验证过持有 namespace FD 后的完整对象存活期,也没有得到新挂载的 procfs;所以本文只把 UTS/IPC 的 inode 与 hostname 变化写成真实结论。后续一旦获得 VM,也必须逐条补到对应实验记录,不能把这次成功 unshare 的返回码一次性分摊给所有 namespace 类型。
当最后一个进程退出,namespace 不一定立即消失:打开的 /proc/<pid>/ns/* FD、bind mount 或特殊的子 namespace 关系都可能维持引用。要验证生命周期须保留独立 FD、放开最后进程、用对象标识核对;本篇只按手册陈述该规则,持有 FD 的正反实验尚未运行,保留 NOT_RUN。
引用计数的思路比猜“进程已经退出,所以 namespace 必定不存在”更可迁移。假设实验进程打开了某个 UTS namespace 句柄,另一受控进程还持有这个 FD:第一个进程退出只减少一个引用,FD 持有者没有关闭前对象仍可能存在。反之,所有引用都释放后,原来的 inode 数字不应被当作长期稳定的租户身份;内核对象与数字标识有生命周期。持有句柄的试验要在专用 VM 创建、传递、关闭自己的 FD,记录关闭前后对象可否被 setns 使用,而不能在共享宿主 bind mount 别人的 /proc 路径来“保活”。
最后还要问隔离是否就是安全。不同 UTS inode 不会限制内存,IPC inode 变化不检查磁盘目录权限;即使进程在自己的 user namespace 内是 root,宿主文件的写入仍要按 UID/GID 映射和权限检查决定。容器能否接收外部流量还受 netns 里的接口、路由、监听地址和外部设备连接制约。先把受影响的对象类别说准确,再到 04–08 补挂载、身份、资源和调用权限,才不会因为一个 readlink 输出不同就宣称构建了安全沙箱。
| 现象 | 证据 | 限定条件 |
|---|---|---|
| hostname 在一侧变化 | UTS inode、两侧主机名 | 不等于两个内核 |
创建成功但 /proc 失败 |
unshare 错误与返回 1 |
不能用旧挂载冒充新 PID 视图 |
| 进程退出仍有 namespace | FD、挂载及内核引用 | 需要观察引用而非只看 PID |
练习
- 预测两个进程都显示同一 hostname 时,是否必然共享 UTS namespace;分别执行
readlink /proc/self/ns/uts并比较 inode,再解释结果。 - 预测关闭最后一个持有 namespace FD 的进程会改变哪些对象;在专用 VM 中先记录 fdinfo、相关进程及 inode,退出前后对照;当前云端暂记
NOT_RUN,不要靠hostname文本推断生命周期。
上一篇:01:进程启动与退出;下一篇:03:PID 1 与退出。
系列总目录:从进程隔离到运行时与编排。





