
很多人第一次接触容器时，会把 `namespace` 和 `cgroup` 混在一起。

其实它们解决的是两类不同的问题：

- `namespace` 解决“这个进程能看见什么”
- `cgroup` 解决“这个进程最多能用多少资源”

如果只用 `namespace`，进程虽然看起来像在独立环境里运行，但仍然可能吃满宿主机 CPU 和内存；如果只用 `cgroup`，资源能被限制，但进程依然和宿主机共享进程树、挂载点、网络等视图。容器之所以既像“独立机器”又不会无限制抢资源，本质上就是把两者组合起来用。[^namespaces-overview][^cgroups-overview]

本文主要基于 Linux 官方文档整理，面向三个层次的读者：

- 入门读者：先建立直觉，知道两者分别做什么
- 有容器基础的读者：补齐常见命令和运行时细节
- 进阶读者：理解 `cgroup v2` 和 `cgroup namespace` 到底解决什么问题

文中命令默认以现代 Linux 发行版为前提，且更偏向 `cgroup v2` 环境。若你的系统还在使用混合层级或纯 `cgroup v1`，部分路径和行为会不同。[^cgroup-v2-doc]

## 先建立一个整体模型

可以先把容器看成两层能力的叠加：

1. `namespace` 提供“视图隔离”
2. `cgroup` 提供“资源治理”

常见 `namespace` 类型包括：

- `mnt`：挂载点视图
- `pid`：进程号空间
- `net`：网络设备、路由、端口等网络栈
- `uts`：主机名和域名
- `ipc`：System V IPC、POSIX 消息队列
- `user`：用户和组 ID 映射
- `cgroup`：cgroup 路径视图[^namespaces-types]

而 `cgroup` 的核心是把进程组织进一个层级树，然后让控制器对这些进程组做限制、统计和隔离。Linux man page 对它的定义很直接：`cgroup` 就是“按层级组织的一组进程”，资源限制、优先级、计量都建立在这个分组之上。[^cgroups-overview]

如果用一句话总结：

- `namespace` 更像“戴上一副只让你看到局部世界的眼镜”
- `cgroup` 更像“给你发了一张有限额度的资源配额卡”

## 为什么容器需要两套机制

看几个典型问题就很容易理解。

### 只有 namespace，不够

假设你用 `unshare` 创建了新的 `pid` 和 `net` namespace。此时进程在自己的命名空间里看不到宿主机的完整进程树，也有了自己的网络栈。

但如果你没有给它绑定 `cgroup`：

- 它仍然可以持续占满 CPU
- 它仍然可能分配大量内存
- 它依然会和宿主机上的其它任务争抢系统资源

也就是说，“看起来隔离”不等于“资源上受控”。

### 只有 cgroup，也不够

再反过来，如果你只把一个进程放进受限的 `cgroup`：

- 这个进程可以被限流、限内存
- 但它看到的仍然是宿主机的挂载、网络、PID 视图

这更像“在同一台机器上给一个普通进程做资源配额”，而不是容器。

所以容器运行时通常会同时创建多个 `namespace`，再把容器内进程加入对应的 `cgroup`。[^namespaces-overview][^cgroups-overview]

## 从最小实验理解 namespace

先看三个最容易感知的隔离：`uts`、`pid`、`mount`。

### 实验 1：隔离 hostname

```bash
sudo unshare --uts --fork /bin/bash
hostname demo-ns
hostname
```

如果命令成功，当前 shell 中看到的 hostname 会变成 `demo-ns`，但宿主机终端里的 hostname 不会改变。

这里发生的事情很简单：`uts namespace` 隔离了主机名和 NIS 域名。也就是说，进程可以拥有自己的机器名视图。[^namespaces-types]

### 实验 2：隔离 PID 视图

```bash
sudo unshare --pid --fork --mount-proc /bin/bash
ps -ef
echo $$
```

在这个新 shell 中，你通常会看到一个很短的进程列表，而且当前 shell 可能变成命名空间里的 `PID 1` 或非常靠前的 PID。

这说明 `pid namespace` 隔离的不是“进程是否存在”，而是“你如何编号和观察这些进程”。容器里的 `PID 1` 并不是宿主机上的真实 `PID 1`，只是这个命名空间内部看到的根进程。[^namespaces-overview]

### 实验 3：隔离挂载点

```bash
sudo unshare --mount /bin/bash
mount -t tmpfs tmpfs /mnt
mount | grep /mnt
```

在新 shell 里挂载一个 `tmpfs` 后，宿主机不一定能看到同样的挂载结果。这是因为 `mount namespace` 隔离了挂载点集合，让不同进程组可以有不同的文件系统视图。[^namespaces-overview]

这个能力是容器镜像、根文件系统切换和“容器里看到的目录树”成立的基础。

## 用 nsenter 进入别人的 namespace

有了 `unshare`，通常还要配套理解 `nsenter`。

容器排障时很常见的一个动作，就是进入目标进程所在的命名空间：

```bash
sudo nsenter --target <pid> --mount --uts --ipc --net --pid
```

这条命令的含义不是“进入一个容器”，而是“把当前进程加入目标进程所在的一组 namespace”。它的底层能力来自 `setns(2)` 这类系统调用。[^namespaces-overview]

这也是为什么很多排障工具本质上只是帮你找到目标 PID，再替你调用 `nsenter`。

## 从最小实验理解 cgroup

和 `namespace` 相比，`cgroup` 更像一套“资源控制文件系统”。

在 `cgroup v2` 中，通常会看到统一挂载点：

```bash
mount | grep cgroup
ls /sys/fs/cgroup
```

你会看到很多控制文件，例如：

- `cpu.max`
- `memory.max`
- `memory.current`
- `cgroup.procs`
- `cgroup.controllers`
- `cgroup.subtree_control`

这些文件不是普通配置文件，而是内核暴露出来的控制接口。[^cgroup-v2-doc]

### 实验 4：用 systemd-run 限制内存

如果你的系统使用 systemd 并启用了 `cgroup v2`，可以直接做一个最小实验：

```bash
systemd-run --user --scope -p MemoryMax=200M -p CPUQuota=50% bash
```

进入这个 shell 后，可以查看当前进程所在的 cgroup：

```bash
cat /proc/self/cgroup
```

再看对应目录下的限制文件：

```bash
cat /sys/fs/cgroup/$(sed 's|0::/||' /proc/self/cgroup)/memory.max
cat /sys/fs/cgroup/$(sed 's|0::/||' /proc/self/cgroup)/cpu.max
```

在 `cgroup v2` 中：

- `memory.max` 表示内存硬上限
- `cpu.max` 表示 CPU 带宽限制，常见格式类似 `50000 100000`，意思是在 100000 微秒周期里最多用 50000 微秒 CPU 时间[^cgroup-v2-cpu-max][^cgroup-v2-memory-max]

如果系统没有 `systemd-run --user` 环境，也可以用 root 手动创建子 cgroup：

```bash
cd /sys/fs/cgroup
sudo mkdir demo
echo $$ | sudo tee demo/cgroup.procs
echo 200M | sudo tee demo/memory.max
echo "50000 100000" | sudo tee demo/cpu.max
```

这几步背后的逻辑其实很统一：

1. 创建一个 cgroup 目录
2. 把进程 PID 写入 `cgroup.procs`
3. 给这个组设置资源限制

这也是 Linux 官方文档强调的做法：进程通过写入 `cgroup.procs` 被迁移到指定 cgroup 中，再由控制器施加资源策略。[^cgroup-v2-doc]

## cgroup v2 里最值得先理解的几个概念

### 1. 统一层级

`cgroup v2` 最大的变化之一，是把控制器收敛到统一层级中。相比 `v1` 多层级、多挂载点的做法，`v2` 更容易理解，也更适合容器运行时统一管理。[^cgroup-v2-doc]

这也是为什么现在讨论容器资源限制时，越来越默认你在说 `cgroup v2`。

### 2. 控制器不是默认就启用

很多人看到 `/sys/fs/cgroup` 里有 `cpu.max`、`memory.max` 之类文件，就以为所有子树天然都可以用。

不是这样。

在 `cgroup v2` 中，控制器需要从父节点向子节点显式启用，典型入口是 `cgroup.subtree_control`。这就是所谓的 top-down 约束：父节点没有授予，子节点就不能随便开启控制器。[^cgroup-v2-doc]

### 3. “内部进程约束”

`cgroup v2` 还有一个很重要但经常被忽略的规则：一个既承担“资源分配节点”又直接放业务进程的中间节点，会受到限制。官方文档通常把它叫做 no internal process constraint。[^cgroup-v2-doc]

直觉上可以这样理解：

- 如果某个 cgroup 还要继续往下切子 cgroup 来做资源分配
- 那这个节点最好不要自己再直接承载业务进程

这样资源树的语义才清晰，否则父子节点之间很难定义一致的控制规则。

### 4. cgroup 是按“进程组”管理，不是按“容器对象”管理

容器运行时会把“容器”这个概念映射成一组进程，再把这些进程放进某个 cgroup。

也就是说，内核并不认识 Docker 容器、Pod、sandbox 这些高级对象。它只认识：

- 哪些进程在同一个 cgroup
- 对这个 cgroup 启用了哪些控制器
- 每个控制器的参数是多少

## cgroup namespace 到底解决什么问题

很多人第一次看到 `cgroup namespace` 这个名字，会以为它提供了新的资源隔离能力。

实际上不是。

根据官方文档，`cgroup namespace` 主要做的是“虚拟化 cgroup 路径视图”，让进程看到的 `/proc/self/cgroup` 以及部分 `/proc/self/mountinfo` 内容相对于它所在的 namespace root 来展示。[^cgroup-namespace-virtualize]

这带来两个直接效果：

### 1. 容器内看到的 cgroup 路径更像“容器自己的根”

如果没有 `cgroup namespace`，容器内进程读 `/proc/self/cgroup` 时，可能会直接看到宿主机上的完整 cgroup 路径，比如很长的一串编排系统路径。

有了 `cgroup namespace` 之后，这个路径会被重写成相对当前 namespace root 的视图，更适合容器内部使用。[^cgroup-namespace-virtualize]

### 2. 它隐藏宿主机层级细节，但不改变实际控制关系

这是关键点。

`cgroup namespace` 改变的是“你怎么看路径”，不是“你实际属于哪个 cgroup”，更不是“你突然获得了新的控制器或新的资源额度”。

所以它更接近：

- 视图虚拟化

而不是：

- 新的资源治理模型

这也是为什么 `cgroup namespace` 经常和容器可见性、可移植性、路径隐藏有关，而不是和 CPU/内存限制本身绑定在一起。[^cgroup-namespace-virtualize]

## 一个真实 Pod 的 cgroup 和 namespace 排查示例

上面的概念单独看不难，但真正容易混淆的是排查现场里同时出现：

- `/proc/<pid>/cgroup`
- `/proc/<pid>/ns/*`
- `nsenter -C`

下面用一个真实 Pod 中进程的排查结果来串起来看。

### 先看它真实属于哪个 cgroup

某个容器进程的 PID 是 `47169`，宿主机看到：

```bash
cat /proc/47169/cgroup
```

输出如下：

```text
0::/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod5b0d290f_4bf3_4b4f_affe_f15d3b14698d.slice/cri-containerd-ff6469c98c0c76d21984ccaa8b9ff356ad52278c0173552f37320a4a49db25f7.scope
```

这里可以直接读出几件事：

- 这是 `cgroup v2`，因为格式是 `0::/...`
- 它位于 `kubepods.slice` 下，说明是 Kubernetes 管理的工作负载
- 它在 `kubepods-burstable.slice` 下，说明这个 Pod 的 QoS 类别是 `Burstable`
- 路径里包含 Pod UID：`5b0d290f-4bf3-4b4f-affe-f15d3b14698d`
- 最后的 `cri-containerd-...scope` 对应具体容器进程[^cgroup-v2-doc]

注意，这条路径是“真实资源控制位置”。也就是说，CPU、内存等限制最终都是对这个 cgroup 生效，而不是对某个抽象的“容器对象”生效。

### 再看它加入了哪些 Linux namespace

查看该进程的 namespace：

```bash
ls -l /proc/47169/ns
```

输出如下：

```text
cgroup -> cgroup:[4026536406]
ipc -> ipc:[4026535991]
mnt -> mnt:[4026536401]
net -> net:[4026535850]
pid -> pid:[4026536402]
pid_for_children -> pid:[4026536402]
time -> time:[4026531834]
time_for_children -> time:[4026531834]
user -> user:[4026531837]
uts -> uts:[4026535990]
```

这里最容易误解的一点是：这些数字不是 Kubernetes namespace，也不是 cgroup ID，而是 namespace 对象本身的 inode 标识。

看这些数字时不要试图记住它们本身，重点是拿它们和宿主机对比。

例如对比宿主机 `PID 1`：

```bash
ls -l /proc/1/ns
```

输出如下：

```text
cgroup -> cgroup:[4026531835]
ipc -> ipc:[4026531839]
mnt -> mnt:[4026531841]
net -> net:[4026531840]
pid -> pid:[4026531836]
pid_for_children -> pid:[4026531836]
time -> time:[4026531834]
time_for_children -> time:[4026531834]
user -> user:[4026531837]
uts -> uts:[4026531838]
```

逐项对比就能得出结论：

- `cgroup` 不同：使用了独立的 `cgroup namespace`
- `ipc` 不同：使用了独立的 `ipc namespace`
- `mnt` 不同：使用了独立的 `mount namespace`
- `net` 不同：使用了独立的 `network namespace`
- `pid` 不同：使用了独立的 `pid namespace`
- `uts` 不同：使用了独立的 `uts namespace`
- `time` 相同：与宿主机共享 `time namespace`
- `user` 相同：与宿主机共享 `user namespace`

所以这个容器进程不是“进入了某一个 namespace”，而是同时加入了多种 namespace，只不过每种 namespace 的隔离范围不同。

### 为什么 cgroup 明明是资源控制，还会有 cgroup namespace

这正是现场最容易绕晕的地方。

要把下面两个概念严格分开：

- `cgroup`：真实的资源控制层级
- `cgroup namespace`：对 cgroup 路径视图的虚拟化

还是用这个例子说：

- 真实 cgroup 是 `/kubepods.slice/.../cri-containerd-...scope`
- `cgroup namespace` 决定进程读 `/proc/self/cgroup` 时，看到的是不是宿主机完整路径

也就是说，`cgroup namespace` 不负责创建新的 CPU 或内存限制，它只负责“怎么看 cgroup 路径”。

### 为什么 `nsenter -C` 看到的是相对路径

在宿主机执行：

```bash
nsenter -t 47169 -C cat /proc/self/cgroup
```

输出是：

```text
0::/../../../../system.slice/ssh.service
```

这个结果乍看会很怪，因为目标进程真实明明在 `kubepods.slice/...` 下，怎么会冒出 `ssh.service`。

原因是这条命令只做了一件事：

- 进入目标进程的 `cgroup namespace`

但它没有做另一件事：

- 把当前新启动的 `cat` 进程迁移到目标进程所在的真实 cgroup

所以此时的 `cat` 进程真实上仍然属于你当前 SSH 会话的 cgroup，也就是 `system.slice/ssh.service`。只不过因为它已经处在目标进程的 `cgroup namespace` 视角里，所以这个路径被重新表达成了相对路径：

```text
/../../../../system.slice/ssh.service
```

这个例子正好说明了 `cgroup namespace` 的本质：

- 它改变的是路径展示方式
- 它不改变进程真实属于哪个 cgroup

换句话说：

- `/proc/<pid>/cgroup` 更适合看“真实资源归属”
- `nsenter -C ... cat /proc/self/cgroup` 更适合看“在目标 cgroup namespace 视角下，路径是如何被虚拟化的”

### 这个 Pod 的最终判断

综合上面的输出，可以对这个 Pod 中的进程做一个简洁判断：

- 它运行在 `cgroup v2` 环境中
- 它属于 Kubernetes `Burstable` QoS Pod
- 它真实位于 `kubepods.slice/.../cri-containerd-...scope` 这个 cgroup 下
- 它使用了独立的 `cgroup/ipc/mnt/net/pid/uts namespace`
- 它与宿主机共享 `user/time namespace`
- 它的 `cgroup namespace` 负责隐藏或重写 cgroup 路径视图，但不负责资源限制本身

## namespace 和 cgroup 在容器里是怎么配合的

现在可以把前面的知识拼起来看。

一个典型容器运行时在启动容器时，通常会做这些事情：

1. 创建或加入一组新的 `namespace`
2. 准备根文件系统和挂载点
3. 配置 veth、路由、hostname 等隔离环境
4. 创建或选择目标 `cgroup`
5. 把容器主进程加入该 `cgroup`
6. 启动容器内的 `PID 1`

于是容器里进程得到两层效果：

- 从 `namespace` 获得“我像运行在独立机器里”的视图
- 从 `cgroup` 获得“我只能使用被允许的资源”的约束

这也是为什么排查容器问题时，经常要分两条线：

- 视图问题：看 `nsenter`、`ip netns`、`/proc/<pid>/ns`
- 资源问题：看 `/proc/<pid>/cgroup`、`/sys/fs/cgroup`、OOM、CPU throttle

## 几个常见误区

### 误区 1：namespace 就是容器

不是。

`namespace` 只是隔离机制的一部分。没有文件系统准备、cgroup、能力控制、seccomp、设备白名单等配套时，它离“容器”还差很远。[^namespaces-overview]

### 误区 2：cgroup 只能做限制，不能做统计

也不对。

`cgroup` 不只提供限制，也提供资源记账和监控接口。例如 `memory.current`、`cpu.stat` 之类文件都能反映运行状态。[^cgroup-v2-doc]

### 误区 3：cgroup namespace 会创建新的资源层级

不会。

它主要是路径视图虚拟化，不会凭空生成一套新的控制树或新的配额体系。[^cgroup-namespace-virtualize]

### 误区 4：容器里看到 PID 1，就等于宿主机 PID 1

这只是 `pid namespace` 内部视图。宿主机看到的真实 PID 往往完全不同。[^namespaces-overview]

## 学习和排障时建议先看哪些文件

如果你想把这套机制真正摸熟，建议先固定看这几类入口：

- `/proc/<pid>/ns/`
- `/proc/<pid>/cgroup`
- `/sys/fs/cgroup/`
- `/proc/self/mountinfo`
- `mount | grep cgroup`

这几个入口基本可以把“这个进程在哪些 namespace 中、属于哪个 cgroup、当前 cgroup 树长什么样”串起来。

## 总结

可以把本文压缩成三句话：

1. `namespace` 负责隔离视图，决定进程看见的世界
2. `cgroup` 负责资源治理，决定进程能用多少 CPU、内存等资源
3. `cgroup namespace` 主要虚拟化 cgroup 路径视图，不是新的资源限制机制

如果你从容器角度理解 Linux 内核，最值得先建立的不是命令记忆，而是这个分工模型。模型对了，后面再看 Docker、containerd、Kubernetes 的资源配置，就会清楚很多。

## 参考链接

- [namespaces(7)](https://man7.org/linux/man-pages/man7/namespaces.7.html)
- [cgroups(7)](https://man7.org/linux/man-pages/man7/cgroups.7.html)
- [cgroup_namespaces(7)](https://man7.org/linux/man-pages/man7/cgroup_namespaces.7.html)
- [Linux kernel: Control Group v2](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html)

[^namespaces-overview]: Linux man-pages, namespaces(7), accessed on 2026-03-17.
[^namespaces-types]: Linux man-pages, namespaces(7), namespace types table, accessed on 2026-03-17.
[^cgroups-overview]: Linux man-pages, cgroups(7), accessed on 2026-03-17.
[^cgroup-namespace-virtualize]: Linux man-pages, cgroup_namespaces(7), accessed on 2026-03-17.
[^cgroup-v2-doc]: Linux kernel documentation, Control Group v2, accessed on 2026-03-17.
[^cgroup-v2-cpu-max]: Linux kernel documentation, Control Group v2, `cpu.max`, accessed on 2026-03-17.
[^cgroup-v2-memory-max]: Linux kernel documentation, Control Group v2, `memory.max`, accessed on 2026-03-17.

