ᕕ( ᐛ )ᕗ Jimyag's Blog

Linux cgroup 和 namespace:从隔离到资源限制

· 947 字 · 约 5 分钟

很多人第一次接触容器时,会把 namespacecgroup 混在一起。

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

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

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

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

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

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

先建立一个整体模型

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

  1. namespace 提供“视图隔离”
  2. 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 创建了新的 pidnet namespace。此时进程在自己的命名空间里看不到宿主机的完整进程树,也有了自己的网络栈。

但如果你没有给它绑定 cgroup

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

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

只有 cgroup,也不够

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

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

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

所以容器运行时通常会同时创建多个 namespace,再把容器内进程加入对应的 cgroup12

从最小实验理解 namespace

先看三个最容易感知的隔离:utspidmount

实验 1:隔离 hostname

1
2
3
sudo unshare --uts --fork /bin/bash
hostname demo-ns
hostname

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

这里发生的事情很简单:uts namespace 隔离了主机名和 NIS 域名。也就是说,进程可以拥有自己的机器名视图。4

实验 2:隔离 PID 视图

1
2
3
sudo unshare --pid --fork --mount-proc /bin/bash
ps -ef
echo $$

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

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

实验 3:隔离挂载点

1
2
3
sudo unshare --mount /bin/bash
mount -t tmpfs tmpfs /mnt
mount | grep /mnt

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

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

用 nsenter 进入别人的 namespace

有了 unshare,通常还要配套理解 nsenter

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

1
sudo nsenter --target <pid> --mount --uts --ipc --net --pid

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

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

从最小实验理解 cgroup

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

cgroup v2 中,通常会看到统一挂载点:

1
2
mount | grep cgroup
ls /sys/fs/cgroup

你会看到很多控制文件,例如:

  • cpu.max
  • memory.max
  • memory.current
  • cgroup.procs
  • cgroup.controllers
  • cgroup.subtree_control

这些文件不是普通配置文件,而是内核暴露出来的控制接口。3

实验 4:用 systemd-run 限制内存

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

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

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

1
cat /proc/self/cgroup

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

1
2
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 时间56

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

1
2
3
4
5
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 中,再由控制器施加资源策略。3

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

1. 统一层级

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

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

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

很多人看到 /sys/fs/cgroup 里有 cpu.maxmemory.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,宿主机看到:

1
cat /proc/47169/cgroup

输出如下:

1
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 对应具体容器进程3

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

再看它加入了哪些 Linux namespace

查看该进程的 namespace:

1
ls -l /proc/47169/ns

输出如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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

1
ls -l /proc/1/ns

输出如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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 看到的是相对路径

在宿主机执行:

1
nsenter -t 47169 -C cat /proc/self/cgroup

输出是:

1
0::/../../../../system.slice/ssh.service

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

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

  • 进入目标进程的 cgroup namespace

但它没有做另一件事:

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

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

1
/../../../../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 获得“我只能使用被允许的资源”的约束

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

  • 视图问题:看 nsenterip netns/proc/<pid>/ns
  • 资源问题:看 /proc/<pid>/cgroup/sys/fs/cgroup、OOM、CPU throttle

几个常见误区

误区 1:namespace 就是容器

不是。

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

误区 2:cgroup 只能做限制,不能做统计

也不对。

cgroup 不只提供限制,也提供资源记账和监控接口。例如 memory.currentcpu.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/mountinfo
  • mount | grep cgroup

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

总结

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

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

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

参考链接


  1. Linux man-pages, namespaces(7), accessed on 2026-03-17. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  2. Linux man-pages, cgroups(7), accessed on 2026-03-17. ↩︎ ↩︎ ↩︎

  3. Linux kernel documentation, Control Group v2, accessed on 2026-03-17. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  4. Linux man-pages, namespaces(7), namespace types table, accessed on 2026-03-17. ↩︎ ↩︎

  5. Linux kernel documentation, Control Group v2, cpu.max, accessed on 2026-03-17. ↩︎

  6. Linux kernel documentation, Control Group v2, memory.max, accessed on 2026-03-17. ↩︎

  7. Linux man-pages, cgroup_namespaces(7), accessed on 2026-03-17. ↩︎ ↩︎ ↩︎ ↩︎

#Linux #Container #Cgroup #Namespace