1979 年,UNIX V7 引入了 chroot 系统调用,允许进程看到一个与根目录不同的文件系统视图,这是容器隔离思想的起点。三十多年后,容器技术已经从简单的目录切换发展为包含 Namespace、Cgroup、OverlayFS、OCI 规范、多层运行时架构的完整体系。docker run 一条命令背后,是四个进程、三个抽象层、两个规范的精密协作。
本章先沿着时间线看清容器是怎么一步步演进到今天的,再总览容器运行时的分层架构,最后辨析几组容易混淆的核心概念。至于三大内核基石的细节、OCI 规范的逐项拆解、runc 与 containerd 的源码,留给后续章节展开。
一、容器技术演进:从 chroot 到 OCI
1.1 早期隔离:chroot(1979)
chroot 是 UNIX 系统中最古老的隔离机制。它的设计极其简单:改变进程的根目录视图,让进程无法访问根目录之外的文件。
# 创建一个最小 rootfsmkdir -p /tmp/myroot/bin /tmp/myroot/lib64cp /bin/bash /tmp/myroot/bin/# 复制 bash 需要的动态链接库ldd /bin/bash | grep -oE '/[^ ]+' | xargs -I{} cp -v --parents {} /tmp/myroot
# 使用 chroot 切换根目录sudo chroot /tmp/myroot /bin/bash# 此时进程的根目录是 /tmp/myroot,无法访问原系统的文件但 chroot 的隔离极其有限:
| 隔离维度 | chroot 是否隔离 | 说明 |
|---|---|---|
| 文件系统视图 | 是 | 改变根目录视图 |
| 进程可见性 | 否 | 仍能看到所有 PID |
| 网络栈 | 否 | 共享宿主机网络 |
| 用户身份 | 否 | 仍是同一个 UID |
| 资源使用 | 否 | 无资源限制 |
| 设备访问 | 否 | 可访问 /dev 下设备 |
从这张表能看出,chroot 只动了一个根目录视图,进程、网络、用户、资源、设备全都和宿主机共享。这种程度的隔离离”安全”差得很远:
chroot 不提供安全隔离。root 用户可以轻松逃逸 chroot 环境,通过 mknod 创建内存设备、通过 ptrace 附加外部进程、通过 /proc 访问宿主文件系统。chroot 的设计目标是”便利”而非”安全”。
1.2 FreeBSD Jail(2000)与 Solaris Zones(2004)
chroot 的局限性催生了更强的隔离方案。FreeBSD Jail 在 chroot 基础上增加了进程隔离、网络隔离和资源限制,Solaris Zones 则提供了更完整的操作系统级虚拟化。
这些方案虽然功能丰富,但都是特定操作系统的实现,无法在 Linux 上使用。Linux 需要自己的容器隔离方案。
1.3 Linux 容器的萌芽:LXC(2008)
Linux 容器技术的真正起点是 Namespace 和 Cgroup 这两大内核特性。Namespace 早在 2002 年就随 Mount Namespace 进入内核,Cgroup 则在 2008 年随 2.6.24 合入主线,到 LXC 之前两者的组合已经具备容器雏形。LXC(Linux Containers)是第一个将这些内核特性组合使用的项目:
# 安装 lxcapt install lxc -y# LXC 下载模版创建容器sudo lxc-create -n mycontainer -t download -- -d ubuntu -r noble -a amd64sudo lxc-start -n mycontainersudo lxc-attach -n mycontainer
# LXC 底层使用的就是 Namespace + Cgroup# 查看 LXC 容器的 Namespacels -la /proc/$(pgrep -f "lxc mycontainer")/ns/# total 0# dr-x--x--x 2 root root 0 Jul 14 23:30 .# dr-xr-xr-x 9 root root 0 Jul 14 23:30 ..# lrwxrwxrwx 1 root root 0 Jul 14 23:30 cgroup -> 'cgroup:[4026531835]'# lrwxrwxrwx 1 root root 0 Jul 14 23:31 ipc -> 'ipc:[4026532212]'# lrwxrwxrwx 1 root root 0 Jul 14 23:31 mnt -> 'mnt:[4026532223]'# lrwxrwxrwx 1 root root 0 Jul 14 23:31 net -> 'net:[4026531840]'# lrwxrwxrwx 1 root root 0 Jul 14 23:31 pid -> 'pid:[4026532225]'# lrwxrwxrwx 1 root root 0 Jul 14 23:31 pid_for_children -> 'pid:[4026532225]'# lrwxrwxrwx 1 root root 0 Jul 14 23:31 time -> 'time:[4026531834]'# lrwxrwxrwx 1 root root 0 Jul 14 23:31 time_for_children -> 'time:[4026531834]'# lrwxrwxrwx 1 root root 0 Jul 14 23:31 user -> 'user:[4026531837]'# lrwxrwxrwx 1 root root 0 Jul 14 23:31 uts -> 'uts:[4026532224]'
# 停止容器lxc-stop -n mycontainer# 销毁容器lxc-destroy -n mycontainer注:如果 wsl2 下的
/dev/null损坏了,需要通过mknod -m 666 /dev/null c 1 3重建,这样上面的命令才能正常工作
LXC 证明了 Namespace + Cgroup 的组合可以创建轻量级隔离环境,但它仍然是一个”系统容器”方案,用户需要管理完整的 init 系统,体验更像轻量级虚拟机而非现代容器。
1.4 Docker 革命(2013)
Docker 的贡献不在于发明新技术,而在于重新定义了容器的使用体验:
- 镜像(Image):将应用及其依赖打包为不可变的分层镜像,取代了 LXC 的模板机制
- Dockerfile:用声明式语法定义镜像构建步骤,实现了”基础设施即代码”
- 分层存储:基于 OverlayFS 的分层文件系统,镜像层可以共享复用
- 一键运行:
docker run一条命令完成所有操作,极大降低了使用门槛
# Docker 之前的 LXC 工作流sudo lxc-create -n myapp -t ubuntu # 创建容器sudo lxc-start -n myapp # 启动容器sudo lxc-attach -n myapp # 进入容器# 在容器内手动安装依赖、配置应用...
# Docker 的工作流docker run -d nginx # 一条命令搞定1.5 OCI 标准化(2015)
Docker 的成功带来了生态碎片化,CoreOS 推出了 rkt,Google 推出了 lmctfy,各家运行时不兼容。2015 年,Docker、CoreOS、Google 等公司共同成立了 OCI(Open Container Initiative),定义容器格式的开放标准:
chroot
UNIX V7 引入目录隔离,容器思想的最早源头
FreeBSD Jail
在 chroot 基础上增强进程与网络隔离
Solaris Zones
操作系统级虚拟化,资源粒度更细
Cgroup v1 + LXC
Cgroup(前 Process Containers)2007 年合入开发树,随 2.6.24 于 2008 年初发布,LXC 提供容器管理工具
Docker
镜像分层与易用 CLI 引爆容器生态
Kubernetes
容器编排成为标配
OCI 成立
镜像与运行时格式走向标准化
Cgroup v2 / CRI-O
Cgroup v2 于 Linux 4.5 合入主线;同年 CRI-O 作为 Kubernetes 专用轻量运行时立项(两者独立、非关联事件)
containerd 独立
运行时与引擎解耦,成为 CNCF 毕业项目
gVisor / Kata
沙箱与轻量虚拟机运行时涌现
Wasm 容器
WebAssembly 成为新的运行时方向
时间线里 2016 年的 Cgroup v2 合入主线值得多说一句。v1 时期每种资源(CPU、内存、IO 等)各自一套层级,控制器归属混乱、嵌套容易出错;v2 把所有资源统一到单一层级,支持控制器委托(让子 cgroup 在父级限制内自主分发资源),管理上清晰很多。需要说明的是,常被一起提起的 PSI(Pressure Stall Information)严格说是 4.20 引入的独立内核子系统,系统级 /proc/pressure/ 不依赖 cgroup,只是 per-cgroup 的压力暴露是 v2 才有的。这些改动让容器资源管理更可控,Kubernetes 从 1.25 起也把 cgroup v2 作为推荐配置。Cgroup 的细节在Cgroup 深入一章展开。
把演进史换一个角度看,每一代技术在隔离维度上是逐层叠加的。下表把几代方案和它们覆盖的隔离维度放在一起,能看出”容器隔离能力”是怎么一块块拼出来的:
| 方案 | 文件系统 | 进程 | 网络 | 用户 | 资源限制 | 安全模块 |
|---|---|---|---|---|---|---|
| chroot(1979) | 是 | 否 | 否 | 否 | 否 | 否 |
| FreeBSD Jail(2000) | 是 | 是 | 是 | 否 | 是 | 否 |
| Solaris Zones(2004) | 是 | 是 | 是 | 是 | 是 | 否 |
| LXC(2008) | 是 | 是 | 是 | 是 | 是 | 部分 |
| Docker / runc(2013+) | 是 | 是 | 是 | 是 | 是 | 是 |
chroot 只有文件系统视图,Jail 补上进程和网络,Zones 补上用户,LXC 用 namespace/cgroup 把内核能力凑齐,Docker 再叠加 seccomp/AppArmor 等安全模块。现代容器的隔离不是一步到位的,是四十多年一层层加上来的。
二、容器运行时架构
2.1 Docker 的架构演进
Docker 的架构经历了从”大而全”到”分层解耦”的演进,中间有一个从”整合者”变成”内核能力直接消费者”的关键转折:
- LXC 驱动时期(Docker 1.0 之前):Docker 早期直接调用 LXC 工具来创建和管理容器,自己只负责镜像和上层体验,底层隔离全靠 LXC。
- libcontainer 自研时期(Docker 0.9 起):Docker 放弃对 LXC 的依赖,自研 libcontainer 直接操作 namespace/cgroup 等内核能力。这一步意味着 Docker 不再是”把别人的工具包一层”,而是自己掌握了容器进程的创建逻辑,也为后来的运行时标准化埋下伏笔。
- containerd + runc 拆分时期(Docker 1.11 起):libcontainer 捐赠给 OCI 后成为 runc 的代码基础,Docker 把容器执行逻辑抽成独立的 containerd 和 runc,daemon 只剩镜像管理和上层 API。
到这一步,才有了下面的三层架构:
2.2 为什么需要三层运行时?
回头看 Docker 的架构演进:早期就是单体 daemon,一个进程干完镜像管理、网络、存储、容器生命周期所有事。当时没有三层架构,是后来”被逼出来”的,而不是一开始就这么设计。
推动拆分的有三股力量。
- 第一股是 Docker 自身的解耦尝试:2014 年 Docker 把容器执行逻辑抽成 containerd,让它专注容器生命周期,daemon 退回管镜像和上层 API,这是单体内部的一次分工。
- 第二股是生态竞争:CoreOS 的 rkt、Google 的 lmctfy 与 Docker 并存,互不兼容,OCI 成立后定下了运行时和镜像的标准格式,让 “高层负责生命周期、低层负责创建进程” 的边界有了规范依据,runc 才得以作为符合 OCI 的低层运行时被独立复用。
- 第三股是 Kubernetes:编排平台不希望自己绑定某个具体实现,于是定义了 CRI 接口,让 containerd、CRI-O 这类高层运行时成为编排层和内核之间的稳定中间层。
所以为什么需要三层运行时的答案即是:“单体想解耦、生态想标准化、编排想中立”,三股力共同作用的结果。下面这张表把”没有分层”和”有分层”放在几个具体问题上对照,就能看出拆分到底解决了什么:
| 问题 | 没有分层 | 有分层 |
|---|---|---|
| Docker 升级 | 升级 dockerd 会杀掉所有容器 | containerd 独立升级,容器不受影响 |
| 运行时替换 | 只能用 Docker 内置运行时 | containerd 支持多种运行时(runc/kata/gvisor) |
| 容器进程管理 | dockerd 是容器进程的父进程,daemon 崩溃影响所有容器 | shim 是容器进程的父进程,containerd 重启不影响容器 |
| Kubernetes 集成 | K8s 需要对接 Docker API | K8s 通过 CRI 直接对接 containerd |
表格里”shim 是容器进程的父进程,containerd 重启不影响容器”这条值得展开。runc 创建完容器进程就会退出,它本身不是常驻进程;真正长时间挂在容器进程上面的是 containerd-shim。shim 作为容器进程的父进程接管它的 stdin/stdout 和退出状态,和 containerd 之间只是松耦合的通信。所以 containerd 即便重启,已经跑起来的容器进程仍在 shim 下继续运行,不受影响。这正是把 daemon 从容器进程父进程位置上挪开的意义。
把 shim 在一次容器启动里的位置画出来,三层是怎么接力、runc 又是怎么”创建完即退出”的,就一目了然了:
从时序能直观看到,runc 在 create 和 start 两步都是”进来干活、干完退出”,常驻的只有 shim;containerd 和 shim 之间是 gRPC 松耦合,重启 containerd 不会牵连到 shim 和容器进程。shim 的设计动机、API 接口、升级安全机制在shim一章深入。
2.3 高层运行时 vs 低层运行时
三层拆分里,编排层和高层、低层之间还有一道更值得拎清楚的边界,就是”高层运行时”和”低层运行时”的分工。这道边界对应的是两件性质不同的活:高层负责容器的一生,拉镜像、建容器、管启停、对外提供 API,关心的是”这个容器该不该存在、状态对不对”;低层负责把容器真正跑起来,按 OCI 规范调用 clone、设置 namespace/cgroup、挂载 rootfs,关心的是”这个进程怎么按隔离配置创建出来”。
分开的好处是编排层只需对接高层(通过 CRI),不用关心底层用 runc 还是 kata;而底层只要符合 OCI Runtime Spec,就能被任意高层复用。同样是高层,containerd 偏通用、CRI-O 专精 Kubernetes 场景;同样是低层,runc 用内核原生隔离、kata 套虚拟机、gVisor 走用户态内核,各有安全与性能取舍。下面这张表和命令对照,能帮你看清两者的职责边界:
| 层级 | 代表 | 职责 | 类比 |
|---|---|---|---|
| 高层运行时(High-level Runtime) | containerd, CRI-O | 镜像管理、容器生命周期、API 服务 | 操作系统 |
| 低层运行时(Low-level Runtime) | runc, kata, gVisor | 创建/启动容器进程、配置内核隔离 | 引导加载程序 |
职责上分得清,落到命令上就更直观了。用 containerd 的 ctr 操作的是容器生命周期,用 runc 直接操作的是单个容器的进程:
# 高层运行时:containerd 管理容器生命周期ctr containers listctr tasks list
# 低层运行时:runc 直接操作容器runc listrunc spec # 生成 OCI Bundle 规范文件2.4 容器运行时生态全景
把前面讲的三层放到整个容器生态里看,从编排平台到内核,各层之间是怎么对接的。下面这张图把 Kubernetes、Swarm、Nomad 这些编排层,通过 CRI 对接高层运行时,再由高层调用 runc/kata/gVisor 等低层运行时,最终落到内核能力的完整链路画了出来:
后续章节将逐层深入这个架构:runc 源码、containerd 架构、shim,以及将它们串起来的完整调用链。
三、关键概念辨析
3.1 容器 vs 虚拟机
容器和虚拟机经常被并列提起,但两者的隔离起点就不同:虚拟机靠硬件虚拟化(VT-x/AMD-V)给每个实例一套独立内核,容器则共享宿主内核,靠 Namespace 和 Cgroup 在进程级制造隔离边界。起点不同,决定了启动速度、资源开销、安全边界这一串差异。下面的对比表把这些维度摊开放在一起:
| 维度 | 容器 | 虚拟机 |
|---|---|---|
| 隔离级别 | 进程级(Namespace) | 硬件级(VT-x/AMD-V) |
| 内核共享 | 共享宿主内核 | 独立内核 |
| 启动速度 | 毫秒级 | 秒级 |
| 资源开销 | MB 级 | GB 级 |
| 镜像大小 | MB~百 MB | GB 级 |
| 密度 | 单机数百~数千 | 单机数十 |
| 安全边界 | 较弱(内核漏洞可逃逸) | 较强(硬件虚拟化隔离) |
| 性能损耗 | 接近原生 | 5%~15% 虚拟化开销 |
需要说明的是,“安全边界较弱”是相对虚拟机而言的,不代表容器不能用于生产。现代容器有 seccomp 默认配置、AppArmor/SELinux、User Namespace、rootless 容器等多层防护,配合合理配置可以把攻击面收得很小,达到生产可用级别。这块在容器安全一章会展开。
3.2 容器进程 vs 普通进程
容器进程本质上就是一个带有特殊 Namespace/Cgroup/OverlayFS 配置的 Linux 进程:
# 在宿主机上,容器进程就是一个普通进程ps aux | grep nginx# root 12345 0.0 0.1 ... nginx: master process
# 查看容器进程的 Namespacels -la /proc/12345/ns/# 与宿主进程不同,容器进程的 Namespace 是独立的
# 查看容器进程的 Cgroupcat /proc/12345/cgroup# 容器进程在独立的 Cgroup 子树中
# 容器进程看到的文件系统sudo nsenter -t 12345 -m -- ls /# 看到的是 OverlayFS 合并后的 rootfs3.3 镜像 vs 容器 vs Bundle
前面说到容器本质是进程,但一个进程要按隔离配置跑起来,需要三样东西配齐:一份可复制的应用打包(镜像)、一个运行实例(容器)、以及交给低层运行时的具体输入(Bundle)。这三者经常被混着说,但它们处在”打包 → 运行 → 执行输入”的不同阶段:镜像是不可变的存储产物,符合 OCI Image Spec;容器是镜像跑起来后的实例;Bundle 是 OCI Runtime Spec 定义的东西,由一份 rootfs 加一个 config.json 组成,是 runc 真正消费的输入。从高层运行时到低层运行时,本质上就是把镜像拆解成一个 Bundle 再交给 runc。下面这张表和命令对照了三者的位置:
| 概念 | 定义 | 格式 | 生命周期 |
|---|---|---|---|
| 镜像(Image) | 不可变的应用打包格式 | OCI Image Spec | 持久化存储 |
| 容器(Container) | 镜像的运行实例 | OCI Runtime Spec | 临时的 |
| Bundle | 运行时输入(rootfs + config.json) | OCI Runtime Spec | 临时的 |
光看定义还抽象,放到命令里就清楚了。下面先用 docker 的高层命令走一遍镜像到容器的流程,再用 ctr 直接操作 containerd,对照看 Bundle 是在哪一步冒出来的:
# 镜像 → 容器 → Bundle 的关系docker pull nginx # 拉取镜像(OCI Image Spec 格式)docker create nginx # 创建容器(生成 OCI Bundle)docker start <id> # 启动容器(runc 读取 Bundle)
# 等价的底层操作ctr image pull docker.io/library/nginx:latestctr container create docker.io/library/nginx:latest mynginxctr task start mynginxBundle 里的 config.json 到底长什么样?runc spec 能生成一份默认配置,下面是精简到关键字段的片段,能看出 runc 启动一个容器到底需要交代哪些事:
{ "ociVersion": "1.0.2", "process": { "terminal": true, "user": { "uid": 0, "gid": 0 }, "args": ["/bin/sh"], "cwd": "/" }, "root": { "path": "rootfs", "readonly": false }, "hostname": "runc", "linux": { "namespaces": [ { "type": "pid" }, { "type": "ipc" }, { "type": "uts" }, { "type": "mount" }, { "type": "network" } ] }}process 告诉 runc 容器里跑什么命令、工作目录在哪,root 指向 rootfs 位置,linux.namespaces 列出要隔离哪些资源。一份 config.json 加上同目录的 rootfs,就是 runc 能直接消费的完整 Bundle。字段细节留给OCI 规范一章展开。
四、本章小结
这一章从 1979 年的 chroot 走到 2015 年的 OCI,又把三层运行时拆开看了一遍。把全章主线拎出来,可以用三个关键词串起来:
| 主题 | 核心要点 | 关键词 |
|---|---|---|
| 演进脉络 | 从 chroot 的目录隔离到 OCI 的标准化,容器技术走过了 40 年 | chroot, OCI |
| 运行时架构 | dockerd → containerd → shim → runc 三层解耦,高层管生命周期,低层管进程创建 | 三层运行时 |
| 核心认知 | 容器进程 = 普通 Linux 进程 + 特殊的 Namespace/Cgroup/OverlayFS 配置 | 容器即进程 |
从演进史能看出,容器的每一次跃迁都在解决前一代的痛点:chroot 太弱,于是有 Jail;Jail 与 OS 绑定,于是有 LXC;LXC 体验像虚拟机,于是有 Docker;Docker 造成碎片化,于是有 OCI。后续章节沿着这条主线深入:Namespace、Cgroup、OverlayFS 三大基石 → OCI 规范 → runc 源码、containerd 架构、shim → 完整调用链 → 安全、网络、镜像构建。
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






