Linux cgroup 和 namespace:从隔离到资源限制
· 947 字 · 约 5 分钟
很多人第一次接触容器时,会把 namespace 和 cgroup 混在一起。
其实它们解决的是两类不同的问题:
namespace解决“这个进程能看见什么”cgroup解决“这个进程最多能用多少资源”
如果只用 namespace,进程虽然看起来像在独立环境里运行,但仍然可能吃满宿主机 CPU 和内存;如果只用 cgroup,资源能被限制,但进程依然和宿主机共享进程树、挂载点、网络等视图。容器之所以既像“独立机器”又不会无限制抢资源,本质上就是把两者组合起来用。12
本文主要基于 Linux 官方文档整理,面向三个层次的读者:
- 入门读者:先建立直觉,知道两者分别做什么
- 有容器基础的读者:补齐常见命令和运行时细节
- 进阶读者:理解
cgroup v2和cgroup namespace到底解决什么问题
文中命令默认以现代 Linux 发行版为前提,且更偏向 cgroup v2 环境。若你的系统还在使用混合层级或纯 cgroup v1,部分路径和行为会不同。3
先建立一个整体模型
可以先把容器看成两层能力的叠加:
namespace提供“视图隔离”cgroup提供“资源治理”
常见 namespace 类型包括:
mnt:挂载点视图pid:进程号空间net:网络设备、路由、端口等网络栈uts:主机名和域名ipc:System V IPC、POSIX 消息队列user:用户和组 ID 映射cgroup:cgroup 路径视图4
而 cgroup 的核心是把进程组织进一个层级树,然后让控制器对这些进程组做限制、统计和隔离。Linux man page 对它的定义很直接:cgroup 就是“按层级组织的一组进程”,资源限制、优先级、计量都建立在这个分组之上。2
如果用一句话总结:
namespace更像“戴上一副只让你看到局部世界的眼镜”cgroup更像“给你发了一张有限额度的资源配额卡”
为什么容器需要两套机制
看几个典型问题就很容易理解。
只有 namespace,不够
假设你用 unshare 创建了新的 pid 和 net namespace。此时进程在自己的命名空间里看不到宿主机的完整进程树,也有了自己的网络栈。
但如果你没有给它绑定 cgroup:
- 它仍然可以持续占满 CPU
- 它仍然可能分配大量内存
- 它依然会和宿主机上的其它任务争抢系统资源
也就是说,“看起来隔离”不等于“资源上受控”。
只有 cgroup,也不够
再反过来,如果你只把一个进程放进受限的 cgroup:
- 这个进程可以被限流、限内存
- 但它看到的仍然是宿主机的挂载、网络、PID 视图
这更像“在同一台机器上给一个普通进程做资源配额”,而不是容器。
所以容器运行时通常会同时创建多个 namespace,再把容器内进程加入对应的 cgroup。12
从最小实验理解 namespace
先看三个最容易感知的隔离:uts、pid、mount。
实验 1:隔离 hostname
|
|
如果命令成功,当前 shell 中看到的 hostname 会变成 demo-ns,但宿主机终端里的 hostname 不会改变。
这里发生的事情很简单:uts namespace 隔离了主机名和 NIS 域名。也就是说,进程可以拥有自己的机器名视图。4
实验 2:隔离 PID 视图
|
|
在这个新 shell 中,你通常会看到一个很短的进程列表,而且当前 shell 可能变成命名空间里的 PID 1 或非常靠前的 PID。
这说明 pid namespace 隔离的不是“进程是否存在”,而是“你如何编号和观察这些进程”。容器里的 PID 1 并不是宿主机上的真实 PID 1,只是这个命名空间内部看到的根进程。1
实验 3:隔离挂载点
|
|
在新 shell 里挂载一个 tmpfs 后,宿主机不一定能看到同样的挂载结果。这是因为 mount namespace 隔离了挂载点集合,让不同进程组可以有不同的文件系统视图。1
这个能力是容器镜像、根文件系统切换和“容器里看到的目录树”成立的基础。
用 nsenter 进入别人的 namespace
有了 unshare,通常还要配套理解 nsenter。
容器排障时很常见的一个动作,就是进入目标进程所在的命名空间:
|
|
这条命令的含义不是“进入一个容器”,而是“把当前进程加入目标进程所在的一组 namespace”。它的底层能力来自 setns(2) 这类系统调用。1
这也是为什么很多排障工具本质上只是帮你找到目标 PID,再替你调用 nsenter。
从最小实验理解 cgroup
和 namespace 相比,cgroup 更像一套“资源控制文件系统”。
在 cgroup v2 中,通常会看到统一挂载点:
|
|
你会看到很多控制文件,例如:
cpu.maxmemory.maxmemory.currentcgroup.procscgroup.controllerscgroup.subtree_control
这些文件不是普通配置文件,而是内核暴露出来的控制接口。3
实验 4:用 systemd-run 限制内存
如果你的系统使用 systemd 并启用了 cgroup v2,可以直接做一个最小实验:
|
|
进入这个 shell 后,可以查看当前进程所在的 cgroup:
|
|
再看对应目录下的限制文件:
|
|
在 cgroup v2 中:
如果系统没有 systemd-run --user 环境,也可以用 root 手动创建子 cgroup:
|
|
这几步背后的逻辑其实很统一:
- 创建一个 cgroup 目录
- 把进程 PID 写入
cgroup.procs - 给这个组设置资源限制
这也是 Linux 官方文档强调的做法:进程通过写入 cgroup.procs 被迁移到指定 cgroup 中,再由控制器施加资源策略。3
cgroup v2 里最值得先理解的几个概念
1. 统一层级
cgroup v2 最大的变化之一,是把控制器收敛到统一层级中。相比 v1 多层级、多挂载点的做法,v2 更容易理解,也更适合容器运行时统一管理。3
这也是为什么现在讨论容器资源限制时,越来越默认你在说 cgroup v2。
2. 控制器不是默认就启用
很多人看到 /sys/fs/cgroup 里有 cpu.max、memory.max 之类文件,就以为所有子树天然都可以用。
不是这样。
在 cgroup v2 中,控制器需要从父节点向子节点显式启用,典型入口是 cgroup.subtree_control。这就是所谓的 top-down 约束:父节点没有授予,子节点就不能随便开启控制器。3
3. “内部进程约束”
cgroup v2 还有一个很重要但经常被忽略的规则:一个既承担“资源分配节点”又直接放业务进程的中间节点,会受到限制。官方文档通常把它叫做 no internal process constraint。3
直觉上可以这样理解:
- 如果某个 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 来展示。7
这带来两个直接效果:
1. 容器内看到的 cgroup 路径更像“容器自己的根”
如果没有 cgroup namespace,容器内进程读 /proc/self/cgroup 时,可能会直接看到宿主机上的完整 cgroup 路径,比如很长的一串编排系统路径。
有了 cgroup namespace 之后,这个路径会被重写成相对当前 namespace root 的视图,更适合容器内部使用。7
2. 它隐藏宿主机层级细节,但不改变实际控制关系
这是关键点。
cgroup namespace 改变的是“你怎么看路径”,不是“你实际属于哪个 cgroup”,更不是“你突然获得了新的控制器或新的资源额度”。
所以它更接近:
- 视图虚拟化
而不是:
- 新的资源治理模型
这也是为什么 cgroup namespace 经常和容器可见性、可移植性、路径隐藏有关,而不是和 CPU/内存限制本身绑定在一起。7
一个真实 Pod 的 cgroup 和 namespace 排查示例
上面的概念单独看不难,但真正容易混淆的是排查现场里同时出现:
/proc/<pid>/cgroup/proc/<pid>/ns/*nsenter -C
下面用一个真实 Pod 中进程的排查结果来串起来看。
先看它真实属于哪个 cgroup
某个容器进程的 PID 是 47169,宿主机看到:
|
|
输出如下:
|
|
这里可以直接读出几件事:
- 这是
cgroup v2,因为格式是0::/... - 它位于
kubepods.slice下,说明是 Kubernetes 管理的工作负载 - 它在
kubepods-burstable.slice下,说明这个 Pod 的 QoS 类别是Burstable - 路径里包含 Pod UID:
5b0d290f-4bf3-4b4f-affe-f15d3b14698d - 最后的
cri-containerd-...scope对应具体容器进程3
注意,这条路径是“真实资源控制位置”。也就是说,CPU、内存等限制最终都是对这个 cgroup 生效,而不是对某个抽象的“容器对象”生效。
再看它加入了哪些 Linux namespace
查看该进程的 namespace:
|
|
输出如下:
|
|
这里最容易误解的一点是:这些数字不是 Kubernetes namespace,也不是 cgroup ID,而是 namespace 对象本身的 inode 标识。
看这些数字时不要试图记住它们本身,重点是拿它们和宿主机对比。
例如对比宿主机 PID 1:
|
|
输出如下:
|
|
逐项对比就能得出结论:
cgroup不同:使用了独立的cgroup namespaceipc不同:使用了独立的ipc namespacemnt不同:使用了独立的mount namespacenet不同:使用了独立的network namespacepid不同:使用了独立的pid namespaceuts不同:使用了独立的uts namespacetime相同:与宿主机共享time namespaceuser相同:与宿主机共享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 看到的是相对路径
在宿主机执行:
|
|
输出是:
|
|
这个结果乍看会很怪,因为目标进程真实明明在 kubepods.slice/... 下,怎么会冒出 ssh.service。
原因是这条命令只做了一件事:
- 进入目标进程的
cgroup namespace
但它没有做另一件事:
- 把当前新启动的
cat进程迁移到目标进程所在的真实 cgroup
所以此时的 cat 进程真实上仍然属于你当前 SSH 会话的 cgroup,也就是 system.slice/ssh.service。只不过因为它已经处在目标进程的 cgroup namespace 视角里,所以这个路径被重新表达成了相对路径:
|
|
这个例子正好说明了 cgroup namespace 的本质:
- 它改变的是路径展示方式
- 它不改变进程真实属于哪个 cgroup
换句话说:
/proc/<pid>/cgroup更适合看“真实资源归属”nsenter -C ... cat /proc/self/cgroup更适合看“在目标 cgroup namespace 视角下,路径是如何被虚拟化的”
这个 Pod 的最终判断
综合上面的输出,可以对这个 Pod 中的进程做一个简洁判断:
- 它运行在
cgroup v2环境中 - 它属于 Kubernetes
BurstableQoS Pod - 它真实位于
kubepods.slice/.../cri-containerd-...scope这个 cgroup 下 - 它使用了独立的
cgroup/ipc/mnt/net/pid/uts namespace - 它与宿主机共享
user/time namespace - 它的
cgroup namespace负责隐藏或重写 cgroup 路径视图,但不负责资源限制本身
namespace 和 cgroup 在容器里是怎么配合的
现在可以把前面的知识拼起来看。
一个典型容器运行时在启动容器时,通常会做这些事情:
- 创建或加入一组新的
namespace - 准备根文件系统和挂载点
- 配置 veth、路由、hostname 等隔离环境
- 创建或选择目标
cgroup - 把容器主进程加入该
cgroup - 启动容器内的
PID 1
于是容器里进程得到两层效果:
- 从
namespace获得“我像运行在独立机器里”的视图 - 从
cgroup获得“我只能使用被允许的资源”的约束
这也是为什么排查容器问题时,经常要分两条线:
- 视图问题:看
nsenter、ip netns、/proc/<pid>/ns - 资源问题:看
/proc/<pid>/cgroup、/sys/fs/cgroup、OOM、CPU throttle
几个常见误区
误区 1:namespace 就是容器
不是。
namespace 只是隔离机制的一部分。没有文件系统准备、cgroup、能力控制、seccomp、设备白名单等配套时,它离“容器”还差很远。1
误区 2:cgroup 只能做限制,不能做统计
也不对。
cgroup 不只提供限制,也提供资源记账和监控接口。例如 memory.current、cpu.stat 之类文件都能反映运行状态。3
误区 3:cgroup namespace 会创建新的资源层级
不会。
它主要是路径视图虚拟化,不会凭空生成一套新的控制树或新的配额体系。7
误区 4:容器里看到 PID 1,就等于宿主机 PID 1
这只是 pid namespace 内部视图。宿主机看到的真实 PID 往往完全不同。1
学习和排障时建议先看哪些文件
如果你想把这套机制真正摸熟,建议先固定看这几类入口:
/proc/<pid>/ns//proc/<pid>/cgroup/sys/fs/cgroup//proc/self/mountinfomount | grep cgroup
这几个入口基本可以把“这个进程在哪些 namespace 中、属于哪个 cgroup、当前 cgroup 树长什么样”串起来。
总结
可以把本文压缩成三句话:
namespace负责隔离视图,决定进程看见的世界cgroup负责资源治理,决定进程能用多少 CPU、内存等资源cgroup namespace主要虚拟化 cgroup 路径视图,不是新的资源限制机制
如果你从容器角度理解 Linux 内核,最值得先建立的不是命令记忆,而是这个分工模型。模型对了,后面再看 Docker、containerd、Kubernetes 的资源配置,就会清楚很多。
参考链接
-
Linux man-pages, namespaces(7), accessed on 2026-03-17. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
-
Linux man-pages, cgroups(7), accessed on 2026-03-17. ↩︎ ↩︎ ↩︎
-
Linux kernel documentation, Control Group v2, accessed on 2026-03-17. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
-
Linux man-pages, namespaces(7), namespace types table, accessed on 2026-03-17. ↩︎ ↩︎
-
Linux kernel documentation, Control Group v2,
cpu.max, accessed on 2026-03-17. ↩︎ -
Linux kernel documentation, Control Group v2,
memory.max, accessed on 2026-03-17. ↩︎ -
Linux man-pages, cgroup_namespaces(7), accessed on 2026-03-17. ↩︎ ↩︎ ↩︎ ↩︎