在 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 v1 和 cgroup 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.stat、cpu.stat)可以帮助你分析应用的实际资源消耗模式,为容量规划提供数据。 - 安全隔离:限制进程数上限(
pids)可缓解 fork 炸弹攻击;限制设备访问(devices)可防止容器访问宿主机的危险设备文件。
cgroups 将原本分散在各处的资源限制、统计和隔离整合成一个统一的框架,赋予了 Linux 细粒度的资源控制能力。它是构建健壮、高效、且可预测的现代计算环境不可或缺的基础设施,值得每一位系统管理者和开发人员理解其运作方式。