控制组 v2

日期:

2015年10月

作者:

Tejun Heo <tj@kernel.org>

这是关于 cgroup v2 的设计、接口和约定的权威文档。它描述了 cgroup 的所有用户空间可见方面,包括核心行为和特定控制器的行为。未来的所有更改都必须反映在本文档中。v1 的文档可在 Documentation/admin-guide/cgroup-v1/index.rst 中找到。

简介

术语

“cgroup” 代表 “control group”(控制组),且永远不首字母大写。单数形式用于指代整个特性,也用作修饰语(如 “cgroup 控制器”)。当明确指代多个独立的控制组时,使用复数形式 “cgroups”。

什么是 cgroup?

cgroup 是一种以分层方式组织进程并沿着层级结构以受控且可配置的方式分配系统资源的机制。

cgroup 主要由两部分组成——核心和控制器。cgroup 核心主要负责分层组织进程。cgroup 控制器通常负责沿着层级结构分配特定类型的系统资源,尽管也有一些服务于资源分配以外目的的辅助控制器。

cgroups 构成一个树状结构,系统中的每个进程都属于且仅属于一个 cgroup。一个进程的所有线程都属于同一个 cgroup。在创建时,所有进程都会被放入其父进程当时所属的 cgroup 中。进程可以被迁移到另一个 cgroup。迁移进程不会影响已经存在的子代进程。

在遵循某些结构限制的前提下,可以在 cgroup 上选择性地启用或禁用控制器。所有控制器的行为都是层级化的——如果某个控制器在某个 cgroup 上被启用,它将影响属于该 cgroup 包含的子层级结构的所有进程。当在嵌套的 cgroup 上启用控制器时,它总是会进一步限制资源分配。在层级结构中靠近根部设置的限制不能被更深层的设置所覆盖。

基本操作

挂载

与 v1 不同,cgroup v2 仅有单一的层级结构。可以使用以下挂载命令挂载 cgroup v2 层级结构

# mount -t cgroup2 none $MOUNT_POINT

cgroup2 文件系统的幻数为 0x63677270(“cgrp”)。所有支持 v2 且未绑定到 v1 层级的控制器都会自动绑定到 v2 层级并在根目录下显示。未在 v2 层级中活跃使用的控制器可以绑定到其他层级。这允许将 v2 层级与传统的 v1 多个层级以完全向下兼容的方式混合使用。

只有当控制器在当前层级中不再被引用后,才能跨层级移动。因为每个 cgroup 的控制器状态是异步销毁的,且控制器可能存有未清除的引用,所以在上一个层级最终卸载(umount)后,控制器可能不会立即在 v2 层级中显示。同样,必须将控制器完全禁用才能将其移出统一层级结构,而被禁用的控制器可能需要一些时间才能在其他层级中可用;此外,由于控制器之间的依赖关系,其他控制器可能也需要被禁用。

尽管动态地在 v2 和其他层级之间移动控制器对开发和手动配置很有用,但强烈不建议在生产环境中使用。建议在系统启动后、开始使用控制器之前,确定好层级结构和控制器的关联关系。

在向 v2 过渡期间,系统管理软件可能仍会自挂载 v1 cgroup 文件系统,从而在系统启动时、手动干预之前劫持所有控制器。为了使测试和实验更容易,内核参数 cgroup_no_v1= 允许在 v1 中禁用控制器,并使其始终在 v2 中可用。

cgroup v2 目前支持以下挂载选项。

nsdelegate

将 cgroup 命名空间视为委派边界。此选项是系统全局的,只能在挂载时设置,或在初始(init)命名空间中通过重新挂载进行修改。在非初始命名空间挂载时,该挂载选项会被忽略。详情请参考“委派”部分。

favordynmods

减少动态 cgroup 修改(例如任务迁移和控制器的启用/禁用)的延迟,代价是使热路径操作(例如 fork 和 exit)开销更大。创建 cgroup、启用控制器然后使用 CLONE_INTO_CGROUP 填充它的静态使用模式不受此选项影响。

memory_localevents

仅用当前 cgroup 的数据填充 memory.events,而不包含任何子树的数据。这是旧版行为,没有此选项时的默认行为是包含子树的计数。此选项是系统全局的,只能在挂载时设置,或在初始命名空间中通过重新挂载进行修改。在非初始命名空间挂载时,该挂载选项会被忽略。

memory_recursiveprot

递归地将 memory.min 和 memory.low 保护应用于整个子树,而不需要显式地向下传播到叶子 cgroup。这允许对整个子树进行相互保护,同时在这些子树内部保留自由竞争。这本应是默认行为,但作为一个挂载选项提供,以避免对依赖原始语义的设置产生退化(例如在较高的树级别指定了过高的“旁路”保护值)。

memory_hugetlb_accounting

将 HugeTLB 内存使用量计入内存控制器中该 cgroup 的总体内存使用量(用于统计报告和内存保护)。这是一种新行为,可能会使现有的设置发生退化,因此必须通过此挂载选项显式启用。

需要注意的几个注意事项

  • 内存控制器中不涉及 HugeTLB 池管理。预分配的池不属于任何人。具体来说,当向池中分配一个新的 HugeTLB folio 时,从内存控制器的角度来看,它并不会被计入账目。只有在它被实际使用时(例如在缺页异常时),才会对 cgroup 进行计费。在配置硬限制时,宿主机内存超卖管理必须考虑到这一点。通常,HugeTLB 池管理应该通过其他机制(例如 HugeTLB 控制器)来完成。

  • 未能向内存控制器计费 HugeTLB folio 将导致 SIGBUS 信号。即使 HugeTLB 池中仍有可用页面,也可能会发生这种情况(但 cgroup 限制已被触发且回收尝试失败)。

  • 将 HugeTLB 内存计入内存控制器会影响内存保护和回收动态。任何用户空间微调(例如对 low、min 限制的调整)都需要考虑到这一点。

  • 在未选择此选项时使用的 HugeTLB 页面不会被内存控制器跟踪(即使稍后重新挂载了 cgroup v2 也是如此)。

pids_localevents

该选项恢复了类似 v1 的 pids.events:max 行为,即只统计本地(cgroup 本身内部)的 fork 失败。没有此选项时,pids.events.max 表示在 cgroup 子树中的任何 pids.max 强制执行。

组织进程和线程

进程

最初,只存在所有进程都属于的根 cgroup。可以通过创建子目录来创建子 cgroup

# mkdir $CGROUP_NAME

给定的 cgroup 可以有多个子 cgroup,从而构成树状结构。每个 cgroup 都有一个可读写的接口文件 “cgroup.procs”。读取时,它会逐行列表显示属于该 cgroup 的所有进程的 PID。PID 是无序的,如果进程被移到另一个 cgroup 然后又移回来,或者在读取时 PID 被循环重用,则同一个 PID 可能会出现多次。

可以通过将进程的 PID 写入目标 cgroup 的 “cgroup.procs” 文件来将进程迁移到该 cgroup。单次 write(2) 调用只能迁移一个进程。如果一个进程由多个线程组成,写入任何一个线程的 PID 都会迁移该进程的所有线程。

当进程派生(fork)子进程时,新进程诞生在执行操作时派生进程所属的 cgroup 中。退出后,进程在被回收(reaped)之前一直与退出时所属的 cgroup 关联;然而,僵尸进程不会出现在 “cgroup.procs” 中,因此无法被移至另一个 cgroup。

没有任何子目录或活动进程的 cgroup 可以通过删除其目录来销毁。请注意,不含任何子目录且仅与僵尸进程关联的 cgroup 被视为空,可以被删除

# rmdir $CGROUP_NAME

“/proc/$PID/cgroup” 列出了进程的 cgroup 成员关系。如果系统中正在使用传统 cgroup,该文件可能包含多行,每个层级各占一行。cgroup v2 的条目格式始终为 “0::$PATH”

# cat /proc/842/cgroup
...
0::/test-cgroup/test-cgroup-nested

如果进程变成僵尸进程,且随后删除了与其关联的 cgroup,则路径后会追加 “ (deleted)”

# cat /proc/842/cgroup
...
0::/test-cgroup/test-cgroup-nested (deleted)

线程

cgroup v2 为一部分控制器支持线程粒度,以支持需要在进程组的线程之间进行层级资源分配的使用场景。默认情况下,进程的所有线程都属于同一个 cgroup,该 cgroup 也作为资源域,承载非特定于某个进程或线程的资源消耗。线程模式允许线程分布在子树中,同时仍为它们维持共同的资源域。

支持线程模式的控制器被称为“线程化控制器(threaded controllers)”。不支持的则被称为“域控制器(domain controllers)”。

将某个 cgroup 标记为线程化(threaded)会使其作为一个线程化 cgroup 加入其父级的资源域。父级可能是另一个在层级结构中更上层的线程化 cgroup。线程化子树的根(即最近的非线程化祖先)交替被称为“线程化域(threaded domain)”或“线程根(thread root)”,并作为整个子树的资源域。

在线程化子树内部,进程 of a process 的线程可以被放入不同的 cgroup 中,并且不受“无内部进程约束”的限制——线程化控制器可以在非叶子 cgroup 上启用,无论其中是否有线程。

由于线程化域 cgroup 承载了子树的所有域资源消耗,因此无论其中是否有进程,它都被视为具有内部 resource 消耗,并且不能拥有非线程化的有活动进程的子 cgroup。因为根 cgroup 不受“无内部进程约束”的限制,所以它既可以作为线程化域,也可以作为域 cgroup 的父级。

cgroup 的当前运行模式或类型显示在 “cgroup.type” 文件中,该文件指示 cgroup 是普通域、作为线程化子树的域,还是线程化 cgroup。

在创建时,cgroup 始终是一个域(domain)cgroup,可以通过向 “cgroup.type” 文件写入 “threaded” 来使其变成线程化。该操作是单向的

# echo threaded > cgroup.type

一旦变成线程化,该 cgroup 就不能再变回域。要启用线程模式,必须满足以下条件。

  • 由于该 cgroup 将加入父级的资源域。父级必须是一个有效的(线程化)域或一个线程化 cgroup。

  • 当父级是一个非线程化域时,它不能启用任何域控制器,也不能有包含活动进程的域子 cgroup。根 cgroup 免受此要求的限制。

在拓扑结构上,cgroup 可能会处于无效状态。请考虑以下拓扑

A (threaded domain) - B (threaded) - C (domain, just created)

C 在创建时是一个域,但未连接到可以承载子域的父级。在 C 转换为线程化 cgroup 之前无法使用。在这些情况下,“cgroup.type” 文件将报告 “domain (invalid)”。由于无效拓扑而失败的操作将使用 EOPNOTSUPP 作为错误码(errno)。

当域 cgroup 的某个子 cgroup 变成线程化,或者在 cgroup 中有进程时在 “cgroup.subtree_control” 文件中启用了线程化控制器,该域 cgroup 就会变成线程化域。当条件清除时,线程化域会恢复为普通域。

读取时,“cgroup.threads” 包含该 cgroup 中所有线程的线程 ID(TID)列表。除了操作是针对每个线程而不是每个进程外,“cgroup.threads” 具有与 “cgroup.procs” 相同的格式和相同的行为方式。虽然可以向任何 cgroup 的 “cgroup.threads” 写入数据,但由于它只能在同一个线程化域内部移动线程,因此其操作仅限于每个线程化子树内部。

线程化域 cgroup 充当整个子树的资源域,虽然线程可以分散在子树中,但所有进程都被视为在线程化域 cgroup 中。“cgroup.procs” 在线程化域 cgroup 中包含子树中所有进程的 PID,并且在子树内部是不可读的。但是,可以从子树的任何位置写入 “cgroup.procs” 以将匹配进程的所有线程迁移到该 cgroup。

在线程化子树中只能启用线程化控制器。当在线程化子树内启用线程化控制器时,它仅统计和控制与该 cgroup 及其后代中的线程相关的资源消耗。所有未绑定到特定线程的消耗都属于线程化域 cgroup。

由于线程化子树不受“无内部进程约束”限制,因此线程化控制器必须能够处理非叶子 cgroup 中的线程与其子 cgroup 之间的竞争。每个线程化控制器定义了如何处理此类竞争。

目前,以下控制器是线程化的,可以在线程化 cgroup 中启用

- cpu
- cpuset
- perf_event
- pids

[无/有]活动进程通知

每个非根 cgroup 都有一个 “cgroup.events” 文件,其中包含 “populated” 字段,指示该 cgroup 的子层级结构中是否有活动进程。如果该 cgroup 及其后代中没有活动进程,则其值为 0;否则为 1。当值改变时,会触发 poll 和 [id]notify 事件。例如,这可以用于在给定子层级的所有进程都退出后启动清理操作。活动状态更新和通知是递归的。考虑以下子层级结构,其中括号中的数字表示每个 cgroup 中的进程数

A(4) - B(0) - C(1)
            \ D(0)

A、B 和 C 的 “populated” 字段将为 1,而 D 的将为 0。在 C 中的一个进程退出后,B 和 C 的 “populated” 字段将翻转为 “0”,并且会在两个 cgroup 的 “cgroup.events” 文件上生成文件修改事件。

控制控制器

可用性

当内核支持某个控制器(即已编译进内核、未禁用且未附加到 v1 层级结构),且其列在 “cgroup.controllers” 文件中时,该控制器在 cgroup 中就是可用的。可用性意味着该控制器的接口文件会暴露在 cgroup 的目录中,从而允许在该 cgroup 内观察或控制目标资源的分配。

启用和禁用

每个 cgroup 都有一个 “cgroup.controllers” 文件,列出了该 cgroup 可以启用的所有控制器

# cat cgroup.controllers
cpu io memory

默认情况下不启用任何控制器。可以通过向 “cgroup.subtree_control” 文件写入数据来启用和禁用控制器

# echo "+cpu +memory -io" > cgroup.subtree_control

只有 “cgroup.controllers” 中列出的控制器才能被启用。当如上指定多个操作时,它们要么全部成功,要么全部失败。如果对同一个控制器指定了多个操作,则最后一个生效。

在 cgroup 中启用控制器表示将控制目标资源在其直接子级之间的分配。考虑以下子层级。启用的控制器列在括号中

A(cpu,memory) - B(memory) - C()
                          \ D()

由于 A 启用了 “cpu” 和 “memory”,A 将控制 CPU 周期和内存向其子级(在此例中为 B)的分配。由于 B 启用了 “memory” 但未启用 “cpu”,C 和 D 将自由竞争 CPU 周期,但它们对 B 可用内存的划分将受到控制。

由于控制器调节目标资源向 cgroup 子级的分配,启用它会在子 cgroup 中创建该控制器的接口文件。在上述示例中,在 B 上启用 “cpu” 将在 C 和 D 中创建以 “cpu.” 为前缀的控制器接口文件。同样,在 B 上禁用 “memory” 将从 C 和 D 中移除以 “memory.” 为前缀的控制器接口文件。这意味着控制器接口文件——任何不以 “cgroup.” 开头的文件——都是由父级而不是 cgroup 本身所拥有的。

自上而下的约束

资源是自上而下分配的,只有当父级将资源分配给 cgroup 时,该 cgroup 才能进一步分配该资源。这意味着所有非根 “cgroup.subtree_control” 文件只能包含在父级 “cgroup.subtree_control” 文件中已启用的控制器。只有当父级启用了该控制器时,才能启用该控制器;如果一个或多个子级启用了该控制器,则不能禁用它。

无内部进程约束

非根 cgroup 只有在自身没有任何进程时,才能向其子级分配域资源。换句话说,只有不包含任何进程的域 cgroup,才能在其 “cgroup.subtree_control” 文件中启用域控制器。

这保证了当域控制器查看启用了该控制器的层级结构部分时,进程始终只存在于叶子节点上。这排除了子 cgroup 与父级的内部进程竞争的情况。

根 cgroup 免受此限制。根 cgroup 包含进程和无法与其他任何 cgroup 关联的匿名资源消耗,并且需要大多数控制器进行特殊处理。如何管理根 cgroup 中的资源消耗取决于每个控制器(有关此主题的更多信息,请参考控制器章节中的非规范性信息部分)。

请注意,如果 cgroup 的 “cgroup.subtree_control” 中没有启用的控制器,则该限制不会产生阻碍。这很重要,因为否则就无法在包含活动进程的 cgroup 下创建子级。为了控制 cgroup 的资源分配,该 cgroup 必须在启用其 “cgroup.subtree_control” 文件中的控制器之前,先创建子级并将所有进程转移到这些子级中。

委派

委派模型

可以通过两种方式委派 cgroup。第一种,通过向特权较低的用户授予该目录及其 “cgroup.procs”、“cgroup.threads” 和 “cgroup.subtree_control” 文件的写访问权限。第二种,如果设置了 “nsdelegate” 挂载选项,则在创建命名空间时自动委派给 cgroup 命名空间。

由于给定目录中的资源控制接口文件控制着父级资源的分配,因此不应允许被委派者(delegatee)对其进行写入。对于第一种方法,这是通过不授予对这些文件的访问权限来实现的。对于第二种方法,命名空间之外的文件应该通过至少挂载命名空间的方式对被委派者隐藏,并且内核会拒绝从 cgroup 命名空间内部向命名空间根目录上的所有文件进行写入,除了在 “/sys/kernel/cgroup/delegate” 中列出的文件(包括 “cgroup.procs”、“cgroup.threads”、“cgroup.subtree_control” 等)。

两种委派类型的最终结果是等效的。一旦委派,用户就可以在目录下构建子层级结构,根据其认为合适的方式在其中组织进程,并进一步分配其从父级获得的资源。所有资源控制器的限制和其他设置都是层级化的,无论在委派的子层级结构中发生什么,任何东西都无法逃脱父级施加的资源限制。

目前,cgroup 没有对委派子层级结构中的 cgroup 数量或嵌套深度施加任何限制;然而,未来可能会对此进行显式限制。

委派隔离性

委派的子层级结构是“受限隔离的(contained)”,这意味着被委派者无法将进程移入或移出该子层级结构。

对于向低特权用户的委派,这是通过对具有非 root euid 的进程提出以下条件来实现的,即允许其通过将 PID 写入 “cgroup.procs” 文件来将目标进程迁移到某个 cgroup 中。

  • 写入者必须对 “cgroup.procs” 文件具有写访问权限。

  • 写入者必须对源 cgroup 和目标 cgroup 的公共祖先的 “cgroup.procs” 文件具有写访问权限。

上述两个限制确保了虽然被委派者可以在委派的子层级结构中自由迁移进程,但它不能从子层级结构外部迁入进程,也不能将进程迁出到子层级结构外部。

举个例子,假设 cgroups C0 和 C1 已委派给用户 U0,U0 在 C0 下创建了 C00、C01,在 C1 下创建了 C10(如下所示),且 C0 和 C1 下的所有进程都属于 U0

~~~~~~~~~~~~~ - C0 - C00
~ cgroup    ~      \ C01
~ hierarchy ~
~~~~~~~~~~~~~ - C1 - C10

再假设 U0 想将目前处于 C10 中的进程的 PID 写入 “C00/cgroup.procs”。U0 拥有该文件的写访问权限;然而,源 cgroup C10 和目标 cgroup C00 的公共祖先在委派点之上,U0 不会对其 “cgroup.procs” 文件具有写访问权限,因此写入将被拒绝,并返回 -EACCES。

对于向命名空间的委派,隔离性是通过要求尝试迁移的进程的命名空间必须能够到达源 cgroup 和目标 cgroup 来实现的。如果任一 cgroup 无法到达,迁移将被拒绝并返回 -ENOENT。

指导原则

一次组织,多次控制

跨 cgroup 迁移进程是一个相对昂贵的操作,并且内存等有状态的资源不会随进程一起移动。这是一个显式的设计决策,因为在同步成本方面,迁移与各种热路径之间通常存在固有的权衡。

因此,不建议频繁地跨 cgroup 迁移进程来施加不同的资源限制。工作负载应该在启动时,根据系统的逻辑和资源结构,一次性分配到 cgroup 中。可以通过接口文件修改控制器配置,动态地对资源分配进行调整。

避免名称冲突

cgroup 的接口文件及其子 cgroup 占用相同的目录,因此可能会创建与接口文件发生冲突的子 cgroup。

所有 cgroup 核心接口文件都带有前缀 “cgroup.”,每个控制器的接口文件都带有控制器名称和点的首缀。控制器的名称由小写字母和 ‘_’ 组成,但绝不以 ‘_’ 开头,因此可以将其用作避免冲突的前缀字符。此外,接口文件名不会以常用于对工作负载进行分类的词(例如 job, service, slice, unit 或 workload)开头或结尾。

cgroup 不会采取任何措施来防止名称冲突,避免冲突是用户的责任。

资源分配模型

根据资源类型和预期的使用场景, cgroup 控制器实现了几种资源分配方案。本节描述了正在使用的主要方案以及它们的预期行为。

权重

父级资源是通过将所有活跃子级的权重相加,并根据每个子级权重占总和的比例来分配给它们的。因为只有当前可以利用该资源的子级才会参与分配,所以这是工作保守的(work-conserving)。由于其动态特性,该模型通常用于无状态资源。

所有权重的范围都在 [1, 10000] 之间,默认值为 100。这允许在足够精细的粒度上在两个方向上实现对称的乘法偏差,同时保持在直观的范围内。

只要权重在范围内,所有配置组合都是有效的,没有理由拒绝配置更改或进程迁移。

“cpu.weight” 将 CPU 周期按比例分配给活跃子级,是该类型的一个示例。

限制

子级最多只能消耗配置的资源量。限制可以被超卖(over-committed)——子级限制的总和可以超过父级可用的资源量。

限制处于 [0, max] 范围,默认值为 “max”(无操作)。

由于限制可以超卖,所有配置组合都是有效的,没有理由拒绝配置更改或进程迁移。

“io.max” 限制了 cgroup 在 IO 设备上可以消耗的最大 BPS 和/或 IOPS,是该类型的一个示例。

保护

只要某个 cgroup 的所有祖先的使用量都在其保护水平以下,该 cgroup 就会受到高达配置资源量的保护。保护可以是强硬的保证,也可以是尽最大努力的软边界。保护也可以超卖,在这种情况下,在子级中最多只有父级可用的资源量会受到保护。

保护的范围处于 [0, max],默认值为 0(无操作)。

由于保护可以超卖,所有配置组合都是有效的,没有理由拒绝配置更改或进程迁移。

“memory.low” 实现了尽力而为的内存保护,是该类型的一个示例。

分配

为一个 cgroup 独占地分配一定量的有限资源。分配不能超卖——子级分配的总和不能超过父级可用的资源量。

分配处于 [0, max] 范围,默认值为 0(无资源)。

由于分配不能超卖,某些配置组合是无效的,应该被拒绝。此外,如果该资源是执行进程所必需的,则进程迁移也可能会被拒绝。

接口文件

格式

只要有可能,所有接口文件都应该采用以下格式之一

New-line separated values
(when only one value can be written at once)

      VAL0\n
      VAL1\n
      ...

Space separated values
(when read-only or multiple values can be written at once)

      VAL0 VAL1 ...\n

Flat keyed

      KEY0 VAL0\n
      KEY1 VAL1\n
      ...

Nested keyed

      KEY0 SUB_KEY0=VAL00 SUB_KEY1=VAL01...
      KEY1 SUB_KEY0=VAL10 SUB_KEY1=VAL11...
      ...

对于可写文件,写入格式通常应该与读取格式匹配;然而,控制器可能允许省略后面的字段,或者针对最常见的使用场景实现限制性快捷方式。

对于扁平键值(flat keyed)和嵌套键值(nested keyed)文件,一次只能写入单个键的值。对于嵌套键值文件,子键对可以以任何顺序指定,并且不必指定所有对。

约定

  • 单个特性的设置应该包含在单个文件中。

  • 根 cgroup 应该免于资源控制,因此不应具有资源控制接口文件。

  • 默认时间单位是微秒。如果使用了不同的单位,则必须存在显式的单位后缀。

  • 分数值应该使用至少带有两位小数的百分比小数——例如 13.40。

  • 如果某个控制器实现了基于权重的资源分配,其接口文件应命名为 “weight”,范围在 [1, 10000] 之间,默认值为 100。选择这些值是为了在两个方向上都允许足够的、对称的偏差,同时保持其直观性(默认值为 100%)。

  • 如果某个控制器实现了绝对资源保证和/或限制,其接口文件应分别命名为 “min” 和 “max”。如果某个控制器实现了尽力而为的资源保证和/或限制,其接口文件应分别命名为 “low” 和 “high”。

    在上述四个控制文件中,应该使用特殊标记 “max” 在读取和写入时表示向上的无穷大。

  • 如果某个设置具有可配置的默认值和特定的键值覆盖项,则默认条目应该以 “default” 为键,并作为文件中的第一个条目出现。

    可以通过写入 “default $VAL” 或 “$VAL” 来更新默认值。

    写入以更新特定的覆盖项时,可以使用 “default” 作为值来表示删除该覆盖项。读取时,绝不能出现以 “default” 作为值的覆盖条目。

    例如,以带有整数值的主设备号:次设备号(major:minor)为键的设置可能如下所示

    # cat cgroup-example-interface-file
    default 150
    8:0 300
    

    默认值可以通过以下方式更新

    # echo 125 > cgroup-example-interface-file
    

    或者

    # echo "default 125" > cgroup-example-interface-file
    

    覆盖项可以通过以下方式设置

    # echo "8:16 170" > cgroup-example-interface-file
    

    并通过以下方式清除

    # echo "8:0 default" > cgroup-example-interface-file
    # cat cgroup-example-interface-file
    default 125
    8:16 170
    
  • 对于频率不是非常高的事件,应该创建一个 “events” 接口文件,其中列出事件键值对。每当发生可通知事件时,应该在该文件上生成文件修改事件。

核心接口文件

所有 cgroup 核心文件都带有前缀 “cgroup.”

cgroup.type

一个存在于非根 cgroup 中的读写单值文件。

读取时,它指示 cgroup 的当前类型,可以是以下值之一。

  • “domain”:普通且有效的域 cgroup。

  • “domain threaded”:充当线程化子树根目录的线程化域 cgroup。

  • “domain invalid”:处于无效状态的 cgroup。它不能包含活动进程,也不能启用控制器。可能允许它成为一个线程化 cgroup。

  • “threaded”:作为线程化子树成员的线程化 cgroup。

可以通过向此文件写入 “threaded” 来将 cgroup 转换为线程化 cgroup。

cgroup.procs

一个存在于所有 cgroup 中的读写、换行符分隔的值文件。

读取时,它会逐行列表显示属于该 cgroup 的所有进程的 PID。PID 是无序的,如果进程被移到另一个 cgroup 然后又移回来,或者在读取时 PID 被循环重用,则同一个 PID 可能会出现多次。

可以写入 PID,以将与该 PID 关联的进程迁移到该 cgroup。写入者应满足以下所有条件。

  • 它必须对 “cgroup.procs” 文件具有写访问权限。

  • 它必须对源 cgroup 和目标 cgroup 的公共祖先的 “cgroup.procs” 文件具有写访问权限。

在委派子层级结构时,应与包含目录一起授予对该文件的写访问权限。

在线程化 cgroup 中,读取此文件会失败并返回 EOPNOTSUPP,因为所有进程都属于线程根。支持写入操作,并将该进程的每个线程移动到该 cgroup。

cgroup.threads

一个存在于所有 cgroup 中的读写、换行符分隔的值文件。

读取时,它会逐行列表显示属于该 cgroup 的所有线程的 TID。TID 是无序的,如果线程被移到另一个 cgroup 然后又移回来,或者在读取时 TID 被循环重用,则同一个 TID 可能会出现多次。

可以写入 TID,以将与该 TID 关联的线程迁移到该 cgroup。写入者应满足以下所有条件。

  • 它必须对 “cgroup.threads” 文件具有写访问权限。

  • 该线程当前所在的 cgroup 必须与目标 cgroup 处于同一个资源域中。

  • 它必须对源 cgroup 和目标 cgroup 的公共祖先的 “cgroup.procs” 文件具有写访问权限。

在委派子层级结构时,应与包含目录一起授予对该文件的写访问权限。

cgroup.controllers

一个存在于所有 cgroup 中的只读、空格分隔的值文件。

它显示了该 cgroup 可用的所有控制器的空格分隔列表。控制器是无序的。

cgroup.subtree_control

一个存在于所有 cgroup 中的读写、空格分隔的值文件。初始为空。

读取时,它会显示已启用的、用于控制从该 cgroup 向其子级分配资源的控制器的空格分隔列表。

可以写入带有前缀 ‘+’ 或 ‘-’ 的控制器空格分隔列表,以启用或禁用控制器。前缀为 ‘+’ 的控制器名称表示启用该控制器,‘-’ 表示禁用。如果某个控制器在列表中多次出现,则最后一个生效。当指定了多个启用和禁用操作时,要么全部成功,要么全部失败。

cgroup.events

一个存在于非根 cgroup 中的只读扁平键值文件。定义了以下条目。除非另有说明,否则此文件中的值更改会生成文件修改事件。

populated

如果 cgroup 或其后代包含任何活动进程,则为 1;否则为 0。

frozen

如果 cgroup 被冻结,则为 1;否则为 0。

cgroup.max.descendants

一个读写单值文件。默认值为 “max”。

允许的最大后代 cgroup 数量。如果实际的后代数量等于或大于该值,则在该层级结构中创建新 cgroup 的尝试将失败。

cgroup.max.depth

一个读写单值文件。默认值为 “max”。

当前 cgroup 之下允许的最大后代深度。如果实际的后代深度等于或大于该值,则创建新子 cgroup 的尝试将失败。

cgroup.stat

带有以下条目的只读扁平键值文件

nr_descendants

可见后代 cgroup 的总数。

nr_dying_descendants

处于消亡中(dying)的后代 cgroup 总数。cgroup 在被用户删除后进入消亡中状态。该 cgroup 将在一段未定义的时间内(取决于系统负载)保持消亡中状态,然后被彻底销毁。

在任何情况下进程都不能进入处于消亡中的 cgroup,且处于消亡中的 cgroup 无法复活。

处于消亡中的 cgroup 可以消耗不超过限制的系统资源,该限制是在 cgroup 删除时有效的限制。

nr_subsys_<cgroup_subsys>

在当前 cgroup 及其之下的活跃 cgroup 子系统(例如内存 cgroup)的总数。

nr_dying_subsys_<cgroup_subsys>

在当前 cgroup 及其之下的消亡中 cgroup 子系统(例如内存 cgroup)的总数。

cgroup.stat.local

一个存在于非根 cgroup 中的只读扁平键值文件。定义了以下条目

frozen_usec

该 cgroup 在冻结和解冻之间花费的累计时间,无论是由其自身还是祖先组引起。注意:这里不统计(未)达到 “frozen” 状态的过程本身。

使用以下 cgroup 冻结器状态的 ASCII 表示形式,

       1    _____
frozen 0 __/     \__
          ab    cd

被测量的持续时间是 a 和 c 之间的跨度。

cgroup.freeze

一个存在于非根 cgroup 中的读写单值文件。允许的值为 “0” 和 “1”。默认值为 “0”。

向该文件写入 “1” 会导致冻结该 cgroup 及其所有后代 cgroup。这意味着所有属于它的进程都将被停止,并且在显式解冻该 cgroup 之前不会运行。冻结 cgroup 可能需要一些时间;当该操作完成时,cgroup.events 控制文件中的 “frozen” 值将更新为 “1”,并发出相应的通知。

cgroup 既可以通过其自身的设置来冻结,也可以通过任何祖先 cgroup 的设置来冻结。如果任何祖先 cgroup 被冻结,该 cgroup 将保持冻结状态。

处于冻结 cgroup 中的进程可以被致命信号杀死。它们也可以进入和离开冻结的 cgroup:要么由用户显式移动,要么在冻结 cgroup 与 fork() 发生竞争时。如果进程被移入冻结的 cgroup,它会停止。如果进程被移出冻结的 cgroup,它将变为运行状态。

cgroup 的冻结状态不会影响任何 cgroup 树操作:可以删除冻结的(且空的)cgroup,也可以创建新的子 cgroup。

cgroup.kill

一个存在于非根 cgroup 中的只写单值文件。唯一允许的值为 “1”。

向该文件写入 “1” 会导致该 cgroup 及其所有后代 cgroup 被杀死。这意味着位于受影响 cgroup 树中的所有进程都将通过 SIGKILL 被杀死。

杀死 cgroup 树将适当处理并发 fork,并防止受到迁移干扰。

在线程化 cgroup 中,写入此文件会失败并返回 EOPNOTSUPP,因为杀死 cgroup 是一项针对进程的操作,即它会影响整个线程组。

cgroup.pressure

一个读写单值文件,允许的值为 “0” 和 “1”。默认值为 “1”。

向该文件写入 “0” 将禁用该 cgroup 的 PSI(压力停顿信息)统计。写入 “1” 将重新启用 cgroup PSI 统计。

该控制属性不是层级化的,因此在 cgroup 中禁用或启用 PSI 统计不会影响其后代的 PSI 统计,并且不需要通过根节点的祖先传递启用设置。

该控制属性存在的原因是 PSI 为每个 cgroup 单独计算停顿,并在层级结构的每个级别上进行聚合。在层级结构的深层部分,对于某些工作负载,这可能会导致不可忽略的开销,在这种情况下,此控制属性可用于禁用非叶子 cgroup 中的 PSI 统计。

irq.pressure

一个读写嵌套键值文件。

显示 IRQ/SOFTIRQ 的压力停顿信息。详情请参考 Documentation/accounting/psi.rst

控制器

CPU

“cpu” 控制器调节 CPU 周期的分配。此控制器为普通调度策略实现了权重和绝对带宽限制模型,并为实时调度策略实现了绝对带宽分配模型。

在上述所有模型中,周期分配仅在时间基础上定义,并且不考虑执行任务的频率。(可选的)利用率夹钳(utilization clamping)支持允许向 schedutil cpufreq 调节器提供提示,告知 CPU 应始终提供的最小期望频率,以及 CPU 不应超过的最大期望频率。

警告:cgroup2 cpu 控制器尚不支持实时进程的(带宽)控制。对于启用了 CONFIG_RT_GROUP_SCHED 选项以进行实时进程组调度的内核,仅当所有实时进程都在根 cgroup 中时,才能启用 cpu 控制器。请注意,系统管理软件可能已在系统启动过程中将实时进程放入非根 cgroup 中,在启用了 CONFIG_RT_GROUP_SCHED 的内核上启用 cpu 控制器之前,可能需要将这些进程移至根 cgroup。

禁用 CONFIG_RT_GROUP_SCHED 后,此限制将不适用,并且某些接口文件要么影响实时进程,要么统计它们。详情请参见下一节。只有 cpu 控制器受 CONFIG_RT_GROUP_SCHED 影响。无论是否启用 CONFIG_RT_GROUP_SCHED,其他控制器都可以用于实时进程的资源控制。

CPU 接口文件

进程与 cpu 控制器的交互取决于其调度策略和底层的调度器。从 cpu 控制器的角度来看,进程可以分类如下

  • 公平类调度器下的进程

  • 带有 cgroup_set_weight 回调的 BPF 调度器下的进程

  • 其他所有进程:SCHED_{FIFO,RR,DEADLINE} 以及没有 cgroup_set_weight 回调的 BPF 调度器下的进程

关于进程何时处于公平类调度器或 BPF 调度器下的详细信息,请参考 Documentation/scheduler/sched-ext.rst

对于以下每个接口文件,都会引用上述分类。所有时间持续时间均以微秒为单位。

cpu.stat

只读扁平键值文件。无论控制器启用与否,该文件都存在。

它总是报告以下三个统计数据,这些数据统计了 cgroup 中的所有进程

  • usage_usec

  • user_usec

  • system_usec

以及当启用控制器时的以下五个统计数据,它们仅统计公平类调度器下的进程

  • nr_periods

  • nr_throttled

  • throttled_usec

  • nr_bursts

  • burst_usec

cpu.weight

存在于非根 cgroup 中的读写单值文件。默认值为 “100”。

对于非空闲组(cpu.idle = 0),权重范围在 [1, 10000] 之间。

如果 cgroup 已被配置为 SCHED_IDLE (cpu.idle = 1),则权重将显示为 0。

此文件仅影响公平类调度器下的进程,以及带有 cgroup_set_weight 回调的 BPF 调度器下的进程(具体取决于回调实际执行的操作)。

cpu.weight.nice

存在于非根 cgroup 中的读写单值文件。默认值为 “0”。

nice 值的范围在 [-20, 19] 之间。

此接口文件是 “cpu.weight” 的替代接口,允许使用与 nice(2) 相同的值来读取和设置权重。由于 nice 值的范围更小且粒度更粗,因此读取的值是当前权重的最接近近似值。

此文件仅影响公平类调度器下的进程,以及带有 cgroup_set_weight 回调的 BPF 调度器下的进程(具体取决于回调实际执行的操作)。

cpu.max

存在于非根 cgroup 中的读写双值文件。默认值为 “max 100000”。

最大带宽限制。格式如下

$MAX $PERIOD

这表示该组在每个 $PERIOD 持续时间内最多可以消耗 $MAX。$MAX 为 “max” 表示没有限制。如果只写入一个数字,则更新 $MAX。

此文件仅影响公平类调度器下的进程。

cpu.max.burst

存在于非根 cgroup 中的读写单值文件。默认值为 “0”。

突发(burst)在 [0, $MAX] 范围内。

此文件仅影响公平类调度器下的进程。

cpu.pressure

一个读写嵌套键值文件。

显示 CPU 的压力停顿信息。详情请参考 Documentation/accounting/psi.rst

此文件统计了 cgroup 中的所有进程。

cpu.uclamp.min

存在于非根 cgroup 中的读写单值文件。默认值为 “0”,即不进行利用率提升。

请求的最小利用率(保护),表示为百分比有理数,例如 12.34 表示 12.34%。

该接口允许读取和设置类似于 sched_setattr(2) 的最小利用率夹钳值。该最小利用率值用于夹钳特定任务的最小利用率夹钳,包括实时进程的夹钳。

请求的最小利用率(保护)总是受到当前最大利用率(限制)值的上限约束,即 cpu.uclamp.max

此文件影响 cgroup 中的所有进程。

cpu.uclamp.max

存在于非根 cgroup 中的读写单值文件。默认值为 “max”,即不限制利用率上限

请求的最大利用率(限制),表示为百分比有理数,例如 98.76 表示 98.76%。

该接口允许读取和设置类似于 sched_setattr(2) 的最大利用率夹钳值。该最大利用率值用于夹钳特定任务的最大利用率夹钳,包括实时进程的夹钳。

此文件影响 cgroup 中的所有进程。

cpu.idle

存在于非根 cgroup 中的读写单值文件。默认值为 0。

这是针对每个任务的 SCHED_IDLE 调度策略的 cgroup 模拟。将此值设置为 1 会将 cgroup 的调度策略设为 SCHED_IDLE。cgroup 内部的线程将保留其自身的相对优先级,但 cgroup 本身相对于其同级 cgroup 将被视为具有非常低的优先级。

此文件仅影响公平类调度器下的进程。

内存

“memory” 控制器调节内存的分配。内存是有状态的,并且实现了限制模型和保护模型。由于内存使用与回收压力之间的交织以及内存的有状态特性,分配模型相对复杂。

虽然并非完全密不透风,但给定 cgroup 的所有主要内存使用情况都会被跟踪,以便在合理范围内统计和控制总内存消耗。目前跟踪以下类型的内存使用情况。

  • 用户空间内存 - 页面缓存和匿名内存。

  • 内核数据结构,例如 dentry 和 inode。

  • TCP 套接字缓冲区。

上述列表未来可能会扩大以提供更好的覆盖范围。

内存接口文件

所有内存量均以字节为单位。如果写入了未与 PAGE_SIZE 对齐的值,则在读回时,该值可能会被向上舍入到最接近的 PAGE_SIZE 倍数。

memory.current

存在于非根 cgroup 中的只读单值文件。

该 cgroup 及其后代当前使用的内存总数。

memory.min

存在于非根 cgroup 中的读写单值文件。默认值为 “0”。

硬内存保护。如果 cgroup 的内存使用量在其有效最小(min)边界之内,则在任何情况下都不会回收该 cgroup 的内存。如果没有可用的未受保护的可回收内存,则调用 OOM 杀手。在有效最小边界(或有效低边界,如果其更高)之上,页面将按超出量的比例被回收,从而减轻较小超出量时的回收压力。

有效最小边界受限于祖先 cgroup 的 memory.min 值。如果存在 memory.min 超卖(子 cgroup 所需的保护内存多于父级允许的量),那么每个子 cgroup 将获得父级保护的一部分,比例与其在 memory.min 以下的实际内存使用量成正比。

不建议在极少可用的情况下在此保护下放入更多内存,否则可能导致持续的 OOM。

memory.low

存在于非根 cgroup 中的读写单值文件。默认值为 “0”。

尽力而为的内存保护。如果 cgroup 的内存使用量在其有效低(low)边界之内,则除非在未受保护的 cgroup 中没有可回收的内存可用,否则不会回收该 cgroup 的内存。在有效低边界(或有效最小边界,如果其更高)之上,页面将按超出量的比例被回收,从而减轻较小超出量时的回收压力。

有效低边界受限于祖先 cgroup 的 memory.low 值。如果存在 memory.low 超卖(子 cgroup 所需的保护内存多于父级允许的量),那么每个子 cgroup 将获得父级保护的一部分,比例与其在 memory.low 以下的实际内存使用量成正比。

不建议在此保护下放入多于通常可用量的内存。

memory.high

存在于非根 cgroup 中的读写单值文件。默认值为 “max”。

内存使用限制(节流限制)。如果 cgroup 的使用量超过 high 边界,该 cgroup 的进程将被节流(限制速度)并承受巨大的回收压力。

超过高限制永远不会触发 OOM 杀手,在极端情况下可能会突破限制。高限制应该用于以下场景:由外部进程监控受限的 cgroup,以减轻重度的回收压力。

如果以 O_NONBLOCK 方式打开 memory.high,则会绕过同步回收。这对于需要动态调整作业内存限制的管理进程很有用,无需在内存回收上消耗自己的 CPU 资源。作业将在其下一次计费请求时触发回收和/或被限制速度。

请注意,在使用 O_NONBLOCK 时,由于计费请求延迟或过度访问其内存以致放缓回收,目标内存 cgroup 可能会耗费无限的时间来将使用量降低到限制以下。

memory.max

存在于非根 cgroup 中的读写单值文件。默认值为 “max”。

内存使用硬限制。这是限制 cgroup 内存使用量的主要机制。如果 cgroup 的内存使用量达到此限制且无法降低,则会在 cgroup 中触发 OOM 杀手。在某些情况下,使用量可能会暂时超过限制。

在默认配置下,常规的 0 阶分配(0-order allocations)总是成功,除非 OOM 杀手选择当前任务作为受害者。

某些分配不会触发 OOM 杀手。调用者可以采用不同方式重试它们、向用户空间返回 -ENOMEM,或者在磁盘预读等情况下静默忽略。

如果以 O_NONBLOCK 方式打开 memory.max,则会绕过同步回收和 OOM 杀手。这对于需要动态调整作业内存限制的管理进程很有用,无需在内存回收上消耗自己的 CPU 资源。作业将在其下一次计费请求时触发回收和/或 OOM 杀手。

请注意,在使用 O_NONBLOCK 时,由于计费请求延迟或过度访问其内存以致放缓回收,目标内存 cgroup 可能会耗费无限的时间来将使用量降低到限制以下。

memory.reclaim

一个存在于所有 cgroup 中的只写嵌套键值文件。

这是一个在目标 cgroup 中触发内存回收的简单接口。

示例

echo "1G" > memory.reclaim

请注意,内核对目标 cgroup 的回收可能会过多或不足。如果回收的字节数少于指定量,则返回 -EAGAIN。

请注意,主动回收(由此接口触发)并不表示内存 cgroup 存在内存压力。因此,在这种情况下,通常不会执行由内存回收触发的套接字内存平衡。这意味着网络层不会根据 memory.reclaim 引起的回收进行自适应调整。

定义了以下嵌套键。

swappiness

用于回收的 Swappiness 值

指定 swappiness 值会指示内核使用该 swappiness 值执行回收。请注意,这与应用于 memcg 回收的 vm.swappiness 具有相同的语义,并且包含所有现有的限制和潜在的未来扩展。

swappiness 的有效范围是 [0-200, max],设置 swappiness=max 将专门回收匿名内存。

memory.peak

一个存在于非根 cgroup 中的读写单值文件。

自 cgroup 创建或该文件描述符(FD)最近一次重置以来,为该 cgroup 及其后代记录的最大内存使用量。

向此文件写入任何非空字符串,会将其重置为当前的内存使用量,以便通过同一文件描述符进行后续读取。

memory.oom.group

存在于非根 cgroup 中的读写单值文件。默认值为 “0”。

决定 OOM 杀手是否应将该 cgroup 视为不可分割的工作负载。如果设置,属于该 cgroup 或其后代(如果内存 cgroup 不是叶子 cgroup)的所有任务都将被一起杀死,或者完全不被杀死。这可以用于避免部分杀死,以保证工作负载的完整性。

具有 OOM 保护(oom_score_adj 设置为 -1000)的任务被视为特例,绝不会被杀死。

如果在一个 cgroup 内触发了 OOM 杀手,它不会杀死该 cgroup 之外的任何任务,无论祖先 cgroup 的 memory.oom.group 值为多少。

memory.events

一个存在于非根 cgroup 中的只读扁平键值文件。定义了以下条目。除非另有说明,否则此文件中的值更改会生成文件修改事件。

请注意,此文件中的所有字段都是层级化的,并且由于层级结构下方的事件,可能会生成文件修改事件。有关 cgroup 级别的本地事件,请参见 memory.events.local。

low

尽管 cgroup 的使用量低于 low 边界,但由于高内存压力而回收该 cgroup 的次数。这通常表示 low 边界超卖了。

high

由于超过高内存边界,cgroup 进程被节流并被路由去执行直接内存回收的次数。对于内存使用量受高限制(而非全局内存压力)控制的 cgroup,此事件的发生是在预料之中的。

max

cgroup 的内存使用量即将超过 max 边界的次数。如果直接回收未能将其降低,则 cgroup 会进入 OOM 状态。

oom

cgroup 内存使用量达到限制且分配即将失败的次数。

如果不将 OOM 杀手视为选项,则不会触发此事件,例如由于高阶分配失败,或者调用者要求不重试尝试。

oom_kill

属于该 cgroup 的、被任何类型的 OOM 杀手杀死的进程数量。

oom_group_kill

组 OOM 发生的次数。

sock_throttled

与该 cgroup 关联的网络套接字被节流的次数。

memory.events.local

类似于 memory.events,但文件中的字段是本地于该 cgroup 的,即非层级化的。在此文件上生成的文件修改事件仅反映本地事件。

memory.stat

存在于非根 cgroup 中的只读扁平键值文件。

这把 cgroup 的内存占用拆分为不同类型的内存、特定于类型的细节,以及有关内存管理系统状态和过去事件的其他信息。

所有内存量均以字节为单位。

条目的顺序是为了便于人类阅读,并且中间可能会出现新条目。不要依赖保持在固定位置的条目;请使用键来查找特定的值!

如果该条目没有每节点计数器(或者未显示在 memory.numa_stat 中)。我们使用 ‘npn’ (non-per-node) 作为标记,以指示它不会显示在 memory.numa_stat 中。

anon

在匿名映射中使用的内存量,例如 brk()sbrk() 和 mmap(MAP_ANONYMOUS)。请注意,如果某种分配的内存中只有一部分(而非全部)仍被映射,某些内核配置可能会统计整个较大的分配(例如 THP)。

file

用于缓存文件系统数据的内存量,包括 tmpfs 和共享内存。

kernel (npn)

内核内存总数,除了其他内核内存使用场景外,还包括 (kernel_stack, pagetables, percpu, vmalloc, slab)。

kernel_stack

分配给内核栈的内存量。

pagetables

为页表分配的内存量。

sec_pagetables

为二级页表分配的内存量,目前包括 x86 和 arm64 上的 KVM mmu 分配以及 IOMMU 页表。

percpu (npn)

用于存储每个 CPU 的内核数据结构的内存量。

sock (npn)

网络传输缓冲区中使用的内存量

vmalloc

用于 vmap 后端内存的内存量。

shmem

有交换区后端的已缓存文件系统数据量,例如 tmpfs、shm 段、共享匿名 mmap()

zswap

zswap 压缩后端消耗的内存量。

zswapped

被交换出到 zswap 的应用程序内存量。

file_mapped

使用 mmap() 映射的已缓存文件系统数据量。请注意,如果某种分配的内存中只有一部分(而非全部)被映射,某些内核配置可能会统计整个较大的分配(例如 THP)。

file_dirty

已修改但尚未写回磁盘的缓存文件系统数据量

file_writeback

已修改且目前正在写回磁盘的缓存文件系统数据量

swapcached

在内存中缓存的交换区数量。swapcache 同时计入内存和交换区使用量。

anon_thp

由透明巨页(THP)支持的匿名映射中使用的内存量

file_thp

由透明巨页支持的缓存文件系统数据量

shmem_thp

由透明巨页支持的 shm、tmpfs、共享匿名 mmap() 的数量

inactive_anon, active_anon, inactive_file, active_file, unevictable

在页面回收算法使用的内部内存 management 列表上,由交换区支持和文件系统支持的内存量。

由于这些代表了内部列表状态(例如,shmem 页面在匿名内存管理列表上),inactive_foo + active_foo 可能不等于 foo 计数器的值,因为 foo 计数器是基于类型的,而不是基于列表的。

slab_reclaimable

“slab” 中可能被回收的部分,例如 dentry 和 inode。

slab_unreclaimable

在内存压力下无法被回收的 “slab” 部分。

slab (npn)

用于存储内核中数据结构的内存量。

workingset_refault_anon

之前被驱逐的匿名页面重新发生缺页的次数。

workingset_refault_file

之前被驱逐的文件页面重新发生缺页的次数。

workingset_activate_anon

被重新发生缺页并立即被激活的匿名页面数量。

workingset_activate_file

被重新发生缺页并立即被激活的文件页面数量。

workingset_restore_anon

在被回收之前已被检测为活跃工作集的、恢复的匿名页面数量。

workingset_restore_file

在被回收之前已被检测为活跃工作集的、恢复的文件页面数量。

workingset_nodereclaim

影子节点被回收的次数

pswpin (npn)

被交换进内存的页面数量

pswpout (npn)

被交换出内存的页面数量

pgscan (npn)

已扫描的页面数量(在非活动 LRU 列表中)

pgsteal (npn)

已回收的页面数量

pgscan_kswapd (npn)

由 kswapd 扫描的页面数量(在非活动 LRU 列表中)

pgscan_direct (npn)

直接扫描的页面数量(在非活动 LRU 列表中)

pgscan_khugepaged (npn)

由 khugepaged 扫描的页面数量(在非活动 LRU 列表中)

pgscan_proactive (npn)

主动扫描的页面数量(在非活动 LRU 列表中)

pgsteal_kswapd (npn)

由 kswapd 回收的页面数量

pgsteal_direct (npn)

直接回收的页面数量

pgsteal_khugepaged (npn)

由 khugepaged 回收的页面数量

pgsteal_proactive (npn)

主动回收的页面数量

pgfault (npn)

发生的缺页异常总数

pgmajfault (npn)

发生的主缺页异常数量

pgrefill (npn)

已扫描的页面数量(在活动 LRU 列表中)

pgactivate (npn)

移动到活动 LRU 列表中的页面数量

pgdeactivate (npn)

移动到非活动 LRU 列表中的页面数量

pglazyfree (npn)

在内存压力下推迟释放的页面数量

pglazyfreed (npn)

已回收的延迟释放(lazyfree)页面数量

swpin_zero

交换进内存并用零填充的页面数量,其中由于在交换出期间检测到页面内容为零,因而优化掉了 I/O 操作。

swpout_zero

由于内容被检测为零,跳过了 I/O 被交换出的零填充页面数量。

zswpin

从 zswap 移入内存的页面数量。

zswpout

从内存移出到 zswap 的页面数量。

zswpwb

从 zswap 写入 swap 的页面数量。

zswap_incomp

当前存储在 zswap 中且未压缩的不可压缩页面所使用的内存量。这些页面无法压缩到小于 PAGE_SIZE 的大小,因此它们按原样存储。

thp_fault_alloc (npn)

为满足缺页异常而分配的透明巨页(THP)数量。当未设置 CONFIG_TRANSPARENT_HUGEPAGE 时,此计数器不存在。

thp_collapse_alloc (npn)

为允许折叠现有页面范围而分配的透明巨页数量。当未设置 CONFIG_TRANSPARENT_HUGEPAGE 时,此计数器不存在。

thp_swpout (npn)

未进行拆分、作为一个整体被换出(swapout)的透明巨页数量。

thp_swpout_fallback (npn)

在换出前被拆分的透明巨页数量。通常是因为未能为该巨页分配连续的交换空间。

numa_pages_migrated (npn)

通过 NUMA 平衡迁移的页面数量。

numa_pte_updates (npn)

其页表项(PTE)被 NUMA 平衡修改,以便在访问时产生 NUMA 提示缺页异常的页面数量。

numa_hint_faults (npn)

NUMA 提示缺页异常的次数。

pgdemote_kswapd

被 kswapd 降级的页面数量。

pgdemote_direct

直接降级的页面数量。

pgdemote_khugepaged

被 khugepaged 降级的页面数量。

pgdemote_proactive

被主动降级的页面数量。

hugetlb

hugetlb 页面使用的内存量。该指标仅在 memory.current 中计算 hugetlb 使用量时才会显示(即 cgroup 挂载了 memory_hugetlb_accounting 选项)。

memory.numa_stat

一个只读的嵌套键值文件,存在于非根 cgroup 中。

该文件将 cgroup 的内存占用拆分为不同类型的内存、特定于类型的详细信息,以及每个节点上关于内存管理系统状态的其他信息。

这有助于提供 memcg(内存 cgroup)内 NUMA 局部性信息的可见性,因为页面允许从任何物理节点分配。其中一个用例是通过将此信息与应用程序的 CPU 分配相结合来评估应用程序性能。

所有内存量均以字节为单位。

memory.numa_stat 的输出格式为

type N0=<bytes in node 0> N1=<bytes in node 1> ...

条目的顺序是为了便于人类阅读,并且中间可能会出现新条目。不要依赖保持在固定位置的条目;请使用键来查找特定的值!

这些条目可以参考 memory.stat。

memory.swap.current

存在于非根 cgroup 中的只读单值文件。

该 cgroup 及其后代当前使用的 swap 总量。

memory.swap.high

存在于非根 cgroup 中的读写单值文件。默认值为 “max”。

Swap 使用率限制阈值(高限制)。如果 cgroup 的 swap 使用量超过此限制,其后续的所有分配都将被节流(限制速度),以允许用户空间实施自定义的内存不足(OOM)处理程序。

此限制标志着该 cgroup 的“不归路”。它并非设计用于管理工作负载在常规运行期间的交换量。相比之下,memory.swap.max 禁止超过设定额度的交换,但只要其他内存可以回收,就允许该 cgroup 继续不受阻碍地运行。

健康的工作负载不应该达到此限制。

memory.swap.peak

一个存在于非根 cgroup 中的读写单值文件。

自该 cgroup 创建或该文件描述符(FD)最近一次重置以来,为该 cgroup 及其后代记录的最大 swap 使用量。

向此文件写入任何非空字符串,会将其重置为当前的内存使用量,以便通过同一文件描述符进行后续读取。

memory.swap.max

存在于非根 cgroup 中的读写单值文件。默认值为 “max”。

Swap 使用硬限制。如果 cgroup 的 swap 使用量达到此限制,该 cgroup 的匿名内存将不会被换出。

memory.swap.events

一个存在于非根 cgroup 中的只读扁平键值文件。定义了以下条目。除非另有说明,否则此文件中的值更改会生成文件修改事件。

high

该 cgroup 的 swap 使用量超过高阈值的次数。

max

该 cgroup 的 swap 使用量即将超过最大限制且 swap 分配失败的次数。

fail

由于全系统 swap 耗尽或达到最大限制而导致 swap 分配失败的次数。

当限制值降低到当前使用量以下时,现有的 swap 条目会被逐步回收,swap 使用量可能会在较长时间内保持高于限制的状态。这可以减少对工作负载和内存管理的影响。

memory.zswap.current

存在于非根 cgroup 中的只读单值文件。

zswap 压缩后端消耗的内存总量。

memory.zswap.max

存在于非根 cgroup 中的读写单值文件。默认值为 “max”。

Zswap 使用硬限制。如果 cgroup 的 zswap 池达到此限制,它将拒绝接受更多的数据存储,直到现有条目被换回内存(fault back in)或写入磁盘。

memory.zswap.writeback

一个可读写的单值文件。默认值为“1”。请注意,此设置是层级结构的,即如果上级层级禁用了回写,则子 cgroup 也会隐式禁用回写。

当此值设置为 0 时,将禁用所有向交换设备进行交换的尝试。这既包括 zswap 回写,也包括由于 zswap 存储失败而导致的交换。如果 zswap 存储失败不断重复(例如页面不可压缩),用户在禁用回写后可能会观察到回收效率低下(因为相同的页面可能会被一次又一次地拒绝)。

请注意,这与将 memory.swap.max 设置为 0 有细微的不同,因为它仍然允许将页面写入 zswap 池。如果禁用了 zswap,此设置将没有效果,并且除非 memory.swap.max 设置为 0,否则允许进行交换。

memory.pressure

一个只读的嵌套键值文件。

显示内存的压力停顿信息(PSI)。有关详细信息,请参阅 Documentation/accounting/psi.rst

使用指南

“memory.high” 是控制内存使用的主要机制。在 high 限制上进行超发/超配(high 限制的总和 > 可用内存),并让全局内存压力根据使用情况分配内存,是一种可行的策略。

由于突破 high 限制不会触发 OOM 杀手,而是会节流(限制速度)违规的 cgroup,因此管理代理有充足的机会进行监控并采取适当的行动,例如分配更多内存或终止工作负载。

确定一个 cgroup 是否有足够的内存并不简单,因为内存使用情况并不能表明工作负载是否能从更多内存中受益。例如,一个将从网络接收的数据写入文件的工作负载可以使用所有可用内存,但也可以在少量内存下高效运行。要确定工作负载是否需要更多内存,必须衡量内存压力——即工作负载因缺少内存而受到多大程度的影响;遗憾的是,内存压力监控机制尚未实现。

回收保护

使用“memory.low”或“memory.min”配置的保护相对于回收的目标(即任何内存 cgroup 限制、主动的 memory.reclaim 或显然位于根 cgroup 中的全局回收)相对应用。为 B 配置的保护值在针对 A 的回收中保持不变地应用(即由于与兄弟 cgroup E 的竞争引起的回收)。

root - ... - A - B - C
              \    ` D
               ` E

当回收的目标是 A 的祖先时,B 的有效保护将受限于为 A 配置的保护值(以及 A 与目标之间的任何其他中间祖先)。

若要对相对兄弟保护表示无关紧要,建议使用 memory_recursiveprot。将具有有限保护的父 cgroup 的所有后代配置为“max”可行,但这可能会不必要地使 memory.events:low 字段发生偏差。

内存所有权

内存区域会记账(charge)到实例化该区域的 cgroup,并保持记账状态直到该区域被释放。将进程迁移到不同的 cgroup 不会将它在之前 cgroup 中实例化时产生的内存使用量移动到新的 cgroup。

一个内存区域可能会被属于不同 cgroup 的进程所使用。该区域会被记账到哪个 cgroup 是不确定的;然而,随着时间的推移,内存区域很可能会落入一个有足够内存余量以避免高回收压力的 cgroup 中。

如果一个 cgroup 扫描并占用了大量预计会被其他 cgroup 反复访问的内存,那么使用 POSIX_FADV_DONTNEED 来放弃属于受影响文件的内存区域的所有权,以确保内存所有权的正确性,可能是有意义的。

IO

“io”控制器调节 IO 资源的分配。此控制器实现了基于权重和基于绝对带宽或 IOPS 限制的分配;然而,基于权重的分配仅在 cfq-iosched 处于使用状态时可用,且这两种方案都不适用于 blk-mq 设备。

IO 接口文件

io.stat

一个只读的嵌套键值文件。

行以 $MAJ:$MIN 设备号为键,且未排序。定义了以下嵌套键。

rbytes

读取的字节数

wbytes

写入的字节数

rios

读取 IO 的次数

wios

写入 IO 的次数

dbytes

丢弃(discard)的字节数

dios

丢弃 IO 的次数

以下是一个读取输出的示例

8:16 rbytes=1459200 wbytes=314773504 rios=192 wios=353 dbytes=0 dios=0
8:0 rbytes=90430464 wbytes=299008000 rios=8950 wios=1252 dbytes=50331648 dios=3021
io.cost.qos

一个可读写的嵌套键值文件,仅存在于根 cgroup 中。

该文件用于配置基于 IO 成本模型控制器(CONFIG_BLK_CGROUP_IOCOST)的服务质量(QoS),该控制器目前实现了 “io.weight” 比例控制。行以 $MAJ:$MIN 设备号为键,且未排序。特定设备的行在首次向该设备的 “io.cost.qos” 或 “io.cost.model” 写入时填充。定义了以下嵌套键。

enable

启用基于权重的控制

ctrl

“auto” 或 “user”

rpct

读取延迟百分位数 [0, 100]

rlat

读取延迟阈值

wpct

写入延迟百分位数 [0, 100]

wlat

写入延迟阈值

min

最小缩放百分比 [1, 10000]

max

最大缩放百分比 [1, 10000]

该控制器默认禁用,可以通过将 “enable” 设置为 1 来启用。“rpct” 和 “wpct” 参数默认为零,控制器使用内部设备饱和状态在 “min” 和 “max” 之间调整整体 IO 速率。

当需要更好的控制质量时,可以配置延迟 QoS 参数。例如

8:16 enable=1 ctrl=auto rpct=95.00 rlat=75000 wpct=95.00 wlat=150000 min=50.00 max=150.0

显示在 sdb 上启用了该控制器,如果读取完成延迟的第 95 百分位数超过 75ms 或写入超过 150ms,则认为设备已饱和,并相应地在 50% 到 150% 之间调整整体 IO 发送速率。

饱和点越低,延迟 QoS 越好,但代价是总带宽。“min” 和 “max” 之间允许的调整范围越窄,IO 行为就越符合成本模型。请注意,IO 发送基准速率可能远非 100%,盲目设置 “min” 和 “max” 可能会导致设备容量或控制质量的重大损失。“min” 和 “max” 对于调节表现出较大临时行为变化的设备非常有用——例如,某个 SSD 在一段时间内以线速接受写入,然后完全停顿数秒。

当 “ctrl” 为 “auto” 时,参数由内核控制,并可能自动更改。将 “ctrl” 设置为 “user” 或设置任何百分位数和延迟参数会将其置于 “user” 模式并禁用自动更改。可以通过将 “ctrl” 设置为 “auto” 来恢复自动模式。

io.cost.model

一个可读写的嵌套键值文件,仅存在于根 cgroup 中。

该文件用于配置基于 IO 成本模型的控制器(CONFIG_BLK_CGROUP_IOCOST)的成本模型,该控制器目前实现了 “io.weight” 比例控制。行以 $MAJ:$MIN 设备号为键,且未排序。特定设备的行在首次向该设备的 “io.cost.qos” 或 “io.cost.model” 写入时填充。定义了以下嵌套键。

ctrl

“auto” 或 “user”

model

正在使用的成本模型——“linear”(线性)

当 “ctrl” 为 “auto” 时,内核可以动态更改所有参数。当 “ctrl” 设置为 “user” 或向任何其他参数写入内容时,“ctrl” 变为 “user”,并禁用自动更改。

当 “model” 为 “linear” 时,定义了以下模型参数。

[r|w]bps

最大顺序 IO 吞吐量

[r|w]seqiops

最大每秒 4k 顺序 IO 数

[r|w]randiops

最大每秒 4k 随机 IO 数

由此,内置的线性模型确定了顺序和随机 IO 的基本成本以及 IO 大小的成本系数。虽然简单,但该模型可以相当好地涵盖大多数常见的设备类别。

IO 成本模型在绝对意义上并不被期望是精确的,它是根据设备行为动态缩放的。

如果需要,可以使用 tools/cgroup/iocost_coef_gen.py 生成特定于设备的系数。

io.weight

一个存在于非根 cgroup 中的可读写扁平键值文件。默认值为“default 100”。

第一行是应用于没有特定覆盖设置的设备的默认权重。其余行是以 $MAJ:$MIN 设备号为键的覆盖设置,且未排序。权重在 [1, 10000] 范围内,指定了该 cgroup 相比其兄弟 cgroup 可以使用的相对 IO 时间量。

可以通过写入 “default $WEIGHT” 或仅写入 “$WEIGHT” 来更新默认权重。可以通过写入 “$MAJ:$MIN $WEIGHT” 来设置覆盖,通过写入 “$MAJ:$MIN default” 来取消设置。

以下是一个读取输出的示例

default 100
8:16 200
8:0 50
io.max

一个存在于非根 cgroup 中的可读写嵌套键值文件。

基于 BPS 和 IOPS 的 IO 限制。行以 $MAJ:$MIN 设备号为键,且未排序。定义了以下嵌套键。

rbps

最大每秒读取字节数

wbps

最大每秒写入字节数

riops

最大每秒读取 IO 操作次数

wiops

最大每秒写入 IO 操作次数

写入时,可以以任何顺序指定任意数量的嵌套键值对。可以指定 “max” 作为值以移除特定限制。如果多次指定相同的键,结果是未定义的。

BPS 和 IOPS 是在每个 IO 方向上进行测量的,如果达到限制,IO 将被延迟。允许临时的突发流量。

为 8:16 设备设置读取限制为 2M BPS,写入限制为 120 IOPS

echo "8:16 rbps=2097152 wiops=120" > io.max

读取会返回以下内容

8:16 rbps=2097152 wbps=max riops=max wiops=120

可以通过写入以下内容来移除写入 IOPS 限制

echo "8:16 wiops=max" > io.max

现在读取会返回以下内容

8:16 rbps=2097152 wbps=max riops=max wiops=max
io.pressure

一个只读的嵌套键值文件。

显示 IO 的压力停顿信息。有关详细信息,请参阅 Documentation/accounting/psi.rst

回写

页缓存(Page cache)通过缓冲写入和共享内存映射(mmap)被弄脏,并通过回写机制异步写入到底层文件系统。回写介于内存和 IO 域之间,通过平衡变脏和写入 IO 来调节脏内存的比例。

io 控制器与内存控制器结合,实现对页缓存回写 IO 的控制。内存控制器定义计算和维护脏内存比例的内存域,而 io 控制器定义为该内存域写出脏页的 io 域。系统范围和每个 cgroup 的脏内存状态都会被检查,并执行两者中更严格的一个。

cgroup 回写需要底层文件系统的显式支持。目前,cgroup 回写已在 ext2、ext4、btrfs、f2fs 和 xfs 上实现。在其他文件系统上,所有回写 IO 均归属于根 cgroup。

内存和回写管理中存在固有的差异,这会影响 cgroup 所有权的跟踪方式。内存是按页跟踪的,而回写是按 inode 跟踪的。出于回写的目的,一个 inode 被分配给一个 cgroup,所有从该 inode 写入脏页的 IO 请求都归属于该 cgroup。

由于内存的 cgroup 所有权是按页跟踪的,因此可能存在与 inode 关联的 cgroup 不同的页面。这些被称为“外部页”(foreign pages)。回写会不断跟踪外部页,如果某个特定的外部 cgroup 在一段时间内占了多数,就会将该 inode 的所有权切换到该 cgroup。

虽然该模型对于大多数用例已经足够(在这些用例中,即使主要的写入 cgroup 随着时间推移而改变,给定的 inode 也主要由单个 cgroup 弄脏),但不支持多个 cgroup 同时向单个 inode 写入的用例。在这些情况下,很大一部分 IO 可能会被错误地归属。由于内存控制器在首次使用时分配页面所有权,并且在释放页面之前不会对其进行更新,因此即使回写严格遵循页面所有权,多个 cgroup 弄脏重叠区域也不会像预期那样工作。建议避免此类使用模式。

影响回写行为的 sysctl 调节参数应用于 cgroup 回写的方式如下。

vm.dirty_background_ratio, vm.dirty_ratio

这些比例同样适用于 cgroup 回写,其可用内存量受到内存控制器所施加限制以及系统范围干净内存的上限约束。

vm.dirty_background_bytes, vm.dirty_bytes

对于 cgroup 回写,这会计算为相对于总可用内存的比例,并以与 vm.dirty[_background]_ratio 相同的方式应用。

IO 延迟

这是一个用于 IO 工作负载保护的 cgroup v2 控制器。你为一个控制组提供一个延迟目标,如果平均延迟超过该目标,控制器将限制(throttle)任何延迟目标低于该受保护工作负载的同级控制组。

限制仅在层级结构中的同级(peer)级别应用。这意味着在下图中,只有控制组 A、B 和 C 会相互影响,控制组 D 和 F 会相互影响。控制组 G 不会影响任何人

          [root]
  /          |            \
  A          B            C
 /  \        |
D    F       G

因此,配置此项的理想方式是在控制组 A、B 和 C 中设置 io.latency。通常你不会希望设置低于你设备支持的延迟的值。通过实验找到最适合你工作负载的值。从高于设备的预期延迟开始,并在启用 blkcg_debug_stats 的情况下,观察工作负载控制组在 io.stat 中的 avg_lat 值,以了解正常运行期间的延迟情况。使用 avg_lat 值作为实际设置的基础,将其设置为比 io.stat 中的值高 10-15%。

IO 延迟节流的工作原理

io.latency 是工作守恒的;因此只要所有人都达到了延迟目标,控制器就不会执行任何操作。一旦一个控制组开始无法达到其目标,它就会开始限制任何延迟目标高于自身的同级控制组。这种节流采取两种形式:

  • 队列深度限制。这是一个控制组被允许拥有的未决(outstanding)IO 数量。我们会相当迅速地进行限制,从无限制开始,一直降到一次仅允许 1 个 IO。

  • 引入人工延迟。某些类型的 IO 在受到限制时可能会对高优先级控制组产生不利影响。这包括 swap 和元数据 IO。这些类型的 IO 被允许正常进行,但它们会被“记账”到发起该 IO 的控制组中。如果发起控制组正在受到节流限制,你将看到 io.stat 中的 use_delay 和 delay 字段增加。delay 值是运行在此控制组中的任何进程被添加的微秒数。因为如果发生大量交换或元数据 IO,该数值可能会变得相当大,所以我们将单个延迟事件限制为一次最多 1 秒。

一旦受影响的控制组再次达到其延迟目标,它将开始解除先前被节流的任何同级控制组的限制。如果受影响的控制组直接停止进行 IO,全局计数器将适当地解除节流。

IO 延迟接口文件

io.latency

此文件采用了与其他控制器类似的格式。

“MAJOR:MINOR target=<以微秒为单位的目标时间>”

io.stat

如果启用了控制器,除了正常统计信息外,你还将在 io.stat 中看到额外的统计信息。这些调试统计信息仅在启用了 blkcg_debug_stats 模块参数(默认禁用)时才会输出。

depth

这是该控制组当前的队列深度。

avg_lat

这是一个以 1/exp 为衰减率并受采样间隔约束的指数移动平均值。衰减率间隔可以通过将 io.stat 中的 win 值乘以基于该 win 值的相应采样数来计算。

win

采样窗口大小,以毫秒为单位。这是评估事件之间的最小持续时间。窗口仅随 IO 活动而推移。空闲期会延长最近的窗口。

IO 优先级

单个属性控制 I/O 优先级 cgroup 策略的行为,即 io.prio.class 属性。该属性接受以下值:

no-change

不修改 I/O 优先级类别。

promote-to-rt

对于非 RT I/O 优先级类别的请求,将其更改为 RT。同时将这些请求的优先级级别更改为 4。不修改具有 RT 优先级类别的请求的 I/O 优先级。

restrict-to-be

对于没有 I/O 优先级类别或具有 RT I/O 优先级类别的请求,将其更改为 BE。同时将这些请求的优先级级别更改为 0。不修改具有 IDLE 优先级类别的请求的 I/O 优先级类别。

idle

将所有请求的 I/O 优先级类别更改为最低的 I/O 优先级类别 IDLE。

none-to-rt

已弃用。只是 promote-to-rt 的别名。

以下数值与 I/O 优先级策略相关联:

no-change

0

promote-to-rt

1

restrict-to-be

2

idle

3

对应于每个 I/O 优先级类别的数值如下:

IOPRIO_CLASS_NONE

0

IOPRIO_CLASS_RT (real-time)

1

IOPRIO_CLASS_BE (best effort)

2

IOPRIO_CLASS_IDLE

3

为一个请求设置 I/O 优先级类别的算法如下:

  • 如果 I/O 优先级类别策略是 promote-to-rt,则将请求的 I/O 优先级类别更改为 IOPRIO_CLASS_RT,并将请求的 I/O 优先级级别更改为 4。

  • 如果 I/O 优先级类别策略不是 promote-to-rt,则将该 I/O 优先级类别策略转换为一个数字,然后将请求的 I/O 优先级类别更改为该 I/O 优先级类别策略数值与数值 I/O 优先级类别中的最大值。

PID

进程数控制器用于允许 cgroup 在达到指定限制后,阻止任何新任务通过 fork() 或 clone() 创建。

cgroup 中的任务数量可能会以其他控制器无法防止的方式被耗尽,因此需要有其专有的控制器。例如,fork 炸弹很可能在触及内存限制之前就已经耗尽了任务数量。

请注意,此控制器中使用的 PID 指的是 TID,即内核所使用的线程 ID。

PID 接口文件

pids.max

存在于非根 cgroup 中的读写单值文件。默认值为 “max”。

进程数量的硬限制。

pids.current

存在于非根 cgroup 中的只读单值文件。

当前存在于该 cgroup 及其后代中的进程数量。

pids.peak

存在于非根 cgroup 中的只读单值文件。

该 cgroup 及其后代中的进程数曾达到的最大值。

pids.events

一个存在于非根 cgroup 中的只读扁平键值文件。除非另有说明,否则此文件中的值发生更改会生成一个文件修改事件。定义了以下条目。

max

该 cgroup 的进程总数达到 pids.max 限制的次数(另见 pids_localevents)。

pids.events.local

类似于 pids.events,但文件中的字段是本地于该 cgroup 的,即非层级结构。在此文件上生成的文件修改事件仅反映本地事件。

组织操作不会被 cgroup 策略阻止,因此可能会出现 pids.current > pids.max 的情况。这可以通过将限制设置为小于 pids.current,或者向该 cgroup 附加足够多的进程以使 pids.current 大于 pids.max 来实现。但是,不可能通过 fork() 或 clone() 违反 cgroup PID 策略。如果创建新进程会导致违反 cgroup 策略,这些调用将返回 -EAGAIN。

Cpuset

“cpuset”控制器提供了一种机制,用于将任务的 CPU 和内存节点分配限制在仅为任务当前 cgroup 中 cpuset 接口文件所指定的资源内。这在大型 NUMA 系统上特别有价值,通过精细的处理器和内存放置将作业放置在合适大小的系统子集上,从而减少跨节点的内存访问和竞争,可以提高整体系统性能。

“cpuset”控制器具有层级结构。这意味着控制器不能使用其父级中不被允许的 CPU 或内存节点。

Cpuset 接口文件

cpuset.cpus

一个存在于已启用 cpuset 的非根 cgroup 中的可读写多值文件。

它列出了该 cgroup 内任务请求使用的 CPU。然而,实际被授予的 CPU 列表会受到其父级施加的约束,并且可能与请求的 CPU 不同。

CPU 编号是用逗号分隔的数字或范围。例如

# cat cpuset.cpus
0-4,6,8-10

空值表示该 cgroup 正在使用与最接近的具有非空 “cpuset.cpus” 的 cgroup 祖先相同的设置;如果找不到,则使用所有可用的 CPU。

“cpuset.cpus” 的值在下一次更新之前保持不变,不会受到任何 CPU 热插拔事件的影响。

cpuset.cpus.effective

一个存在于所有已启用 cpuset 的 cgroup 中的只读多值文件。

It lists the onlined CPUs that are actually granted to this cgroup by its parent. These CPUs are allowed to be used by tasks within the current cgroup.

如果 “cpuset.cpus” 为空,则 “cpuset.cpus.effective” 文件将显示父 cgroup 中所有可供该 cgroup 使用的 CPU。否则,它应该是 “cpuset.cpus” 的子集,除非 “cpuset.cpus” 中列出的 CPU 均无法被授予。在这种情况下,它将被视为与空的 “cpuset.cpus” 相同。

其值将受到 CPU 热插拔事件的影响。

cpuset.mems

一个存在于已启用 cpuset 的非根 cgroup 中的可读写多值文件。

它列出了该 cgroup 内任务请求使用的内存节点。然而,实际授予的内存节点列表会受到其父级施加的约束,并且可能与请求的内存节点不同。

内存节点编号是用逗号分隔的数字或范围。例如

# cat cpuset.mems
0-1,3

空值表示该 cgroup 正在使用与最接近的具有非空 “cpuset.mems” 的 cgroup 祖先相同的设置;如果找不到,则使用所有可用的内存节点。

“cpuset.mems” 的值在下一次更新之前保持不变,不会受到任何内存节点热插拔事件的影响。

如果 cgroup 内的任务当前正在使用指定节点之外的内存,则为 “cpuset.mems” 设置一个非空值会导致这些任务的内存被迁移到指定的节点。

这种内存迁移是有开销的。迁移可能不会完全,一些内存页可能会留在原处。因此,建议在将新任务生成(spawn)到 cpuset 之前,先正确设置 “cpuset.mems”。即使在有活动任务的情况下需要更改 “cpuset.mems”,也不应该频繁地进行。

cpuset.mems.effective

一个存在于所有已启用 cpuset 的 cgroup 中的只读多值文件。

它列出了父级实际授予该 cgroup 的、已上线的内存节点。这些内存节点允许被当前 cgroup 内的任务使用。

如果 “cpuset.mems” 为空,则显示父 cgroup 中所有可供该 cgroup 使用的内存节点。否则,它应该是 “cpuset.mems” 的子集,除非 “cpuset.mems” 中列出的内存节点均无法被授予。在这种情况下,它将被视为与空的 “cpuset.mems” 相同。

其值将受到内存节点热插拔事件的影响。

cpuset.cpus.exclusive

一个存在于已启用 cpuset 的非根 cgroup 中的可读写多值文件。

它列出了允许用于创建新 cpuset 分区的所有独占(exclusive)CPU。除非该 cgroup 成为一个有效的分区根(partition root),否则不会使用其值。有关什么是 cpuset 分区的说明,请参阅下面的 “cpuset.cpus.partition” 部分。

当 cgroup 成为分区根时,分配给该分区的实际独占 CPU 会列在 “cpuset.cpus.exclusive.effective” 中,这可能与 “cpuset.cpus.exclusive” 不同。如果之前设置了 “cpuset.cpus.exclusive”,则 “cpuset.cpus.exclusive.effective” 始终是它的子集。

用户可以手动将其设置为与 “cpuset.cpus” 不同的值。设置时的一个约束是,CPU 列表必须与其同级控制组的 “cpuset.cpus.exclusive” 和 “cpuset.cpus.exclusive.effective” 保持互斥(独占)。另一个约束是,它不能是其同级控制组 “cpuset.cpus” 的超集,以便在拿走独占 CPU 时,为该同级控制组保留至少一个可用的 CPU。

对于父 cgroup,其任何一个独占 CPU 只能分发给最多一个子 cgroup。不允许独占 CPU 出现在两个或多个其子 cgroup 中(独占性规则)。违反独占性规则的值将被拒绝并返回写入错误。

根 cgroup 是一个分区根,其所有可用的 CPU 都包含在其独占 CPU 集合中。

cpuset.cpus.exclusive.effective

一个存在于所有非根、已启用 cpuset 的 cgroup 中的只读多值文件。

该文件显示了可用于创建分区根的有效独占 CPU 集合。如果父 cgroup 不是根 cgroup,则该文件的内容将始终是其父级 “cpuset.cpus.exclusive.effective” 的子集。如果设置了 “cpuset.cpus.exclusive”,它也将是其子集。此文件只有在设置了 “cpuset.cpus.exclusive” 或当前 cpuset 是有效的分区根时才应为非空。

cpuset.cpus.isolated

一个只读且仅存在于根 cgroup 中的多值文件。

此文件显示了在现有隔离分区中使用的所有隔离 CPU 集合。如果未创建隔离分区,它将为空。

cpuset.cpus.partition

一个存在于非根、已启用 cpuset 的 cgroup 中的可读写单值文件。该标志由父 cgroup 所有,不可委托。

写入时它仅接受以下输入值。

“member”

分区的非根成员

“root”

分区根

“isolated”

无负载均衡的分区根

cpuset 分区是启用 cpuset 的 cgroup 的集合,其层级结构顶部是一个分区根,并包含其后代,但本身作为独立分区根的 cgroup 及其后代除外。分区对其分配的独占 CPU 集合具有独占访问权。该分区之外的其他 cgroup 不能使用该集合中的任何 CPU。

分区有两种类型:本地分区和远程分区。本地分区是指其父 cgroup 也是有效分区根的分区。远程分区是指其父 cgroup 本身不是有效分区根的分区。

对于创建本地分区,写入 “cpuset.cpus.exclusive” 是可选的,因为如果未设置,其 “cpuset.cpus.exclusive” 文件将采用与 “cpuset.cpus” 相同的隐式值。而对于创建远程分区,在目标分区根之前的 cgroup 层级结构中向下写入适当的 “cpuset.cpus.exclusive” 值是强制性的。

并非 “cpuset.cpus.exclusive” 中请求的所有 CPU 都能用于组成新分区。只有存在于其父级 “cpuset.cpus.exclusive.effective” 控制文件中的 CPU 才能被使用。对于未设置 “cpuset.cpus.exclusive” 而创建的分区,在同级控制组的 “cpuset.cpus.exclusive” 或 “cpuset.cpus.exclusive.effective” 中指定的独占 CPU 也不能使用。

目前,不能在本地分区下创建远程分区。除了根 cgroup 之外,远程分区根的所有祖先都不能是分区根。

根 cgroup 始终是分区根,其状态无法更改。所有其他非根 cgroup 的初始状态均为 “member”。尽管根 cgroup 中不存在 “cpuset.cpus.exclusive*” 和 “cpuset.cpus” 控制文件,但它们隐式地与 “/sys/devices/system/cpu/possible” sysfs 文件相同。

当设置为 “root” 时,当前 cgroup 是新分区或调度域的根。独占 CPU 集合由其 “cpuset.cpus.exclusive.effective” 的值确定。

当设置为 “isolated” 时,该分区中的 CPU 将处于隔离状态,调度器不会对其进行任何负载均衡,并且会将其排除在未绑定的工作队列之外。为了获得最佳性能,放入此类具有多个 CPU 的分区中的任务应该进行仔细分配并绑定到各个单独的 CPU。

分区根(“root” 或 “isolated”)可以处于两种可能状态之一:有效或无效。无效的分区根处于降级状态,虽然可能会保留某些状态信息,但其行为更类似于 “member”。

“member”、“root” 和 “isolated” 之间所有可能的状态转换都是允许的。

在读取时,“cpuset.cpus.partition” 文件可能会显示以下值。

“member”

分区的非根成员

“root”

分区根

“isolated”

无负载均衡的分区根

“root invalid (<原因>)”

无效的分区根

“isolated invalid (<原因>)”

无效的隔离分区根

如果是无效的分区根,括号内会包含一段关于该分区为何无效的描述性字符串。

为了使本地分区根有效,必须满足以下条件。

  1. 父 cgroup 是一个有效的分区根。

  2. “cpuset.cpus.exclusive.effective” 文件不能空,尽管它可能包含下线的 CPU。

  3. “cpuset.cpus.effective” 不能空,除非没有与该分区关联的任务。

为了使远程分区根有效,除了第一条之外,必须满足上述所有条件。

诸如热插拔之类的外部事件或对 “cpuset.cpus” 或 “cpuset.cpus.exclusive” 的修改可能会导致有效的分区根变为无效,反之亦然。请注意,不能将任务移动到 “cpuset.cpus.effective” 为空的 cgroup。

当没有与之关联的任务时,一个有效的非根父分区可以将其所有的 CPU 分发给其子本地分区。

将有效的分区根更改为 “member” 时必须小心,因为其所有的子本地分区(如果存在)都将变为无效,从而对在这些子分区中运行的任务造成干扰。如果将其父级切换回具有合适 “cpuset.cpus” 或 “cpuset.cpus.exclusive” 值的分区根,这些失效的分区可以得到恢复。

每当 “cpuset.cpus.partition” 的状态发生变化时,都会触发 poll 和 inotify 事件。这包括由于写入 “cpuset.cpus.partition”、CPU 热插拔或其他修改分区有效状态的更改。这将允许用户空间代理监控对 “cpuset.cpus.partition” 的意外更改,而无需进行持续轮询。

用户可以在启动时通过 “isolcpus” 内核启动命令行选项,将某些 CPU 预配置为禁用负载均衡的隔离状态。如果要将这些 CPU 放入一个分区,则必须在隔离分区中使用它们。

设备控制器

设备控制器管理对设备文件的访问。它既包括创建新设备文件(使用 mknod),也包括访问现有的设备文件。

Cgroup v2 设备控制器没有接口文件,它是基于 cgroup BPF 实现的。为了控制对设备文件的访问,用户可以创建类型为 BPF_PROG_TYPE_CGROUP_DEVICE 的 bpf 程序,并使用 BPF_CGROUP_DEVICE 标志将其附加到 cgroup。当尝试访问设备文件时,将执行相应的 BPF 程序,并根据返回值,该尝试将成功或失败并返回 -EPERM。

BPF_PROG_TYPE_CGROUP_DEVICE 程序接受一个指向 bpf_cgroup_dev_ctx 结构的指针,该结构描述了设备访问尝试:访问类型(mknod/read/write)和设备(类型、主设备号和次设备号)。如果程序返回 0,尝试将失败并返回 -EPERM,否则成功。

在内核源码树的 tools/testing/selftests/bpf/progs/dev_cgroup.c 中可以找到 BPF_PROG_TYPE_CGROUP_DEVICE 程序的示例。

RDMA

“rdma”控制器调节 RDMA 资源的分配和记账。

RDMA 接口文件

rdma.max

一个存在于除根 cgroup 之外所有 cgroup 中的可读写嵌套键值文件,描述了当前为 RDMA/IB 设备配置的资源限制。

行以设备名称为键,且未排序。每行包含以空格分隔的资源名称及其配置的可分配限制。

定义了以下嵌套键。

hca_handle

HCA 句柄的最大数量

hca_object

HCA 对象的最大数量

以下是 mlx4 和 ocrdma 设备的示例

mlx4_0 hca_handle=2 hca_object=2000
ocrdma1 hca_handle=3 hca_object=max
rdma.current

一个描述当前资源使用情况的只读文件。它存在于除根 cgroup 之外的所有 cgroup 中。

以下是 mlx4 和 ocrdma 设备的示例

mlx4_0 hca_handle=1 hca_object=20
ocrdma1 hca_handle=1 hca_object=23
rdma.peak

一个存在于除根 cgroup 之外所有 cgroup 中的只读嵌套键值文件。它显示了自 cgroup 创建以来每个设备资源使用情况的历史最高水位线。

以下是 mlx4 和 ocrdma 设备的示例

mlx4_0 hca_handle=1 hca_object=20
ocrdma1 hca_handle=0 hca_object=23
rdma.events

一个存在于非根 cgroup 中的只读嵌套键值文件。定义了以下嵌套键。

max

该 cgroup 及其后代中的进程尝试进行 RDMA 资源分配、因触及子树中的 rdma.max 限制而被拒绝的次数。这是一个层级计数器:事件会向上流动传播到所有祖先 cgroup。此文件中的值发生更改会生成一个文件修改事件。

alloc_fail

源于该 cgroup 及其后代、由于达到 rdma.max 限制而失败的 RDMA 资源分配尝试次数。这是一个向上流动传播的层级计数器。

以下是 mlx4 设备的示例

mlx4_0 hca_handle.max=5 hca_handle.alloc_fail=3 hca_object.max=0 hca_object.alloc_fail=0
rdma.events.local

类似于 rdma.events,但文件中的字段是本地于该 cgroup 的,即非层级结构。在此文件上生成的文件修改事件仅反映本地事件。

定义了以下嵌套键。

max

该 cgroup 及其后代中的进程尝试进行 RDMA 资源分配、由于达到该 cgroup 自身的 rdma.max 限制而被拒绝的次数。

alloc_fail

源于该 cgroup、由于该 cgroup 或其祖先的 rdma.max 限制而失败的 RDMA 资源分配尝试次数。

以下是 mlx4 设备的示例

mlx4_0 hca_handle.max=5 hca_handle.alloc_fail=0 hca_object.max=0 hca_object.alloc_fail=0

DMEM

“dmem”控制器调节设备内存区域的分发和记账。因为每个内存区域可能有自己的页面大小,不一定等于系统页面大小,所以单位始终为字节。

DMEM 接口文件

dmem.max, dmem.min, dmem.low

一个存在于除根 cgroup 之外的所有 cgroup 中的可读写嵌套键值文件,描述了当前为区域配置的资源限制。

以下是 xe 的示例

drm/0000:03:00.0/vram0 1073741824
drm/0000:03:00.0/stolen max

语义与内存 cgroup 控制器相同,计算方式也相同。

dmem.capacity

一个描述最大区域容量的只读文件。它仅存在于根 cgroup 上。由于内核保留了部分内存供内部使用,并非所有内存都可以由 cgroup 分配。

以下是 xe 的示例

drm/0000:03:00.0/vram0 8514437120
drm/0000:03:00.0/stolen 67108864
dmem.current

一个描述当前资源使用情况的只读文件。它存在于除根 cgroup 之外的所有 cgroup 中。

以下是 xe 的示例

drm/0000:03:00.0/vram0 12550144
drm/0000:03:00.0/stolen 8650752

HugeTLB

HugeTLB 控制器允许限制每个控制组的 HugeTLB 使用量,并在发生缺页异常时强制执行控制器限制。

HugeTLB 接口文件

hugetlb.<hugepagesize>.current

显示“hugepagesize”规格 hugetlb 的当前使用量。它存在于除根 cgroup 之外的所有 cgroup 中。

hugetlb.<hugepagesize>.max

设置/显示“hugepagesize”规格 hugetlb 使用量的硬限制。默认值为 “max”。它存在于除根 cgroup 之外的所有 cgroup 中。

hugetlb.<hugepagesize>.events

存在于非根 cgroup 中的只读扁平键值文件。

max

由于达到 HugeTLB 限制而导致的分配失败次数

hugetlb.<hugepagesize>.events.local

类似于 hugetlb.<hugepagesize>.events,但文件中的字段是本地于该 cgroup 的,即非层级结构。在此文件上生成的文件修改事件仅反映本地事件。

hugetlb.<hugepagesize>.numa_stat

类似于 memory.numa_stat,它显示此 cgroup 中 <hugepagesize> 规格 hugetlb 页面的 NUMA 信息。仅包含正在使用的活跃 hugetlb 页面。每个节点的值以字节为单位。

杂项

杂项(Miscellaneous)cgroup 提供了针对无法像其他 cgroup 资源那样进行抽象的标量资源的限制和跟踪机制。控制器由 CONFIG_CGROUP_MISC 配置选项启用。

可以通过 include/linux/misc_cgroup.h 文件中的 enum misc_res_type{} 将资源添加到该控制器,并在 kernel/cgroup/misc.c 文件中通过 misc_res_name[] 添加相应的名称。资源的提供者在投入使用该资源前,必须通过调用 misc_cg_set_capacity() 来设置其容量。

设置容量后,可以使用记账(charge)和撤销记账(uncharge)API 来更新资源使用情况。与 misc 控制器交互的所有 API 都位于 include/linux/misc_cgroup.h 中。

杂项接口文件

杂项控制器提供了以下接口文件。如果注册了两个杂项资源(res_a 和 res_b),则

misc.capacity

一个仅在根 cgroup 中显示的只读扁平键值文件。它显示了平台上可用的各种杂项标量资源及其数量

$ cat misc.capacity
res_a 50
res_b 10
misc.current

一个在所有 cgroup 中显示的只读扁平键值文件。它显示了该 cgroup 及其子级中资源的当前使用情况。

$ cat misc.current
res_a 3
res_b 0
misc.peak

一个在所有 cgroup 中显示的只读扁平键值文件。它显示了该 cgroup 及其子级中资源的历史最大使用情况。

$ cat misc.peak
res_a 10
res_b 8
misc.max

一个在非根 cgroup 中显示的可读写扁平键值文件。允许该 cgroup 及其子级中资源的最大使用量。

$ cat misc.max
res_a max
res_b 4

可以通过以下方式设置限制

# echo res_a 1 > misc.max

可以通过以下方式将限制设置为最大值(max)

# echo res_a max > misc.max

限制值可以设置得比 misc.capacity 文件中的容量值更高。

misc.events

一个存在于非根 cgroup 中的只读扁平键值文件。定义了以下条目。除非另有说明,否则此文件中的值发生更改会生成一个文件修改事件。此文件中的所有字段都是层级结构的。

max

该 cgroup 的资源使用量即将超过最大限制的次数。

misc.events.local

类似于 misc.events,但文件中的字段是本地于该 cgroup 的,即非层级结构。在此文件上生成的文件修改事件仅反映本地事件。

迁移与所有权

一个杂项标量资源会记账到它首次被使用的 cgroup 中,并一直保持在该 cgroup 中,直到该资源被释放。将进程迁移到不同的 cgroup 不会把已记账的资源额度移动到进程所迁往的目标 cgroup。

其他

perf_event

如果 perf_event 控制器未挂载在传统(legacy)层级结构上,它会在 v2 层级结构上自动启用,以便始终可以通过 cgroup v2 路径过滤 perf 事件。在 v2 层级结构填充后,该控制器仍然可以移动到传统层级结构。

非规范性信息

本节包含不被视为稳定内核 API 的一部分、因此可能会发生变化的信息。

CPU 控制器根 cgroup 进程行为

在根 cgroup 中分配 CPU 周期时,该 cgroup 中的每个线程都被视为宿主在根 cgroup 的独立子 cgroup 中。该子 cgroup 的权重取决于其线程的 nice 级别。

有关此映射的详细信息,请参阅 kernel/sched/core.c 文件中的 sched_prio_to_weight 数组(该数组中的值应进行适当缩放,以便中性值——nice 为 0——为 100,而不是 1024)。

IO 控制器根 cgroup 进程行为

根 cgroup 进程宿主在一个隐式的叶子子节点中。在分配 IO 资源时,该隐式子节点会被纳入考虑,就好像它是根 cgroup 的一个普通子 cgroup,其权重值为 200。

命名空间

基础知识

cgroup 命名空间提供了一种机制,用于虚拟化 “/proc/$PID/cgroup” 文件和 cgroup 挂载点的视图。CLONE_NEWCGROUP 克隆标志可以与 clone(2) 和 unshare(2) 一起使用来创建新的 cgroup 命名空间。在 cgroup 命名空间内运行的进程将使其 “/proc/$PID/cgroup” 输出受限于 cgroupns 的根(cgroupns root)。cgroupns 根是创建 cgroup 命名空间时该进程所在的 cgroup。

如果没有 cgroup 命名空间,“/proc/$PID/cgroup” 文件会显示进程 cgroup 的完整路径。在旨在隔离进程的容器设置中(包含一组 cgroup 和命名空间),“/proc/$PID/cgroup” 文件可能会向被隔离的进程泄露潜在的系统级信息。例如

# cat /proc/self/cgroup
0::/batchjobs/container_id1

路径 ‘/batchjobs/container_id1’ 可以被视为系统数据,不希望暴露给隔离的进程。cgroup 命名空间可用于限制此路径的可见性。例如,在创建 cgroup 命名空间之前,用户会看到

# ls -l /proc/self/ns/cgroup
lrwxrwxrwx 1 root root 0 2014-07-15 10:37 /proc/self/ns/cgroup -> cgroup:[4026531835]
# cat /proc/self/cgroup
0::/batchjobs/container_id1

在使用 unshare 创建新命名空间后,视图会发生变化

# ls -l /proc/self/ns/cgroup
lrwxrwxrwx 1 root root 0 2014-07-15 10:35 /proc/self/ns/cgroup -> cgroup:[4026532183]
# cat /proc/self/cgroup
0::/

当多线程进程中的某个线程 unshare 其 cgroup 命名空间时,新的 cgroupns 将应用于整个进程(所有线程)。这对于 v2 层级结构是很自然的;然而,对于传统层级结构,这可能是出乎意料的。

只要内部还有进程或者有挂载点钉住(pin)它,cgroup 命名空间就会保持活跃。当最后一次使用消失时,cgroup 命名空间就会被销毁。cgroupns 根和实际的 cgroup 仍会保留。

根与视图

cgroup 命名空间的 ‘cgroupns 根’ 是调用 unshare(2) 的进程当时正在运行的 cgroup。例如,如果 /batchjobs/container_id1 cgroup 中的进程调用了 unshare,则 cgroup /batchjobs/container_id1 成为 cgroupns 根。对于 init_cgroup_ns,这是真正的根(‘/’)cgroup。

即使命名空间创建者进程后来迁移到不同的 cgroup,cgroupns 根 cgroup 也不会改变

# ~/unshare -c # unshare cgroupns in some cgroup
# cat /proc/self/cgroup
0::/
# mkdir sub_cgrp_1
# echo 0 > sub_cgrp_1/cgroup.procs
# cat /proc/self/cgroup
0::/sub_cgrp_1

每个进程都会获得其特定于命名空间的 “/proc/$PID/cgroup” 视图

在 cgroup 命名空间内运行的进程将只能在其根 cgroup 内看到 cgroup 路径(在 /proc/self/cgroup 中)。在进行 unshare 的 cgroupns 内部

# sleep 100000 &
[1] 7353
# echo 7353 > sub_cgrp_1/cgroup.procs
# cat /proc/7353/cgroup
0::/sub_cgrp_1

从初始 cgroup 命名空间中,可以看到真实的 cgroup 路径

$ cat /proc/7353/cgroup
0::/batchjobs/container_id1/sub_cgrp_1

从同级 cgroup 命名空间(即以不同 cgroup 为根的命名空间)中,将显示相对于其自身 cgroup 命名空间根的 cgroup 路径。例如,如果 PID 7353 的 cgroup 命名空间根位于 ‘/batchjobs/container_id2’,那么它将看到

# cat /proc/7353/cgroup
0::/../container_id2/sub_cgrp_1

请注意,相对路径始终以 ‘/’ 开头,以表示它是相对于调用者的 cgroup 命名空间根的。

迁移与 setns(2)

如果对外部 cgroup 具有适当的访问权限,cgroup 命名空间内的进程可以迁入和迁出命名空间根。例如,从 cgroupns 根位于 /batchjobs/container_id1 的命名空间内部,并假设全局层级结构在 cgroupns 内部仍然可访问

# cat /proc/7353/cgroup
0::/sub_cgrp_1
# echo 7353 > batchjobs/container_id2/cgroup.procs
# cat /proc/7353/cgroup
0::/../container_id2

请注意,不鼓励这种设置。cgroup 命名空间内的任务应该只暴露给其自身的 cgroupns 层级结构。

当满足以下条件时,允许通过 setns(2) 进入另一个 cgroup 命名空间:

  1. 进程对其当前用户命名空间具有 CAP_SYS_ADMIN 权限

  2. 进程对目标 cgroup 命名空间的 userns(用户命名空间)具有 CAP_SYS_ADMIN 权限

附加(attach)到另一个 cgroup 命名空间时,不会发生隐式的 cgroup 更改。预计应由某人将进行附加的进程移动到目标 cgroup 命名空间根之下。

与其他命名空间的交互

特定于命名空间的 cgroup 层级结构可以由在非初始 cgroup 命名空间内运行的进程来挂载

# mount -t cgroup2 none $MOUNT_POINT

这将把统一的 cgroup 层级结构挂载,并以 cgroupns 根作为文件系统根。进程需要对其用户命名空间和挂载命名空间(mount namespace)具有 CAP_SYS_ADMIN 权限。

/proc/self/cgroup 文件的虚拟化与通过命名空间私有的 cgroupfs 挂载来限制 cgroup 层级结构视图相结合,在容器内提供了一个妥善隔离的 cgroup 视图。

内核编程指南信息

本节包含在需要与 cgroup 交互的领域中的内核编程信息。不涵盖 cgroup 核心和控制器。

文件系统对回写的支持

文件系统可以通过更新 address_space_operations->writepages() 来支持 cgroup 回写,以便使用以下两个函数对 bio 进行标注。

wbc_init_bio(@wbc, @bio)

应当为每个携带回写数据的 bio 调用,它将 bio 与 inode 所有者的 cgroup 以及相应的请求队列关联起来。这必须在队列(设备)与 bio 关联之后、且在提交之前调用。

wbc_account_cgroup_owner(@wbc, @folio, @bytes)

应该在写出每个数据段时调用。虽然该函数并不关心它在回写会话期间确切是在何时被调用的,但最简单、最自然的方法是在将数据段添加到 bio 时调用它。

标注了回写 bio 之后,可以通过在 ->s_iflags 中设置 SB_I_CGROUPWB,从而对每个 super_block 启用 cgroup 支持。这允许选择性地禁用 cgroup 回写支持,这在某些文件系统特性(例如日志数据模式,journaled data mode)不兼容时很有帮助。

wbc_init_bio() 将指定的 bio 绑定到其 cgroup。根据配置,该 bio 可能会以较低的优先级执行,如果回写会话持有共享资源(例如日志条目),则可能会导致优先级反转。该问题没有简单的单一解决方案。文件系统可以通过跳过 wbc_init_bio() 并直接使用 bio_associate_blkg() 来尝试解决特定问题。

已弃用的 v1 核心特性

  • 不支持包括命名层级结构在内的多个层级结构。

  • 不支持所有 v1 挂载选项。

  • “tasks”文件已被移除,且 “cgroup.procs” 未经排序。

  • “cgroup.clone_children” 已被移除。

  • 对于 v2,/proc/cgroups 是毫无意义的。请改用根目录下的 “cgroup.controllers” 或 “cgroup.stat” 文件。

v1 的问题与 v2 的设计原理

多个层级结构

cgroup v1 允许任意数量的层级结构,并且每个层级结构可以托管任意数量的控制器。虽然这似乎提供了高度的灵活性,但在实践中并不实用。

例如,由于每个控制器只有唯一一个实例,因此像 freezer 这样本可以在所有层级结构中派上用场的工具型控制器只能用在其中一个层级结构中。由于一旦填充了层级结构,控制器就无法再移动到其他层级结构,从而加剧了这一问题。另一个问题是,绑定到层级结构的所有控制器都被迫具有完全相同的层级结构视图。无法根据特定控制器来改变粒度。

在实践中,这些问题严重限制了哪些控制器可以放在同一个层级结构上,大多数配置都诉诸于将每个控制器放在其自己的层级结构上。只有密切相关的控制器(例如 cpu 和 cpuacct 控制器)才有意义放在同一个层级上。这通常意味着,每当需要进行层级管理操作时,用户空间最终不得不管理多个相似的层级结构,并在每个层级上重复相同的步骤。

此外,支持多个层级结构付出了高昂的代价。它极大地复杂化了 cgroup 核心实现,但更重要的是,支持多个层级结构限制了 cgroup 的一般使用方式以及控制器所能做的事情。

层级结构的数量没有限制,这意味着无法用有限的长度来描述线程的 cgroup 成员身份。键可能包含任意数量的条目,且长度不受限制,这使得操作起来非常尴尬,并导致添加了一些仅为了识别成员身份而存在的控制器,这反过来又加剧了最初层级结构数量激增的问题。

此外,由于一个控制器无法对其他控制器可能所在的层级结构的拓扑结构做任何预期,因此每个控制器都必须假设所有其他控制器都附加在完全正交的层级结构上。这使得控制器之间协同工作变得不可能,或者至少非常繁琐。

在大多数用例中,将控制器放在完全相互正交的层级结构上是没有必要的。通常需要的是能够根据特定控制器提供不同水平的粒度。换句话说,从特定控制器来看,层级结构可以从叶子向根折叠。例如,给定的配置可能不关心超过一定级别之后的内存分布,但仍然希望控制 CPU 周期的分配。

线程粒度

cgroup v1 允许进程的线程属于不同的 cgroup。这对于某些控制器来说没有意义,这些控制器最终采用了不同的方式来忽略这种情况,但更重要的是,它模糊了向单个应用程序公开的 API 与系统管理接口之间的界限。

通常,进程内部的信息仅对进程本身可用;因此,与服务级别的进程组织不同,对进程的线程进行分类需要拥有目标进程的应用程序的主动参与。

cgroup v1 拥有定义含糊的委托(delegation)模型,并与线程粒度结合被滥用。cgroup 被委托给单个应用程序,以便它们可以创建和管理自己的子层级结构并在其中控制资源分配。这实际上将 cgroup 提升为了类似于系统调用的 API,并向普通程序公开。

首先,cgroup 的接口从根本上不适合以此种方式公开。为了让一个进程访问其自己的控制参数,它必须从 /proc/self/cgroup 中提取目标层级结构上的路径,通过在路径末尾追加参数名称来构建路径,打开然后读取和/或写入。这不仅极其笨拙且不寻常,而且本身存在竞争条件(racy)。没有常规的方法可以跨这些必要步骤来定义事务,也无法保证进程实际上是在其自己的子层级上操作。

cgroup 控制器实现了许多永远不会被接受为公共 API 的调节参数,因为它们只是向系统管理伪文件系统添加控制参数。cgroup 最终留下了未妥善抽象或提炼、且直接暴露内核内部细节的接口控制参数。这些参数通过定义不明的委托机制暴露给了单个应用程序,这实际上是将 cgroup 滥用作在不经过必要审查的情况下实现公共 API 的捷径。

这对于用户空间和内核来说都是痛苦的。用户空间最终得到了行为不正常且抽象程度不佳的接口,而内核在无意中暴露并锁死在了某些设计结构上。

内部节点与线程之间的竞争

cgroup v1 允许线程存在于任意 cgroup 中,这引发了一个有趣的问题:属于父 cgroup 的线程与其子 cgroup 会竞争资源。这很棘手,因为两种不同类型的实体相互竞争,且没有显而易见的处理方法。不同的控制器采取了不同的处理方式。

cpu 控制器将线程和 cgroup 视为同等实体,并将 nice 级别映射为 cgroup 的权重。这在某些情况下可行,但当子 cgroup 希望分配到特定比例的 CPU 周期且内部线程数量发生波动时就会失效——随着竞争实体数量的波动,这一比例也在不断变化。此外还有其他问题。从 nice 级别到权重的映射既不直观页不通用,而且还有各种其他控制开关对线程来说根本不可用。

io 控制器会为每个 cgroup 隐式创建一个隐藏的叶子节点来托管线程。该隐藏叶子节点拥有所有带有 leaf_ 前缀的控制开关的副本。虽然这允许对内部线程进行同等控制,但也带来了严重的弊端。它总是会增加一层原本不必要的嵌套,使接口变得混乱,并显著增加了实现的复杂性。

memory 控制器无法控制内部任务与子 cgroup 之间发生的情况,其行为也未明确定义。曾有一些尝试,试图通过添加临时(ad-hoc)行为和开关来针对特定工作负载定制行为,但长期来看,这会导致极难解决的问题。

多个控制器都在处理内部任务时遇到了困难,并想出了不同的应对方法;不幸的是,所有这些方法都存在严重的缺陷,此外,大相径庭的行为导致 cgroup 整体上高度不一致。

这显然是一个需要从 cgroup 核心以统一方式解决的问题。

其他接口问题

cgroup v1 在缺乏监管的情况下野蛮生长,产生了大量特异行为和不一致性。cgroup 核心端的一个问题是如何通知空 cgroup——对于每个事件,都会 fork 并执行一个用户空间辅助二进制文件。事件传递既不是递归的,也无法委派。该机制的局限性还导致了内核中的事件传递过滤机制,从而使接口进一步复杂化。

控制器接口也存在问题。一个极端的例子是,控制器完全忽略层级结构,将所有 cgroup 都视为直接位于根 cgroup 之下。一些控制器向用户空间暴露了大量不一致的实现细节。

控制器之间也缺乏一致性。当创建一个新的 cgroup 时,一些控制器默认不施加额外限制,而另一些控制器在显式配置之前则不允许使用任何资源。用于同类型控制的配置开关采用了大相径庭的命名方案和格式。统计和信息开关命名随意,甚至在同一个控制器中也使用不同的格式和单位。

cgroup v2 在适当的地方建立了通用规范,并更新了控制器,使其暴露最小且一致的接口。

控制器问题及补救措施

内存

最初的下限(即软限制 soft limit)被定义为默认不设置的限制。其结果是,全局回收(global reclaim)所偏好的 cgroup 集合是“加入”(opt-in)制,而非“退出”(opt-out)制。优化这些大多为否定的查找(negative lookups)的成本是如此之高,以至于尽管实现代码量巨大,却连最基本的期望行为都无法提供。首先,软限制没有层级意义。所有配置的 group 都被组织在一个全局红黑树(rbtree)中,并被视为平等的同级实体,而不管它们在层级结构中处于什么位置。这使得子树委派变得不可能。其次,软限制回收过程过于激进,不仅给系统引入了高分配延迟,而且由于过度回收(overreclaim)影响了系统性能,以至于该功能变得适得其反。

另一方面,memory.low 边界是自上而下分配的保留值。当 cgroup 处于其有效低值(low)以内时,它享有回收保护,这使得子树的委派成为可能。当超出其有效低值时,它所承受的回收压力与超出的部分成正比。

最初的上限(即硬限制 hard limit)被定义为一个不能动摇的严格限制,即使必须调用 OOM killer 也是如此。但这通常与充分利用可用内存的目标相违背。工作负载的内存消耗在运行期间是变化的,这需要用户进行超配(overcommit)。但在严格的上限下进行超配,要么需要对工作集大小(working set size)进行相当准确的预测,要么需要在限制上增加冗余空间。由于工作集大小的估算既困难又容易出错,一旦估算错误就会导致 OOM 杀掉进程,因此大多数用户宁可设置较宽松的限制,结果导致宝贵资源的浪费。

另一方面,memory.high 边界可以设置得保守得多。当达到该限制时,它会通过强制分配进入直接回收(direct reclaim)来消除多余部分从而限制分配速度,但它绝不会触发 OOM killer。因此,即使选择了一个过于激进的高边界,也不会终止进程,而是会导致性能逐渐下降。用户可以对此进行监控并做出修正,直到找到既能提供可接受性能又能实现最小内存占用的平衡点。

在极端情况下,如果存在大量并发分配,且该 group 内的回收进程完全崩溃,则可能会超过高边界。但即便如此,通常也比杀掉该 group 更好——即从其他 group 或系统其余部分可用的空闲空间中满足该分配需求。否则,memory.max 可以用来限制这种溢出,并最终遏制有缺陷甚至恶意的应用程序。

将原来的 memory.limit_in_bytes 设置为低于当前使用量的值时,容易受到竞态条件的影响,其中并发的记账(charges)可能导致限制设置失败。相比之下,memory.max 会先设置限制以阻止新的记账,然后进行回收和 OOM 杀进程,直到满足新的限制——或者向 memory.max 写入的任务被杀掉。

组合的内存+交换分区(memory+swap)记账和限制被对交换空间的真实控制所取代。

在最初的 cgroup 设计中,支持组合 memory+swap 功能的主要理由是,无论子 cgroup 自身的配置如何(可能不可信),全局或父级的压力始终能够换出其所有的匿名内存。然而,不可信的 cgroup 可以通过其他手段来破坏交换(swap)——例如在紧密循环中引用其匿名内存——因此管理员在超配不可信作业时,不能假设它们可以完全被换出。

另一方面,对于可信作业,组合计数器并不是一个直观的用户空间接口,而且它违背了 cgroup 控制器应该对特定物理资源进行记账和限制的基本理念。交换空间与系统中所有其他资源一样是一种资源,这就是为什么统一层级结构允许对其进行独立分配。