Linux Cgroup 详解:从 v1 到 v2 的演进与实践

cgroup(Control Groups) 是 Linux 内核的一项核心功能,用于精细化地管理和限制进程组使用的系统资源,如 CPU、内存、I/O 等。它是实现操作系统级虚拟化(如容器技术 Docker、Kubernetes)的关键基石。

随着 Linux 内核的发展,cgroup 经历了从 v1 到 v2 的重要演进。cgroup v2 旨在解决 v1 在设计和使用上的一些复杂性和不一致性。本文将深入探讨 cgroup 的基本概念,详细对比 v1 和 v2 的核心差异,并通过实例展示如何使用它们来控制资源。


1. 什么是 cgroup?

cgroup 主要提供以下能力:

  1. 资源限制(Resource Limiting):限制进程组可以使用的资源上限(例如,内存使用量、CPU 核心数)。
  2. 优先级控制(Prioritization):控制不同进程组对资源的访问优先级(例如,CPU 时间片分配、块 I/O 调度)。
  3. 资源审计(Accounting):统计进程组使用的资源量,用于监控和计费。
  4. 进程控制(Control):将进程分组管理,可以冻结(freeze)或恢复(thaw)组内所有进程。

主要应用场景:

  • 容器技术:Docker、Kubernetes 等使用 cgroup 隔离和限制容器资源。
  • 系统服务管理:Systemd 使用 cgroup 管理服务的资源。
  • 性能调优:限制特定应用的资源消耗,保障关键服务性能。
  • 虚拟化:配合 Namespace 等技术提供轻量级隔离环境。

2. 为何需要 cgroup v2?

cgroup v1 虽然功能强大,但在设计和使用上存在一些问题:

  • 多层级结构混乱:每种资源控制器(子系统)可以有自己独立的层级树,导致进程可能属于多个不同的层级,管理复杂且容易出错。
  • 控制器行为不一致:不同控制器之间的启用、禁用和管理方式存在差异。
  • 接口不统一:控制文件的命名和功能缺乏一致性。
  • 进程关联复杂:需要将进程 ID 写入每个相关控制器的 tasks 文件中。

cgroup v2 的设计目标就是解决这些问题,提供一个更统一、简洁、一致的资源控制框架。


3. cgroup v1 与 v2 的核心区别

3.1 层级结构(Hierarchy)

  • cgroup v1:允许多个独立的层级结构。一个进程可以同时属于多个不同控制器的 cgroup 组。例如,一个进程可能在 cpu 控制器的 /cpusetA 组,同时在 memory 控制器的 /memoryB 组。
    1
    2
    3
    4
    5
    # v1 示例挂载点
    /sys/fs/cgroup/cpu/
    /sys/fs/cgroup/memory/
    /sys/fs/cgroup/blkio/
    # ... 可能还有 cpuset, devices 等独立挂载
  • cgroup v2:强制使用统一的层级结构。所有可用的控制器都挂载在同一个层级树下。一个进程只能属于一个 cgroup 组。
    1
    2
    # v2 示例挂载点 (通常是 /sys/fs/cgroup)
    /sys/fs/cgroup/
    这种统一结构极大地简化了管理,避免了 v1 中复杂的层级关系和潜在冲突。

3.2 控制器管理(Controller Management)

  • cgroup v1:控制器(子系统)在挂载时确定。不同的挂载点可以挂载不同的控制器组合。
  • cgroup v2:控制器在层级内部进行管理。
    • 根 cgroup (/sys/fs/cgroup/) 的 cgroup.controllers 文件显示所有可用的控制器。
    • 父 cgroup 的 cgroup.subtree_control 文件用于启用或禁用哪些控制器可以传递给其子 cgroup 使用。例如,向 cgroup.subtree_control 写入 +cpu +memory 表示允许子 cgroup 使用 CPU 和内存控制器。

3.3 进程关联(Process Association)

  • cgroup v1:将进程 PID 写入特定控制器层级下的 tasks 文件,以将进程加入该 cgroup。如果需要同时受多个控制器管理,可能需要写入多个 tasks 文件。
  • cgroup v2:将进程 PID 写入目标 cgroup 目录下的 cgroup.procs 文件。由于是统一层级,只需写入一次即可。注意: 在 v2 中,只有叶子节点(没有子 cgroup 的节点)才能包含进程(除非是根 cgroup)。一个 cgroup 要么管理资源分配给子 cgroup(通过 cgroup.subtree_control),要么直接包含进程。

3.4 接口文件(Interface Files)

cgroup v2 努力统一和规范接口文件。

  • 通用文件

    • cgroup.procs: 组内进程列表(PID)。
    • cgroup.controllers: 当前 cgroup 可用的控制器。
    • cgroup.subtree_control: 为子 cgroup 启用/禁用控制器。
  • 资源控制文件对比:v2 的命名更规范,功能更聚合。

    功能 (示例) cgroup v1 参数 (示例) cgroup v2 参数 (示例) 说明
    CPU 带宽限制 cpu.cfs_quota_us, cpu.cfs_period_us cpu.max v2 使用 <quota> <period> 格式,更直观。
    CPU 权重 cpu.shares cpu.weight v2 范围 1-10000,默认 100。v1 范围 2-262144,默认 1024。
    内存上限 memory.limit_in_bytes memory.max v2 接口更简洁。
    内存低水位 memory.low_limit_in_bytes (部分内核支持) memory.low 内存回收保护。
    内存高水位 无直接对应 memory.high 内存压力调节,超过此值会尝试回收。
    当前内存使用 memory.usage_in_bytes memory.current
    I/O 带宽/IOPS 限制 blkio.throttle.read_bps_device, blkio.throttle.write_iops_device io.max v2 统一接口,格式如 maj:min rbps=N wips=M (读写带宽/IOPS)。

3.5 内部进程约束(Internal Process Constraint)

  • cgroup v1:允许非叶子节点(即同时拥有子 cgroup 的节点)包含进程。
  • cgroup v2:默认情况下,只有叶子节点才能包含进程。一个 cgroup 节点要么是资源分配的“分发者”(通过 cgroup.subtree_control 控制子节点可用资源),要么是资源的“使用者”(包含进程)。这使得资源分配模型更清晰。

4. 实践:限制 CPU 使用率

我们使用一个简单的 Go 程序来模拟 CPU 密集型任务,它会启动两个 goroutine 无限循环,尝试占满两个 CPU 核心。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// busyloop.go
package main

import "runtime"

func main() {
runtime.GOMAXPROCS(2) // 确保我们尝试使用两个核心

go func() {
for {
}
}()

go func() {
for {
}
}()

// 主 goroutine 也忙碌,确保至少有两个线程在运行
for {
}
}

编译并运行:

1
2
3
4
go build -o busyloop
./busyloop & # 后台运行
BUSYLOOP_PID=$! # 获取进程 PID
echo "Busyloop PID: $BUSYLOOP_PID"

此时运行 top 命令,会看到 busyloop 进程的 CPU 使用率接近 200%。

1
2
3
4
top -p $BUSYLOOP_PID
# 输出类似:
# PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
# 12345 user 20 0 702356 1024 640 R 199.9 0.0 0:18.75 busyloop

现在,我们将使用 cgroup 将其 CPU 使用率限制在 10% (相当于 0.1 个 CPU 核心)。

4.1 使用 cgroup v1 限制 CPU

假设 cgroup v1 的 CPU 子系统挂载在 /sys/fs/cgroup/cpu

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1. 创建 cgroup 目录
sudo mkdir /sys/fs/cgroup/cpu/cpulimit_v1_demo

# 2. 设置 CPU 带宽限制 (10% = 10000us / 100000us)
# cfs_period_us: CPU 带宽统计周期,单位微秒 (通常是 100ms = 100000us)
# cfs_quota_us: 在一个周期内,该 cgroup 可使用的 CPU 时间,单位微秒
sudo sh -c 'echo 100000 > /sys/fs/cgroup/cpu/cpulimit_v1_demo/cpu.cfs_period_us'
sudo sh -c 'echo 10000 > /sys/fs/cgroup/cpu/cpulimit_v1_demo/cpu.cfs_quota_us'

# 3. 将进程移动到 cgroup
sudo sh -c "echo $BUSYLOOP_PID > /sys/fs/cgroup/cpu/cpulimit_v1_demo/tasks"

# 4. 观察 top 输出,CPU 使用率应下降到 10% 左右
top -p $BUSYLOOP_PID

4.2 使用 cgroup v2 限制 CPU

假设 cgroup v2 挂载在 /sys/fs/cgroup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 1. 创建 cgroup 目录
sudo mkdir /sys/fs/cgroup/cpulimit_v2_demo

# 2. (可选) 确保父 cgroup 允许传递 cpu 控制器给子 cgroup
# 如果 /sys/fs/cgroup/cgroup.subtree_control 中没有 +cpu,则需要添加
# sudo sh -c 'echo "+cpu" > /sys/fs/cgroup/cgroup.subtree_control'
# 注意:这会影响 /sys/fs/cgroup 下的所有直接子 cgroup

# 3. 设置 CPU 带宽限制 (10% = 10000us / 100000us)
# cpu.max 格式: "<quota> <period>"
sudo sh -c 'echo "10000 100000" > /sys/fs/cgroup/cpulimit_v2_demo/cpu.max'

# 4. 将进程移动到 cgroup
# 注意:需要将进程的所有线程都移动过去,写入 cgroup.procs 会自动处理
sudo sh -c "echo $BUSYLOOP_PID > /sys/fs/cgroup/cpulimit_v2_demo/cgroup.procs"

# 5. 观察 top 输出,CPU 使用率应下降到 10% 左右
top -p $BUSYLOOP_PID

清理(示例结束后):

1
2
3
4
5
6
7
8
# 停止进程
kill $BUSYLOOP_PID

# 删除 cgroup 目录 (确保里面没有进程)
# 对于 v1:
# sudo rmdir /sys/fs/cgroup/cpu/cpulimit_v1_demo
# 对于 v2:
# sudo rmdir /sys/fs/cgroup/cpulimit_v2_demo

5. 如何检查系统使用的是 cgroup v1 还是 v2?

检查 cgroup 文件系统的挂载类型是区分 v1 和 v2 的最常用方法:

1
mount | grep cgroup
  • cgroup v2: 如果输出中看到类似 cgroup2 on /sys/fs/cgroup type cgroup2 的行,表示系统主要使用 cgroup v2(通常是统一挂载)。
  • cgroup v1: 如果看到多行 cgroup 挂载,每行对应一个或多个控制器(子系统),例如 cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory),则表示系统主要使用 cgroup v1
  • 混合模式: 某些系统可能同时挂载了 v1 和 v2(例如,systemd 可能使用 v2,而 Docker 仍配置为使用 v1),需要仔细查看挂载点和类型。

另一个方法是检查 /sys/fs/cgroup 目录结构:

  • 如果该目录下直接包含 cgroup.controllers, cgroup.procs 等文件,并且有子目录用于组织 cgroup,则很可能是 cgroup v2
  • 如果该目录下包含 cpu, memory, blkio 等以控制器命名的子目录,则很可能是 cgroup v1

6. 可视化对比

下图简要展示了 v1 和 v2 在层级结构上的差异:

(图示:左侧为 cgroup v1 的多层级结构,右侧为 cgroup v2 的统一层级结构)


7. 总结

Cgroup 是 Linux 资源管理的核心机制。从 v1 到 v2 的演进,体现了 Linux 内核在追求更统一、简洁、高效的资源控制模型方面的努力。Cgroup v2 以其统一的层级结构和更一致的接口,简化了资源管理配置,是现代容器化和系统管理的发展趋势。理解 cgroup v1 和 v2 的差异对于系统管理员和开发者进行性能调优、资源隔离和容器管理至关重要。


Linux Cgroup 详解:从 v1 到 v2 的演进与实践
https://mfzzf.github.io/2025/03/13/Linux中的Cgroup/
作者
Mzzf
发布于
2025年3月13日
许可协议