容器到底在跑什么 — Docker 运行原理图解

Namespace 隔离 / Cgroup 限额 / UnionFS 分层 / 完整运行流程

Docker Engine Linux Namespace Cgroup UnionFS OverlayFS

1. 容器 vs 虚拟机——到底有什么区别

虚拟机虚拟硬件,容器共享内核——这是本质差异。

虚拟机 (VM) 硬件 (CPU / RAM / Disk) 宿主机 OS (Linux/Windows) Hypervisor (VMware / KVM) ← vmware_start_vm() Guest OS Libs / Deps App A Guest OS Libs / Deps App B 容器 (Container) 硬件 (CPU / RAM / Disk) 宿主机 OS (Linux Kernel) ← 共享! Docker Engine ← containerd::Create() Libs / Deps App A 无 Guest OS Libs / Deps App B 无 Guest OS VS
VM 每个实例运行完整 Guest OS;容器共享宿主机内核,无需 Guest OS
对比项虚拟机 (VM)容器 (Container)
虚拟化层级硬件级 (Hypervisor)操作系统级 (Namespace + Cgroup)
Guest OS每个 VM 自带完整 OS共享宿主机内核,无 Guest OS
磁盘占用GB 级别 (数 GB ~ 数十 GB)MB 级别 (数 MB ~ 数百 MB)
内存占用需预分配 GB 级内存按需分配,MB 级起步
启动时间分钟级 (1~5 min)秒级 (0.5~2 sec)
性能有 Hypervisor 损耗 (5%~20%)接近原生 (损耗 <2%)
隔离性强隔离 (硬件级)进程级隔离 (共享内核)
密度单机几个 ~ 十几个单机数十 ~ 数百个
🏠 生活类比

虚拟机 = 独栋别墅:自带地基、水电管道、花园,完全隔离但造价高、占地大

容器 = 合租公寓:共享地基和水电管道,用隔断墙分出独立空间,便宜高效

Docker Engine = 物业管理:统一管理各户的入住、退房、资源分配

Explorer
📦 vm-example/
start_vm.sh
📦 container-example/
run_container.sh
# 虚拟机:启动完整 Guest OS,耗时分钟级 vmware_start_vm() # ← 分配 2GB 内存 + 20GB 磁盘 virsh start my-vm # ← KVM 启动,需要加载完整内核 # 容器:共享宿主机内核,秒级启动 docker run -d \ --name my-app \ --memory="512m" \ # ← Cgroup 限制内存 --cpus="1.5" \ # ← Cgroup 限制 CPU -p 8080:80 \ # ← Namespace 网络映射 nginx:latest # ← containerd::Create() # 对比:VM 耗时 ~2min,容器耗时 ~0.5s time docker run --rm hello-world # ~0.3s time virsh start my-vm # ~90s

2. Namespace 六大隔离——容器的"墙壁"

Namespace 是 Linux 内核提供的进程隔离机制,让容器以为自己独占整台机器。

Linux Namespace 六大隔离机制 PID Namespace 进程隔离 Host: PID 1(init) → PID 10000 Container: PID 1(自己的init) 容器内 PID 1 ≠ 宿主 PID 1 ← clone(CLONE_NEWPID) 进程编号 NET Namespace 网络隔离 Host: eth0 (真实网卡) Container: eth0 (veth虚拟对) 各容器有独立 IP/端口/路由 ← clone(CLONE_NEWNET) 独立网络栈 MNT Namespace 挂载隔离 Host: / /home /var 全可见 Container: 只看到自己的 / 容器内挂载不影响宿主机 ← clone(CLONE_NEWNS) 独立文件树 IPC Namespace 通信隔离 共享内存 / 信号量 / 消息队列 容器间 IPC 互不可见 防止跨容器进程通信干扰 ← clone(CLONE_NEWIPC) 独立IPC UTS Namespace 主机名隔离 Host: hostname = server-01 Container: hostname = my-app 每个容器可以有独立主机名 ← clone(CLONE_NEWUTS) 独立主机名 USER Namespace 用户隔离 Container: root (uid 0) Host: uid 100000 (普通用户) UID/GID 映射,容器内root≠宿主root ← clone(CLONE_NEWUSER) UID映射 docker run 触发 Namespace 创建流程 docker run → containerd → runc runc::setupNamespace() 6个 Namespace ① clone(CLONE_NEWPID) → PID隔离 ② clone(CLONE_NEWNET) → 网络隔离 ③ clone(CLONE_NEWNS) → 挂载隔离 ④ clone(CLONE_NEWIPC) → IPC隔离 ⑤ clone(CLONE_NEWUTS) → 主机名隔离 ⑥ clone(CLONE_NEWUSER)→ 用户隔离
6 种 Namespace 构成容器的隔离墙,每个 docker run 逐一创建
🏠 生活类比

Namespace = 合租公寓的隔断墙:每户被墙隔开,互不干扰

PID = 每个人自己编号:我说"1号"是我家老大,你说"1号"是你家老大,互不冲突

NET = 每户有自己的门牌号和信箱,邮件不会串门

MNT = 每户看不到别人的房间,只能看到自己的

USER = 管理员在楼里是大爷,在楼外是普通人

Explorer
📦 linux-kernel/
kernel/
nsproxy.c
pid_namespace.c
net_namespace.c
include/
linux/nsproxy.h
// Linux 内核 unshare() 系统调用创建 Namespace // 文件: kernel/nsproxy.c SYSCALL_DEFINE1(unshare, unsigned long, unshare_flags) { // unshare_flags 指定要创建哪些 Namespace if (unshare_flags & CLONE_NEWPID) create_pid_namespace(); // ← PID 隔离 if (unshare_flags & CLONE_NEWNET) create_net_namespace(); // ← 网络隔离 if (unshare_flags & CLONE_NEWNS) create_mnt_namespace(); // ← 挂载隔离 if (unshare_flags & CLONE_NEWIPC) create_ipc_namespace(); // ← IPC 隔离 if (unshare_flags & CLONE_NEWUTS) create_uts_namespace(); // ← 主机名隔离 if (unshare_flags & CLONE_NEWUSER) create_user_namespace(); // ← 用户隔离 // runc::setupNamespace() 逐一调用上述 return 0; } // 验证:容器内看到的 PID // Host: docker inspect --format '{{.State.Pid}}' my-app // → 28457 (宿主机上的真实 PID) // Container: exec 进入后 ps aux → PID 1

3. Cgroup 资源限制——容器的"用电配额"

Namespace 隔离了"看",Cgroup 限制了"用"——防止一个容器吃光所有资源。

Cgroup 层级结构 /sys/fs/cgroup/ cpu/ memory/ blkio/ pids/ container-abc123/ cpu.cfs_quota_us = 50000 cpu.cfs_period_us = 100000 container-xyz789/ cpu.cfs_quota_us = 30000 cpu.cfs_period_us = 100000 container-abc123/ memory.limit_in_bytes = 512MB memory.oom_control = 1 container-xyz789/ memory.limit_in_bytes = 256MB ← cgroup_attach_task() 将进程加入 cgroup ← cpu_cfs_quota_read() Container A: 50% Container B: 30% ← mem_cgroup_out_of_memory() 超限触发 OOM Kill
Cgroup 以文件系统形式组织,每个容器在各子系统下有独立目录
CPU 配额分配 & OOM Kill 示意 CPU 时间片 (每 100ms 周期) Container A: 50% (quota=50000) Container B: 30% 空闲 20% ← cpu_cfs_quota_read() 读取配额 内存限制 & OOM Kill memory.limit = 512MB (已分配) 已使用 ~410MB 进程申请 600MB! 超出限制 → OOM Kill ← mem_cgroup_out_of_memory() 内核直接杀死进程 ⚠ 不设 Cgroup 限制 = 一个容器用光整台机器资源!
CFS 配额控制 CPU 时间片;超内存限制触发 OOM Killer 终止进程
🏠 生活类比

Cgroup = 房东限电限水:每个租户有配额,不能无限制使用

CPU 限制 = 限电功率:你家最多用 50% 的电,超了就跳闸

Memory 限制 = 限水容量:水桶就 512 升,装不下就溢出

OOM Kill = 水桶溢出自动关水:超了直接停,不商量

不加限制 = 一个租客用光整栋楼的水电,其他人都没得用

Explorer
📁 /sys/fs/cgroup/
cpu/
container-abc/
cpu.cfs_quota_us
memory/
container-abc/
memory.limit_in_bytes
# Cgroup 文件系统结构 (cgroup v1) /sys/fs/cgroup/ ├── cpu/ │ └── container-abc123/ │ ├── cpu.cfs_quota_us # ← 50000 (50% of 100ms) │ ├── cpu.cfs_period_us # ← 100000 (100ms 周期) │ └── cgroup.procs # ← 进程 PID 列表 ├── memory/ │ └── container-abc123/ │ ├── memory.limit_in_bytes # ← 536870912 (512MB) │ ├── memory.usage_in_bytes # ← 当前使用量 │ └── memory.oom_control # ← OOM 开关 └── blkio/ └── container-abc123/ └── blkio.throttle.read_bps_device # ← IO 限速 # Docker 命令 → Cgroup 参数映射 docker run --cpus="1.5" # → cpu.cfs_quota_us = 150000 docker run --memory="512m" # → memory.limit_in_bytes = 536870912 docker run --memory-swap="1g" # → memory.memsw.limit_in_bytes docker run --cpuset-cpus="0-2" # → cpuset.cpus = "0-2" # 查看 Container 的 Cgroup 设置 cat /sys/fs/cgroup/cpu/docker/<container-id>/cpu.cfs_quota_us # 输出: 50000 (即 50% CPU)

4. Union FS 与镜像分层——千层饼的秘密

镜像不是单个文件,而是一层层只读文件堆叠——这就是 Docker 轻量的核心。

Docker 镜像分层结构 (OverlayFS) Container Layer (可写层) 容器运行时修改写入此层 ← overlayfs_mount() Layer 5: COPY . /app (5MB) ← diffid:sha256:ef3b... Layer 4: RUN pip install -r requirements.txt (80MB) ← diffid:sha256:a7c2... Layer 3: COPY requirements.txt (2KB) ← diffid:sha256:9f01... Layer 2: RUN apt-get install python3 (150MB) ← diffid:sha256:b5d3... Base Layer: ubuntu:22.04 (72MB) ← diffid:sha256:c1e0... 只读层 → 可写层 每层只存差异 Base: 72MB + Layer2: 150MB + Layer3: 2KB + Layer4: 80MB + Layer5: 5MB Total ≈ 307MB Container Layer: 动态增长 共享基础层 Container A: ubuntu:22.04 Container B: ubuntu:22.04 ↑ 同一基础层只存一份! ← 共享只读层, 节省磁盘
OverlayFS 将只读层叠加为统一视图,最上层为可写的容器层
Copy-on-Write (COW) 写时复制 读取文件 (不复制) Container 读取 /etc/passwd → 直接从 Base Layer 读取,零开销 ✓ 读取不需要复制 ← 直接读 lower layer (不触发 ovl_copy_up) 写入文件 (复制后修改) Container 写入 /etc/passwd → 先复制到 Container Layer,再修改 ✗ 原始层文件不会被修改 ← ovl_copy_up() 写时复制到 upper layer 之后该文件读 upper, 不再读 lower
读操作直接穿透到低层;写操作先复制到顶层再修改,不破坏原始层
两个容器共享同一基础镜像 Container A Container Layer (可写层, 各自独立) Layer 5: COPY . /app Layer 4: RUN pip install Container B Container Layer (可写层, 各自独立) Layer 5: COPY . /app (不同!) Layer 4: RUN npm install 共享只读层: ubuntu:22.04 + python3 (222MB 只存一份!) ← 共享只读层, 节省磁盘
多个容器共享只读基础层,仅可写层不同——镜像轻量化的关键
🏠 生活类比

镜像 = 千层饼:每层是只读的,叠在一起才是一个完整的饼

Container Layer = 顶上盖了张保鲜膜:只有这层可写,底下都是只读

COW = 在透明纸上写字:想修改就盖一层新透明纸写,不破坏底下那层

共享基础层 = 多人看同一本书:书是共享的(只读),各加自己的笔记(可写层)

Explorer
📦 my-app/
Dockerfile
app.py
requirements.txt
📦 overlayfs/
overlay.c
# Dockerfile → 每条指令生成一个 Layer FROM ubuntu:22.04 # ← Layer 1: 72MB (Base) RUN apt-get update && \ apt-get install -y python3 # ← Layer 2: 150MB COPY requirements.txt /app/ # ← Layer 3: 2KB RUN pip install -r requirements.txt # ← Layer 4: 80MB COPY . /app # ← Layer 5: 5MB CMD ["python3", "app.py"] # ← 元数据 (不产生 Layer) # OverlayFS 内核源码 (overlay.c) static int ovl_copy_up(struct dentry *dentry) { // 写时复制: 将 lower 层文件复制到 upper 层 ovl_copy_up_data(lower, upper); # ← COW 核心 ovl_set_attr(upper); # ← 复制元数据 } # 查看镜像层信息 docker history my-app:latest # IMAGE CREATED SIZE # abc123def456 2 min ago 5MB ← COPY . /app # 789ghi012jkl 3 min ago 80MB ← RUN pip install # mno345pqr678 4 min ago 2KB ← COPY requirements.txt # stu901vwx234 5 min ago 150MB ← RUN apt-get install # yza567bcd890 2 weeks ago 72MB ← ubuntu:22.04

5. docker build 到 docker run——完整流程

从 Dockerfile 到容器运行,一条命令背后的完整链路。

docker build 构建流程 Dockerfile FROM/RUN/COPY... Parse 指令 语法解析 Execute 指令 逐条执行 Create Layer 每条→一个层 Tag Image 最终镜像打标签 Dockerfile ← builder::Build() ← ADD/COPY/RUN ← diffid → layer ← image tag 每条 Dockerfile 指令对应一个 Layer FROM ubuntu:22.04 → Layer 1 (72MB) sha256:c1e0... RUN apt-get install python3 → Layer 2 (150MB) sha256:b5d3... COPY requirements.txt /app/ → Layer 3 (2KB) sha256:9f01... RUN pip install -r requirements.txt → Layer 4 (80MB) sha256:a7c2... COPY . /app → Layer 5 (5MB) sha256:ef3b... CMD ["python3", "app.py"] → 元数据 (不产生 Layer)
docker build 逐条执行 Dockerfile 指令,每条生成一个只读层
docker run 完整执行链路 Image Layers 只读镜像层 OverlayFS Mount 联合挂载 Create Namespaces 6大隔离 Set Cgroups 资源限额 Exec Entrypoint 执行入口进程 Running 运行中 image layers ← overlayfs_mount() ← setupNamespaces() ← setCgroups() ← execEntrypoint() docker run -d --name my-app -p 8080:80 nginx:latest ① docker CLI → 发送请求到 dockerd ② dockerd → 调用 containerd::Create() 创建容器 ③ containerd → 启动 runc 运行时 ④ runc → setupNamespaces() 创建6个 Namespace ⑤ runc → setCgroups() 设置资源限制 ⑥ runc → overlayfs_mount() 联合挂载 ⑦ runc → execEntrypoint() 执行入口进程 ⑧ 容器运行 → PID 1 = nginx master ⑨ runc 退出 (容器由 containerd-shim 管理) ⑩ 端口映射: host:8080 → container:80
docker run 完整链路:镜像层挂载 → Namespace 隔离 → Cgroup 限额 → 执行入口
容器网络 (Bridge 模式) Container A eth0: 172.17.0.2 NET Namespace Container B eth0: 172.17.0.3 NET Namespace veth-a veth-b docker0 172.17.0.1 虚拟网桥 Bridge / veth pair NAT / iptables 端口映射规则 Host eth0 物理网卡 Internet ← libnetwork::CreateEndpoint() 创建 veth pair ← iptables DNAT: host:8080 → 172.17.0.2:80
Bridge 模式:veth pair 连接容器和 docker0 网桥,通过 NAT 访问外网
🏠 生活类比

docker build = 照着食谱做千层饼:每一步加一层,烤好了就不能改

docker run = 把千层饼端上桌盖保鲜膜:饼是只读的,保鲜膜上可以随便写

Network Bridge = 合租公寓的公共走廊:各户有自己的门牌号,通过走廊对外沟通

veth pair = 每户门内外的对讲机:一头在屋里,一头在走廊

Explorer
📦 moby/
daemon/
create.go
containerd/
create_container.go
📦 runc/
start.go
namespace.go
cgroup.go
// docker run 完整执行链路 (简化源码) // 1. dockerd 接收请求 → containerd::Create() func (d *Daemon) ContainerCreate(config *Config) { container := containerd.Create(config) // ← containerd::Create() return container } // 2. containerd 调用 runc 创建容器 func (c *Containerd) Create(config *Config) { // 2a. OverlayFS 联合挂载 overlayfs_mount(lowerDir, upperDir, mergedDir) // ← overlayfs_mount() // 2b. 创建 Namespace ns := setupNamespaces() // ← 6 个 clone(NEW_*) // 2c. 设置 Cgroup setCgroups(containerID, cpu, memory) // ← 写 cgroup 文件 // 2d. 配置网络 if config.NetworkMode == "bridge" { veth := libnetwork.CreateEndpoint() // ← veth pair connectToBridge(veth, "docker0") } // 2e. 执行入口进程 execEntrypoint(config.Entrypoint, config.Cmd) // ← exec } // 3. runc 启动容器进程 func (r *Runtime) Start(containerID string) { // clone() 创建新进程,同时设置 Namespace syscall.Clone( CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS | CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWUSER, ) // 容器进程 PID 1 = Entrypoint }

6. 面试速答

Docker 内核原理高频面试题,30秒内给到面试官满意的回答。

Q1: 容器和虚拟机的本质区别是什么?
核心区别:虚拟化层级不同。虚拟机在硬件层虚拟化(Hypervisor),每个 VM 自带完整 Guest OS;容器在操作系统层虚拟化(Namespace + Cgroup),共享宿主机内核,无需 Guest OS。
因此容器更轻量(MB vs GB)、启动更快(秒 vs 分钟)、密度更高(百 vs 十),但隔离性弱于 VM(共享内核有逃逸风险)。
一句话回答

虚拟机虚拟硬件自带 OS,容器共享内核只隔离进程——重量差 100 倍。

Q2: Namespace 有哪几种?分别隔离什么?
6 种 Namespace:
PID 进程编号隔离(容器内 PID 1 ≠ 宿主 PID 1)
NET 网络栈隔离(独立 IP/端口/路由)
MNT 挂载点隔离(独立文件系统视图)
IPC 进程间通信隔离(共享内存/信号量互不可见)
UTS 主机名隔离(每个容器可有独立 hostname)
USER 用户隔离(容器 root ≠ 宿主 root,UID 映射)
一句话回答

6 种:PID/NET/MNT/IPC/UTS/USER——分别隔离进程号、网络、文件系统、通信、主机名、用户。

Q3: Cgroup 是怎么限制资源的?OOM Kill 是什么?
Cgroup 通过文件系统控制资源:CFS 配额控制 CPU 时间片(cpu.cfs_quota_us),memory.limit_in_bytes 控制内存上限。
OOM Kill:当容器进程申请内存超过 memory.limit,内核的 OOM Killer 直接杀死该进程(不是拒绝分配,是直接杀进程)。
一句话回答

Cgroup 通过 CFS 配额限 CPU、limit 文件限内存;超限则 OOM Killer 直接杀进程。

Q4: 镜像分层和 Copy-on-Write 是什么?为什么重要?
镜像分层:Dockerfile 每条指令生成一个只读层,通过 OverlayFS 联合挂载为统一视图。
Copy-on-Write:读文件直接从底层读(零开销);写文件时先复制到顶层再修改(不破坏底层)。
为什么重要:① 共享基础层节省磁盘(同一 ubuntu 镜像只存一份);② 构建缓存加速(未变层复用);③ 容器启动快(只需加可写层)。
一句话回答

镜像是只读层叠叠乐,写时复制到顶层不改底层——共享层省空间、缓存层省时间。

Q5: docker run 背后发生了什么?
完整链路:
① docker CLI 发请求 → dockerd
② dockerd 调用 containerd::Create()
③ containerd 启动 runc
④ runc 调用 overlayfs_mount() 联合挂载镜像层
⑤ runc 调用 setupNamespaces() 创建 6 个 Namespace
⑥ runc 调用 setCgroups() 设置资源限额
⑦ runc 调用 execEntrypoint() 执行入口进程
⑧ runc 退出,containerd-shim 接管容器生命周期
一句话回答

CLI→dockerd→containerd→runc:挂载镜像→建 Namespace→设 Cgroup→跑入口进程。

Q6: 容器网络 (Bridge 模式) 是怎么工作的?
Bridge 模式工作原理:
① 创建 veth pair(虚拟网线,一头在容器,一头在 docker0 网桥)
② 容器 eth0 分配 172.17.0.x 网段 IP
③ docker0 网桥充当路由器,连接所有容器
④ 通过 iptables DNAT 实现 host:8080 → 172.17.0.2:80 端口映射
⑤ 通过 SNAT/MASQUERADE 实现容器访问外网
一句话回答

veth pair 连容器和 docker0 网桥,网桥做路由,iptables 做 NAT 端口映射。

Docker 运行原理核心公式

Docker Container = Namespace(隔离) + Cgroup(限额) + UnionFS(分层)

Namespace 让容器以为自己独占机器 → Cgroup 让容器不能吃光资源 → UnionFS 让镜像轻量可复用

核心三原则速记

Docker三原则:① 多阶段构建减小镜像体积 → ② .dockerignore排除不需要的文件 → ③ 数据持久化用Volume不要存容器内