Docker Namespace 原理:容器为什么能“以为自己是台独立的机器“
Docker Namespace 原理:容器为什么能"以为自己是台独立的机器"
一、开篇引言
学启动控制那阵子,我在 [[7.30 控制启动过程]] 结尾写过一句话:“chroot 现在我也只是知道了它能切换根目录,但具体在 Docker 或容器场景里怎么用,我还不太清楚,后面学容器的时候再回来看这部分。” 没想到这么快就真到容器了。
这周云原生课讲 Docker,老师上来先抛了个问题:“你们觉得容器和虚拟机到底有什么区别?” 我第一反应是"虚拟机重、容器轻、容器启动快",但这只是表象。真正把我问住的是后面这两句:
- 容器里明明跑着进程,为什么容器内
ps看不到宿主机的进程? - 为什么容器里改主机名,宿主机一点反应都没有?
我当时脑子里只有一个模糊的答案:“因为容器是隔离的。” 但"隔离"到底是怎么实现的?是像虚拟机那样,每个容器里装一个完整系统吗?显然不是——所有容器共享宿主机同一个内核。
老师没直接回答,而是扔给我一个词:Namespace。
顺带记一下环境:Docker 装在我那台 CentOS 7 虚拟机上,照例先打了快照。后面因为 unshare 需要权限,还加过 sudo,坑不少,下面慢慢说。
二、Namespace 核心概述:不是"独立电脑",是"独立视野"
什么是 Linux Namespace
先想想 docker run -it centos:7 bash 之后到底发生了什么。你进到了一个 shell,里面有自己的一套 /、自己的进程列表、自己的网卡,看起来像一台独立的机器。
但事实是:容器里的进程就是跑在宿主机上的普通进程,它只是被"加了层滤镜",看不到自己外面的世界。
这层"滤镜"就是 Linux 内核的 Namespace(命名空间) 机制。它是内核提供的一种资源视图隔离手段:给不同进程组提供不同的"系统资源视图"。内核里的进程表、网络栈、挂载点、主机名这些资源,Namespace 会分别给它们"复制一份假象",让每个 namespace 里的进程都以为资源是自己独享的。
我自己的理解:Namespace 不是把进程真的关起来,而是改写了进程看世界的视角。就像戴着 VR 眼镜,你以为自己在火星,其实人还在客厅里。
Namespace 核心能力:资源视图隔离
"视图隔离"这个词我一开始没懂,后来想通了:同一个东西,不同的人看,看到的不一样。
比如宿主机上有个 PID 是 2000 的进程,在某个 PID namespace 里,它被"重新编号"成了 PID 1。同一个进程,换个视角,编号就不同了。
Namespace 和虚拟机隔离的区别
这是老师强调的重点,直接上对比表:
| 维度 | 虚拟机 | 容器 |
|---|---|---|
| 隔离层次 | 硬件级:虚拟出 CPU、内存、磁盘 | 内核级:共享宿主机内核 |
| 是否独占内核 | 每个 VM 有自己的 Guest OS 和内核 | 所有容器共享宿主机一个内核 |
| 隔离强度 | 强,连内核都隔离 | 弱,只是"视图"隔离 |
| 启动速度 | 分钟级,要引导整个系统 | 秒级,本质就是起一个进程 |
| 资源开销 | 大,每个 VM 都要占一份系统资源 | 小,多个容器共用内核 |
一句话记忆:虚拟机是"每人发一台电脑",容器是"好多人共用一台电脑,但每人开了个独立桌面"。所以容器才这么轻、这么快,但也注定不如虚拟机"硬隔离"。
三、Docker 六大核心 Namespace 详解
Docker 创建容器时,默认会给容器建立 6 种核心 namespace。其实内核里还有 cgroup、time 等 namespace,但课堂上重点讲这 6 个。
| 类型 | 隔离内容 | 直观表现 |
|---|---|---|
| PID | 进程隔离,独立的 PID 编号空间 | 容器内 PID=1,宿主机上是另一个号 |
| NET | 网络隔离,独立网络栈:网卡、IP、路由、端口 | 容器有自己的 eth0、ip addr |
| MNT | 挂载隔离,独立挂载树 | 容器内 / 是镜像 rootfs,不是宿主机的 / |
| UTS | 主机名隔离,独立主机名和域名 | 容器里改 hostname,宿主机无感 |
| USER | 用户隔离,独立 UID/GID 映射 | 容器内 root 可能是宿主机的普通用户 |
| IPC | 进程间通信隔离 | 隔离消息队列、共享内存、信号量 |
下面一个一个说,尽量配一个能直接上手的验证命令。
1. PID Namespace:进程隔离
最直观的一种。容器里 ps 只能看到自己 namespace 里的进程,PID 从 1 开始(1 一般是容器主进程)。宿主机上同一批进程是另外一套 PID。
# 容器内
ps -ef | head -3
# PID PPID CMD
# 1 0 bash ← 容器主进程,在容器内是 PID=1
# 宿主机上找到这个 bash 的真实 PID
docker inspect --format '{{.State.Pid}}' <容器名>
# 输出:2057 ← 同一个进程,在宿主机上真实 PID 是 2057
同一个进程,容器内看是 1,宿主机看是 2057,这就是 PID namespace 干的事。
2. NET Namespace:网络栈隔离
每个容器有一套独立的网络设备、IP 地址、路由表和防火墙规则。容器里 ip addr 看到的是自己的网卡(默认 bridge 模式是 eth0),宿主机的物理网卡它看不到。所以宿主机和容器可以同时监听 80 端口,互不冲突。
docker run -d nginx
docker exec -it <容器ID> ip addr
# 1: lo: <LOOPBACK,UP>...
# 2: eth0@if7: ... inet 172.17.0.2/16 ← 容器自己的 IP
3. MNT Namespace:挂载隔离
每个容器有自己的挂载树,容器里的 / 是镜像分层 + 可写层联合挂载出来的(就是 [[8.4 张炜炜 容器知识点问答]] 里讲的 Overlay2),不是宿主机的 /。所以你在容器里改挂载、甚至乱删,影响不到宿主机文件系统——至少挂载点这个维度是隔离的。
4. UTS Namespace:主机名隔离
容器可以有自己的 hostname,改了也不影响宿主机。这个验证最简单,跑一个容器改个名字就行:
docker run -it --hostname my-container centos:7 bash
hostname # 容器内输出:my-container
# 退出容器后,宿主机 hostname 一点没变
5. USER Namespace:用户隔离
容器里你看着是 root(UID=0),但这个 root 只是"看起来像"。通过 USER namespace 的 UID/GID 映射,容器内的 UID 0 可能对应宿主机上一个普通用户(比如 UID 1000)。
好处很直接:就算容器被攻破,容器内的 root 在宿主机上也只是个普通用户,动不了宿主机的系统文件。这也是容器安全体系里很关键的一环。
6. IPC Namespace:进程间通信隔离
隔离 System V 的消息队列、共享内存、信号量。容器内进程的 IPC 资源互相可见,但看不到宿主机的 IPC 资源,宿主机也看不到容器里的。
四、Namespace 底层实现原理
三个核心系统调用:clone / unshare / setns
Namespace 的创建和切换全靠这三个系统调用:
| 系统调用 | 作用 | 典型场景 |
|---|---|---|
clone() |
创建新进程时带上 namespace 标志位,可顺带创建新 namespace | docker run 时 runc 创建容器进程 |
unshare() |
让当前进程脱离原 namespace,创建新的 | 命令行工具 unshare |
setns() |
让当前进程加入一个已存在的 namespace | docker exec、nsenter |
关键点:namespace 是被"继承"的。 子进程默认和父进程共享同一个 namespace。要开新 namespace,必须在 clone 时带上标志位,比如 CLONE_NEWPID、CLONE_NEWNET、CLONE_NEWNS(挂载)等。
对应到 Docker:docker run 时,底层 runc 用 clone 创建了一个带着一堆 CLONE_NEW* 标志的进程,这个进程就成了容器里的第一个进程(PID=1),之后它 fork 出来的子进程全部待在这个新 namespace 里。
进程和 Namespace 怎么绑定
每个 namespace 在 /proc/<PID>/ns/ 下都有一个软链接:
ls -l /proc/$$/ns/
# lrwxrwxrwx ... mnt -> 'mnt:[4026531840]'
# lrwxrwxrwx ... net -> 'net:[4026532009]'
# ...
括号里的数字是 namespace 的 inode 号。两个进程某个 namespace 的 inode 号相同,就说明它们在这个维度共享同一个 namespace。 判断两个进程是不是"一家人",就看 ns inode 对不对得上。
父子 Namespace 的可见性
以 PID namespace 为例,有一条"单向可见"的规则:
- 父 namespace 能看到子 namespace 里的所有进程(宿主机能看到容器里的进程)
- 子 namespace 看不到父 namespace 里的进程(容器里看不到宿主机的其他进程)
原因是 PID 是"逐层重新编号"的:宿主机给容器进程编了 2057,容器内又给同一批进程编了一套从 1 开始的号,两套编号通过映射对应起来。
我一开始想找个生活化的比喻,什么"领导能看到你工位,你工位看不到领导办公室",越想越别扭。算了,直接记结论:父见子,子不见父。
五、Namespace 实操验证
方法一:看容器开了哪些 namespace
# 1. 起一个容器,拿到它在宿主机的 PID
docker run -d --name demo nginx
container_pid=$(docker inspect --format '{{.State.Pid}}' demo)
# 2. 看这个进程的所有 namespace
ls -l /proc/$container_pid/ns/
会看到一堆 ns:[402653xxxx],这些就是 Docker 给容器创建的独立 namespace。
方法二:用 unshare 手工"造"一个隔离环境
不用 Docker,光靠内核的 unshare 命令就能体验 namespace:
# 创建一个新的 PID + UTS namespace,并重新挂一个空的 /proc
sudo unshare --pid --uts --fork --mount-proc bash
# 进去之后:
ps -ef # 只能看到自己这少数几个进程
hostname # 还是宿主机的,因为只创建了 namespace 没改名字
踩坑记录:我第一次敲的是
unshare --pid bash,然后ps -ef,结果进程列表跟宿主机一模一样,以为 unshare 没生效。查了半天才明白:PID namespace 其实生效了,但 /proc 还是宿主机那份,ps读的/proc没跟着切。加上--mount-proc(在新 mount namespace 里重新挂载空的 /proc)就正常了。后来老师一句话点醒我:namespace 管的是"视图",而 /proc 这个文件系统本身就是一种视图,它也得跟着切。另外
--pid一般要配--fork,否则 namespace 的行为会很怪;普通用户直接跑还容易报Operation not permitted,得加 sudo。
方法三:对比容器内外的主机名和进程
docker run -it --hostname container-test centos:7 bash
# 容器内:
hostname # container-test
ps -ef | wc -l # 很少,只有容器自己的进程
# 宿主机另开一个终端:
hostname # 还是原来的名字,没被容器影响
六、Namespace 的局限性:它只"隔离视野",不"限制用量"
学到这里,我一度以为容器隔离就是 Namespace 一己之力。但老师马上泼了盆冷水:Namespace 只解决"看不看得见",不解决"用不用得起"。
局限一:内核不隔离
所有容器共享宿主机内核,Namespace 只是"改视角",不是真的砌了一堵墙。如果内核有漏洞,攻击者有可能从容器里"逃逸"到宿主机——这就是常说的容器逃逸。虚拟机里每个 Guest OS 有独立内核,就没这个问题(除非 Hypervisor 本身出问题)。
局限二:部分资源没法隔离
Namespace 能隔离进程、网络、挂载、主机名、用户、IPC,但它管不了 CPU、内存这些硬件资源的使用量。一个容器可以疯狂 fork 把宿主机内存吃光,把整个系统拖垮,其他容器跟着遭殃。
局限三:光靠 Namespace 远远不够
所以 Docker 的隔离是"组合拳":
| 机制 | 管什么 |
|---|---|
| Namespace | 隔离"视图":进程、网络、挂载、主机名、用户、IPC |
| Cgroups | 限制"用量":CPU、内存、磁盘 IO 上限 |
| Capabilities | 收窄容器内 root 的特权(容器内 root ≠ 有全部内核能力) |
| Seccomp | 限制容器内能调用的系统调用 |
| SELinux / AppArmor | 强制访问控制,给容器打安全标签 |
记法:Namespace 管"看得见",Cgroups 管"用得着",Seccomp/Capabilities 管"碰不得"。
七、总结
把整个逻辑串一遍:
docker run 一个容器
→ runc 用 clone 带着一堆 CLONE_NEW* 标志创建容器进程
→ 进程落在全新的几个 namespace 里,从此"眼睛"被换掉
→ 看到的是:独立 PID 表、独立网络栈、独立挂载树、独立主机名……
→ 但它的 CPU / 内存用量没人管
→ 于是 Cgroups 上场:限制它最多能用多少
→ 内核还是共享的,所以 Capabilities + Seccomp + SELinux 继续加固
所以容器隔离的整体逻辑是:Namespace 造出"独立世界"的幻觉,Cgroups 控制"饭量",安全机制防止"越狱",三者配合才是一个能上生产的容器。
学这块之前我一直以为"容器 = 轻量虚拟机",现在才明白这俩隔离的本质完全不同:虚拟机隔离在硬件层,容器隔离在内核的软件层。这也是容器能秒级启动、资源开销小的原因,同时也是它隔离强度不如虚拟机的根源。
速记清单
容器共享宿主机内核,不是硬件隔离
Namespace 管"看得见",Cgroups 管"用得着"
6 大 Namespace:PID / NET / MNT / UTS / USER / IPC
进程和 namespace 靠 /proc/<PID>/ns/ 的 inode 号对上关系
clone 开新 namespace,setns 进已有 namespace,unshare 给自己换
父 namespace 能看到子,子看不到父
我踩过的坑合集
| 坑 | 我的翻车经历 | 正确做法 |
|---|---|---|
unshare 没权限 |
普通用户跑 unshare --pid 报 Operation not permitted |
前面加 sudo |
--pid 忘配 --fork |
PID namespace 行为很怪 | 用 unshare --pid --fork --mount-proc bash |
unshare --pid 后 ps 没变 |
以为没生效,其实是 /proc 没隔离 |
加 --mount-proc 重新挂空的 /proc |
| 容器内外 PID 对不上 | docker inspect 里找容器 PID 一时找不到 |
用 {{.State.Pid}} 拿宿主机的真实 PID |
| Namespace 和 Cgroups 搞混 | 以为 Namespace 能限制内存 | Namespace 只管视图,资源限制归 Cgroups |
| chroot 和 MNT namespace 混着记 | 以为容器隔离就是 chroot | 容器是 MNT namespace + pivot_root,chroot 只是切 / 视角,范围小得多 |
最后说两句
写这篇的时候,我在虚拟机上反复 unshare、docker run、ls /proc/*/ns/,中间还因为 Operation not permitted 卡了一会儿,最后是靠 sudo + --fork --mount-proc 一套组合拳跑通的。
还没完全搞懂的地方:USER namespace 的 UID 映射细节我只能看懂流程,真要让我自己配一组映射还是配不来;setns 和 nsenter 怎么进别人的 namespace 也还停留在概念阶段。这两块下学期打算补上,顺便把 Cgroups 单独写一篇。
对了,回看 [[7.30 控制启动过程]] 里那句"chroot 在容器里怎么用还不清楚",现在算是有答案了——容器用的是 Mount namespace + pivot_root 这一整套,比 chroot 彻底得多:chroot 只是切了 / 的路径视角,MNT namespace 是把整个挂载树都换掉。果然学过的东西是会串起来的。
2026年8月 · 写于云原生课 Namespace 整理 · 虚拟机里 unshare 出来的隔离环境被我拆了又建、建了又拆
更多推荐


所有评论(0)