Control Group 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 控制器通常负责沿层次结构分配特定类型的系统资源,尽管也有一些实用程序控制器用于资源分配以外的目的。

cgroup 形成树结构,系统中的每个进程都属于一个且仅属于一个 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 的控制器状态都是异步销毁的,并且控制器可能存在挥之不去的引用,因此在上次卸载先前层次结构后,控制器可能不会立即显示在 v2 层次结构上。同样,应完全禁用控制器才能将其移出统一层次结构,并且禁用的控制器可能需要一段时间才能用于其他层次结构;此外,由于控制器之间的依赖关系,可能还需要禁用其他控制器。

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

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

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

nsdelegate

将 cgroup 命名空间视为委托边界。此选项是系统范围的,只能在挂载时设置或通过从 init 命名空间重新挂载来修改。在非 init 命名空间挂载上,将忽略挂载选项。请参阅委托部分了解详细信息。

favordynmods

降低动态 cgroup 修改(例如任务迁移和控制器开关)的延迟,但代价是使热路径操作(例如 fork 和 exit)更加昂贵。创建 cgroup、启用控制器,然后使用 CLONE_INTO_CGROUP 播种它的静态使用模式不受此选项的影响。

memory_localevents

仅使用当前 cgroup 的数据填充 memory.events,而不填充任何子树。这是旧版行为,没有此选项的默认行为是包括子树计数。此选项是系统范围的,只能在挂载时设置或通过从 init 命名空间重新挂载来修改。在非 init 命名空间挂载上,将忽略挂载选项。

memory_recursiveprot

递归地将 memory.min 和 memory.low 保护应用于整个子树,而无需显式向下传播到叶 cgroup。这允许保护整个子树彼此隔离,同时保留这些子树内的自由竞争。这应该是默认行为,但它是一个挂载选项,用于避免依赖原始语义的设置(例如,在更高的树级别上指定虚假的高“绕过”保护值)。

memory_hugetlb_accounting

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

需要记住的一些注意事项

  • 内存控制器不涉及 HugeTLB 池管理。预分配的池不属于任何人。具体来说,当一个新的 HugeTLB 页面被分配到池中时,从内存控制器的角度来看,它不会被计算在内。只有在实际使用时(例如在页面错误时),才会向 cgroup 收费。在配置硬限制时,主机内存过度提交管理必须考虑这一点。一般来说,HugeTLB 池管理应该通过其他机制(例如 HugeTLB 控制器)来完成。

  • 未能向内存控制器收费 HugeTLB 页面会导致 SIGBUS。即使 HugeTLB 池仍然有可用页面(但达到 cgroup 限制且回收尝试失败),也可能发生这种情况。

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

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

pids_localevents

该选项恢复了 pids.events:max 的 v1 式行为,即仅计算本地(cgroup 内部)的 fork 失败。如果没有此选项,pids.events.max 表示 cgroup 子树中的任何 pids.max 强制执行。

组织进程和线程

进程

最初,只存在根 cgroup,所有进程都属于该 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 一个子进程时,新进程会出生到 fork 进程在操作时所属的 cgroup 中。退出后,一个进程会一直与它在退出时所属的 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 也充当托管非特定于进程或线程的资源消耗的资源域。线程模式允许线程分布在子树中,同时仍然保持它们的公共资源域。

支持线程模式的控制器称为线程控制器。不支持线程模式的控制器称为域控制器。

将 cgroup 标记为线程化会使其加入其父级的资源域,作为线程化的 cgroup。父级可能是另一个线程化的 cgroup,其资源域在层次结构中更高。线程化子树的根,即最近的非线程化祖先,被称为线程域或线程根,可以互换使用,并充当整个子树的资源域。

在线程化子树中,进程的线程可以放在不同的 cgroup 中,并且不受内部进程约束的限制 - 无论非叶 cgroup 中是否有线程,都可以在这些 cgroup 上启用线程控制器。

由于线程化域 cgroup 托管子树的所有域资源消耗,因此它被认为具有内部资源消耗,无论其中是否有进程,并且不能有未线程化的子 cgroup。由于根 cgroup 不受内部进程约束的限制,因此它可以同时充当线程化域和域 cgroup 的父级。

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

创建时,cgroup 始终是域 cgroup,可以通过将 “threaded” 写入 “cgroup.type” 文件来将其线程化。该操作是单向的

# echo threaded > cgroup.type

一旦线程化,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.subtree_control” 文件中启用了线程控制器并且 cgroup 中存在进程时,该域 cgroup 将转换为线程化域。当条件清除时,线程化域将恢复为普通域。

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

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

只有线程控制器可以在线程化子树中启用。当线程控制器在线程化子树中启用时,它仅对与 cgroup 及其后代中的线程关联的资源消耗进行记帐和控制。所有未绑定到特定线程的消耗都属于线程化域 cgroup。

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

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

- cpu
- cpuset
- perf_event
- pids

[未]填充通知

每个非根 cgroup 都有一个 “cgroup.events” 文件,其中包含 “populated” 字段,指示 cgroup 的子层次结构中是否存在活动进程。如果 cgroup 及其后代中没有活动进程,则其值为 0;否则为 1。当值更改时,会触发轮询和 [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” 文件上生成文件修改事件。

控制控制器

启用和禁用

每个 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.subtree_control” 文件只能包含在父级的 “cgroup.subtree_control” 文件中启用的控制器。只有在父级启用了控制器的情况下才能启用控制器,并且如果一个或多个子级启用了控制器,则无法禁用控制器。

无内部进程约束

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

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

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

请注意,如果 cgroup 的 “cgroup.subtree_control” 中没有启用的控制器,则该限制不会妨碍。这很重要,否则将无法创建已填充 cgroup 的子级。要控制 cgroup 的资源分配,cgroup 必须创建子级并在其 “cgroup.subtree_control” 文件中启用控制器之前将其所有进程转移到子级。

委托

委托模型

可以通过两种方式委托 cgroup。首先,通过授予用户目录及其 “cgroup.procs”、“cgroup.threads” 和 “cgroup.subtree_control” 文件的写入权限,将其委托给权限较低的用户。其次,如果设置了 “nsdelegate” 挂载选项,则在创建命名空间时自动委托给 cgroup 命名空间。

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

对于两种委托类型,最终结果是等效的。委托后,用户可以在目录下构建子层次结构,按照自己的意愿组织内部进程,并进一步分配从父级收到的资源。所有资源控制器的限制和其他设置都是分层的,并且无论委托的子层次结构中发生什么,都不会逃脱父级施加的资源限制。

目前,cgroup 不对委托的子层次结构中的 cgroup 数量或嵌套深度施加任何限制;但是,将来可能会明确限制这一点。

委托包含

委托的子层次结构是包含的,因为进程不能由被委托者移入或移出子层次结构。

对于委托给权限较低的用户,这是通过要求非 root euid 的进程满足以下条件来实现的,即通过将其 PID 写入 “cgroup.procs” 文件来将目标进程迁移到 cgroup。

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

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

以上两个约束确保了虽然被委托者可以在委托的子层次结构中自由迁移进程,但它不能从子层次结构之外拉入或推出。

例如,让我们假设 cgroup C0 和 C1 已委托给用户 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 都可以从尝试迁移的进程的命名空间访问来实现的。如果任何一个都无法访问,则迁移将被拒绝,并显示 -ENOENT。

准则

组织一次并控制

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

因此,不鼓励经常跨 cgroup 迁移进程,以此作为应用不同资源限制的手段。工作负载应根据系统在启动时的逻辑和资源结构分配给 cgroup。可以通过更改接口文件中的控制器配置来动态调整资源分配。

避免名称冲突

cgroup 及其子 cgroup 的接口文件占用相同的目录,并且可以创建与接口文件冲突的子 cgroup。

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

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

资源分配模型

cgroup 控制器根据资源类型和预期用例实现多种资源分配方案。本节介绍使用中的主要方案及其预期行为。

权重

通过将所有活动子级的权重相加,然后将每个子级的权重与其权重与总和的比率相匹配的分数,来分配父级的资源。由于只有当前可以使用该资源的子级参与分配,因此这是工作量守恒的。由于其动态特性,此模型通常用于无状态资源。

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

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

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

限制

子级只能消耗高达配置数量的资源。限制可以过度提交 - 子级的限制之和可以超过可供父级使用的资源量。

限制在 [0, max] 范围内,默认为 “max”,即无操作。

由于限制可以过度提交,因此所有配置组合都是有效的,并且没有理由拒绝配置更改或进程迁移。

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

保护

只要其所有祖先的使用量都在其保护级别之下,cgroup 就会受到高达配置数量的资源保护。保护可以是硬保证或尽力而为的软边界。保护也可以过度提交,在这种情况下,只有高达父级可用资源量的资源才能在子级之间受到保护。

保护在 [0, max] 范围内,默认为 0,即无操作。

由于保护可以过度提交,因此所有配置组合都是有效的,并且没有理由拒绝配置更改或进程迁移。

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

分配

cgroup 专门分配一定数量的有限资源。分配不能过度提交 - 子级的分配之和不能超过可供父级使用的资源量。

分配在 [0, max] 范围内,默认为 0,即无资源。

由于分配不能过度提交,因此某些配置组合是无效的,应拒绝。此外,如果该资源对于进程的执行是强制性的,则可以拒绝进程迁移。

“cpu.rt.max” 硬分配实时切片,是这种类型的一个示例。

接口文件

格式

所有接口文件应尽可能采用以下格式之一

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...
      ...

对于可写文件,写入格式通常应与读取格式匹配;但是,控制器可以允许省略后面的字段或为最常见的用例实施受限快捷方式。

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

约定

  • 单个功能的设置应包含在单个文件中。

  • 根 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

正在消亡的后代 cgroup 的总数。在用户删除 cgroup 后,该 cgroup 将变为消亡状态。cgroup 将在消亡状态中保持一段时间(时间未定义,可能取决于系统负载),然后才会被完全销毁。

在任何情况下,进程都不能进入消亡 cgroup,消亡 cgroup 也不能复活。

消亡 cgroup 可以消耗不超过删除 cgroup 时活动的限制的系统资源。

nr_subsys_<cgroup_subsys>

当前 cgroup 及其下方的活动 cgroup 子系统(例如,内存 cgroup)的总数。

nr_dying_subsys_<cgroup_subsys>

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

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 周期的分配。此控制器实现了正常调度策略的权重和绝对带宽限制模型,以及实时调度策略的绝对带宽分配模型。

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

警告:cgroup2 cpu 控制器尚未支持实时进程的(带宽)控制。对于启用 CONFIG_RT_GROUP_SCHED 选项以进行实时进程组调度的内核,只有当所有 RT 进程都位于根 cgroup 中时,才能启用 cpu 控制器。请注意,系统管理软件可能已在系统启动过程中将 RT 进程放入非根 cgroup 中,并且在启用启用了 CONFIG_RT_GROUP_SCHED 的内核之前,可能需要将这些进程移动到根 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”。

突发范围为 [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 本身将被视为相对于其对等方的非常低的优先级。

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

内存

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

虽然不是完全水密的,但会跟踪给定 cgroup 的所有主要内存使用情况,以便可以将总内存消耗记入帐并控制到合理的程度。目前,跟踪以下类型的内存使用情况。

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

  • 内核数据结构,如目录项和 inode。

  • TCP 套接字缓冲区。

上述列表将来可能会扩展以获得更好的覆盖范围。

内存接口文件

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

memory.current

一个只读单值文件,存在于非根 cgroup 上。

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

memory.min

一个读写单值文件,存在于非根 cgroup 上。默认值为“0”。

硬内存保护。如果 cgroup 的内存使用量在其有效最小边界内,则在任何情况下都不会回收 cgroup 的内存。如果没有未受保护的可回收内存可用,则会调用 OOM killer。高于有效最小边界(或有效低边界(如果较高)),页面将按超额比例回收,从而降低较小超额的回收压力。

有效最小边界受所有祖先 cgroup 的 memory.min 值限制。如果存在 memory.min 过度承诺(子 cgroup 或多个 cgroup 需要的受保护内存多于父级允许的内存),则每个子 cgroup 将获得父级保护的一部分,该部分与其低于 memory.min 的实际内存使用量成比例。

不鼓励在此保护下放置比通常可用的内存更多的内存,并且可能导致持续的 OOM。

如果内存 cgroup 未填充进程,则会忽略其 memory.min。

memory.low

一个读写单值文件,存在于非根 cgroup 上。默认值为“0”。

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

有效低边界受所有祖先 cgroup 的 memory.low 值限制。如果存在 memory.low 过度承诺(子 cgroup 或多个 cgroup 需要的受保护内存多于父级允许的内存),则每个子 cgroup 将获得父级保护的一部分,该部分与其低于 memory.low 的实际内存使用量成比例。

不鼓励在此保护下放置比通常可用的内存更多的内存。

memory.high

一个读写单值文件,存在于非根 cgroup 上。默认值为“max”。

内存使用率限制。如果 cgroup 的使用率超过高边界,则会限制 cgroup 的进程并使其承受繁重的回收压力。

超过高限制永远不会调用 OOM killer,并且在极端情况下,可能会违反该限制。高限制应用于外部进程监视受限 cgroup 以减轻繁重的回收压力的场景。

如果 memory.high 以 O_NONBLOCK 打开,则会绕过同步回收。这对于需要动态调整作业内存限制的管理员进程很有用,而无需花费自己的 CPU 资源进行内存回收。作业将在其下一个收费请求时触发回收和/或受到限制。

请注意,使用 O_NONBLOCK,目标内存 cgroup 可能需要无限的时间才能将使用率降低到低于限制,这是因为延迟收费请求或忙于访问其内存以减慢回收速度。

memory.max

一个读写单值文件,存在于非根 cgroup 上。默认值为“max”。

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

在默认配置中,除非 OOM killer 选择当前任务作为牺牲品,否则常规 0 阶分配始终会成功。

某些类型的分配不会调用 OOM killer。调用者可以以不同的方式重试它们,作为 -ENOMEM 返回到用户空间,或者在诸如磁盘预读之类的情况下静默忽略。

如果 memory.max 以 O_NONBLOCK 打开,则会绕过同步回收和 oom-kill。这对于需要动态调整作业内存限制的管理员进程很有用,而无需花费自己的 CPU 资源进行内存回收。作业将在其下一个收费请求时触发回收和/或 oom-kill。

请注意,使用 O_NONBLOCK,目标内存 cgroup 可能需要无限的时间才能将使用率降低到低于限制,这是因为延迟收费请求或忙于访问其内存以减慢回收速度。

memory.reclaim

一个只写嵌套键文件,存在于所有 cgroup 上。

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

示例

echo "1G" > memory.reclaim

请注意,内核可以从目标 cgroup 中过度或不足地回收内存。如果回收的字节数少于指定的数量,则返回 -EAGAIN。

请注意,主动回收(由此接口触发)并不表示内存 cgroup 上的内存压力。因此,通常在这种情况下不会执行由内存回收触发的套接字内存平衡。这意味着网络层不会根据内存.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 killer 是否应将 cgroup 视为不可分割的工作负载。如果设置,则属于 cgroup 或其后代的所有任务(如果内存 cgroup 不是叶 cgroup)将一起杀死或根本不杀死。这可以用于避免部分杀死,以保证工作负载的完整性。

具有 OOM 保护的任务(oom_score_adj 设置为 -1000)被视为例外,并且永远不会被杀死。

如果在 cgroup 中调用了 OOM killer,则无论祖先 cgroup 的 memory.oom.group 值如何,它都不会杀死此 cgroup 之外的任何任务。

memory.events

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

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

low

由于内存压力过大,即使 cgroup 的使用率低于低边界,也会回收 cgroup 的次数。这通常表明低边界已过度承诺。

high

由于超过了高内存边界,因此限制 cgroup 的进程并将其路由以执行直接内存回收的次数。对于内存使用率受高限制而不是全局内存压力限制的 cgroup,预计会发生此事件。

max

cgroup 的内存使用率即将超过最大边界的次数。如果直接回收无法将其降低,则 cgroup 将进入 OOM 状态。

oom

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

如果 OOM killer 不被视为一种选择,例如对于失败的高阶分配或调用者要求不要重试尝试,则不会引发此事件。

oom_kill

属于此 cgroup 的进程被任何类型的 OOM killer 杀死的次数。

oom_group_kill

发生组 OOM 的次数。

memory.events.local

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

memory.stat

一个只读扁平键文件,存在于非根 cgroup 上。

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

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

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

如果该条目没有每个节点计数器(或未在 memory.numa_stat 中显示)。我们使用“npn”(非每个节点)作为标记来指示它不会在 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 (npn)

用于 vmap 支持的内存的内存量。

shmem

已缓存的文件系统数据量,这些数据由交换空间支持,例如 tmpfs、shm 段、共享匿名 mmap()。

zswap

zswap 压缩后端使用的内存量。

zswapped

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

file_mapped

使用 mmap() 映射的已缓存文件系统数据量。请注意,如果只有一些而不是所有此类分配的内存不再映射,则某些内核配置可能会考虑完整的较大分配(例如,THP)。

file_dirty

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

file_writeback

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

swapcached

缓存在内存中的交换空间量。交换缓存同时计入内存和交换空间使用量。

anon_thp

由透明大页支持的匿名映射中使用的内存量

file_thp

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

shmem_thp

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

inactive_anon, active_anon, inactive_file, active_file, unevictable

页面回收算法使用的内部内存管理列表上的内存量,包括交换空间支持和文件系统支持。

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

slab_reclaimable

“slab”中可以回收的部分,例如目录项和 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)

回收的延迟释放页面的数量

swpin_zero

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

swpout_zero

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

zswpin

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

zswpout

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

zswpwb

从 zswap 写入到交换空间的页面数量。

thp_fault_alloc (npn)

分配用于满足缺页中断的透明巨页的数量。 如果未设置 CONFIG_TRANSPARENT_HUGEPAGE,则不存在此计数器。

thp_collapse_alloc (npn)

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

thp_swpout (npn)

未分割的情况下,以一个整体进行交换输出的透明巨页数量。

thp_swpout_fallback (npn)

交换输出之前分割的透明巨页的数量。通常是因为未能为大页分配一些连续的交换空间。

numa_pages_migrated (npn)

NUMA 平衡迁移的页面数。

numa_pte_updates (npn)

通过 NUMA 平衡修改其页表条目的页面数,以便在访问时产生 NUMA 提示错误。

numa_hint_faults (npn)

NUMA 提示错误的数量。

numa_task_migrated (npn)

NUMA 平衡进行的任务迁移次数。

numa_task_swapped (npn)

NUMA 平衡进行的任务交换次数。

pgdemote_kswapd

kswapd 降级的页面数。

pgdemote_direct

直接降级的页面数。

pgdemote_khugepaged

khugepaged 降级的页面数。

pgdemote_proactive

主动降级的页面数。

hugetlb

hugetlb 页面使用的内存量。 只有在 memory.current 中计算 hugetlb 使用情况时才显示此指标(即,使用 memory_hugetlb_accounting 选项挂载 cgroup)。

memory.numa_stat

只读嵌套键文件,存在于非根 cgroup 上。

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

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

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

memory.numa_stat 的输出格式为

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

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

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

memory.swap.current

一个只读单值文件,存在于非根 cgroup 上。

cgroup 及其后代当前使用的交换空间总量。

memory.swap.high

一个读写单值文件,存在于非根 cgroup 上。默认值为“max”。

交换空间使用量限制。如果 cgroup 的交换空间使用量超过此限制,则将限制其所有进一步的分配,以允许用户空间实施自定义的内存不足程序。

此限制标志着 cgroup 的不归路。它不是为了管理工作负载在正常操作期间进行的交换量而设计的。与 memory.swap.max 相比,后者禁止交换超出设定数量,但只要可以回收其他内存,就允许 cgroup 继续不受阻碍地运行。

不希望健康的工作负载达到此限制。

memory.swap.peak

一个读写单值文件,存在于非根 cgroup 上。

自 cgroup 创建或最近一次重置该 FD 以来,为 cgroup 及其后代记录的最大交换空间使用量。

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

memory.swap.max

一个读写单值文件,存在于非根 cgroup 上。默认值为“max”。

交换空间硬限制。如果 cgroup 的交换空间使用量达到此限制,则不会交换出 cgroup 的匿名内存。

memory.swap.events

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

high

cgroup 的交换空间使用量超过高阈值的次数。

max

cgroup 的交换空间使用量即将超过最大边界且交换空间分配失败的次数。

fail

由于系统范围内的交换空间不足或达到最大限制而导致交换空间分配失败的次数。

当减少到低于当前使用量时,现有交换空间条目会逐渐回收,并且交换空间使用量可能会在较长时间内保持高于限制。这减少了对工作负载和内存管理的影响。

memory.zswap.current

一个只读单值文件,存在于非根 cgroup 上。

zswap 压缩后端使用的内存总量。

memory.zswap.max

一个读写单值文件,存在于非根 cgroup 上。默认值为“max”。

Zswap 使用量硬限制。 如果 cgroup 的 zswap 池达到此限制,它将拒绝接受任何更多存储,直到现有条目错误地返回或写入磁盘。

memory.zswap.writeback

一个读写单值文件。 默认值为 “1”。 请注意,此设置是分层的,即,如果较高层级这样做,则将隐式禁用子 cgroup 的回写。

当此设置为 0 时,所有交换尝试都将禁用为交换设备。 这包括 zswap 回写和由于 zswap 存储故障导致的交换。 如果 zswap 存储故障反复发生(例如,如果页面不可压缩),则用户可以在禁用回写后观察到回收效率低下(因为相同的页面可能会一遍又一遍地被拒绝)。

请注意,这与将 memory.swap.max 设置为 0 略有不同,因为它仍然允许将页面写入 zswap 池。 如果禁用 zswap,则此设置无效,除非将 memory.swap.max 设置为 0,否则允许交换。

memory.pressure

只读嵌套键文件。

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

使用指南

“memory.high” 是控制内存使用量的主要机制。 在高限制上过度提交(高限制之和 > 可用内存)并让全局内存压力根据使用情况分配内存是一种可行的策略。

由于违反高限制不会触发 OOM killer,而是限制违规的 cgroup,因此管理代理有足够的机会进行监视并采取适当的措施,例如授予更多内存或终止工作负载。

确定 cgroup 是否有足够的内存并不简单,因为内存使用量并不能表明工作负载是否可以从更多内存中受益。 例如,将从网络接收的数据写入文件的工作负载可以使用所有可用内存,但也可以使用少量内存进行同样高效的运行。 需要一种衡量内存压力的方法 - 由于缺少内存而对工作负载产生多大影响 - 以确定工作负载是否需要更多内存; 遗憾的是,尚未实现内存压力监控机制。

内存所有权

内存区域被计入实例化它的 cgroup,并一直计入该 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

丢弃的字节数

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),该控制器当前实现 “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 个百分位数高于 75 毫秒或写入 150 毫秒,则控制器已启用,将认为设备已饱和,并相应地调整 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 大小的成本系数。 虽然简单,但此模型可以令人满意地覆盖大多数常见设备类别。

IO 成本模型预计在绝对意义上并不准确,并且会动态缩放到设备行为。

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

io.weight

一个读写平面键文件,存在于非根 cgroup 上。 默认值为 “default 100”。

第一行是应用于没有特定覆盖的设备的默认权重。 其余的是由 $MAJ:$MIN 设备号键控的覆盖,并且未排序。 权重范围为 [1, 10000],并指定 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

回写

页面缓存通过缓冲写入和共享 mmaps 变脏,并由回写机制异步写入到后备文件系统。 回写位于内存和 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 不同的页面。 这些称为外来页面。 回写不断跟踪外来页面,并且如果特定的外来 cgroup 在一段时间内占据多数,则会将 inode 的所有权切换到该 cgroup。

虽然此模型对于给定的 inode 主要由单个 cgroup 弄脏的大多数用例来说已经足够了,即使主要写入 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 控制器。 您为组提供一个延迟目标,如果平均延迟超过该目标,则控制器将限制任何延迟目标低于受保护工作负载的对等方。

这些限制仅在层次结构中的对等级别应用。 这意味着在下图中,只有组 A、B 和 C 会相互影响,并且组 D 和 F 会相互影响。 组 G 不会影响任何人

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

因此,配置此功能的理想方式是在组 A、B 和 C 中设置 io.latency。 通常,您不希望设置低于您的设备支持的延迟的值。 进行实验以找到最适合您的工作负载的值。 从高于您的设备的预期延迟开始,并观察您的工作负载组的 io.stat 中的 avg_lat 值,以了解您在正常操作期间看到的延迟。 使用 avg_lat 值作为您的实际设置的基础,设置为高于 io.stat 中的值 10-15%。

IO 延迟限制的工作原理

io.latency 是工作保守的; 因此,只要每个人都满足其延迟目标,控制器就不会执行任何操作。 一旦组开始错过其目标,它就会开始限制任何目标高于自身的对等组。 这种限制采用 2 种形式

  • 队列深度限制。 这是允许组拥有的未完成 IO 的数量。 我们将相对快速地进行钳制,从没有限制开始,一直到一次 1 个 IO。

  • 人工延迟诱导。 有些类型的 IO 如果不受到限制,可能会对优先级较高的组产生不利影响。 这包括交换和元数据 IO。 允许正常发生这些类型的 IO,但是它们会“收费”到原始组。 如果原始组受到限制,您将看到 io.stat 中的 use_delay 和 delay 字段增加。 delay 值是添加到在此组中运行的任何进程的微秒数。 因为如果有大量交换或元数据 IO 发生,此数字可能会变得非常大,所以我们将各个延迟事件限制为一次 1 秒。

一旦受害者组再次开始满足其延迟目标,它将开始取消限制先前受到限制的任何对等组。 如果受害者组只是停止执行 IO,则全局计数器将适当地取消限制。

IO 延迟接口文件

io.latency

这采用与其他控制器相似的格式。

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

io.stat

如果启用了控制器,您将在 io.stat 中看到额外的统计信息以及正常的统计信息。

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 优先级类或具有 I/O 优先级类 RT 的请求,将其更改为 BE。 还要将这些请求的优先级级别更改为 0。 不修改具有优先级类 IDLE 的请求的 I/O 优先级类。

idle

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

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 (实时)

1

IOPRIO_CLASS_BE (尽力而为)

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 上。

它列出了其父级实际授予此 cgroup 的联机 CPU。当前 cgroup 中的任务允许使用这些 CPU。

如果“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”的值保持不变,直到下次更新,并且不会受到任何内存节点热插拔事件的影响。

将非空值设置为“cpuset.mems”会导致 cgroup 中任务的内存迁移到指定的节点,如果它们当前正在使用指定节点之外的内存。

此内存迁移会产生费用。迁移可能不完整,并且可能会留下一些内存页。因此,建议在将新任务生成到 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 分区的独占 CPU。除非 cgroup 成为有效的分区根目录,否则不使用其值。有关 cpuset 分区的描述,请参见下面的“cpuset.cpus.partition”部分。

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

用户可以手动将其设置为与“cpuset.cpus”不同的值。设置它的一个约束是 CPU 列表必须相对于其同级的“cpuset.cpus.exclusive”是独占的。如果未设置同级 cgroup 的“cpuset.cpus.exclusive”,则其“cpuset.cpus”值(如果已设置)不能是它的子集,以便在删除独占 CPU 时至少保留一个可用的 CPU。

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

根 cgroup 是分区根目录,其所有可用 CPU 都在其独占 CPU 集中。

cpuset.cpus.exclusive.effective

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

此文件显示可用于创建分区根目录的有效独占 CPU 集。如果其父级不是根 cgroup,则此文件的内容始终是其父级的“cpuset.cpus.exclusive.effective”的子集。如果已设置,它也将是“cpuset.cpus.exclusive”的子集。如果未设置“cpuset.cpus.exclusive”,则在形成本地分区时,将其视为具有“cpuset.cpus”的隐式值。

cpuset.cpus.isolated

一个只读且仅根 cgroup 的多值文件。

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

cpuset.cpus.partition

一个读写单值文件,存在于启用 cpuset 的非根 cgroup 上。此标志由父 cgroup 拥有,并且不可委托。

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

“member”

分区的非根成员

“root”

分区根目录

“isolated”

没有负载平衡的分区根目录

cpuset 分区是启用 cpuset 的 cgroup 的集合,其层次结构的顶部有一个分区根目录,以及它的后代,除了那些单独的分区根目录及其后代。分区具有对其分配的独占 CPU 集的独占访问权限。该分区之外的其他 cgroup 不能使用该集合中的任何 CPU。

有两种类型的分区 - 本地和远程。本地分区是指其父 cgroup 也是有效分区根目录的分区。远程分区是指其父 cgroup 本身不是有效分区根目录的分区。对于本地分区的创建,写入“cpuset.cpus.exclusive”是可选的,因为如果未设置其“cpuset.cpus.exclusive”文件,则将采用与其“cpuset.cpus”相同值的隐式值。在目标分区根目录之前,沿 cgroup 层次结构向下写入正确的“cpuset.cpus.exclusive”值对于远程分区的创建是强制性的。

目前,无法在本地分区下创建远程分区。远程分区根目录的所有祖先(根 cgroup 除外)都不能是分区根目录。

根 cgroup 始终是分区根目录,并且其状态无法更改。所有其他非根 cgroup 都从“member”开始。

设置为“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 (<reason>)”

无效的分区根目录

“isolated invalid (<reason>)”

无效的隔离分区根目录

在无效分区根目录的情况下,括号内包含一个描述该分区为何无效的描述性字符串。

要使本地分区根目录有效,必须满足以下条件。

  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”的状态更改时,都会触发轮询和 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,否则它将成功。

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

RDMA

“rdma”控制器规范了 RDMA 资源的分配和记帐。

RDMA 接口文件

rdma.max

一个读写嵌套键文件,除根目录外,所有 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 都存在该文件。

以下是 mlx4 和 ocrdma 设备的示例

mlx4_0 hca_handle=1 hca_object=20
ocrdma1 hca_handle=1 hca_object=23

DMEM

“dmem”控制器规范了设备内存区域的分配和记帐。因为每个内存区域可能具有自己的页面大小,该页面大小不必等于系统页面大小,所以单位始终为字节。

DMEM 接口文件

dmem.max, dmem.min, dmem.low

一个读写嵌套键文件,除根目录外,所有 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 都存在该文件。

以下是 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 都存在该文件。

hugetlb.<hugepagesize>.max

设置/显示 “hugepagesize” hugetlb 使用量的硬限制。默认值为 “max”。除根目录外,所有 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 页面。每个节点的值以字节为单位。

Misc

杂项 cgroup 提供了标量资源的资源限制和跟踪机制,这些资源无法像其他 cgroup 资源那样被抽象化。通过 CONFIG_CGROUP_MISC config 选项启用控制器。

可以通过 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 中。

Misc 接口文件

杂项控制器提供 3 个接口文件。如果注册了两个杂项资源(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

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

# 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 控制器未安装在旧版层次结构上,则会在 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 标志可以与 clone(2) 和 unshare(2) 一起使用,以创建新的 cgroup 命名空间。在 cgroup 命名空间内运行的进程将使其 “/proc/$PID/cgroup” 输出限制为 cgroupns 根目录。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

在取消共享新的命名空间后,视图会更改

# 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::/

当多线程进程中的某个线程取消共享其 cgroup 命名空间时,新的 cgroupns 将应用于整个进程(所有线程)。对于 v2 层次结构来说,这是很自然的;但是,对于旧版层次结构来说,这可能是意外的。

只要有进程在内部运行或挂载固定,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 中)。从取消共享的 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 命名空间的用户空间具有 CAP_SYS_ADMIN

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

与其他命名空间的交互

命名空间特定的 cgroup 层次结构可以由在非 init cgroup 命名空间内运行的进程挂载

# mount -t cgroup2 none $MOUNT_POINT

这将挂载统一的 cgroup 层次结构,并将 cgroupns 根目录作为文件系统根目录。该进程需要针对其用户和挂载命名空间具有 CAP_SYS_ADMIN。

虚拟化 /proc/self/cgroup 文件以及通过命名空间专用 cgroupfs 挂载限制 cgroup 层次结构的视图,可在容器内部提供正确隔离的 cgroup 视图。

有关内核编程的信息

本节包含内核编程信息,这些信息需要在与 cgroup 交互的区域中。不包括 cgroup 核心和控制器。

文件系统对回写的支持

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

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” 已删除。

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

v1 的问题以及 v2 的理由

多个层次结构

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

例如,由于每个控制器只有一个实例,因此像 freezer 这样的实用程序类型控制器(在所有层次结构中都可能有用)只能在一个中使用。当层次结构填充后,控制器无法移动到另一个层次结构,这使得问题更加严重。另一个问题是,绑定到层次结构的所有控制器都被迫具有完全相同的层次结构视图。无法根据特定控制器改变粒度。

在实践中,这些问题严重限制了哪些控制器可以放在同一层次结构上,并且大多数配置都选择将每个控制器放在其自己的层次结构上。只有密切相关的控制器,例如 cpu 和 cpuacct 控制器,才适合放在同一层次结构上。这通常意味着用户空间最终会管理多个相似的层次结构,并在每次需要层次结构管理操作时在每个层次结构上重复相同的步骤。

此外,对多个层次结构的支持付出了巨大的代价。它极大地复杂化了 cgroup 核心实现,但更重要的是,对多个层次结构的支持限制了 cgroup 的总体使用方式以及控制器能够做什么。

对可能有多少层次结构没有限制,这意味着无法以有限的长度描述线程的 cgroup 成员身份。键可能包含任意数量的条目,并且长度不受限制,这使得操作起来非常麻烦,并导致添加仅用于识别成员身份的控制器,这反过来又加剧了层次结构数量激增的原始问题。

此外,由于控制器无法对其他控制器可能位于的层次结构的拓扑结构有任何期望,因此每个控制器都必须假设所有其他控制器都连接到完全正交的层次结构。这使得控制器之间不可能或者至少非常难以相互协作。

在大多数用例中,将控制器放置在彼此完全正交的层次结构上是不必要的。通常需要的是根据特定控制器具有不同级别的粒度的能力。换句话说,从特定控制器看,可以从叶子向根折叠层次结构。例如,给定的配置可能不关心内存如何在特定级别之外分配,但仍然希望控制 CPU 周期如何分配。

线程粒度

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

通常,进程内的知识仅对进程本身可用;因此,与进程的服务级组织不同,对进程线程进行分类需要拥有目标进程的应用程序的积极参与。

cgroup v1 有一个模糊定义的委派模型,该模型与线程粒度结合使用时遭到了滥用。cgroup 被委派给各个应用程序,以便它们可以创建和管理自己的子层次结构,并控制沿它们的资源分配。这实际上将 cgroup 提升为类似系统调用的 API 的地位,该 API 暴露给普通程序。

首先,cgroup 具有从根本上不充分的接口,无法以这种方式暴露。为了让进程访问自己的旋钮,它必须从 /proc/self/cgroup 中提取目标层次结构上的路径,通过将旋钮的名称附加到路径来构造该路径,打开然后读取和/或写入它。这不仅非常笨拙和不寻常,而且本质上是竞争的。没有传统的方式来定义跨所需步骤的事务,并且无法保证进程实际上会在其自己的子层次结构上运行。

cgroup 控制器实现了一些永远不会被接受为公共 API 的旋钮,因为它们只是将控制旋钮添加到系统管理伪文件系统。cgroup 最终得到了一些接口旋钮,这些旋钮没有得到适当的抽象或改进,并且直接揭示了内核内部细节。这些旋钮通过定义不明确的委派机制暴露给各个应用程序,从而有效地滥用 cgroup 作为实现公共 API 的捷径,而无需经过必要的审查。

这对于用户空间和内核来说都是痛苦的。用户空间最终得到了行为不端且抽象性差的接口,而内核则无意中暴露并锁定到构造中。

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

cgroup v1 允许线程位于任何 cgroup 中,这产生了一个有趣的问题,即属于父 cgroup 及其子 cgroup 的线程争夺资源。这是令人讨厌的,因为两种不同类型的实体发生了竞争,并且没有明显的方法来解决它。不同的控制器做了不同的事情。

cpu 控制器将线程和 cgroup 视为等效项,并将 nice 级别映射到 cgroup 权重。这适用于某些情况,但在子项想要分配特定 CPU 周期比率并且内部线程数量波动时会失败 - 随着竞争实体数量的波动,比率会不断变化。还存在其他问题。从 nice 级别到权重的映射并不明显或通用,并且还有各种其他旋钮根本不适用于线程。

io 控制器隐式地为每个 cgroup 创建一个隐藏的叶节点来托管线程。隐藏的叶节点拥有所有旋钮的自己的副本,并带有 leaf_ 前缀。虽然这允许对内部线程进行等效控制,但存在严重的缺点。它总是添加一个额外的嵌套层,否则这是没有必要的,使接口混乱并大大简化了实现。

内存控制器无法控制内部任务和子 cgroup 之间发生的事情,并且该行为没有明确定义。有人试图添加临时行为和旋钮,以使该行为适应特定的工作负载,从长远来看,这会导致极难解决的问题。

多个控制器都在内部任务方面苦苦挣扎,并提出了不同的处理方式;不幸的是,所有方法都存在严重缺陷,此外,差异很大的行为使 cgroup 作为一个整体非常不一致。

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

其他接口问题

cgroup v1 在没有监督的情况下增长,并发展出大量怪癖和不一致之处。cgroup 核心方面的一个问题是如何通知一个空的 cgroup - 为每个事件 fork 并执行一个用户空间助手二进制文件。事件传递不是递归的或可委派的。该机制的限制还导致了内核事件传递过滤机制,进一步复杂化了接口。

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

控制器之间也没有一致性。创建新的 cgroup 时,某些控制器默认不施加额外的限制,而另一些控制器则不允许任何资源使用,直到明确配置为止。用于相同类型控制的配置旋钮使用差异很大的命名方案和格式。统计信息旋钮是任意命名的,即使在同一控制器中也使用不同的格式和单位。

cgroup v2 建立适当的通用约定,并更新控制器,以便它们公开最小且一致的接口。

控制器问题和补救措施

内存

原始的下限(软限制)被定义为一个默认未设置的限制。因此,全局回收首选的 cgroup 集合是选择加入,而不是选择退出。优化这些大多数负查找的成本非常高,以至于该实现尽管规模庞大,但甚至没有提供基本的可取行为。首先,软限制没有层次结构意义。所有配置的组都组织在一个全局 rbtree 中,并被视为平等的对等方,而不管它们在层次结构中的位置如何。这使得子树委派成为不可能。其次,软限制回收过程非常激进,不仅会将高分配延迟引入系统,还会因过度回收而影响系统性能,以至于该功能适得其反。

另一方面,memory.low 边界是自上而下分配的保留。当 cgroup 在其有效 low 内时,它可以享受回收保护,这使得子树的委派成为可能。当它高于其有效 low 时,它还可以享受与其超额成比例的回收压力。

原始的上限(硬限制)被定义为一个严格的限制,即使必须调用 OOM killer 也无法移动。但这通常与最大限度地利用可用内存的目标背道而驰。工作负载的内存消耗在运行时会发生变化,这需要用户过度提交。但是,使用严格的上限执行此操作需要对工作集大小进行相当准确的预测,或者向限制添加松弛。由于工作集大小估计很困难且容易出错,并且出错会导致 OOM 终止,因此大多数用户倾向于在较宽松的限制方面犯错,最终浪费了宝贵的资源。

另一方面,memory.high 边界可以设置得更加保守。当达到 high 时,它会通过强制分配进入直接回收来解决过剩问题来限制分配,但它永远不会调用 OOM killer。因此,选择过于激进的 high 边界不会终止进程,而是会导致性能逐渐下降。用户可以监控此情况并进行更正,直到找到仍然可以接受的性能的最小内存占用量。

在极端情况下,当有许多并发分配并且组内回收进度完全崩溃时,可能会超过 high 边界。但即使那样,从其他组或系统其余部分的松弛可用性中满足分配也比杀死组更好。否则,memory.max 用于限制此类溢出,并最终包含有缺陷甚至恶意的应用程序。

将原始的 memory.limit_in_bytes 设置为低于当前使用量会受到竞争条件的影响,在这种情况下,并发收费可能会导致限制设置失败。另一方面,memory.max 将首先设置限制以防止新的收费,然后进行回收和 OOM 终止,直到达到新的限制 - 或者写入 memory.max 的任务被终止。

组合的 memory+swap 记帐和限制被对交换空间的真实控制所取代。

在原始 cgroup 设计中,组合的 memory+swap 设施的主要论点是,全局或父级压力始终能够交换子组的所有匿名内存,而不管子组自身(可能不受信任)的配置如何。但是,不受信任的组可以通过其他方式破坏交换 - 例如,在紧密的循环中引用其匿名内存 - 并且管理员无法在过度提交不受信任的作业时假设完全可交换性。

另一方面,对于受信任的作业,组合的计数器不是一个直观的用户空间接口,并且它与 cgroup 控制器应该对特定物理资源进行记帐和限制的想法背道而驰。交换空间是一种与系统中所有其他资源类似的资源,这就是为什么统一层次结构允许单独分配它。