Cgroup内核文档翻译(9)——Documentation/admin-guide/cgroup-v2.rst

基于 Linux-5.10.101


=================

控制组 v2

=================

:日期:2015 年 10 月
:Author: Tejun Heo <tj@kernel.org>

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

.. 内容

一、简介
1-1.术语
1-2.什么是 cgroup?


二、基本操作
2-1.安装
2-2.组织进程和线程
2-2-1.进程
2-2-2.线程
2-3. 填充/取消通知
2-4.控制控制器
2-4-1.启用和禁用
2-4-2.自顶向下约束
2-4-3.无内部进程约束
2-5.委托
2-5-1.委托模型
2-5-2.委托遏制
2-6.指导方针
2-6-1.一次组织和控制
2-6-2.避免名称冲突


3. 资源分配模型
3-1.权重
3-2.限制
3-3.保护
3-4.分配


4. 接口文件
4-1.格式
4-2.约定
4-3.核心接口文件


5. 控制器
5-1.CPU
5-1-1. CPU 接口文件
5-2.Memory
5-2-1.内存接口文件
5-2-2.使用指南
5-2-3.内存所有权
5-3. IO
5-3-1. IO 接口文件
5-3-2.回写
5-3-3. IO 延迟
5-3-3-1. IO 延迟限制的工作原理
5-3-3-2. IO 延迟接口文件
5-3-4. IO 优先级
5-4. PID
5-4-1. PID 接口文件
5-5. Cpuset
5-5-1. Cpuset 接口文件
5-6. Device
5-7. RDMA
5-7-1. RDMA 接口文件
5-8. HugeTLB
5.8-1. HugeTLB 接口文件
5-8.杂项
5-8-1. perf_event
5-N.非规范信息
5-N-1. CPU 控制器根 cgroup 进程行为
5-N-2. IO 控制器根 cgroup 进程行为


6. Namespace
6-1.基本
6-2.根和视图
6-3.迁移和设置(2)
6-4.与其他命名空间的交互


P. 内核编程信息
P-1.文件系统对回写的支持
D. 弃用的 v1 核心功能
R. v1 的问题和 v2 的基本原理
R-1.多个层次结构
R-2.线程粒度
R-3.内部节点和线程之间的竞争
R-4.其他接口问题
R-5.控制器问题和补救措施
R-5-1. Memory


一、介绍

=============

术语
------------

“cgroup”代表“control group”,从不大写。 单数形式用于指定整个功能,也用作“cgroup 控制器”中的限定符。 当明确提到多个单独的控制组时,使用复数形式“cgroups”。


什么是 cgroup?
---------------

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

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

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

遵循某些结构性约束,控制器可以在 cgroup 上选择性地启用或禁用。 所有控制器行为都是分层的 - 如果在 cgroup 上启用控制器,它会影响属于 cgroup 的所有进程,包括这些 cgroup 构成的子层次结构的。 在嵌套的 cgroup 上启用控制器时,它总是会进一步限制资源分配。 在层次结构中更接近根设置的限制不能被更远的地方覆盖。

 

二、基本操作

=================

2-1 Mounting

--------

与 v1 不同,cgroup v2 只有一个层次结构。 可以使用以下挂载命令挂载 cgroup v2 层次结构:

# mount -t cgroup2 none $MOUNT_POINT

cgroup2 文件系统的幻数为 0x63677270(“cgrp”)。 所有支持 v2 且未绑定到 v1 层次结构的控制器都会自动绑定到 v2 层次结构并显示在根目录中。 在 v2 层次结构中未处于 active 状态的控制器可以绑定到其他层次结构。 这允许以完全向后兼容的方式将 v2层次结构与旧的 v1 的多个层次结构混合。

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

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

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

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

nsdelegate

将 cgroup 命名空间视为委托边界。 此选项是系统范围的,只能在挂载时设置或通过从 init 命名空间重新挂载进行修改。 挂载选项在非初始化命名空间上挂载上被忽略。 详情请参阅委托部分。


memory_localevents

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

memory_recursiveprot

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

 

2-1. 组织进程和线程

--------------------------------

2-2-1 进程
~~~~~~~~~

最初,仅存在所有进程所属的根 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中。 exit退出后,进程与退出时所属的 cgroup保持关联,直到它被回收; 但是,僵尸进程不会出现在“cgroup.procs”中,因此不能移动到其它 cgroup。

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

# rmdir $CGROUP_NAME

"/proc/$PID/cgroup" 列出进程的 cgroup 成员资格。 如果系统中正在使用旧版 cgroup,则此文件可能包含多行,每个层次结构各一行。 cgroup v2 的条目始终采用格式“0::$PATH”:: (可以从格式上区分v1还是v2的)

# cat /proc/842/cgroup
...
3:cpuset:/top-app
...
0::/test-cgroup/test-cgroup-nested

如果进程成为僵尸进程并且随后删除了与之关联的 cgroup,则将“(deleted)”附加到路径::

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


2-2-2 Threads
~~~~~~~

cgroup v2 支持控制器子集到线程粒度,以支持需要跨一组进程的线程进行分层资源分配的用例。 默认情况下,一个进程的所有线程都属于同一个 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。

- 当父域是非线程化的域时,它不能启用任何域控制器或填充域子域。 根不受此要求的约束。

在拓扑方面,cgroup 可能处于无效状态。 请考虑以下拓扑:

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

C 被创建为域,但未连接到可以托管子域的父级。 C 在变成线程化的 cgroup 之前不能使用。 在这些情况下,“cgroup.type”文件将报告“domain (invalid)”。 由于无效拓扑而失败的操作使用 EOPNOTSUPP 作为 errno。

当域 cgroup 的其中一个子 cgroup 变为线程化的域,或在 cgroup 中有进程时在“cgroup.subtree_control”文件中启用线程控制器时,域 cgroup 将变为线程域。 当条件清除时,线程域恢复为正常域。

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

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

在线程子树中只能启用线程控制器。 当线程控制器在线程子树中启用时,它只考虑和控制与 cgroup 中的线程及其后代相关的资源消耗。 所有与特定线程无关的消费都属于线程域 cgroup。

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


2-3 填充/取消通知

--------------------------

每个非根 cgroup 都有一个“cgroup.events”文件,其中包含“populated”字段,指示 cgroup 的子层次结构中是否有live进程。 如果 cgroup 及其后代中没有live进程,则其值为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.events”上生成两个 cgroup 的文件修改事件。

 

2-4 控制控制器

----------------------

2-4-1 启用和禁用
~~~~~~~~~~~~~~~~~~~~~~

每个 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 只启用了“memory”但没有启用“CPU”,C 和 D 在 CPU 周期上将自由竞争,但它们对 B 可用内存的划分将受到控制。

作为控制器调节目标资源到 cgroup 的子级的分配,使其能够在子级 cgroup 中创建控制器的接口文件。 在上面的示例中,在 B 上启用“cpu”将创建“cpu”。 在 C 和 D 中添加前缀控制器接口文件。同样,从 B 禁用“memory”将删除“memory”。 来自 C 和 D 的控制器接口文件前缀。这意味着控制器接口文件 - 任何不以“cgroup”开头的东西。 由父级而不是 cgroup 本身拥有。


2-4-2 自顶向下约束
~~~~~~~~~~~~~~~~~~~

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


2-4-3 无内部进程约束
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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

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

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

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


2-5 委托

----------

2-5-1 委托模型
~~~~~~~~~~~~~~~~~~~

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

因为给定目录中的资源控制接口文件控制父资源的分配,所以不应允许被委托者写入它们。 对于第一种方法,这是通过不授予对这些文件的访问权限来实现的。 对于第二种方法,内核拒绝从命名空间内部对命名空间根目录上的“cgroup.procs”和“cgroup.subtree_control”以外的所有文件进行写入。

两种委托类型的最终结果是相同的。 一旦委托,用户可以在目录下构建子层次结构,在其中组织它认为合适的进程,并进一步分发它从父级接收到的资源。 所有资源控制器的限制和其他设置都是分层的,无论委托的子层次结构中发生什么,都无法逃脱父级施加的资源限制。

目前,cgroup 对委托子层次结构中的 cgroup 数量或嵌套深度没有任何限制; 但是,将来可能会对此进行明确限制。


2-5-2 委托遏制
~~~~~~~~~~~~~~~~~~~~~~

委托的子层次结构包含在受委托者不能将进程移入或移出子层次结构的意义上。

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

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

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

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

例如,假设 cgroups C0 和 C1 已委托给用户 U0,用户 U0 在 C0 下创建了 C00 和 C01,在 C1 下创建了 C10,如下所示,并且 C0 和 C1 下的所有进程都属于 U0:

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

假设 U0 想要将当前在 C10 中的进程的 PID 写入“C00/cgroup.procs”。 U0 对文件有写访问权; 然而,源 cgroup C10和目标 cgroup C00 的共同祖先在委派点之上,U0 将没有对其“cgroup.procs”文件的写访问权,因此写入将被 -EACCES 拒绝。

对于命名空间的委托,通过要求源和目标 cgroup 都可以从尝试迁移的进程的命名空间访问来实现遏制。 如果其中任何一个不可达,则使用 -ENOENT 拒绝迁移。


2-6 指导方针

----------

2-6-1 一次组织和控制
~~~~~~~~~~~~~~~~~~~~~~~~~

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

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


2-6-2 避免名称冲突
~~~~~~~~~~~~~~~~~~~~~

cgroup 及其子 cgroup 的接口文件位于同一目录中,并且可以创建与接口文件名冲突的子 cgroup。

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

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


三、资源分配模型

==============================

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


3-1. 权重

--------

父级的资源是通过将所有活动子级的权重相加并给每个孩子的权重与总和的比率相匹配的分数来分配的。 由于目前只有可以利用资源的孩子参与分配,因此这是节省工作的。 由于动态特性,该模型通常用于无状态资源。

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

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

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


3-2. 限制

------

子级只能消耗配置的资源量。 限制可以过度使用 - 子限制的总和可能超过父可用的资源量。

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

由于限制可能被过度使用,所有配置组合都是有效的,没有理由拒绝配置更改或进程迁移。

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


3-2. 保护

------------

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

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

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

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


3-4. 分配

------------

一个 cgroup 被排他地分配了一定数量的有限资源。 分配不能过度使用 - 子项分配的总和不能超过父项可用的资源量。

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

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

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


四、接口文件

================

4-1. 格式

------

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

换行符分隔值(一次只能写入一个值时)

VAL0\n
VAL1\n
...

空格分隔值(只读或可以一次写入多个值时)

VAL0 VAL1 ...\n

平键

KEY0 VAL0\n
KEY1 VAL1\n
...

嵌套键控

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

对于可写文件,写的格式一般要和读的匹配; 但是,对于大多数常见用例,控制器可能允许省略后面的字段或实施受限的快捷方式。

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


4-2. 约定

------------

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

- 根 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”,其中列出了事件键值对。 每当发生可通知事件时,应在文件上生成文件修改事件。


4-3. 核心接口文件

--------------------

所有 cgroup 核心文件都以“cgroup”为前缀。

cgroup.type

存在于非根 cgroup 上的可读写单值文件。

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

- "domain" : 一个正常的有效 domain cgroup。

- "domain threaded" : 一个线程化的 domain 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.procs”文件具有写入权限。

委托子层次结构时,应授予对该文件以及包含目录的写访问权。

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


cgroup.threads

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

读取时,它会逐行列出属于 cgroup 的所有线程的 TID。 TID 没有排序,如果线程移动到另一个 cgroup 然后 又返回,或者 TID 在读取时被回收,则相同的 TID 可能会出现多次。

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

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

- 线程当前所在的 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.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。


五、控制器

===========

5-1. CPU

---

“cpu”控制器调节 CPU 时间片的分配。 该控制器实现了正常调度策略的权重和绝对带宽限制模型和实时调度策略的绝对带宽分配模型。

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

警告:cgroup2 还不支持实时进程的控制,只有当所有 RT 进程都在根 cgroup 中时,才能启用 cpu 控制器。 请注意,系统管理软件可能已经在系统引导过程中将 RT 进程置于非 root cgroup 中,并且可能需要将这些进程移至 root cgroup才能启用 cpu 控制器。


5-1-1. CPU 接口文件
~~~~~~~~~~~~~~~~~~~

所有持续时间都以微秒为单位。

cpu.stat
一个只读的平键文件。
无论控制器是否启用,此文件都存在。

它总是报告以下三个统计数据:

- usage_usec
- user_usec
- system_usec

以及enable控制器时的以下三个:

- nr_periods
- nr_throttled
- throttled_usec

cpu.weight
存在于非根 cgroup 上的可读写单值文件。 默认值为“100”。

[1, 10000] 范围内的权重。

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.pressure
存在于非根 cgroup 上的只读嵌套密钥文件。

显示 CPU 的 pressure stall 信息。 有关详细信息,请参阅 :ref:`Documentation/accounting/psi. rst <psi>`。

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) 的最大利用率钳位值。 此最大利用率值用于限制特定于任务 的最大利用率限制。


5-2 Memory

------

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

虽然不是完全无懈可击,但跟踪给定 cgroup 的所有主要内存使用情况,以便可以在合理范围内计算和控制总内存消耗。 目前,跟踪以下类型的内存使用情况。

- 用户态内存 - 页面缓存和匿名内存。

- 内核数据结构,例如 dentries 和 inode。

- TCP 套接字缓冲区。

上述列表可能会在未来扩大,以获得更好的覆盖范围。


5-2-1 内存接口文件
~~~~~~~~~~~~~~~~~~~~~~

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

memory.current

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

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


memory.min

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

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

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

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

如果内存 cgroup 没有填充进程,则会忽略它的 memory.min。


memory.low

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

尽力而为的内存保护。如果 cgroup 的内存使用量在其有效 low 之内,则除非可以从未受保护的 cgroup 中回收内存,否则不会回收 cgroup 的内存。在有效 low 限之上(或有效min,如果更高),页面将按超额比例回收,从而减少较小超额的回收压力。

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

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


memory.high

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

内存使用限制。这是控制 cgroup 内存使用的主要机制。如果 cgroup 的使用量超出上限,则 cgroup 的进程将受到限制并承受巨大的回收压力。

超过上限永远不会调用 OOM 终止程序,在极端情况下可能会突破限制。


memory.max

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

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

这是最终的保护机制。只要正确使用和监控上限,此限制的效用就仅限于提供最终的安全网。


memory.oom.group

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

确定 OOM killer 程序是否应将 cgroup 视为不可分割的工作负载。如果设置,则属于该 cgroup 或其后代(如果内存 cgroup 不是叶 cgroup)的所有任务将被一起终止或根本不终止。这可用于避免部分终止以保证工作负载的完整性。

具有 OOM 保护(oom_score_adj 设置为 -1000)的任务被视为异常,永远不会被kill

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


memory.events

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

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

low:
cgroup 因高内存压力而被回收的次数,即使其使用量低于low限。这通常表示low限已过度使用。

high:
cgroup 的进程因超出high内存边界而被限制并路由以执行直接内存回收的次数。对于内存使用量受high限而非全局内存压力限制的组,此事件的发生是意料之中的。

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

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

根据上下文,结果可能是调用 OOM killer 并重试分配或分配失败。

失败的分配反过来可能会作为 -ENOMEM 返回到用户空间,或者在磁盘预读等情况下默默忽略。目前,如果内存中发生 OOM,则 cgroup 会终止任务,前提是页面错误中发生了短缺。

如果 OOM killer 程序不被视为选项,则不会引发此事件,例如,对于失败的高阶(high-order)分配。

oom_kill:
属于此 cgroup 的被任何类型的 OOM killer程序终止的进程数。


memory.events.local

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

memory.stat:
存在于非根 cgroup 上的只读平面键文件。

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

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

条目按顺序排列,以便于阅读,新条目可以显示在中间。不要依赖保持在固定位置的项目;使用键来查找特定值!

anon:
匿名映射中使用的内存量,例如 brk()、sbrk() 和 mmap(MAP_ANONYMOUS)

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

kernel_stack:
分配给内核堆栈的内存量。

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

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

shmem:
交换支持的缓存文件系统数据量,例如 tmpfs、shm 段、共享匿名 mmap()

file_mapped:
使用 mmap() 映射的缓存文件系统数据量

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

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

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

inactive_anon、active_anon、inactive_file、active_file、unevictable :
页面回收算法使用的内部内存管理列表上的交换支持和文件系统支持的内存量

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

slab_unreclaimable:
内存压力导致无法回收的“slab”部分。

pgfault:
发生的页面错误总数

pgmajfault:
发生的重大页面错误数

workingset_refault:
先前被驱逐的页面的重新故障次数

workingset_activate:
立即激活的重新故障页面数

workingset_nodereclaim:
影子节点被回收的次数

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

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

pgsteal:
回收的页面数量

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

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

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

pglazyfreed:
回收的 lazyfree 页面数量

thp_fault_alloc:
分配透明大页面以满足页面错误(包括 COW 错误)。未设置 CONFIG_TRANSPARENT_HUGEPAGE 时,此计数器不存在。

thp_collapse_alloc:
分配透明大页面以允许折叠现有页面范围的数量。未设置 CONFIG_TRANSPARENT_HUGEPAGE 时,此计数器不存在。

memory.swap.current:
存在于非根 cgroup 上的只读单值文件。

cgroup 及其后代当前正在使用的交换总量。

memory.swap.max:
存在于非根 cgroup 上的读写单值文件。默认值为“max”。

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

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

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

fail:
由于系统范围的交换用尽或最大限制而导致交换分配失败的次数。

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

memory.pressure:
存在于非根 cgroup 上的只读嵌套键文件。

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


5-2-2.使用指南

~~~~~~~~~~~~~~~~~~~~~~

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

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

确定 cgroup 是否有足够的内存并非易事,因为内存使用情况并不能表明工作负载是否可以从更多内存中受益。例如,将从网络接收的数据写入文件的工作负载可以使用所有可用内存,但也可以使用少量内存运行。内存压力的衡量标准(由于内存不足,工作负载受到的影响有多大)对于确定工作负载是否需要更多内存是必要的;不幸的是,内存压力监控机制尚未实现。


5-2-3.内存所有权

~~~~~~~~~~~~~~~~~~~~~~

内存区域由实例化它的 cgroup 占用(charged),并一直由该 cgroup 占用,直到该区域被释放。将进程迁移到其他 cgroup 不会将其在前一个 cgroup 中实例化的内存使用量移至新 cgroup。

内存区域可能由属于不同 cgroup 的进程使用。该区域将由哪个 cgroup 占用尚不确定;但是,随着时间的推移,内存区域可能会最终进入具有足够内存余量的 cgroup,以避免高回收压力。

如果 cgroup 清除了大量内存,而这些内存预计会被其他 cgroup 重复访问,则使用 POSIX_FADV_DONTNEED 放弃受影响文件的内存区域的所有权以确保正确的内存所有权可能是有意义的。

 

 

5-3. IO

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

5-3-1. IO 接口文件
~~~~~~~~~~~~~~~~~~

io.stat

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

行由 $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”参数默认为零,控制器使用内部设备饱和状态在“最小”和“最大”之间调整整体 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 毫秒,则将认为设备已饱和,并相应地将总体 IO 发出率调整为 50% 到 150% 之间。

饱和点越低,延迟 QoS 越好,但会牺牲总带宽。“最小值”和“最大值”之间允许的调整范围越窄,IO 行为越符合成本模型。请注意,IO 发出基准率可能远离 100%,盲目设置“最小值”和“最大值”可能会导致设备容量或控制质量的显著损失。“最小值”和“最大值”对于调节表现出广泛临时行为变化的设备很有用 - 例如,SSD 以线速接受写入一段时间,然后完全停滞数秒。

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


io.cost.model

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

此文件配置基于 IO 成本模型的控制器 (CONFIG_BLK_CGROUP_IOCOST) 的成本模型,该控制器当前实现“io.weight”比例控制。行由 $MAJ:$MIN 设备编号作为键,且无序。给定设备的行在“io.cost.qos”或“io.cost.model”上对设备进行第一次写入时填充。定义了以下嵌套键。

=======================================
ctrl “auto”或“user”
model 使用的成本模型 - “linear”
=======================================

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

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

======================================================
[r|w]bps 最大连续 IO 吞吐量
[r|w]seqiops 每秒最大 4k 连续 IO
[r|w]randiops 每秒最大 4k 随机 IO
========================================================

从上面可以看出,内置线性模型确定了连续和随机 IO 的基本成本以及 IO 大小的成本系数。虽然简单,但该模型可以覆盖大多数常见设备类别。

IO 成本模型并不要求绝对准确,而是会根据设备行为动态调整。

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


io.weight

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

第一行是应用于没有特定覆盖的设备的默认权重。其余的覆盖由 $MAJ:$MIN 设备编号键控且无序。权重在 [1, 10000] 范围内,并指定 cgroup 相对于其同级可以使用的相对 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”作为值以删除特定限制。如果多次指定相同的键,则结果未定义。

在每个 IO 方向上测量 BPS 和 IOPS,如果达到限制,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

存在于非 root cgroups 上的只读嵌套键文件。

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


5-3-2.回写

页面缓存通过缓冲写入和共享 mmap 被弄脏,并通过写回机制异步写入到后备文件系统。写回位于内存和 IO 域之间,通过平衡弄脏和写入 IO 来调节脏内存的比例。

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

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

内存和写回管理存在固有差异,这会影响跟踪 cgroup 所有权的方式。内存按页跟踪,而写回按 inode 跟踪。为了进行写回,将 inode 分配给 cgroup,并且所有从 inode 写入脏页的 IO 请求都归属于该 cgroup。

由于内存的 cgroup 所有权按页跟踪,因此可能存在与 inode 所关联的 cgroup 不同的页面。这些页面称为外部页面。写回会不断跟踪外部页面,如果某个外部 cgroup 在一定时间内成为多数,则将 inode 的所有权切换为该 cgroup。

虽然此模型足以满足大多数用例,其中即使主写入 cgroup 随时间发生变化,给定的 inode 也主要由单个 cgroup 弄脏,但多个 cgroup 同时写入单个 inode 的用例无法得到很好的支持。在这种情况下,很大一部分 IO 可能会被错误地归因。由于内存控制器在第一次使用时分配页面所有权,并且在页面释放之前不会更新它,即使写回严格遵循页面所有权,多个 cgroup 弄脏重叠区域也不会按预期工作。建议避免这种使用模式。

影响写回行为的 sysctl 旋钮应用于 cgroup 写回,如下所示。

vm.dirty_background_ratio、vm.dirty_ratio:

这些比率同样适用于 cgroup 写回,但可用内存量受内存控制器和系统范围的清洁内存的限制。

vm.dirty_background_bytes、vm.dirty_bytes:

对于 cgroup 写回,这是根据总可用内存计算出的比率,应用方式与 vm.dirty[_background]_ratio 相同。


5-3-3. 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%。


5-3-3-1. IO 延迟限制的工作原理

io.latency 是节省工作量的;因此,只要每个人都达到其延迟目标,控制器就不会做任何事情。一旦某个组开始未达到其目标,它就会开始限制任何目标比其更高的对等组。这种限制有两种形式:

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

- 人为延迟诱导。某些类型的 IO 无法节流,否则可能会对优先级较高的组产生不利影响。这包括交换和元数据 IO。这些类型的 IO 可以正常发生,但它们会被“收取”到发起组的费用。如果发起组受到节流,您将看到 io.stat 中的 use_delay 和 delay 字段增加。延迟值是添加到此组中运行的任何进程的微秒数。由于如果发生大量交换或元数据 IO,这个数字可能会非常大,因此我们将单个延迟事件限制为每次 1 秒。

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


5-3-3-2. IO 延迟接口文件

io.latency

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

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

io.stat

如果启用了控制器,您将在 io.stat 中看到除常规统计数据之外的额外统计数据。

depth: 这是该组的当前队列深度。

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

win: 采样窗口大小(以毫秒为单位)。这是评估事件之间的最短持续时间。窗口仅在 IO 活动时流逝。空闲期会延长最近的窗口。


5-3-4. IO 优先级 //这个msm-5.4上没有,5.10上才有

 

5-4. PID

进程号控制器用于允许 cgroup 在达到指定限制后停止对任何新任务进行 fork() 或 clone()。

cgroup 中的任务数量可能会以其他控制器无法阻止的方式耗尽,因此需要自己的控制器。例如,fork 炸弹可能会在达到内存限制之前耗尽任务数量。

请注意,此控制器中使用的 PID 指的是内核使用的 TID(进程 ID)。


PID 接口文件
~~~~~~~~~~~~~~~~~~~~~

pids.max

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

进程数的硬性限制。


pids.current

存在于所有 cgroup 上的只读单值文件。

cgroup 及其后代中当前的进程数。

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


5-5. Cpuset

------

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

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


5-5-1 Cpuset 接口文件
~~~~~~~~~~~~~~~~~~~~~~

cpuset.cpus
存在于非 root 启用 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
存在于非 root 启用 cpuset 的 cgroup 上的可读写多值文件。

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

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

# cat cpuset.mems
0-1,3

空值表示 cgroup 使用与最近的具有非空的“cpuset.mems”的 cgroup 祖先相同的设置,或所有可用内存节点 (如果没有找到)。

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

cpuset.mems.effective
存在于所有启用 cpuset 的 cgroup 上的只读多值文件。

它列出了由其父级实际授予此 cgroup 的在线内存节点。 这些内存节点允许当前 cgroup 中的任务使用。

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

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

cpuset.cpus.partition
存在于非 root 启用 cpuset 的 cgroup 上的可读写单值文件。 此标志由父 cgroup 拥有,不可委托。

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

"root" - 分区的根
"member" - 分区的非根成员

当设置为分区根时,当前 cgroup 是新分区或调度域的根,该分区或调度域包括它自己及其所有后代,除了那些 本身是单独的分区根及其后代的那些。 根 cgroup 始终是分区根。

可以设置分区根的位置存在限制。 如果以下所有条件都为真,则只能在 cgroup 中设置。

1) “cpuset.cpus”不为空,CPU 列表是独占的,即它们不被任何兄弟共享。
2) 父 cgroup 是一个分区根。
3)“cpuset.cpus”也是父“cpuset.cpus.effective”的真子集。
4) 没有启用 cpuset 的子 cgroup。 这是为了消除在这种情况允许的情况下必须处理的极端情况。

将其设置为分区根将使 CPU 远离父 cgroup 的有效 CPU。 设置后,如果有任何启用了 cpuset 的子 cgroup,则此文件无法恢复为“member”。

父分区不能将其所有 CPU 分配给其子分区。 父分区中必须至少有一个 cpu。

一旦成为分区根,通常允许对“cpuset.cpus”进行更改,只要上述第一个条件为真,更改不会从父分区中带走所有 CPU,并且新的“cpuset.cpus”值是其孩子的“cpuset.cpus”值的超集。

有时,诸如祖先的“cpuset.cpus”或 cpu hotplug 更改等外部因素可能会导致分区根的状态发生更改。 读取 时,“cpuset.sched.partition”文件可以显示以下值。

"member" 分区的非根成员
"root" 分区根
“root invalid” 无效的分区根

如果上面的前 2 个分区根条件为真,并且父 cgroup 授予了“cpuset.cpus”中的至少一个 CPU,则它是一个分 区根。

如果父 cgroup 无法授予“cpuset.cpus”中请求的 CPU 或父 cgroup 本身不再是分区根,则分区根可能会变得 无效。 在这种情况下,即使上述第一个分区根条件的限制仍然适用,它也不是真正的分区。 然后 cgroup 中所有任 务的 cpu 亲和性将与最近的祖先分区中的 CPU 相关联。

如果至少一个请求的 CPU 现在可以由其父级授予,则可以将无效的分区根转换回真实的分区根。 在这种情况下,之前 无效分区中所有任务的 cpu亲和性将与新形成的分区的 CPU 相关联。即使存在子 cpuset,也始终允许将无效分区根 的分区状态更改为“member”。


5-6. Device

-----------------

TODO: 待翻译


5-7. RDMA

----

TODO: 待翻译


5-8. HugeTLB

-------

TODO: 待翻译


5-9. Misc

----

TODO: 待翻译

 

六、Namespace

=========


弃用的 v1 核心功能
============================

- 不支持包括命名的多个层次结构。

- 不支持所有 v1 安装选项。

- “tasks”文件被删除,“cgroup.procs”未排序。

- “cgroup.clone_children”被删除。

- /proc/cgroups 对 v2 没有意义。 在根目录下使用“cgroup.controllers”文件。


v1 的问题和 v2 的基本原理
=====================================

多个层次结构
--------------------

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

例如,由于每个控制器只有一个实例,因此可以在所有层次结构中使用的实用程序类型控制器(例如冷冻机)只能在一个实例中使用。 一旦填充了层次结构,控制器就无法移动到另一个层次结构,这一事实加剧了这个问题。 另一个问题是绑定到层次结构的所有控制器都被迫具有完全相同的层次结构视图。 无法根据特定控制器改变粒度。

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

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

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

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

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


Thread Granularity
------------------

TODO: 待翻译

posted on 2022-09-20 22:41  Hello-World3  阅读(426)  评论(0编辑  收藏  举报

导航