控制组 v2¶
- 日期:
2015 年 10 月
- 作者:
Tejun Heo <tj@kernel.org>
这是关于 cgroup v2 的设计、接口和约定的权威文档。它描述了 cgroup 的所有用户可见方面,包括核心和特定的控制器行为。所有未来的更改都必须反映在此文档中。v1 的文档可在 Documentation/admin-guide/cgroup-v1/index.rst 下找到。
简介¶
术语¶
“cgroup” 代表“控制组”,并且永远不会大写。单数形式用于指定整个特性,也用作限定符,如“cgroup 控制器”。当明确指代多个单独的控制组时,使用复数形式“cgroups”。
什么是 cgroup?¶
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
以增加热路径操作(如 fork 和 exit)的成本为代价,减少动态 cgroup 修改(如任务迁移和控制器开启/关闭)的延迟。创建 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 内存分配给内存控制器会影响内存保护和回收机制。任何用户空间调整(例如,低限制、最小限制)都需要考虑到这一点。
如果未选择此选项,则使用的 HugeTLB 页面将不会被内存控制器跟踪(即使稍后重新挂载 cgroup v2 也是如此)。
- pids_localevents
该选项恢复了 pids.events:max 的 v1 式行为,即仅计算本地(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 一个子进程时,新进程会诞生在 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.type” 文件中,该文件指示 cgroup 是正常的域、充当线程子树的域的域还是线程 cgroup。
在创建时,cgroup 始终是域 cgroup,可以通过将 “threaded” 写入 “cgroup.type” 文件来使其成为线程 cgroup。该操作是单向的
# echo threaded > cgroup.type
一旦线程化,cgroup 就不能再次成为域。要启用线程模式,必须满足以下条件。
由于 cgroup 将加入父级的资源域。父级必须是有效的(线程化)域或线程 cgroup。
当父级是非线程域时,它不能启用任何域控制器或填充域子级。根目录不受此限制。
从拓扑结构上讲,cgroup 可能处于无效状态。请考虑以下拓扑
A (threaded domain) - B (threaded) - C (domain, just created)
C 被创建为域,但未连接到可以托管子域的父级。在将其转换为线程 cgroup 之前,无法使用 C。“cgroup.type” 文件在这些情况下将报告 “domain (invalid)”。由于无效拓扑而失败的操作使用 EOPNOTSUPP 作为错误号。
当一个子 cgroup 变为线程化时,或者当 cgroup 中有进程时在 “cgroup.subtree_control” 文件中启用线程控制器时,域 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。当值更改时,将触发 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” 文件上生成文件修改事件。
控制控制器¶
启用和禁用¶
每个 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.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 有效用户 ID 的进程将目标进程迁移到 cgroup 时,需要满足以下条件来实现的,即通过将其 PID 写入 “cgroup.procs” 文件。
写入者必须具有对 “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 删除时活动的限制的系统资源。
- 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 可能需要一些时间;此操作完成后,“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 尚不支持实时进程的控制。对于使用启用了实时进程组调度的 CONFIG_RT_GROUP_SCHED 选项构建的内核,只有当所有 RT 进程都在根 cgroup 中时,才能启用 cpu 控制器。如果禁用 CONFIG_RT_GROUP_SCHED,则此限制不适用。请注意,系统管理软件可能已经在系统启动过程中将 RT 进程放置到非根 cgroup 中,并且在启用具有 CONFIG_RT_GROUP_SCHED 的内核的 cpu 控制器之前,可能需要将这些进程移动到根 cgroup。
CPU 接口文件¶
所有持续时间均以微秒为单位。
- cpu.stat
一个只读扁平键值文件。无论是否启用控制器,此文件都存在。
它始终报告以下三个统计信息
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。
- cpu.weight.nice
一个读写单值文件,存在于非根 cgroup 上。默认为 “0”。
nice 值在 [-20, 19] 范围内。
此接口文件是 “cpu.weight” 的替代接口,允许使用 nice(2) 使用的相同值读取和设置权重。因为 nice 值的范围较小且粒度较粗,所以读取值是当前权重的最接近近似值。
- 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。
- cpu.uclamp.min
一个读写单值文件,存在于非根 cgroup 中。默认值为“0”,即不启用利用率提升。
请求的最小利用率(保护),以百分比形式的实数表示,例如 12.34 表示 12.34%。
此接口允许读取和设置最小利用率钳制值,类似于 sched_setattr(2)。此最小利用率值用于钳制任务特定的最小利用率钳制。
请求的最小利用率(保护)始终受限于当前最大利用率(限制)值,即 cpu.uclamp.max。
- cpu.uclamp.max
一个读写单值文件,存在于非根 cgroup 中。默认值为“max”,即不进行利用率限制。
请求的最大利用率(限制),以百分比形式的实数表示,例如 98.76 表示 98.76%。
此接口允许读取和设置最大利用率钳制值,类似于 sched_setattr(2)。此最大利用率值用于钳制任务特定的最大利用率钳制。
- 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 杀手。在有效最小值边界(或有效低边界,如果它更高)之上,页面将按超出量比例回收,从而减少较小超出量的回收压力。
有效最小值边界受所有祖先 cgroup 的 memory.min 值的限制。如果存在 memory.min 过度承诺(子 cgroup 或多个 cgroup 需要的受保护内存多于父 cgroup 允许的内存),则每个子 cgroup 将获得父 cgroup 保护的一部分,该部分与其 memory.min 以下的实际内存使用量成比例。
不鼓励在这种保护下放置比通常可用更多的内存,这可能会导致持续的 OOM。
如果内存 cgroup 中未填充进程,则会忽略其 memory.min。
- memory.low
一个读写单值文件,存在于非根 cgroup 上。默认为 “0”。
尽力而为的内存保护。如果 cgroup 的内存使用量在其有效低边界内,则除非在不受保护的 cgroup 中没有可用的可回收内存,否则不会回收该 cgroup 的内存。在有效低边界(或有效最小值边界,如果它更高)之上,页面将按超出量比例回收,从而减少较小超出量的回收压力。
有效低边界受所有祖先 cgroup 的 memory.low 值的限制。如果存在 memory.low 过度承诺(子 cgroup 或多个 cgroup 需要的受保护内存多于父 cgroup 允许的内存),则每个子 cgroup 将获得父 cgroup 保护的一部分,该部分与其 memory.low 以下的实际内存使用量成比例。
不鼓励在这种保护下放置比通常可用更多的内存。
- memory.high
一个读写单值文件,存在于非根 cgroup 中。默认值为“max”。
内存使用量限制。如果 cgroup 的使用量超过高边界,则该 cgroup 的进程将受到限制并承受严重的回收压力。
超过高限制永远不会调用 OOM 杀手,并且在极端情况下,可能会突破该限制。高限制应在外部进程监视受限 cgroup 以减轻严重回收压力的场景中使用。
- memory.max
一个读写单值文件,存在于非根 cgroup 中。默认值为“max”。
内存使用硬限制。这是限制 cgroup 内存使用的主要机制。如果 cgroup 的内存使用量达到此限制且无法减少,则会在该 cgroup 中调用 OOM 杀手。在某些情况下,使用量可能会暂时超过该限制。
在默认配置中,常规的 0 阶分配总是会成功,除非 OOM 杀手选择当前任务作为牺牲品。
某些类型的分配不会调用 OOM 杀手。调用方可以以不同的方式重试它们,以 -ENOMEM 返回到用户空间或在磁盘预读等情况下静默忽略。
- memory.reclaim
一个只写嵌套键值文件,存在于所有 cgroup 中。
这是一个在目标 cgroup 中触发内存回收的简单接口。
示例
echo "1G" > memory.reclaim请注意,内核可以从目标 cgroup 中过度或不足回收内存。如果回收的字节数少于指定数量,则返回 -EAGAIN。
请注意,主动回收(由此接口触发)并不意味着内存 cgroup 上的内存压力。因此,通常在这种情况下不执行由内存回收触发的套接字内存平衡。这意味着网络层不会根据由 memory.reclaim 引发的回收进行调整。
定义了以下嵌套键。
swappiness
要回收的交换性值
指定交换性值会指示内核使用该交换性值执行回收。请注意,这与应用于 memcg 回收的 vm.swappiness 具有相同的语义,具有所有现有限制和潜在的未来扩展。
- memory.peak
一个读写单值文件,存在于非根 cgroup 上。
自 cgroup 创建或该 FD 最近一次重置以来,为 cgroup 及其后代记录的最大内存使用量。
向此文件写入任何非空字符串都会将其重置为当前内存使用量,以便后续通过同一文件描述符进行读取。
- memory.oom.group
一个读写单值文件,存在于非根 cgroup 中。默认值为“0”。
确定 OOM 杀手是否应将 cgroup 视为不可分割的工作负载。如果设置,则属于 cgroup 或其后代(如果内存 cgroup 不是叶 cgroup)的所有任务将一起终止或根本不终止。这可以用来避免部分终止,以保证工作负载的完整性。
具有 OOM 保护(oom_score_adj 设置为 -1000)的任务被视为例外,永远不会被终止。
如果在 cgroup 中调用 OOM 杀手,则无论祖先 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 杀手不被视为一个选项,例如对于失败的高阶分配或如果调用方要求不重试尝试,则不会引发此事件。
- oom_kill
属于此 cgroup 的进程被任何类型的 OOM 杀手终止的次数。
- 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))中使用的内存量
- 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() 映射的缓存文件系统数据量
- 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” 中可以回收的部分,例如目录项 (dentries) 和 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
影子节点被回收的次数
- pgscan (npn)
扫描的页面量(在非活动 LRU 列表中)
- pgsteal (npn)
回收的页面量
- pgscan_kswapd (npn)
kswapd 扫描的页面量(在非活动 LRU 列表中)
- pgscan_direct (npn)
直接扫描的页面量(在非活动 LRU 列表中)
- pgscan_khugepaged (npn)
khugepaged 扫描的页面量(在非活动 LRU 列表中)
- pgsteal_kswapd (npn)
kswapd 回收的页面量
- pgsteal_direct (npn)
直接回收的页面量
- pgsteal_khugepaged (npn)
khugepaged 回收的页面量
- 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 提示错误的数量。
- pgdemote_kswapd
kswapd 降级的页面数。
- pgdemote_direct
直接降级的页面数。
- pgdemote_khugepaged
khugepaged 降级的页面数。
- hugetlb
hugetlb 页面使用的内存量。仅当在 memory.current 中考虑 hugetlb 使用情况时(即,cgroup 安装了 memory_hugetlb_accounting 选项)才会显示此指标。
- 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
写入延迟阈值
最小值
最小缩放百分比 [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”
模型
正在使用的成本模型 - “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 相对于其同级可以使用的相对 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。
回写¶
页面缓存通过缓冲写入和共享 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 不同的 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 优先级类或具有 RT I/O 优先级类的请求,将其更改为尽力而为 (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” 中列出,这可能与 “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。不允许在两个或多个子 cgroup 中出现独占 CPU(独占规则)。违反独占规则的值将被拒绝并出现写入错误。
根 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 层次结构中写入正确的 “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 (<原因>)”
无效分区根
“isolated invalid (<原因>)”
无效隔离分区根
在无效分区根的情况下,括号内会包含一个描述性字符串,说明分区无效的原因。
要使本地分区根有效,必须满足以下条件。
父 cgroup 是有效分区根。
“cpuset.cpus.exclusive.effective” 文件不能为空,尽管它可能包含离线 CPU。
除非没有与此分区关联的任务,“cpuset.cpus.effective” 不能为空。
要使远程分区根有效,必须满足除第一个条件以外的所有上述条件。
诸如热插拔或对“cpuset.cpus”或“cpuset.cpus.exclusive”的更改之类的外部事件可能导致有效的分区根变为无效,反之亦然。请注意,不能将任务移动到“cpuset.cpus.effective”为空的 cgroup 中。
当非根父分区没有关联的任务时,它可以将其所有 CPU 分配给其子本地分区。
必须小心地将有效的分区根更改为“成员”,因为其所有子本地分区(如果存在)都将变为无效,从而导致在这些子分区中运行的任务中断。如果它们的父分区切换回在“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/读取/写入)和设备(类型、主设备号和次设备号)。如果程序返回 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
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 页面。每个节点的值以字节为单位。
其他¶
杂项 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 中。
杂项接口文件¶
杂项控制器提供 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 - 值而不是 1024 为 100)。
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 根是创建 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 命名空间
该进程对其当前用户命名空间具有 CAP_SYS_ADMIN 权限
该进程对目标 cgroup 命名空间的用户命名空间具有 CAP_SYS_ADMIN 权限
附加到另一个 cgroup 命名空间时,不会发生隐式的 cgroup 更改。预期有人会将附加进程移动到目标 cgroup 命名空间根下。
与其他命名空间的交互¶
运行在非初始化 cgroup 命名空间内的进程可以挂载特定于命名空间的 cgroup 层次结构
# mount -t cgroup2 none $MOUNT_POINT
这将挂载统一的 cgroup 层次结构,其中 cgroupns 根作为文件系统根。该进程需要对其用户和挂载命名空间具有 CAP_SYS_ADMIN 权限。
/proc/self/cgroup 文件的虚拟化与通过命名空间私有的 cgroupfs 挂载来限制 cgroup 层次结构的视图相结合,提供了容器内部正确隔离的 cgroup 视图。
内核编程信息¶
本节包含内核编程信息,这些信息涉及与 cgroup 交互的必要领域。不包括 cgroup 核心和控制器。
对写回的文件系统支持¶
文件系统可以通过更新 address_space_operations->writepage[s]() 来使用以下两个函数注释 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 写回支持,当某些文件系统功能(例如,日志数据模式)不兼容时,此功能很有用。
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 提升为暴露给普通程序的类似 syscall 的 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 集合是选择加入的,而不是选择退出的。优化这些主要为负查找的成本非常高,以至于该实现尽管规模庞大,甚至没有提供基本可取的行为。首先,软限制没有层级含义。所有配置的组都组织在一个全局红黑树中,并且被视为平等的对等点,无论它们在层次结构中的位置如何。这使得子树委托成为不可能。其次,软限制回收过程非常激进,它不仅给系统带来了较高的分配延迟,而且由于过度回收而影响系统性能,以至于该功能适得其反。
另一方面,memory.low 边界是自上而下分配的保留。当一个 cgroup 在其有效 low 值内时,它可以享受回收保护,这使得子树的委托成为可能。当它高于其有效 low 值时,它还可以享受与其超额量成正比的回收压力。
最初的高边界,即硬限制,被定义为一个严格的限制,即使必须调用 OOM killer 也不能改变。但这通常与最大限度利用可用内存的目标背道而驰。工作负载的内存消耗在运行时会发生变化,这需要用户进行超额提交。但是,使用严格的上限进行超额提交需要对工作集大小进行相当准确的预测,或者在限制中添加松弛。由于工作集大小的估计很困难且容易出错,并且出错会导致 OOM 杀掉,因此大多数用户倾向于采用较宽松的限制,最终浪费宝贵的资源。
另一方面,memory.high 边界可以设置得更加保守。当命中时,它会通过强制分配进入直接回收来消除超额,从而限制分配,但它永远不会调用 OOM killer。因此,选择过于激进的 high 边界不会终止进程,而是会导致性能逐渐下降。用户可以监控这种情况并进行纠正,直到找到仍然可以接受性能的最小内存占用量。
在极端情况下,如果存在许多并发分配和组内回收进度的完全崩溃,则可能会超过 high 边界。但即使那样,从其他组或系统的其余部分中的松弛中满足分配,也比杀死组更好。否则,memory.max 可以限制这种类型的溢出,并最终遏制有缺陷甚至恶意的应用程序。
将原始的 memory.limit_in_bytes 设置为低于当前使用量会受到竞争条件的影响,在这种情况下,并发收费可能会导致限制设置失败。另一方面,memory.max 将首先设置限制以防止新的收费,然后回收和 OOM 杀掉,直到达到新的限制——或者杀死写入 memory.max 的任务。
组合的内存+交换会计和限制被对交换空间的实际控制所取代。
在原始 cgroup 设计中,组合内存+交换工具的主要论点是,全局或父级压力始终能够交换子组的所有匿名内存,而不管子组自身的(可能不受信任的)配置。但是,不受信任的组可以通过其他方式破坏交换——例如,在紧密循环中引用其匿名内存——并且当超额提交不受信任的作业时,管理员不能假设完全的可交换性。
另一方面,对于受信任的作业,组合计数器不是一个直观的用户空间接口,并且它与 cgroup 控制器应该核算和限制特定物理资源的想法背道而驰。交换空间是系统中像其他所有资源一样的资源,这就是为什么统一的层次结构允许单独分配它的原因。