# 容器实现原理 —— 总纲(namespace + cgroup + rootfs 三大支柱)

> 这是"容器"文档簇的总纲。承接内核底座 [../process/task-resources/namespaces-cgroups.md](/concepts/process/task-resources/namespaces-cgroups.md):容器不是虚拟机,而是**一组被 namespace 隔离视图、被 cgroup 限额、和宿主共享同一个内核的普通进程**。本篇站在框架层把"一个容器是怎么拼出来的"讲透——三大支柱、启动全流程、镜像/容器/运行时的概念澄清——每块深入之处点到对应分篇。

## 零、一句话认知

> **容器 = 一个受隔离(namespace)、受限额(cgroup)、看到独立文件系统(rootfs+overlayfs)的普通进程,和宿主共享同一个内核——不是虚拟机(没有 guest 内核)。** 记住这一条,下面所有机制都是围绕"怎么把一个普通进程装扮成'它以为自己独占一台机器'"展开的。

## 一、三大支柱(核心)

一个容器靠三件事撑起来:**namespace 决定"看得见什么"(隔离)、cgroup 决定"能用多少"(限额)、rootfs+overlayfs 决定"文件系统长什么样"(镜像视图)**。三者正交,都作用在普通进程上。

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<ns>> #E3F2FD
  BorderColor<<ns>> #1976D2
  BackgroundColor<<cg>> #C8E6C9
  BorderColor<<cg>> #388E3C
  BackgroundColor<<fs>> #FFE0B2
  BorderColor<<fs>> #F57C00
  BackgroundColor<<box>> #FFF9C4
  BorderColor<<box>> #F9A825
}
rectangle "namespace(隔离 / 看得见什么)\n8 种视图裁剪:\nPID / NET / MNT / UTS /\nIPC / USER / CGROUP / TIME" <<ns>> as N
rectangle "cgroup(限额 / 能用多少)\n限制并计量一组进程:\nCPU / 内存 / IO / pids" <<cg>> as C
rectangle "rootfs + overlayfs(镜像 / 文件系统视图)\n只读镜像层 + 可写层\n叠成容器的根目录 /" <<fs>> as F
rectangle "容器 = 一个普通进程\n被裁剪视图 + 被限量 + 看到独立 rootfs\n(与宿主共享同一个内核)" <<box>> as B
N -down-> B
C -down-> B
F -down-> B
note bottom of B : 不是虚拟机!没有 guest 内核
@enduml
```

### 1. namespace —— 隔离(看得见什么)

8 种 namespace 各裁剪一类系统视图,让进程以为自己独占系统。**这块在内核底座篇已完整展开,这里只列一句话**:

| namespace | 一句话 |
|-----------|--------|
| **PID** | 容器内进程从 PID 1 编号,看不到宿主/别的容器 |
| **NET** | 独立网卡/IP/端口/路由,容器有自己的 `eth0` |
| **MNT** | 独立挂载树,容器的 `/` 是自己的镜像 |
| **UTS** | 独立主机名/域名 |
| **IPC** | 隔离共享内存/信号量/消息队列 |
| **USER** | uid/gid 映射,**容器内 root ≠ 宿主 root**(安全关键) |
| **CGROUP** | 隐藏容器所属 cgroup 的真实层级 |
| **TIME** | 独立的系统时钟偏移 |

> **一句话**:namespace 是"隔离视图",8 种各管一类,让进程看到被裁剪过的系统。种类、内核细节、`unshare`/`nsenter` 观测见 [../process/task-resources/namespaces-cgroups.md](/concepts/process/task-resources/namespaces-cgroups.md) §一。

### 2. cgroup —— 限额(能用多少)

cgroup 把一组进程归到一起,对它们的资源用量限额 + 计量:

| 控制器 | 限什么 |
|--------|--------|
| **cpu / cpuset** | CPU 配额(`cpu.max`)、权重、绑核 |
| **memory** | 内存上限(`memory.max`),超限触发 cgroup 内 OOM |
| **io** | 块设备读写带宽/IOPS |
| **pids** | 最多创建多少进程(防 fork 炸弹) |

> **一句话**:cgroup 是"资源限额",K8s 的 CPU/内存 limit 就落在 `cpu.max`/`memory.max`。v1/v2 层级、观测命令见 [../process/task-resources/namespaces-cgroups.md](/concepts/process/task-resources/namespaces-cgroups.md) §二;配置走 `/sys/fs/cgroup`,见 [../../tools/proc/kernel-tuning.md](/tools/proc/kernel-tuning.md)。

### 3. rootfs + overlayfs —— 镜像(文件系统视图)

前两根支柱在底座篇讲透了,**这根本篇展开**。容器要有自己的 `/`(根文件系统),但又不能每起一个容器就拷贝一整份几百 MB 的镜像——秒启动、省空间靠的是**镜像分层 + overlayfs 叠加**。

- **镜像分层(只读 layers)**:一个镜像是**若干只读层叠起来**的。比如 `FROM ubuntu` 是一层、`apt install` 是一层、`COPY 应用` 又是一层。每层只记录相对上一层的**增量改动**。多个镜像/容器共享底层公共层(如都基于 ubuntu 就只存一份),这就是**省空间**的来源。
- **overlayfs 叠加**:overlayfs 是 Linux 内核的联合挂载(union mount)文件系统,把**多个只读的 lowerdir(镜像层)+ 一个可写的 upperdir(容器可写层)**叠成一个统一视图(merged),挂到容器的 `/`。
- **copy-on-write(CoW)**:容器读文件时直接读下面的只读层;**一旦要写某个文件,才把它从只读层"上拷"(copy-up)到可写层再改**——只读层始终不变、可被所有容器共享。这与 [../process/process-creation.md](/concepts/process/process-creation.md) §二讲的 fork CoW 是**同一种"谁写谁复制"的省资源思想**,只是一个作用在物理内存页、一个作用在文件。
- **pivot_root 切进去**:rootfs 叠好后,runtime 用 `pivot_root`(或早期的 `chroot`)把容器进程的根目录**切换**到这个 merged 视图——从此容器进程眼里的 `/` 就是镜像根,再也看不到宿主的文件系统。

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<ro>> #ECEFF1
  BorderColor<<ro>> #607D8B
  BackgroundColor<<rw>> #C8E6C9
  BorderColor<<rw>> #388E3C
  BackgroundColor<<m>> #FFF9C4
  BorderColor<<m>> #F9A825
}
rectangle "容器看到的根 /(merged 视图)\noverlayfs 联合挂载" <<m>> as M
rectangle "可写层 upperdir(每容器一份,CoW 落这)" <<rw>> as U
rectangle "镜像层 3:COPY 应用(只读,共享)" <<ro>> as L3
rectangle "镜像层 2:apt install(只读,共享)" <<ro>> as L2
rectangle "镜像层 1:FROM ubuntu 基础层(只读,共享)" <<ro>> as L1
M -down-> U
U -down-> L3
L3 -down-> L2
L2 -down-> L1
note right of U : 只读层被所有容器共享\n写时才 copy-up 到可写层\n→ 秒启动 + 省空间
@enduml
```

> **一句话**:镜像是一叠只读层,overlayfs 把只读层 + 每容器一份的可写层叠成 `/`,写时才 copy-up——只读层共享所以省空间、不用拷贝所以秒启动,最后 `pivot_root` 把进程切进这个根。

## 二、一个容器怎么拼出来(启动全流程)

把三大支柱按顺序装到一个进程上,就得到容器。以 **runc**(OCI 运行时)视角看,起一个容器大致是这几步:

```plantuml
@startuml
skinparam shadowing false
skinparam sequenceMessageAlign center
participant "runc" as R
participant "内核" as K
participant "容器进程" as P
R -> K : ① clone(CLONE_NEWPID|NEWNET|\nNEWNS|NEWUTS|NEWUSER...)
K --> P : 带一组新 namespace 的进程诞生
R -> K : ② 挂 overlayfs rootfs + pivot_root
note right of K : 只读镜像层+可写层叠成 /\n切根进镜像
R -> K : ③ 配网络:建 veth pair\n一端进容器 net ns、一端接网桥
R -> K : ④ 把进程加入 cgroup\n设 cpu.max/memory.max
R -> P : ⑤ execve 容器入口程序
note right of P : 从此:看到裁剪的系统 +\n独立 rootfs + 资源有上限
@enduml
```

1. **clone(CLONE_NEW*)**:创建带一组新 namespace 的进程——这就是隔离视图的起点(clone flags 见 [../process/process-creation.md](/concepts/process/process-creation.md) §三)。
2. **挂 overlayfs rootfs + pivot_root**:把镜像只读层 + 可写层叠成容器的 `/`,切根进去(本篇 §一.3)。
3. **配网络**:创建 `veth pair`(虚拟网线对),一端塞进容器的 net namespace 当 `eth0`、另一端接到宿主网桥(如 `docker0`),容器由此能通网。
4. **加入 cgroup 设限**:把进程放进新 cgroup 节点,设好 CPU/内存/IO/pids 上限。
5. **execve 入口**:执行镜像里指定的入口程序(如 `nginx`)。至此进程"看到裁剪的系统 + 独立 rootfs + 资源有上限"——就是一个容器。

> **一句话**:runc 起容器 = clone 出带新 namespace 的进程 → 叠 rootfs 切根 → 配 veth 通网 → 塞进限额 cgroup → execve 入口,五步把三大支柱装到一个普通进程上。

## 三、镜像 vs 容器 vs 运行时(概念澄清)

日常说"容器"其实混了几个层次的概念,澄清如下:

| 概念 | 是什么 | 类比 |
|------|--------|------|
| **镜像(image)** | 静态的、只读的分层文件包(rootfs 各层 + 元数据),存在磁盘/仓库里 | 程序的"可执行文件" |
| **容器(container)** | 镜像的一个**运行实例**——由镜像叠出可写层 + namespace/cgroup 跑起来的进程 | 可执行文件跑起来的"进程" |
| **运行时(runtime)** | 负责把镜像变成运行中容器的软件,**分层**:见下 | 加载并运行程序的"加载器/调度器" |
| **OCI 标准** | Open Container Initiative:统一了**镜像格式**和**运行时接口**,让镜像和运行时可互换 | "可执行文件格式(如 ELF)+ 系统调用约定" |

**运行时是分层的**(从底到高):

- **runc**:最底层的 OCI 运行时,真正调 `clone`/`pivot_root`/cgroup 那些系统调用把容器跑起来。
- **containerd**:上一层的容器**管理**守护进程,管镜像拉取、生命周期,调用 runc 干脏活。
- **dockerd(Docker Engine)**:再上层,提供 `docker` CLI、构建镜像、网络/存储编排,底下用 containerd。

> **一句话**:镜像是静态分层文件(像可执行文件)、容器是它的运行实例(像进程)、运行时(dockerd→containerd→runc 逐层下沉)负责把前者变成后者,OCI 标准让镜像格式和运行时接口通用可换。

## 四、路由表 —— 容器簇怎么读

| 想搞清的问题 | 去哪篇 |
|--------------|--------|
| 容器和虚拟机到底差在哪、KVM 原理、安全容器选型 | [container-vs-vm.md](/concepts/container/container-vs-vm.md) |
| 容器性能开销、和裸机差多少、怎么测 | [performance.md](/concepts/container/performance.md) |
| namespace 8 种细节 + cgroup v1/v2 + 观测命令(内核底座) | [../process/task-resources/namespaces-cgroups.md](/concepts/process/task-resources/namespaces-cgroups.md) |
| clone/CLONE_NEW* flags、fork 的 CoW 原理 | [../process/process-creation.md](/concepts/process/process-creation.md) |
| cgroup 配置入口(`/sys/fs/cgroup`) | [../../tools/proc/kernel-tuning.md](/tools/proc/kernel-tuning.md) |
| 为什么容器内 `/proc/cpuinfo`/`meminfo` 是宿主值(监控坑) | [../../tools/proc/procfs.md](/tools/proc/procfs.md) |

## 五、一句话总结

> **容器 = 一个受 namespace 隔离视图、受 cgroup 限额、看到 overlayfs 叠出的独立 rootfs 的普通进程,和宿主共享同一个内核(非虚拟机)。三大支柱:namespace 管"看得见什么"(8 种视图裁剪)、cgroup 管"能用多少"(CPU/内存/IO/pids 限额)——这两根是内核底座、详见 namespaces-cgroups.md;rootfs+overlayfs 管"文件系统视图"——镜像是一叠只读层,overlayfs 把只读层+每容器可写层叠成 `/`,写时才 copy-up(与 fork 的 CoW 同思想),所以共享省空间、免拷贝秒启动,最后 pivot_root 切根进去。runc 起容器五步:clone(CLONE_NEW*)→ 叠 rootfs 切根 → veth 通网 → 塞进限额 cgroup → execve 入口。概念上镜像(静态分层文件)/容器(运行实例)/运行时(dockerd→containerd→runc 逐层下沉)分工明确,OCI 统一格式与接口。容器 vs 虚拟机看 container-vs-vm.md,性能看 performance.md。**
