人人都会AI编程

26.2 Cgroups 控制组:资源限制的核心机制

更新时间:2026-07-12

在 Linux 系统中,如果某个进程无限制地消耗 CPU 或内存,很可能导致整台服务器响应变慢甚至完全不可用。Cgroups(Control Groups,控制组)正是为解决这一问题而诞生的内核机制。它允许系统管理员将一系列进程组织到一个个“控制组”中,然后以组为单位,精确限制、记录和隔离这些进程所能使用的物理资源(如 CPU 时间、内存大小、磁盘 I/O、网络带宽等)。Cgroups 是现代容器技术(Docker、Kubernetes、LXC 等)得以安全地在一台物理机上运行成百上千个隔离实例的底层基石。

1. Cgroups 的核心概念

理解 Cgroups 需要掌握三个关键要素:

  • 子系统(Subsystem,也称资源控制器)

每个子系统对应一类系统资源,并负责对该资源实施具体的限制或统计。常见的子系统包括:

  • cpu:限制进程组的 CPU 使用份额(通过 CFS 调度器);
  • cpuset:将进程组绑定到特定的 CPU 核心或 NUMA 节点;
  • memory:限制内存使用上限,并触发 OOM(Out Of Memory)行为;
  • blkio:限制块设备(磁盘)的 I/O 速率;
  • devices:控制进程组可以访问哪些设备;
  • freezer:挂起或恢复进程组中的所有进程;
  • pids:限制进程组内可创建的最大进程数。
  • 层级(Hierarchy)

Cgroups 以树状结构组织控制组。每个层级可以附加一个或多个子系统。一个子系统只能附加到某一个层级上(即一个子系统不能同时在两个独立的层级中生效)。系统启动时,通常会挂载一个默认的层级,将大部分子系统联合挂载到 /sys/fs/cgroup 目录下(这在 cgroup v2 中被统一为单一层级,v1 则允许分离挂载)。每个挂载点对应一个层级,其下属目录代表各个具体的控制组。

  • 控制组(cgroup)

这是实际施加限制的实体。每个 cgroup 可以包含一个或多个进程,同时拥有该子系统的资源限制参数。控制组可以嵌套,子 cgroup 会继承父 cgroup 的属性,并接受二次限制。例如,你可以创建 /sys/fs/cgroup/memory/myapp 目录,向其中添加进程,并设定该组最大内存使用为 512MB。

2. Cgroups 的两个版本

目前 Linux 内核存在两个 cgroup 版本:cgroup v1cgroup v2。cgroup v2 对设计做了大幅简化,统一了层级,解决了 v1 中同一子系统可能被滥用的混乱问题,并且引入了更一致的接口。现代发行版(如 Ubuntu 22.04+ 或设置 systemd.unified_cgroup_hierarchy=1 的系统)默认使用 v2。实际查看当前系统使用的版本,可以检查 /sys/fs/cgroup 的结构:

  • v1:每个子系统都有独立的挂载点,如 /sys/fs/cgroup/cpu/sys/fs/cgroup/memory
  • v2:所有控制器都挂载在同一个层级下,即 /sys/fs/cgroup,其根目录下包含 cgroup.controllers 文件列出可用控制器。

除非有遗留容器或特殊需求,通常建议使用 v2,因为它的接口更加规范,且内核对 v2 的支持越来越全面。

3. 如何使用 Cgroups:以内存限制为例

多数情况下用户并不需要直接操作 Cgroups 文件系统,因为 systemd、Docker 等工具已经代为管理。但直接操作能够帮助你理解原理,也是排除资源问题时的有效手段。下面以 cgroup v2 为例演示手工创建一个控制组,限制进程的内存使用。

首先确认 cgroup v2 是否已挂载:

mount | grep cgroup2
# 通常输出:cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,seclabel)

手工创建一个新的控制组(以 root 或 sudo 操作):

mkdir /sys/fs/cgroup/myapp

这个目录创建后,内核会自动生成一系列控制文件:

ls /sys/fs/cgroup/myapp
# 会看到:cgroup.controllers、cgroup.procs、memory.max、cpu.weight 等文件

将当前 shell 进程或某个已知进程 PID 移入该控制组,只需将 PID 写入 cgroup.procs

echo $$ > /sys/fs/cgroup/myapp/cgroup.procs   # 将当前 shell 加入 myapp 组

现在就可以施加限制了。例如,限制该组最多使用 128MB 内存:

echo "134217728" > /sys/fs/cgroup/myapp/memory.max   # 128MB 以字节为单位

或者使用更直观的写法(如果内核支持):

echo "128M" > /sys/fs/cgroup/myapp/memory.max

随后,在该 shell 中运行的任何命令及其子进程都会受到这个内存上限的约束。如果内存使用超过 128MB,内核将触发 OOM 杀死组内进程(或者根据 memory.oom.group 设置决定行为)。你可以用 stress 工具测试:

stress --vm 1 --vm-bytes 200M   # 在受限制的 shell 中运行,会被迅速杀死

清理也很简单,删除目录即可:

# 先将进程移回根控制组(或直接杀死组内进程)
echo 1 > /sys/fs/cgroup/cgroup.procs   # 将自己的进程移出(PID 1 通常为 systemd 根)
rmdir /sys/fs/cgroup/myapp

4. Cgroups 与 systemd、容器的协作

在现代 Linux 系统中,直接操作 cgroup 文件系统并不多见,因为 systemd 已经接管了 cgroup 层级的管理。每一个 systemd 服务(unit)都会自动获得一个对应的 cgroup,例如 system.slice/sshd.service。你可以通过 systemctl 指令为服务设置资源限制,systemd 会将其翻译为 cgroup 参数。比如:

systemctl set-property nginx.service MemoryMax=512M CPUQuota=50%

这样,nginx 服务及其所有子进程的内存上限就是 512MB,CPU 使用量限制在单个核心的 50% 以内。查看服务资源使用情况:

systemctl status nginx.service   # 会显示 cgroup 树状图
systemd-cgtop                   # 动态显示各 cgroup 的资源占用

而容器运行时(Docker、containerd、Podman 等)更是深度依赖 cgroups。当你执行 docker run -m 512m --cpus=1.5 myimage 时,Docker 会在后台创建相应的 cgroup,并设置内存限制和 CPU 份额,从而保证容器不会侵占主机资源。Kubernetes 中的 Pod 资源 requests/limits 最终也是通过 cgroups 落实的。

5. 实用价值总结

  • 防止资源挤占:在多租户环境或混部服务器上,cgroups 可以确保关键服务始终拥有最低限度的 CPU 和内存,同时限制“邻居”的故障爆炸半径。
  • 性能调优:通过 cpuset 将数据库进程绑定到特定核心,避免缓存失效,提升性能;通过 blkio 限制备份任务的磁盘带宽,保障在线服务响应速度。
  • 精确记账:cgroups 提供的资源统计信息(如 memory.statcpu.stat)可以帮助你分析应用的实际资源消耗模式,为容量规划提供数据。
  • 安全隔离:限制进程数上限(pids)可缓解 fork 炸弹攻击;限制设备访问(devices)可防止容器访问宿主机的危险设备文件。

cgroups 将原本分散在各处的资源限制、统计和隔离整合成一个统一的框架,赋予了 Linux 细粒度的资源控制能力。它是构建健壮、高效、且可预测的现代计算环境不可或缺的基础设施,值得每一位系统管理者和开发人员理解其运作方式。