Linux 基础-CGroup

Linux 基础系列之 CGroup

CGroup 介绍

Linux Cgroups(Control Groups)提供了对一组进程及将来子进程的资源限制、控制和统计的能力,这些资源包括 CPU、内存、存储、网络等。通过 Cgroups,可以方便地限制某个进程的资源占用,并且可以实时地监控进程的监控和统计信息。

在容器场景中,通过 CGroup 就可以非常方便地分配某个容器资源的上限,避免一个容器抢占过多的资源,导致其他容器或宿主机因资源不足而崩溃。

由于 CGroup 有 v1 和 v2 的区别,但我个人觉得差异不算很大,本文主要以 v1 进行介绍和研究

Cgroups 的构成

cgroup 是对进程分组管理的一种机制,一个 cgroup 包含一组进程,并可以在这个 cgroup 上增加 Linux subsystem 的各种参数配置,将一组进程和一组 subsystem 的系统参数关联起来。

Cgroups 整体由 3 个组件构成:

  1. cgroup:实际管理和控制进程的实体
  2. subsystem:每个子系统专门用于控制一种类型的系统资源
  3. hierarchy: 描述 cgroup 的组织形式

subsystem

cgroup 其实就是一组控制组,由 subsystem(子系统),或称控制器构成。

subsystem 又由以下几个资源控制模块构成:

  1. blkio:设置对块设备(比如硬盘)输入输出的访问控制
  2. cpu:控制 CPU 的分配
  3. cpuacct:可以统计 cgroup 中进程的 CPU 占用
  4. cpuset:在多核机器上设置 cgroup 中进程可以使用的 CPU 和内存(此处内存仅使用于 NUMA 架构)
  5. devices:控制 cgroup 中进程对设备的访问
  6. freezer 用于挂起(suspend)和恢复(resume)cgroup 中的进程
  7. memory:用于控制 cgroup 中进程的内存使用
  8. net_cls:用于将 cgroup 中进程产生的网络包分类,以便 Linux 的 tc(traffic controller)可以根据分类区分出来自某个 cgroup 的包并做限流或监控
  9. net_prio:设置 cgroup 中进程产生的网络流量的优先级
  10. ...

若 cgroup 绑定了一个 subsystem,subsystem 就会对这个 cgroup 中的进程做相应的限制和控制。这些 subsystem 是逐步合并到内核中的,通过 lssubsysapt-get install cgroup-bin)可以看到当前 Kernel 支持的 subsystem。

hierarchy

hierarchy 是一种树状的组织结构,Kernel 为了使对 Cgroups 的配置更直观,是通过一个虚拟的树状文件系统配置 Cgroups 的,通过层级的目录虚拟出 cgroup 树。

通过 hierarchy(层级)可以描述 cgroup 的组织方式。它的功能是把一组 cgroup 串成一个树状的结构,一个这样的树便是一个 hierarchy。通过这种树状结构,Cgroups 也有继承的机制,比如系统对一组定时的任务进程通过 cgroup1 限制了 CPU 的使用率,然后其中有一个定时 dump 日志的进程还需要限制磁盘 IO,为了避免限制了磁盘 IO 之后影响到其他进程,就可以创建 cgroup2,使其继承于 cgroup1 并限制磁盘的 IO,这样 cgroup2 便继承了 cgroup1 中对 CPU 使用率的限制,并且增加了磁盘 IO 的限制而不影响到 cgroup1 中的其他进程。

1
2
3
4
5
6
Root cgroup
├── Group A
│ ├── Group A1
│ └── Group A2

├── Group B

Hierarchy 需要通过 mount 命令来创建,而在这个目录下再创建出的目录就是一个个 cgroup。

那么如何判断一个目录是 Hierarchy 还是 Cgroup 呢?就可以通过 mount 下手:mount | grep 'type cgroup',如果目录在这里面就说明是进行了 mount 的,也就是一个 Hierarchy;反之在这个目录下的便是 cgroup 目录。其实 Hierarchy 目录下也是有 subsystem 的,也可以起到 cgroup 的作用。

三者协作模式

系统在创建了新的 hierarchy 之后 ,系统中所有的进程都会加入这个 hierarchy 的 cgroup 根节点,这个 cgroup 根节点是 hierarchy 默认创建的。

细节点:

  1. 一个 subsystem 只能附加到一个 hierarchy 上面,但一个 hierarchy 可以附加多个 subsystem
    1. 在 Cgroups v1 中,一个子系统只能附加到一个 hierarchy 上,不能重复附加
    2. 在 Cgroups v2(从 Linux 内核 4.5 开始引入的,在 Linux 内核 5.x 以后逐渐成熟)中,所有子系统强制使用单一的 hierarchy,因此不存在重复挂载的问题
  2. 一个进程可以作为多个 cgroup 的成员,但是这些 cgroup 必须在不同的 hierarchy 中
  3. 一个进程 fork 出子进程时,子进程是和父进程在同一个 cgroup 中的,也可以根据需要将其移动到其他 cgroup 中
  4. 有趣的是,一旦创建完一个 cgroup 目录,该目录就只能新建文件夹、删除空白文件夹、修改文件内容,无法删除、新建文件;同时,对于 Hierarchy 目录来说,需要 umount 后才能 rm 删除;对于子目录(即具体的 cgroup)来说,移除的时候无法 umount,必须要用 rmdir 来删除,如果用直接 rm -rf 会报错,即使 tasks 之类的的确是空的:rm: cannot remove 'cgroup-demo/cgroup.procs': Operation not permitted,具体原理有待研究(目前推测原因是 rm -rf 会尝试删除里面的文件,而 rmdir 则是直接删除目录,但是理论上 rmdir 会判断目录是否有文件的,而 hierarchy 目录的确是会有一些配置项的,估计是内核做了什么特殊处理吧)

CGroup 实验

  1. 创建并挂载一个 hierarchy(cgroup 树)

    这些文件就是这个 hierarchy 中 cgroup 根节点的配置项:
    1. cgroup.clone_children:cpuset 的 subsystem 会读取这个配置文件,如果这个值是 1(默认是 0),子 cgroup 才会继承父 cgroup 的 cpuset 的配置
    2. cgroup.procs:是树中当前节点 cgroup 中的进程组 ID,现在的位置是在根节点,这个文件中会有现在系统中所有进程组的 ID
    3. notify_on_releaserelease_agent 会一起使用
      1. notify_on_release:标识当这个 cgroup 最后一个进程退出的时候是否执行了 release_agent
      2. release_agent:则是一个路径,通常用作进程退出之后自动清理掉不再使用的 cgroup
    4. tasks:标识该 cgroup 下面的进程 ID,如果把一个进程 ID 写到 tasks 文件中,便会将相应的进程加入到这个 cgroup 中
  2. cgroup-demo 中加入新的 CGroup

    可以看到,在一个 cgroup 的目录下创建文件夹时,Kernel 会把文件夹标记为这个 cgroup 的子 cgroup,它们会继承父 cgroup 的属性
  3. 在 cgroup 中添加和移动进程

    一个进程在一个 Cgroups 的 hierarchy 中,只能在一个 cgroup 节点上存在,系统的所有进程都会默认在根节点上存在,可以将进程移动到其他 cgroup 节点,只需要将进程 ID 写到移动到的 cgroup 节点的 tasks 文件中即可
  4. 通过 subsystem 限制 cgroup 中进程的资源
    以 memory 限制器为例子,在上面创建 hierarchy 的时候,这个 hierarchy 并没有关联到 memory 这个 subsystem,因为这个已经被 /sys/fs/cgroup/memory(系统默认创建出来的 hierarchy)占用了,无法挂给 cgroup-demo,为了测试,我们可以在 /sys/fs/cgroup/memory 下创建一个新的 CGroup,把当前 bash 的 pid 移动到它下面去。然后我们在 memory 中进行内存限制:

    可以看到 got signal 9(即 SIGKILL,这里实际上是触发了 OOM),通过 dmesg | grep -i "killed process" 可以证实的确是出现了 OOM 被杀的。测试过程中发现,限制在 100m 时,98m 的压测可以正常运行,99m 就会有问题。推测这里是除了进程的实际内存分配外,还需要为内核态开销、Page Cache 和动态库等分配内存

mount -t cgroup -o none,name=cgroup-test cgroup-test ./cgroup-test 命令解释:

  • -t:用于指定挂载的文件系统类型。在这条命令中,文件系统类型是 cgroup,表示要挂载的是 cgroup 的虚拟文件系统。
  • -o:用于传递挂载时的选项。这部分选项 none,name=cgroup-test 的具体含义是:
    • none:用于 cgroup 的选项,表示不启用默认的资源子系统(如 CPU、内存等)。默认情况下,每个 cgroup 挂载点会关联到一个或多个资源子系统,none 表示禁用这些默认的功能。
    • name=cgroup-test:表示创建一个名为 cgroup-test 的子系统,可以通过这个子系统组织和管理进程。
  • cgroup-test:这是挂载点的源(通常是虚拟源),用于标识这个挂载点。这在这里可以是一个任意名称(比如 cgroup-test),但它主要作为一个符号参考,并不代表实际的磁盘或文件
  • ./cgroup-test:挂载的目标路径,表示将 cgroup 文件系统挂载到当前目录下的 ./cgroup-test 文件夹中
  • 挂载完成后,如果不再需要,可以使用以下命令卸载:umount ./cgroup-test

CGroup v1 与 v2 的差异

  1. Hierarchy 差异
    1. v1 的每个 subsystem 可以挂载到一个单独的 Hierarchy,比如可以在 /sys/fs/cgroup/memory 挂载 memory 子系统,在 /sys/fs/cgroup/cpu 挂载 cpu 子系统
    2. v2 是单一 Hierarchy,所有 subsystem 共享一个统一的层次结构,默认挂载点为 /sys/fs/cgroup/,目录下有 memory.maxcpu.max,就没有 /sys/fs/cgroup/memory 这种 Hierarchy 了。文件命名更统一,如 memory.max 替代 memory.limit_in_bytes
  2. 进程分配差异
    1. v1 单一进程可以同时属于多个 Hierarchy 中的不同 Cgroup,因为每个 subsystem 独立对应 Hierarchy,因此进程可以分散到不同的 Hierarchy 中的不同 Cgroup
    2. v2 每个进程只能被分配到统一的 Hierarchy 中的一个 Cgroup 节点
  3. subsystem 管理差异
    1. v1 由于 subsystem 可以单独挂载到不同的 Hierarchy,因此每个 subsystem 可以独立启用和管理
    2. v2 子系统以统一的方式管理,通过 cgroup.controllerscgroup.subtree_control 文件来启用或禁用子系统,比如 echo "+memory" > /sys/fs/cgroup/cgroup.subtree_control

现在大部分服务器用的应该还都是 cgroup v1 版本。Ubuntu 20.04/22.04、Debian 10+、CentOS Stream 8+ 默认开始切换到 cgroup v2,Docker 20.10+ 和 Kubernetes 1.22+ 已经逐步支持 Cgroup v2,但像 RHEL 7、CentOS 7 以及老版本的 Ubuntu(例如 18.04)仍使用 cgroup v1。

可以通过 mount | grep cgroup 来检查:

  • 如果输出是 cgroup v1 格式(多层次挂载点,每个子系统有独立的挂载点),如 cgroup on /sys/fs/cgroup type cgroup (rw,relatime,perf_event,blkio,cpuacct,...) 则说明系统使用的是 cgroup v1
  • 如果输出是 cgroup v2 格式(单一层次挂载点):cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime) 则说明系统使用的是 cgroup v2

对于 docker 来说,可以用 docker info | grep -i cgroup 来进行判断。

总结

之前在 ChatGPT 指导下的 TOA 伪造之旅 中用到了 cgroup 对进程进行 toa 篡改的测试,最近总算完整地看了一遍它的作用,用处还是很大的。


进度 2/n
冲鸭


Linux 基础-CGroup
https://www.tr0y.wang/2025/03/07/linux-cgroup/
作者
Tr0y
发布于
2025年3月7日
更新于
2025年3月11日
许可协议