登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  linux

用 namespace 理解容器进程、网络与挂载隔离

来源:17golang原创

时间:2026-10-08 15:52:08 268浏览 收藏

第一次认真排查容器里的“进程明明存在,宿主机和容器看到的 PID 却不同”时,我才意识到,把容器理解成轻量虚拟机很容易把问题带偏。更准确的模型是:容器中的进程仍由宿主机同一个 Linux 内核运行,只是多个 namespace 为它组合出不同的资源视图。PID namespace 改变进程号与进程可见范围,network namespace 隔离网卡、路由、端口等网络资源,mount namespace 隔离挂载点列表和文件系统层级视图。

这三类隔离可以单独存在,也可以组合使用。它们不负责限制 CPU 和内存,也不能单独构成完整安全沙箱;资源配额要看 cgroup,权限收敛还要结合 capabilities、seccomp 和 LSM 等机制。

Linux namespace 参考手册:https://man7.org/linux/man-pages/man7/namespaces.7.html

升级范围:从“轻量虚拟机”改成“多视图组合”

以前我习惯用虚拟机类比容器:似乎每个容器都有自己的一套系统。这个类比适合解释使用感受,却不适合定位底层问题。虚拟机通常运行独立内核,而基于 Linux namespace 的容器共享宿主机内核。被“复制”的不是内核本身,而是进程能看到和操作的若干资源视图。

namespace 将原本全局的系统资源包装为抽象,让某组进程看起来拥有独立实例。一个进程属于多种 namespace,因此容器边界不是一个总开关,而是 PID、mount、network、UTS、IPC、user、cgroup、time 等边界的组合。本文只聚焦最容易在日常排障中碰到的三种:

  • PID namespace:控制进程号空间、进程可见范围和命名空间内的 PID 1。
  • network namespace:控制网络设备、协议栈、路由表、防火墙规则和端口空间等。
  • mount namespace:控制进程看到的挂载点集合与目录层级视图。

变更表:PID、network、mount 各自隔离什么

namespace主要隔离对象容器内直观表现常见误解
PIDPID 编号空间、进程可见性容器主进程显示为 PID 1,只能看到本层和后代层可见进程误以为宿主机上不存在这些进程
network网卡、IPv4/IPv6 栈、路由、端口、防火墙规则容器有独立接口和路由,同一端口可在不同网络命名空间重复监听误以为创建 netns 后天然能访问外网
mount挂载点列表、挂载传播与文件系统视图容器内挂载或卸载可不影响宿主机视图误以为它自动复制了文件内容或改变磁盘权限
Linux 内核上的 PID、network 与 mount namespace 三种隔离视图
结构图:三类 namespace 共享同一个 Linux 内核,但分别为进程、网络与挂载资源提供独立视图。

这个表带来的一个重要变化是:排障时先问“当前进程处于哪个 namespace”,再问“资源是否存在”。例如端口冲突只在同一个 network namespace 的端口空间里成立;某个挂载点只对处于对应 mount namespace 的进程可见;同一个内核进程在不同 PID 层级中可能有不同编号。

旧认知风险:为什么只看进程号会误判

PID namespace 是分层的。新命名空间中的第一个进程会成为该层的 PID 1,它承担特殊职责,例如在该命名空间中处理孤儿进程。宿主机所在的祖先 PID namespace 能看到后代命名空间里的进程,但子层不能反过来看见父层的全部进程。

因此,同一个内核进程可以在容器里显示为 PID 1,在宿主机上显示为 PID 24860。它不是两个进程,也不是 PID 被“改掉”了,而是观察者位于不同 PID namespace,看到的是同一进程在各层的编号。

同一进程在宿主机与容器 PID namespace 中的不同编号
说明图:宿主机与子 PID namespace 对同一进程使用不同可见编号,父层可见后代,子层看不到父层全部进程。

这也解释了为什么容器里的 ps 结果与宿主机不同。更细一点说,/proc 展示哪些进程,还与这个 procfs 实例是从哪个 PID namespace 挂载出来的有关。只创建 PID namespace、却继续沿用旧的 /proc,会让观察结果显得混乱。

新写法一:用 /proc 与 unshare 观察 PID 和挂载

每个进程的 /proc/PID/ns/ 目录都保存了它所属 namespace 的句柄。符号链接内容包含 namespace 类型和 inode 标识;两个进程对应条目的设备号与 inode 相同时,说明它们处于同一个该类 namespace。先观察当前 shell:

# 查看当前 shell 所属的 PID、网络和挂载 namespace
for ns in pid net mnt; do
  readlink "/proc/$$/ns/$ns"
done

# 列出当前进程的全部 namespace 句柄
ls -l "/proc/$$/ns"

接着用 unshare 创建一次性 PID 实验环境。以下命令需要 Linux,并通常需要相应 capability 或 sudo 权限。--pid 为后续子进程建立新 PID namespace,--fork 确保启动一个子进程,--mount-proc 为新视图挂载匹配的 procfs:

# 创建新的 PID namespace,并挂载与其匹配的 /proc
sudo unshare --fork --pid --mount-proc /bin/bash

# 在新 shell 内查看进程;当前 bash 通常显示为 PID 1
ps -ef

# 确认当前 shell 的 PID namespace 句柄
readlink "/proc/$$/ns/pid"

# 退出后一次性 namespace 随最后一个进程结束
exit

如果只想理解 mount namespace,可以再做一个独立实验。新的 mount namespace 初始会复制调用者的挂载列表,但后续挂载变化可以只保留在新视图中。显式将传播设为递归 private,可以避免实验挂载传播到其他命名空间:

# 新建 mount namespace,并在其中启动 shell
sudo unshare --mount /bin/bash

# 将根挂载传播设为递归 private,限制实验影响范围
mount --make-rprivate /

# 只在当前 mount namespace 内挂载一个 16 MiB 的 tmpfs
mkdir -p /mnt/ns-lab
mount -t tmpfs -o size=16m tmpfs /mnt/ns-lab

# 查看当前视图中的挂载信息
findmnt /mnt/ns-lab

# 退出后临时挂载随 namespace 生命周期结束
exit

mount namespace 隔离的是“挂载视图”,不是文件权限万能开关。多个命名空间仍可挂载同一底层文件系统,也可能看到相同文件内容;是否允许读写仍受 UID/GID、capability、只读挂载、LSM 策略等共同影响。

新写法二:用 netns 和 veth 观察网络边界

network namespace 会隔离网络设备、路由表、端口、协议栈状态与多类网络配置。新建命名空间后,它不会凭空获得与宿主机相连的网卡。常见做法是创建一对 veth:两端像一根虚拟网线,一端留在宿主机,另一端移动到目标 network namespace。

# 创建名为 lab 的 network namespace
sudo ip netns add lab

# 创建一对相连的 veth 虚拟网卡
sudo ip link add veth-host type veth peer name veth-lab

# 将 veth-lab 移入 lab namespace
sudo ip link set veth-lab netns lab

# 为宿主机一端配置文档专用地址并启用接口
sudo ip address add 192.0.2.1/24 dev veth-host
sudo ip link set veth-host up

# 在 lab 内启用回环和 veth,并配置另一端地址
sudo ip netns exec lab ip link set lo up
sudo ip netns exec lab ip address add 192.0.2.2/24 dev veth-lab
sudo ip netns exec lab ip link set veth-lab up

# 从 lab 内验证与宿主机 veth 端点的连通性
sudo ip netns exec lab ping -c 1 192.0.2.1

这个实验只建立了点对点连接,不等于已经配置外网访问。要访问其他网络,还需根据环境配置路由、转发和 NAT,并谨慎处理防火墙。实验结束后删除命名空间和宿主机 veth:

# 删除 namespace;其中的 veth-lab 会随之销毁
sudo ip netns delete lab

# 删除宿主机剩余的 veth 端,避免遗留测试接口
sudo ip link delete veth-host 2>/dev/null || true

我觉得 veth 实验最有价值的地方,不是记住几条命令,而是亲眼看到“网络隔离”和“网络连通”是两件事。namespace 先划出独立网络世界,veth、bridge、路由与 NAT 再决定这些世界如何通信。

回归检查:确认隔离而不是猜测

当容器网络、进程或挂载出现异常时,可以按下面的证据顺序核对:

  1. 比较 namespace 句柄:读取两个进程的 /proc/PID/ns/pid、net、mnt。标识相同才表示位于同一视图。
  2. 从目标进程视角进入:用 nsenter 选择目标 PID 及具体 namespace,而不是只在宿主机执行同名命令。
  3. PID 检查:确认新 PID namespace 是否有正确的 PID 1,以及 procfs 是否从对应 PID 视图挂载。
  4. 网络检查:查看目标 netns 内的接口、地址、路由和监听端口;不要拿宿主机的 ip route 代替。
  5. 挂载检查:读取目标进程的 /proc/PID/mountinfo,同时关注 shared、master 等传播标记。
# 将 TARGET_PID 替换为目标容器主进程的宿主机 PID
TARGET_PID=24860

# 比较当前 shell 与目标进程的三类 namespace 标识
for ns in pid net mnt; do
  readlink "/proc/$$/ns/$ns"
  readlink "/proc/$TARGET_PID/ns/$ns"
done

# 进入目标进程的 PID、网络和挂载视图后启动诊断 shell
sudo nsenter --target "$TARGET_PID" --pid --net --mount /bin/bash

nsenter 需要相应权限,生产环境中也不应把它当作绕过容器边界的日常入口。它适合受控排障:先记录目标 PID、进入了哪些 namespace、执行了什么只读检查,再退出。

迁移清单:把 namespace 放回容器全栈

理解 namespace 后,我对容器配置的判断也发生了变化:不再问“容器有没有隔离”,而是逐项确认隔离层是否完整。

目标主要机制检查重点
隐藏其他进程并提供容器 PID 1PID namespace进程层级、信号处理、僵尸进程回收、procfs 视图
独立网卡、路由和端口空间network namespaceveth/bridge、路由、DNS、转发、NAT、防火墙
独立挂载视图mount namespace只读挂载、传播属性、bind mount、敏感路径
限制 CPU、内存和 I/Ocgroup配额、压力、OOM 行为、统计与层级
减少特权操作capabilities按需授予,避免保留不必要的 CAP_SYS_ADMIN
限制系统调用seccomp默认策略、必需调用、异常行为
强化访问控制SELinux/AppArmor 等 LSM策略标签、拒绝日志、最小权限
  • 不要把 namespace 当成虚拟机边界:它共享宿主机内核。
  • 不要把 namespace 当成资源配额:CPU 和内存限制属于 cgroup 的职责。
  • 不要只检查容器内输出:同时从宿主机和目标 namespace 两个视角取证。
  • 不要忽略 PID 1:应用作为 PID 1 时,信号转发与子进程回收行为会影响优雅退出。
  • 不要默认 mount 变化绝不传播:检查挂载传播属性,尤其是宿主机与容器共享路径。
  • 不要默认 netns 自动联网:接口、路由、DNS、转发和 NAT 都要分别配置。

常见问题

namespace 和 cgroup 有什么区别?

namespace 主要改变进程“看见什么”,cgroup 主要控制和统计进程“能使用多少资源”。容器通常同时使用两者:前者提供资源视图隔离,后者提供 CPU、内存、I/O 等控制。

为什么容器内 PID 1 在宿主机不是 1?

PID namespace 是分层的,同一进程在每个可见层级中可以有不同 PID。它在容器子层是 PID 1,在宿主机祖先层有另一个普通 PID。

mount namespace 会复制整个文件系统吗?

不会。创建 mount namespace 时获得的是挂载列表视图,后续可独立调整挂载。底层文件系统和文件数据仍可能共享,权限与读写控制也需要其他机制配合。

两个容器为什么都能监听 80 端口?

如果它们位于不同 network namespace,就拥有独立的端口空间和网络栈,因此都可以在各自视图中监听 80。宿主机如何把外部流量送到它们,则由端口映射、代理、路由或负载均衡决定。

把容器从“缩小版虚拟机”升级成“多个 namespace 组合出的资源视图”后,很多看似矛盾的现象都会变得直观:PID 不同不是两个进程,端口相同不一定冲突,挂载存在也不代表所有进程都能看到。对日常排障来说,最实用的起点就是 /proc/PID/ns/——先确认观察者站在哪个视图里,再解释它看到的世界。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>