引言
「容器是什么?」——答案是 Linux 内核的两个特性:namespaces(隔离视图) + cgroups(限制资源)。Docker 只是给这套内核机制加了便捷的打包与分发层。本文从内核视角讲透容器:先拆解 6 大 namespaces 各自隔离什么,再讲 cgroup 如何限 CPU/内存,接着梳理从 chroot → 容器 → Docker 的演进与镜像分层原理,最后用脚本「手搓一个最小容器」,让你真正理解容器运行时的本质。
前置:/linux-process-management/(进程模型)、/linux-systemd-services/(cgroup 资源控制)。容器操作见 [[docker]]。
目录
- 1. 容器 = namespaces + cgroup
- 2. namespaces:隔离「视图」
- 3. cgroup:限制「资源」
- 4. 从 chroot 到容器的演进
- 5. 容器运行时:runc 与 containerd
- 6. 镜像与分层:Union FS
- 7. 手写最小容器:动手理解本质
- 8. 安全边界:为什么容器不是 VM
- 9. 容器排错与调试
- 10. 速查表
- 延伸阅读
1. 容器 = namespaces + cgroup
一句话定义:
容器 = namespaces(隔离视图,假装独占系统)
+ cgroups(限制资源,不许抢占机器)
+ 镜像(打包应用 + 依赖)
内核层面容器与普通进程的本质差异:
| 维度 | 普通进程 | 容器进程 |
|---|---|---|
| 看到进程列表 | 全部 | 只有自己的 namespace |
| 看到网络 | 全部网卡 | 自己的网络栈 |
| 资源使用 | 不限 | cgroup 限 CPU/内存 |
| 文件系统 | 主机根 | 镜像根目录 |
心智:容器进程「以为自己是唯一」——namespace 给它假象,cgroup 给它笼子,镜像给它行李。
2. namespaces:隔离「视图」
Linux 的 8 大 namespaces,各隔离一类资源:
| Namespace | 隔离什么 | 作用 |
|---|---|---|
| PID | 进程编号 | 容器内 PID 1 是它自己 |
| Network | 网络栈 | 独立网卡/IP/防火墙 |
| Mount | 挂载点 | 独立文件系统视图 |
| UTS | 主机名 | 容器有自己的 hostname |
| User | 用户映射 | 容器内 root 可映射普通用户 |
| IPC | 消息队列 | 隔离进程间通信 |
| cgroup | cgroup 根 | 隔离资源限制视图 |
| Time | 时钟(新) | 隔离时间 |
举例:PID namespace——容器里的进程 ID 独立编号:
# 容器内看进程
docker exec <container> ps aux
# PID 1 是容器自己的主进程,看不到宿主机进程
User namespace 的安全价值:
容器内 uid=0(root)→ 映射到宿主机的非 root uid
→ 容器里的「root」没有宿主机的 root 权限
→ 即使被攻破,也只是宿主机的普通用户
记忆:namespace 是「掩眼法」——PID/网络/挂载各自造一个小世界,User namespace 则让容器内的 root 是「假 root」。
3. cgroup:限制「资源」
cgroup v2 用树形结构限制进程资源:
# 查看 cgroup 文件系统挂载
mount | grep cgroup
cat /sys/fs/cgroup/cpu.max # CPU 配额(如 100000 100000 = 1核)
cat /sys/fs/cgroup/memory.max # 内存上限
cgroup 限制维度:
| cgroup 控制器 | 限制什么 | 文件 |
|---|---|---|
| cpu | CPU 配额/权重 | cpu.max / cpu.weight |
| memory | 内存上限/软限 | memory.max / memory.high |
| io | 磁盘 IO | io.max / io.weight |
| pids | 线程数 | pids.max |
临时给进程限资源(systemd 视角见 /linux-systemd-services/):
# 用 systemd-run 起一个限内存进程
systemd-run --user --property=MemoryMax=256M --pty bash
# 在容器内验证
cat /proc/self/cgroup
cgroup 与 OOM:
内存超限 → 内核 OOM Killer 杀掉超限进程组
memory.max 是硬墙,memory.high 是软压(触发回收,不杀)
记忆:cgroup 是「资源配额」——cpu.max 限核、memory.max 限内存、pids.max 限线程,超了要么限流要么 OOM。
4. 从 chroot 到容器的演进
容器不是新东西,是逐步演化:
chroot(1979) → 只换根目录,无资源限制,无网络隔离
BSD jail(2000) → chroot + 独立网络 + 用户限制
Linux VServer / OpenVZ → 内核分区
LXC(2008) → namespaces + cgroup 组合 → 现代容器雏形
Docker(2013) → 加上「镜像分层 + 便捷 CLI」→ 引爆容器化
chroot 的最小示例:
# 用 debootstrap 造一个最小根文件系统,chroot 进去
debootstrap --arch amd64 jammy /opt/rootfs http://archive.ubuntu.com/ubuntu/
chroot /opt/rootfs /bin/bash
# 现在你「以为」自己在最小系统里,但网络/进程/资源毫无隔离
chroot vs 容器:
| 维度 | chroot | 容器(namespace+cgroup) |
|---|---|---|
| 根目录 | 隔离 | 隔离 |
| 进程 | 共享 | 隔离(PID ns) |
| 网络 | 共享 | 隔离(Network ns) |
| 资源 | 不限 | cgroup 限制 |
| 安全 | 弱 | 强得多 |
心智:容器 = chroot 的「全面隔离加强版」——chroot 只藏文件,容器连进程、网络、资源全隔离。
5. 容器运行时:runc 与 containerd
Docker 的运行时栈:
docker CLI
└─ dockerd(守护进程)
└─ containerd(容器生命周期管理)
└─ runc(真正创建容器:调用 namespace/cgroup)
各组件职责:
| 组件 | 职责 |
|---|---|
| runc | 按 OCI 标准创建/运行容器(最底层) |
| containerd | 管理容器生命周期、镜像、快照 |
| dockerd | 提供 Docker API 与 CLI |
| CRI(k8s) | 通过 containerd 的 CRI 接口集成 |
OCI 标准:config.json 描述容器配置(namespace、cgroup、命令):
{
"ociVersion": "1.0.0",
"process": { "args": ["/bin/sh"] },
"root": { "path": "rootfs" },
"linux": {
"namespaces": [
{ "type": "pid" },
{ "type": "network" },
{ "type": "mount" }
]
}
}
记忆:docker → dockerd → containerd → runc 是一条命令链——runc 是真正碰内核的,其余是管理壳。
6. 镜像与分层:Union FS
镜像 = 只读分层(overlayfs):
镜像分层:
layer1: base(Ubuntu 根文件系统)
layer2: + python
layer3: + 你的代码
layer4: 容器可写层(容器运行时才产生)
overlayfs 的读写原理:
读:下钻只读层
写:拷贝到最顶层可写层(copy-on-write)
删:白名单隐藏底层文件
分层的好处:
| 特性 | 说明 |
|---|---|
| 复用 | 相同 base 只存一份 |
| 增量 | 只传变更层 |
| 缓存 | 层命中可缓存 |
| 快 | 构建/拉取增量 |
记忆:镜像分层 + copy-on-write = 容器镜像「又小又快又复用」——这是 Docker 席卷世界的关键工程创新。
7. 手写最小容器:动手理解本质
用系统调用实现一个最小容器(概念演示):
// 最小容器:unshare 进入新 PID/UTS/挂载 namespace
package main
import (
"os"
"os/exec"
"syscall"
)
func main() {
cmd := exec.Command("/bin/sh")
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWPID |
syscall.CLONE_NEWUTS |
syscall.CLONE_NEWNS |
syscall.CLONE_NEWNET, // 新的 PID/主机名/挂载/网络
}
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.Run()
}
# 运行后,sh 的 PID 从 1 开始(宿主机的 PID 它看不到)
go run container.go
# 在里面执行 ps → 只有自己(PID namespace 隔离生效)
验证隔离:
ps aux # 容器内只看得到容器进程
hostname foo # 改主机名不影响宿主机(UTS ns)
mount | head # 挂载视图隔离(Mount ns)
记忆:容器不是神秘技术,就是
unshare/clone打开几个 flag——理解了这几个系统调用,你就理解了容器本质。
8. 安全边界:为什么容器不是 VM
容器共享宿主内核,VM 有独立内核——这是安全差异的根本:
| 维度 | 容器 | 虚拟机 |
|---|---|---|
| 内核 | 共享宿主 | 独立 guest 内核 |
| 隔离 | namespace/cgroup(软隔离) | 硬件虚拟化(硬隔离) |
| 启动 | 秒级 | 分钟级 |
| 开销 | 极小 | 大(虚拟硬件) |
| 逃逸风险 | 内核漏洞可穿透 | 需突破 hypervisor |
| 适用 | 微服务/快速迭代 | 强隔离/异构系统 |
容器安全加固要点:
1. User namespace 映射(假 root)
2. 只读根文件系统(--read-only)
3. 丢弃能力(--cap-drop ALL --cap-add 最小集)
4. seccomp 限制系统调用
5. 不用 privileged 模式
6. 镜像扫描漏洞
铁律:容器是「软隔离」,永远别拿它当 VM 用——同一宿主上跑不可信工作负载前,先评估逃逸风险。
9. 容器排错与调试
排查思路(结合 [[docker]] 操作):
1. 起不来? → 看日志 / 前台运行定位
2. 能起但错?→ exec 进去看 / 复制文件出来
3. 资源被限?→ 看 cgroup 统计
4. 网络不通?→ 看网络 namespace 与端口映射
实用命令:
# 进入容器调试(临时加调试工具)
docker run -it --rm myimage sh
# 查看 cgroup 使用
cat /sys/fs/cgroup/memory.current
# 看容器底层 PID(宿主机视角)
docker inspect -f '{{.State.Pid}}' <container>
# 用宿主机视角看容器进程
ps -ef | grep <容器主进程>
记忆:容器排错两条腿——
exec进容器看、宿主机视角看 PID——别忘了容器只是「带着 namespace 的进程」。
10. 速查表
| 需求 | 做法 |
|---|---|
| 容器本质 | namespaces(隔离视图)+ cgroup(限资源) |
| PID 隔离 | PID namespace |
| 网络隔离 | Network namespace |
| 限内存 | memory.max |
| 限 CPU | cpu.max |
| 假 root | User namespace 映射 |
| 运行时链 | dockerd → containerd → runc |
| 镜像复用 | overlayfs 分层 + copy-on-write |
| 手写容器 | clone/unshare + flag |
| 安全 | 只读根 + 丢能力 + seccomp |
| 排错 | exec 进容器 / 宿主机看 PID |
一句话记忆:容器 = namespaces 造「以为独占」的假象 + cgroup 限资源 + 镜像分层打包;Docker 只是壳,runc 才是内核里的执行者;chroot 到容器是隔离的进化史;容器共享内核软隔离、VM 独立内核硬隔离——理解这四层,容器对你无秘密。
延伸阅读
- /linux-systemd-services/ — cgroup 资源控制与安全指令
- /linux-process-management/ — 进程与 PID
- [[docker]] — 容器实战与编排
- [[os]] — 操作系统原理
- [[security]] — 容器安全与逃逸
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。