上一篇我们先把 Docker 的问题意识和历史脉络立住了。
我们提到过一个很关键的判断:
容器本质上还是进程,不是轻量版虚拟机。
这句话很重要,但它也会立刻带来另一个问题:
既然容器本质上还是宿主机上的进程,那它为什么会“看起来像一台独立机器”?
为什么进入容器之后,你会感觉:
如果不把这个问题讲清楚,Docker 就始终像黑盒。你会知道它好用,但不知道它为什么能这样工作。
这一篇我们先把容器“像一台机器”的第一层原因讲清楚:
Linux 可以让不同进程看到彼此不同的系统世界。
这个能力,在 Linux 里主要靠的是一组机制:namespace。
一、容器的“独立感”,本质上是视图被隔离了
很多人第一次理解容器时,会下意识把它想成“系统里又装了一套小 Linux”。
但从内核视角看,不是这样。
更准确地说,宿主机内核并没有真的重新造出好几台机器,也没有在每个容器里都复制出一套完整操作系统。它做的事情更像是:
同样是一台宿主机、同样是一份内核,但让不同进程看到不同的系统视图。
这里“系统视图”这个词很关键。
你平时在命令行里看到的很多东西,其实都不是“绝对真实世界”,而是内核在当前上下文里给你呈现出来的那一层视图。比如:
如果这些视图都能按进程或进程组被分开,那么进程自然就会产生一种“我像在独立环境里运行”的感觉。
所以先记一句:
容器的独立感,首先不是来自重新造机器,而是来自重新组织进程看到的世界。
二、namespace 到底是什么
先用最通俗的话讲:
namespace 就是 Linux 给进程加的一层“视图边界”。
加入同一个 namespace 的进程,更容易互相看见彼此;不在同一个 namespace 里的进程,看到的世界可能就不一样。
再准确一点说:
namespace 是 Linux 内核提供的一种隔离机制,用来把一些全局系统资源拆成多个彼此隔离的实例。这样一来,不同进程就可以分别拥有不同的资源视图。
这里“资源”不只是文件,也包括:
所以 namespace 不是一个单独功能,而是一组机制的统称。
你可以把它理解成:
namespace 不是创建新机器,而是把同一台机器上的“全局世界”切成多份局部视图。
如果是第一次接触容器原理,一般先从 UTS namespace 开始。
因为它最直观。
UTS namespace 主要隔离的是:
也就是说,它决定的是一个进程看到的“主机名视图”。
为什么这件事重要?
因为“我在一台什么机器上”的最直观感受之一,就是 hostname。你一进容器,shell 提示符上经常会出现一个和宿主机完全不同的名字,这就是最直接的“独立感”来源之一。
可以在 Linux 上直接做实验。
先看宿主机当前主机名:
hostname
然后创建一个新的 UTS namespace 并进入一个 shell:
sudo unshare --uts bash
你会发现,在这个 shell 里看到的主机名已经变成了 my-container。
但如果你另开一个宿主机终端再执行:
hostname
通常还是原来的宿主机名字。
这说明什么?
说明改的不是“全局唯一主机名”,而是当前这个 UTS namespace 里的 hostname 视图。
所以 UTS namespace 最适合用来建立第一层直觉:同一台宿主机上的不同进程,确实可以看到不同的“机器名”。
这也是容器像机器的第一层表象。
三、PID namespace
如果说 UTS namespace 只是“改了名字”,那 PID namespace 就是让容器真正开始“像一套独立系统”的关键机制之一。
它隔离的是:
进程号空间,也就是进程看到的进程树。
先说直白点的意思。
正常情况下,宿主机上的进程能看到整台机器上、自己有权限看到的大量进程。你执行 ps aux,看到的往往是整机范围里的许多任务。
但容器不希望这样。因为如果一个容器能直接看到宿主机全部进程,那它的“边界感”就弱很多,也更不安全。
PID namespace 的作用就是:
让进程只看到自己所在那套 PID 空间里的进程。
也就是说,在容器里执行 ps,你往往只会看到容器自己的那批进程,而不是整台宿主机的进程树。
这时容器里通常会有一个很重要的现象:
容器里的第一个进程,往往会看到自己像是 PID 1。
为什么这件事重要?
因为在传统 Linux 系统里,PID 1 往往有特殊地位,它是 init/systemd 那条线上的起点。容器里的“主进程”如果在自己的 PID namespace 里是 1,就会更像“这就是一个独立小系统的入口进程”。
可以做个实验。
先尝试执行:sudo unshare --pid --fork bash
进入后再执行:ps aux以及:echo $$
你会发现,这个 shell 里看到的进程数量会明显少很多,而且当前 shell 的 PID 也可能变得很小。
这里为什么常常要配合 --fork?
因为 PID namespace 的生效方式和当前进程、子进程有关。很多时候,为了让新的 shell 真正处在新的 PID 视图里,需要 fork 一个新进程进去。
这一点后面你如果手写 Docker,用 clone() 或相关调用时会更明显。
所以 PID namespace 可以用一句话概括:
它让容器像是拥有自己的一套进程宇宙。
四、mount namespace
到这里,主机名和进程树都能隔离了,但还不够。
因为一个进程对系统最深的感觉,除了“我是谁”“我能看到谁”,还有一个极关键问题:
我脚下这棵目录树到底是谁的?
也就是:
这类问题,主要就和 mount namespace 有关。
mount namespace 隔离的是:
挂载点视图,也就是进程看到的文件系统挂载结构。
先解释一下什么叫“挂载点视图”。
Linux 里的文件系统不是一整块天然长在一起的。很多目录树,其实是通过挂载组合起来的。比如一个新的文件系统挂载到某个目录下,那个目录下面的内容视图就变了。
如果所有进程都共享完全相同的挂载视图,那么一个容器里的挂载操作就可能直接影响宿主机或其他容器。这显然不行。
所以 mount namespace 的意义是:
让不同进程组可以拥有不同的挂载树。
这样容器里看到的根目录 /、挂载结构、/proc 等内容,就可以和宿主机分开组织。
这也是为什么容器能拥有自己的一套 rootfs 视图。
你可以先在宿主机查看挂载信息,比如执行 mount | head,或者执行 cat /proc/self/mounts | head。
然后再尝试新建一个 mount namespace:sudo unshare --mount bash
在新 shell 里再看挂载信息。后续如果再配合 mount 操作和更完整的 rootfs 组织,就会更明显地看到:
这棵目录树已经不再是宿主机原样照搬。
所以 mount namespace 解决的是:
容器为什么能有自己的一套文件系统视图。
五、network namespace
如果说 UTS 解决“这台机器叫什么”,PID 解决“这台机器里有哪些进程”,mount 解决“这台机器看到的目录树是什么”,那 network namespace 解决的就是:
这台机器的网络世界是什么样。
它隔离的是:
这就是为什么一个容器里可以有自己的一张 eth0、自己的 IP、自己的路由规则,而不会直接和宿主机完全混成一锅。
换句话说,network namespace 让一个进程组感觉自己像拥有独立网络栈。
你可以先在宿主机看网络接口,比如执行 ip addr,或者执行 ip link。
然后创建新的 network namespace 并进入:sudo unshare --net bash
在新的 shell 里再执行 ip link,很多环境下你会发现,接口视图会发生明显变化,通常不会再直接看到宿主机那套完整网络接口配置。
当然,单纯 unshare --net 之后,这个新的 network namespace 可能还比较“空”。因为真实容器运行时通常还会结合很多后续网络配置,比如:
也就是说,network namespace 只负责:
隔离网络视图。
它并不自动替你把网络全配好。
所以你可以把它理解成:
network namespace 提供了独立网络空间的骨架,真正让容器联网,还需要后续网络配置把它接起来。
六、IPC namespace
Linux 里进程之间不是只能靠 socket 和文件通信。还有一类传统 IPC 资源,比如:
如果这些对象完全全局共享,那么不同容器之间就容易互相干扰。
所以 Linux 还有 IPC namespace,用来隔离这类进程间通信对象视图。
它解决的是:
让不同容器里的进程,不随便共享和碰撞彼此的 IPC 资源。
这个 namespace 在日常直觉上不像 PID 和网络那么强烈,因为你平时不是天天肉眼看消息队列和信号量。但从隔离完整性上,它很重要。
你可以在宿主机看 IPC 对象,比如执行:ipcs
如果进入不同 IPC namespace,再看这类对象,看到的集合可能就不同了。
所以 IPC namespace 的意义可以简单记成:
把“看不见但真实存在的进程通信资源”也切开。
七、user namespace
上面几种 namespace 主要是在改“你看到什么”。
但还有一个更敏感的问题:
你是谁?你的权限怎么算?
这就涉及 user namespace。
它隔离的是用户 ID、组 ID 及其映射关系。
这件事初看比较抽象,但它很重要,因为容器里经常会出现一种现象:
这背后就和用户命名空间及权限映射有关。
user namespace 的核心价值是:
把容器内的身份视图和宿主机上的真实权限边界拆开。
这对安全非常重要。否则容器里一个“看起来是 root 的进程”,如果直接等价于宿主机 root,风险就太高了。
这一块比前面几种更容易牵涉到安全模型和 uid/gid 映射,初学时不一定要一口吃透。先记住它解决的问题就够了:
容器里的“我是 root”,不一定等于宿主机上我真是最高权限。
八、namespace 到底在解决什么
如果把上面这些机制放在一起看,就很清楚了。
Linux 不是为容器单独发明了一个“容器模式”,而是通过多种 namespace,把进程周围的世界一块块切开:
- PID namespace:隔离进程号和进程树视图
- mount namespace:隔离挂载点和文件系统视图
- network namespace:隔离网络设备和网络栈视图
- IPC namespace:隔离消息队列、信号量等 IPC 资源视图
- user namespace:隔离身份与权限映射视图
所以容器之所以像一台机器,不是因为内核重新造了一台机器,而是因为:
进程周围和“机器感”有关的那些关键视图,都被分开了。
九、相关命令:先建立一个最小工具箱
如果你准备继续写容器原理,或者后面手写极简 Docker,下面这些命令建议先熟。
1. 观察进程和系统视图
常用命令:
主要用途:
2. 观察文件系统和挂载
常用命令:
主要用途:
3. 观察网络视图
常用命令:
主要用途:
4. 观察 IPC 资源
常用命令:
主要用途:
5. 最关键的实验入口:unshare
可以分别尝试:
sudo unshare --pid --fork bashsudo unshare --mount bash
也可以组合使用:
sudo unshare --uts --pid --mount --net --fork bash
这些命令的意义,不是让你“直接做出完整 Docker”,而是帮你亲手感受到:
原来一个普通 shell,真的可以被放进一套不同的系统视图里。
这一点非常重要。
这里有一个必须强调的边界:只有 namespace,还不等于完整容器
讲到这里,很多人会产生一个误解:
“既然把这些 namespace 一开,不就已经是 Docker 了吗?”
还不是。
因为 namespace 主要解决的是:
你看见什么。
但完整容器还至少要继续解决两类问题。
第一,资源边界。
如果只是让视图变了,但一个容器里的进程仍然可以把 CPU、内存、I/O 都吃满,那容器就没有真正的资源治理能力。
这一部分,主要要靠 cgroups。
第二,文件系统和运行环境组织。
只开了 namespace,并不自动等于你已经拥有了一套整理好的 rootfs、镜像层、容器启动流程。
这一部分,后面会牵涉到:
所以 namespace 是容器“像机器”的第一层基础,但不是全部。
十、为什么这一篇对后面“手写 Docker”特别关键
如果你后面准备自己手写一个最小 Docker,最先遇到的往往不是 Dockerfile,不是镜像仓库,而是这些很底层的动作:
也就是说,真正“写容器”的起点,不是学会更多 Docker 命令,而是先把这些 Linux 原生机制拆明白。
容器不是一个神秘盒子。
它首先是一组被重新组织了视图和边界的进程。
- 容器本质上还是进程,但它看到的系统世界可以和宿主机不同。
- namespace 的核心作用,是把全局系统资源切成多份隔离视图。
- UTS、PID、mount、network、IPC、user 这些 namespace 一起构成了容器的“独立感”。
- namespace 让容器“像一台机器”,但还没有解决资源边界和完整运行时组织。
如果你对这条线感兴趣,欢迎继续关注后续内容。也欢迎加入芯航线交流群,后面我们会继续把 Docker 这条线往底层拆,直到自己手写一个最小可运行版本。
芯航线主理人胡同学: