# namespaces + cgroups —— 命名空间与资源限额（容器的内核底座）

> 这是 [../task-struct.md](/concepts/process/task-struct.md) 里 `task->nsproxy` 和 cgroup 那一项的展开。**容器(Docker/K8s)不是虚拟机**——它就是"一组被 namespace 圈起来看不见外面、被 cgroup 限量的普通进程"。这两样都是 `task_struct` 挂着的资源对象:**namespace 决定进程"看得见什么"(隔离)、cgroup 决定它"能用多少"(限额)**。本篇讲清这两套机制、各自的种类、以及它们怎么合起来撑起容器。

## 零、一句话认知：隔离(看得见什么) + 限额(能用多少) = 容器

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<n>> #E3F2FD
  BorderColor<<n>> #1976D2
  BackgroundColor<<c>> #C8E6C9
  BorderColor<<c>> #388E3C
  BackgroundColor<<box>> #FFF9C4
  BorderColor<<box>> #F9A825
}
rectangle "namespace(隔离视图)\n让进程以为自己独占系统:\n· 只看得到自己的进程(PID ns)\n· 独立的网卡/端口(net ns)\n· 独立的文件系统挂载(mnt ns)\n· 独立的主机名(uts ns)" <<n>> as N
rectangle "cgroup(资源限额)\n限制进程组能用多少:\n· CPU 配额/权重\n· 内存上限(超了 OOM)\n· IO 带宽\n· PID 数量" <<c>> as C
rectangle "容器 = namespace + cgroup\n一组普通进程,\n被隔离视图 + 被限量" <<box>> as B
N -down-> B
C -down-> B
note bottom of B : 不是虚拟机!没有 guest 内核,\n和宿主共享同一个内核
@enduml
```

> **核心记忆**:**namespace 管"隔离"——进程看到的是被裁剪过的系统视图;cgroup 管"限额"——进程能消耗的资源有上限。** 两者正交、都作用于普通进程,合起来就是容器。容器和宿主**共享同一个内核**(区别于虚拟机的 guest 内核)。

## 一、namespace：8 种隔离视图

每个进程通过 `task->nsproxy` 指向它所属的一组 namespace。同一 namespace 里的进程共享某类资源视图,不同 namespace 互相看不见。

| namespace | 隔离什么 | 效果 |
|-----------|---------|------|
| **PID** | 进程 ID 空间 | 容器内进程从 PID 1 开始编号,看不到宿主/别的容器的进程 |
| **NET** | 网络栈 | 独立的网卡、IP、端口、路由表、防火墙——容器有自己的 `eth0` |
| **MNT** | 文件系统挂载点 | 独立的挂载树(容器的 `/` 是自己的镜像) |
| **UTS** | 主机名/域名 | 容器有自己的 hostname |
| **IPC** | System V IPC / POSIX 消息队列 | 隔离共享内存、信号量 |
| **USER** | uid/gid 映射 | 容器内 uid=0(root) 映射到宿主的非特权 uid——**容器内 root ≠ 宿主 root**(安全关键) |
| **CGROUP** | cgroup 根视图 | 隐藏容器所属 cgroup 的真实层级 |
| **TIME** | 系统时钟偏移 | 容器可有不同的启动时间基准 |

```bash
ls -l /proc/<pid>/ns/       # 该进程所属的每个 namespace(inode 号相同=在同一 ns)
#   pid -> pid:[4026531836]   net -> net:[4026531840]  ...
unshare --net --pid --fork bash   # 手动创建新 net+pid namespace 跑一个 shell
nsenter -t <pid> -n ip addr       # 进入某进程的 net namespace 执行命令
```

- **PID namespace 的层级**:容器里的 PID 1 是那个 namespace 的"init",它在宿主机里其实是另一个大 PID。`/proc/<pid>/ns/pid` 的 inode 相同 = 在同一 PID namespace。
- **USER namespace 是安全基石**:容器内进程可以是"root"(uid 0),但通过 uid 映射,它在宿主看来只是个普通用户——即使逃逸也没有宿主 root 权限。呼应 [cred.md](/concepts/process/task-resources/cred.md) 的 capabilities:容器内的 root 权限也被 capability 边界裁剪。

## 二、cgroup：资源限额与统计

**cgroup(control group)** 把一组进程归到一起,对它们的资源用量**限额 + 计量**。`task` 记录自己属于哪些 cgroup。

| 控制器(controller) | 限制/统计什么 |
|--------------------|--------------|
| **cpu** | CPU 时间配额(`cpu.max`:每周期最多用多少)、权重(`cpu.weight`) |
| **cpuset** | 把一组 CPU 核/NUMA 节点划给这组进程(见 [../thread-affinity.md](/concepts/process/thread-affinity.md)) |
| **memory** | 内存上限(`memory.max`);超限触发**cgroup 内 OOM kill** |
| **io** | 块设备读写带宽/IOPS 上限 |
| **pids** | 最多能创建多少进程(防 fork 炸弹) |

### cgroup v1 vs v2

- **v1**:每个控制器一棵独立层级树,`/sys/fs/cgroup/cpu/`、`/sys/fs/cgroup/memory/`… 各管各的,一个进程在不同控制器里可属于不同组——灵活但混乱。
- **v2(统一层级)**:所有控制器共用**一棵**层级树(`/sys/fs/cgroup/` 下统一),一个进程只属于一个 cgroup 节点,在该节点上启用需要的控制器。现在的主流。

```bash
# cgroup v2:看某进程属于哪个 cgroup
cat /proc/<pid>/cgroup            # 0::/system.slice/docker-xxx.scope
# 看/设某个 cgroup 的限额(v2)
cat /sys/fs/cgroup/<path>/memory.max      # 内存上限
cat /sys/fs/cgroup/<path>/cpu.max         # CPU 配额  "50000 100000" = 每 100ms 最多 50ms
cat /sys/fs/cgroup/<path>/memory.current  # 当前用量
```

- **K8s 的 CPU limit/request** 底层就是设 cgroup 的 `cpu.max`/`cpu.weight`;**memory limit** 就是 `memory.max`,容器 OOM 常是撞了它。
- cgroup 配置属于 `/sys` 入口(见 [../../proc/kernel-tuning.md](/tools/proc/kernel-tuning.md))。

## 三、容器 = 怎么拼起来的

一个容器启动时,runtime(runc 等)大致做：

1. **`clone(CLONE_NEWPID|CLONE_NEWNET|CLONE_NEWNS|...)`**:创建带一组新 namespace 的进程(见 [../process-creation.md](/concepts/process/process-creation.md) §三 的 clone flags)。
2. **配 namespace 内容**:挂镜像文件系统(mnt)、建虚拟网卡(net)、设 hostname(uts)、uid 映射(user)。
3. **加进 cgroup**:把这个进程放进一个新 cgroup 节点,设好 CPU/内存上限。
4. **`execve`** 容器里的入口程序。

于是这个进程(及其子孙)**看到的是裁剪过的系统、能用的资源有上限**——这就是容器。

## 四、观测

```bash
ls -l /proc/<pid>/ns/       # 所属各 namespace(对比两进程 inode 是否相同=同 ns)
cat /proc/<pid>/cgroup      # 所属 cgroup 路径
cat /proc/<pid>/status | grep -E 'NSpid|Uid'   # NSpid:在各层 PID ns 里的 pid;Uid:user ns 映射后
systemd-cgls                # 树状看 cgroup 层级和里面的进程
systemd-cgtop               # 按 cgroup 看实时资源用量(类似 top)
```

- **容器里看 `/proc/cpuinfo`/`meminfo` 是宿主机的值**(procfs 不感知 cgroup,见 [../../proc/procfs.md](/tools/proc/procfs.md) §五)——要看容器真实可用资源得读 cgroup 文件,或用 lxcfs 矫正。这是容器内监控的经典坑。

## 五、和其它文档的关系

- **容器专题**:本篇是容器的**内核底座**;容器完整实现原理(+overlayfs 镜像分层)、与虚拟机的差异、性能损耗见独立簇 [../../container/](/demos/crash-signals/README.md)。
- **父篇**:[../task-struct.md](/concepts/process/task-struct.md)(nsproxy/cgroup 是 task 的资源对象)。
- **创建**:[../process-creation.md](/concepts/process/process-creation.md)(clone 的 `CLONE_NEW*` flags 建 namespace)。
- **cpuset 绑核**:[../thread-affinity.md](/concepts/process/thread-affinity.md)(cgroup cpuset 把核划给一组进程)。
- **权限**:[cred.md](/concepts/process/task-resources/cred.md)(user namespace 与 capabilities 一起裁剪容器内 root)。
- **配置入口**:[../../proc/kernel-tuning.md](/tools/proc/kernel-tuning.md)(cgroup 走 `/sys/fs/cgroup`)。
- **观测坑**:[../../proc/procfs.md](/tools/proc/procfs.md)(容器内 /proc 显示宿主值)。

## 六、一句话总结

> **namespace 和 cgroup 是容器的内核底座,都作用于普通进程、和宿主共享内核(非虚拟机)。namespace 管隔离——8 种(PID/NET/MNT/UTS/IPC/USER/CGROUP/TIME)各裁剪一类系统视图,让容器以为自己独占系统;其中 user namespace 把容器内 root 映射成宿主非特权用户,是安全关键。cgroup 管限额——cpu/cpuset/memory/io/pids 等控制器限制并计量一组进程的资源用量(v2 用统一层级),K8s 的 CPU/内存 limit 就落在 cpu.max/memory.max。容器 = clone 出带新 namespace 的进程 + 塞进限额 cgroup + exec 入口。透过 `/proc/<pid>/ns/` 和 `/proc/<pid>/cgroup` 观测;注意容器内 /proc 的 cpu/mem 是宿主值。**
