1979 年,UNIX V7 引入了 chroot 系统调用,允许进程看到一个与根目录不同的文件系统视图——这是容器隔离思想的起点。四十多年后的今天,容器技术已经从简单的目录切换发展为包含 Namespace、Cgroup、OverlayFS、OCI 规范、多层运行时架构的完整体系。docker run 一条命令背后,是四个进程、三个抽象层、两个规范的精密协作。
本章将建立容器运行时的全景认知。在后续章节深入 Namespace、Cgroup、OverlayFS 的细节之前,你需要先看到完整的地图:容器技术是怎么一步步演进的?三大内核基石各自解决什么问题?OCI 规范定义了什么?Docker、containerd、runc 之间是什么关系?
一、容器技术演进:从 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 -o '/lib64/.*' | xargs -I{} cp {} /tmp/myroot/lib64/
# 使用 chroot 切换根目录sudo chroot /tmp/myroot /bin/bash# 此时进程的根目录是 /tmp/myroot,无法访问原系统的文件但 chroot 的隔离极其有限:
| 隔离维度 | chroot 是否隔离 | 说明 |
|---|---|---|
| 文件系统视图 | 改变根目录视图 | |
| 进程可见性 | 仍能看到所有 PID | |
| 网络栈 | 共享宿主机网络 | |
| 用户身份 | 仍是同一个 UID | |
| 资源使用 | 无资源限制 | |
| 设备访问 | 可访问 /dev 下设备 |
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 容器技术的真正起点是 2008 年合并入内核的 Namespace 和 Cgroup。LXC(Linux Containers)是第一个将这些内核特性组合使用的项目:
# LXC 创建容器sudo lxc-create -n mycontainer -t ubuntusudo lxc-start -n mycontainersudo lxc-attach -n mycontainer
# LXC 底层使用的就是 Namespace + Cgroup# 查看 LXC 容器的 Namespacels -la /proc/$(pgrep -f "lxc mycontainer")/ns/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),定义容器格式的开放标准:
二、容器三大基石
容器的本质是一组 Linux 内核特性的组合使用。其中三个特性最为核心:Namespace 提供视图隔离,Cgroup 提供资源限制,OverlayFS 提供分层文件系统。
2.1 Namespace:视图隔离
Namespace 让进程看到一个”缩小”的系统视图——只看到自己的 PID、自己的网络栈、自己的挂载点。Linux 内核提供了 8 种 Namespace:
| Namespace | 隔离内容 | 系统调用 | 内核版本 |
|---|---|---|---|
| Mount | 文件系统挂载点 | CLONE_NEWNS | 2.4.19 |
| PID | 进程 ID | CLONE_NEWPID | 2.6.24 |
| Network | 网络栈 | CLONE_NEWNET | 2.6.29 |
| IPC | System V IPC/POSIX 消息队列 | CLONE_NEWIPC | 2.6.30 |
| UTS | 主机名和域名 | CLONE_NEWUTS | 2.6.19 |
| User | 用户和组 ID | CLONE_NEWUSER | 3.8 |
| Cgroup | Cgroup 根目录视图 | CLONE_NEWCGROUP | 4.6 |
| Time | 系统时钟 | CLONE_NEWTIME | 5.6 |
# 查看当前进程的所有 Namespacels -la /proc/self/ns/
# 输出示例:# cgroup -> 'cgroup:[4026531835]'# ipc -> 'ipc:[4026531839]'# mnt -> 'mnt:[4026531840]'# net -> 'net:[4026531992]'# pid -> 'pid:[4026531836]'# user -> 'user:[4026531837]'# uts -> 'uts:[4026531838]'Namespace 提供的是视图隔离而非安全隔离。它让进程”看不到”其他进程,但不阻止进程在获得特权后突破隔离边界。容器安全需要 Namespace + Cgroup + seccomp + AppArmor 的组合使用,详见 Ch10 容器安全。
2.2 Cgroup:资源限制
如果说 Namespace 是”你看不到别人”,那 Cgroup 就是”你不能用太多”。Cgroup 限制进程组可以使用的系统资源:
# 查看容器的 Cgroup 路径(Cgroup v2)cat /proc/$(pgrep -f "nginx")/cgroup
# 输出示例(Cgroup v2 统一层级):# 0::/system.slice/docker-abc123.scope
# 查看容器的 CPU 限制cat /sys/fs/cgroup/system.slice/docker-abc123.scope/cpu.max
# 查看容器的内存限制cat /sys/fs/cgroup/system.slice/docker-abc123.scope/memory.maxCgroup v2 的关键控制器:
| 控制器 | 功能 | 关键文件 |
|---|---|---|
| cpu | CPU 时间分配 | cpu.max, cpu.weight |
| memory | 内存使用限制 | memory.max, memory.current |
| io | 块设备 IO 限制 | io.max, io.stat |
| pids | 进程数限制 | pids.max, pids.current |
| cpuset | CPU 亲和性 | cpuset.cpus, cpuset.mems |
| hugetlb | 大页内存限制 | hugetlb.2MB.max |
2.3 OverlayFS:分层文件系统
OverlayFS 将多个目录”叠加”为一个统一的文件系统视图。容器镜像的每一层对应一个目录,最上层是可写的容器层:
# 查看 Docker 容器的 OverlayFS 挂载mount | grep overlay
# 输出示例:# overlay on /var/lib/docker/overlay2/abc123/merged type overlay# (rw,relatime,# lowerdir=/var/lib/docker/overlay2/layer1:/var/lib/docker/overlay2/layer2,# upperdir=/var/lib/docker/overlay2/abc123/diff,# workdir=/var/lib/docker/overlay2/abc123/work)
# 手动创建 OverlayFSmkdir -p /tmp/overlay/{lower1,lower2,upper,work,merged}echo "base layer" > /tmp/overlay/lower1/file1.txtecho "app layer" > /tmp/overlay/lower2/file2.txt
sudo mount -t overlay overlay /tmp/overlay/merged \ -o lowerdir=/tmp/overlay/lower2:/tmp/overlay/lower1,\upperdir=/tmp/overlay/upper,workdir=/tmp/overlay/work
# 查看合并后的文件系统ls /tmp/overlay/merged/# file1.txt file2.txt2.4 三大基石的协作关系
三、容器运行时架构
3.1 Docker 的架构演进
Docker 的架构经历了从”大而全”到”分层解耦”的演进:
早期架构(Docker 1.11 之前):docker daemon 直接管理容器生命周期,所有功能(镜像、网络、存储、运行时)集成在一个进程中。
现代架构(Docker 1.11+):Docker 将容器运行时拆分为三层:
3.2 为什么需要三层运行时?
很多人会问:为什么不能让 Docker 直接调用 runc?为什么要引入 containerd 和 shim?
| 问题 | 没有分层 | 有分层 |
|---|---|---|
| Docker 升级 | 升级 dockerd 会杀掉所有容器 | containerd 独立升级,容器不受影响 |
| 运行时替换 | 只能用 Docker 内置运行时 | containerd 支持多种运行时(runc/kata/gvisor) |
| 容器进程管理 | dockerd 是容器进程的父进程,daemon 崩溃影响所有容器 | shim 是容器进程的父进程,containerd 重启不影响容器 |
| Kubernetes 集成 | K8s 需要对接 Docker API | K8s 通过 CRI 直接对接 containerd |
3.3 高层运行时 vs 低层运行时
容器运行时分为两层:
| 层级 | 代表 | 职责 | 类比 |
|---|---|---|---|
| 高层运行时(High-level Runtime) | containerd, CRI-O | 镜像管理、容器生命周期、API 服务 | 操作系统 |
| 低层运行时(Low-level Runtime) | runc, kata, gVisor | 创建/启动容器进程、配置内核隔离 | 引导加载程序 |
# 高层运行时:containerd 管理容器生命周期ctr containers listctr tasks list
# 低层运行时:runc 直接操作容器runc listrunc spec # 生成 OCI Bundle 规范文件3.4 容器运行时生态全景
四、OCI 规范体系
4.1 OCI 的三大规范
OCI 定义了容器生态的三个核心规范:
| 规范 | 定义内容 | 当前版本 |
|---|---|---|
| Image Spec | 镜像格式、manifest、config、layer | v1.1 |
| Runtime Spec | 运行时接口、OCI Bundle、config.json | v1.2 |
| Distribution Spec | 镜像分发协议、Registry API | v1.1 |
# OCI Image Spec:镜像 manifest 示例{ "schemaVersion": 2, "mediaType": "application/vnd.oci.image.manifest.v1+json", "config": { "mediaType": "application/vnd.oci.image.config.v1+json", "size": 7023, "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" }, "layers": [ { "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", "size": 32654, "digest": "sha256:9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0" } ]}4.2 OCI Bundle:运行时的输入
OCI Runtime Spec 定义了”OCI Bundle”——一个包含 rootfs 和 config.json 的目录,是低层运行时的标准输入:
# 创建 OCI Bundlemkdir -p /tmp/bundle/rootfscd /tmp/bundle
# 导出 rootfsdocker export $(docker create busybox) | tar -C rootfs -xvf -
# 生成 config.json(OCI Runtime Spec 格式)runc spec
# 查看 config.json 的关键配置cat config.json | python3 -m json.tool | head -40{ "ociVersion": "1.0.2", "process": { "terminal": true, "user": { "uid": 0, "gid": 0 }, "args": ["sh"], "env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"], "cwd": "/" }, "root": { "path": "rootfs", "readonly": true }, "linux": { "namespaces": [ { "type": "pid" }, { "type": "mount" }, { "type": "ipc" }, { "type": "uts" }, { "type": "network" } ] }}4.3 从镜像到运行:OCI 规范的串联
五、关键概念辨析
5.1 容器 vs 虚拟机
| 维度 | 容器 | 虚拟机 |
|---|---|---|
| 隔离级别 | 进程级(Namespace) | 硬件级(VT-x/AMD-V) |
| 内核共享 | 共享宿主内核 | 独立内核 |
| 启动速度 | 毫秒级 | 秒级 |
| 资源开销 | MB 级 | GB 级 |
| 镜像大小 | MB~百 MB | GB 级 |
| 密度 | 单机数百~数千 | 单机数十 |
| 安全边界 | 较弱(内核漏洞可逃逸) | 较强(硬件虚拟化隔离) |
| 性能损耗 | 接近原生 | 5%~15% 虚拟化开销 |
5.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 合并后的 rootfs5.3 镜像 vs 容器 vs Bundle
| 概念 | 定义 | 格式 | 生命周期 |
|---|---|---|---|
| 镜像(Image) | 不可变的应用打包格式 | OCI Image Spec | 持久化存储 |
| 容器(Container) | 镜像的运行实例 | OCI Runtime Spec | 临时的 |
| Bundle | 运行时输入(rootfs + config.json) | OCI Runtime Spec | 临时的 |
# 镜像 → 容器 → 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 mynginx六、动手实践:用 runc 手动创建容器
这个实践让你绕过 Docker 和 containerd,直接用 runc 体验 OCI Runtime Spec 的工作方式:
#!/bin/bash# 手动创建 OCI Bundle 并运行容器
# 1. 创建 Bundle 目录mkdir -p /tmp/mycontainer/rootfscd /tmp/mycontainer
# 2. 导出 rootfs(从 busybox 镜像)docker export $(docker create busybox) | tar -C rootfs -xvf -
# 3. 生成 OCI Runtime Spec 配置runc spec
# 4. 修改 config.json(可选)# 例如:修改 process.args 改变启动命令# 修改 linux.namespaces 添加/删除 Namespace
# 5. 用 runc 运行容器sudo runc run mycontainer
# 在另一个终端查看容器状态sudo runc list
# 查看容器的 Namespacesudo ls -la /proc/$(pgrep -f "mycontainer")/ns/// 用 Go 代码创建 OCI Bundlepackage main
import ( "encoding/json" "fmt" "os" "os/exec"
specs "github.com/opencontainers/runtime-spec/specs-go")
func main() { // 创建 OCI Runtime Spec spec := &specs.Spec{ Version: "1.0.2", Process: &specs.Process{ Terminal: true, User: specs.User{UID: 0, GID: 0}, Args: []string{"sh"}, Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}, Cwd: "/", }, Root: &specs.Root{ Path: "rootfs", Readonly: true, }, Linux: &specs.Linux{ Namespaces: []specs.LinuxNamespace{ {Type: specs.PIDNamespace}, {Type: specs.MountNamespace}, {Type: specs.IPCNamespace}, {Type: specs.UTSNamespace}, {Type: specs.NetworkNamespace}, }, }, }
// 写入 config.json data, _ := json.MarshalIndent(spec, "", " ") os.WriteFile("config.json", data, 0644)
// 调用 runc 运行 cmd := exec.Command("runc", "run", "my-container") cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Fprintf(os.Stderr, "runc run failed: %v\n", err) os.Exit(1) }}七、本章小结
| 主题 | 核心要点 | 关键词 |
|---|---|---|
| 演进脉络 | 从 chroot 的目录隔离到 OCI 的标准化,容器技术走过了 40 年 | chroot, OCI |
| 三大基石 | Namespace(视图隔离)+ Cgroup(资源限制)+ OverlayFS(分层文件系统) | Namespace, Cgroup, OverlayFS |
| 分层架构 | Docker(CLI)→ containerd(高层运行时)→ shim(进程监督)→ runc(低层运行时) | containerd, shim, runc |
| OCI 规范 | Image Spec(镜像格式)+ Runtime Spec(运行时接口)+ Distribution Spec(分发协议) | Image Spec, Runtime Spec |
| 核心认知 | 容器进程 = 普通 Linux 进程 + 特殊的 Namespace/Cgroup/OverlayFS 配置 | 容器即进程 |
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






