利用率钳制

1. 简介

利用率钳制,也称为 util 钳制或 uclamp,是一种调度器特性,允许用户空间帮助管理任务的性能需求。它在 v5.3 版本中引入。CGroup 支持在 v5.4 中合并。

Uclamp 是一种提示机制,它允许调度器理解任务的性能需求和限制,从而帮助调度器做出更好的决策。当使用 schedutil cpufreq 管理器时,util 钳制也会影响 CPU 频率的选择。

由于调度器和 schedutil 都是由 PELT (util_avg) 信号驱动的,因此 util 钳制通过将信号钳制到某个点来实现其目标;因此得名。也就是说,通过钳制利用率,我们使系统在某个性能点上运行。

看待 util 钳制的正确方式是将其视为一种请求或提示性能约束的机制。它包含两个可调参数:

  • UCLAMP_MIN,设置下限。

  • UCLAMP_MAX,设置上限。

这两个边界将确保任务在系统的这个性能范围内运行。UCLAMP_MIN 表示提升任务,而 UCLAMP_MAX 表示限制任务。

可以告诉系统(调度器)某些任务需要最低性能点才能运行,以提供所需的用户体验。或者可以告诉系统某些任务应限制消耗过多资源,并且不应超过特定的性能点。从用户空间的角度来看,将 uclamp 值视为性能点而不是利用率是一种更好的抽象。

例如,游戏可以使用 util 钳制与其感知的每秒帧数 (FPS) 形成反馈回路。它可以动态增加其显示管道所需的最低性能点,以确保不会丢帧。如果它知道在接下来的几百毫秒内将发生计算密集型场景,它也可以动态地“启动”这些任务。

在设备能力差异很大的移动硬件上,这种动态反馈回路提供了很大的灵活性,以确保在任何系统的能力下都能获得最佳的用户体验。

当然,也可以进行静态配置。确切的用法将取决于系统、应用程序和所需的结果。

另一个例子是在 Android 中,任务被分类为后台、前台、顶层应用等。Util 钳制可用于通过限制后台任务可以运行的性能点来限制它们消耗多少资源。这种限制有助于为重要任务保留资源,例如属于当前活动应用(顶层应用组)的任务。除此之外,还有助于限制它们消耗多少电量。这在异构系统中可能更明显(例如,Arm big.LITTLE);这种限制将有助于使后台任务倾向于停留在小核上,这将确保:

  1. 大核可以立即运行顶层应用任务。顶层应用任务是用户当前正在与之交互的任务,因此是系统中最重要的任务。

  2. 即使它们是 CPU 密集型任务,它们也不会在高耗能的内核上运行并耗尽电池电量。

注意

小核:

容量 < 1024 的 CPU

大核:

容量 = 1024 的 CPU

通过发出这些 uclamp 性能请求,或者更确切地说是提示,用户空间可以确保系统资源得到最佳利用,以提供最佳的用户体验。

另一个用例是帮助**克服调度器利用率信号计算中固有的启动延迟**。

另一方面,例如需要以最大性能点运行的繁忙任务,调度器需要花费大约 200 毫秒(PELT HALFIFE = 32 毫秒)才能意识到这一点。已知这会影响移动设备上的游戏等工作负载,由于选择任务及时完成工作所需的高频率的响应时间较慢,会导致帧丢失。设置 UCLAMP_MIN=1024 将确保此类任务在开始运行时始终看到最高性能级别。

如果有效使用,整体可见效果不仅可以带来更好的感知用户体验/性能,还可以帮助实现更好的整体性能/瓦特。

用户空间还可以与热子系统形成反馈回路,以确保设备不会过热到需要节流的地步。

SCHED_NORMAL/OTHER 和 SCHED_FIFO/RR 都遵循 uclamp 请求/提示。

在 SCHED_FIFO/RR 情况下,uclamp 提供了在任何性能点运行 RT 任务的选项,而不是始终绑定到最大频率。这在电池供电设备上运行的通用系统中可能很有用。

请注意,按照设计,RT 任务没有每个任务的 PELT 信号,并且必须始终以恒定频率运行,以应对不确定的 DVFS 启动延迟。

请注意,使用 schedutil 始终意味着在 RT 任务唤醒时修改频率会产生单个延迟。使用 uclamp 不会改变此成本。Uclamp 仅有助于选择请求的频率,而不是 schedutil 始终为所有 RT 任务请求最大频率。

有关默认值,请参阅第 3.4 节,有关如何更改 RT 任务的默认值,请参阅3.4.1

2. 设计

Util 钳制是系统中每个任务的属性。它设置其利用率信号的边界;充当一种偏置机制,影响调度器内的某些决策。

实际上,任务的实际利用率信号永远不会被钳制。如果在任何时间点检查 PELT 信号,您应该会继续看到它们完好无损。仅在需要时才会发生钳制,例如:当任务唤醒并且调度器需要选择合适的 CPU 以在其上运行时。

由于 util 钳制的目标是允许请求任务在其上运行的最小和最大性能点,因此它必须能够影响频率选择以及任务放置,才能最有效。这两者都对 CPU 运行队列(简称 rq)级别的利用率值产生影响,这给我们带来了主要的设计挑战。

当任务在 rq 上唤醒时,rq 的利用率信号将受到在其上排队的所有任务的 uclamp 设置的影响。例如,如果一个任务请求以 UTIL_MIN = 512 运行,那么 rq 的 util 信号需要尊重此请求以及所有排队任务的所有其他请求。

为了能够聚合附加到 rq 的所有任务的 util 钳制值,uclamp 必须在每次入队/出队时进行一些整理,这是调度器的热路径。因此必须小心,因为任何减速都会对许多用例产生重大影响,并可能阻碍其在实践中的可用性。

解决此问题的方法是将利用率范围划分为存储桶(struct uclamp_bucket),这使我们能够将搜索空间从 rq 上的每个任务减少到仅限最顶层存储桶上的任务子集。

当任务入队时,匹配存储桶中的计数器会递增,而出队时会递减。这使得跟踪 rq 级别的有效 uclamp 值变得容易得多。

随着任务的入队和出队,我们会跟踪 rq 的当前有效 uclamp 值。有关其工作原理的详细信息,请参阅第 2.1 节

稍后,在任何想要识别 rq 的有效 uclamp 值的路径中,它只需在需要做出决策的确切时刻读取 rq 的有效 uclamp 值即可。

对于任务放置的情况,目前只有能量感知和容量感知调度 (EAS/CAS) 使用 uclamp,这意味着它仅应用于异构系统。当任务唤醒时,调度器将查看每个 rq 的当前有效 uclamp 值,并将其与如果任务排队到那里时的潜在新值进行比较。倾向于最终产生最高能效组合的 rq。

类似地,在 schedutil 中,当需要进行频率更新时,它会查看 rq 的当前有效 uclamp 值,该值受当前排队在那里的任务集的影响,并选择满足请求约束的适当频率。

其他路径(例如设置过度利用率状态(这实际上禁用了 EAS))也使用 uclamp。此类情况被认为是必要的整理,以允许上述 2 个主要用例,并且在此处不会详细介绍,因为它们可能会随着实现细节而变化。

2.1. 存储桶

                         [struct rq]

(bottom)                                                    (top)

  0                                                          1024
  |                                                           |
  +-----------+-----------+-----------+----   ----+-----------+
  |  Bucket 0 |  Bucket 1 |  Bucket 2 |    ...    |  Bucket N |
  +-----------+-----------+-----------+----   ----+-----------+
     :           :                                   :
     +- p0       +- p3                               +- p4
     :                                               :
     +- p1                                           +- p5
     :
     +- p2

注意

上图是一个说明,而不是内部数据结构的真实描述。

为了在任务入队/出队时减少尝试确定 rq 的有效 uclamp 值时的搜索空间,整个利用率范围被划分为 N 个桶,其中 N 在编译时通过设置 CONFIG_UCLAMP_BUCKETS_COUNT 来配置。默认情况下,它设置为 5。

rq 为每个 uclamp_id 可调参数都有一个桶:[UCLAMP_MIN, UCLAMP_MAX]。

每个桶的范围是 1024/N。例如,对于默认值 5,将有 5 个桶,每个桶将覆盖以下范围

DELTA = round_closest(1024/5) = 204.8 = 205

Bucket 0: [0:204]
Bucket 1: [205:409]
Bucket 2: [410:614]
Bucket 3: [615:819]
Bucket 4: [820:1024]

当一个任务 p 具有以下可调参数时

p->uclamp[UCLAMP_MIN] = 300
p->uclamp[UCLAMP_MAX] = 1024

入队到 rq 时,UCLAMP_MIN 的桶 1 将会递增,而 UCLAMP_MAX 的桶 4 将会递增,以反映 rq 在此范围内有任务。

然后,rq 会跟踪其每个 uclamp_id 的当前有效 uclamp 值。

当任务 p 入队时,rq 的值会变为

// update bucket logic goes here
rq->uclamp[UCLAMP_MIN] = max(rq->uclamp[UCLAMP_MIN], p->uclamp[UCLAMP_MIN])
// repeat for UCLAMP_MAX

类似地,当 p 出队时,rq 的值会变为

// update bucket logic goes here
rq->uclamp[UCLAMP_MIN] = search_top_bucket_for_highest_value()
// repeat for UCLAMP_MAX

当所有桶都为空时,rq 的 uclamp 值将重置为系统默认值。有关默认值的详细信息,请参阅第 3.4 节

2.2. 最大聚合

Util clamp 经过调整以满足需要最高性能点的任务的请求。

当多个任务附加到同一个 rq 时,util clamp 必须确保需要最高性能点的任务能够获得它,即使有另一个任务不需要它或不允许达到此点。

例如,如果多个任务附加到具有以下值的 rq

p0->uclamp[UCLAMP_MIN] = 300
p0->uclamp[UCLAMP_MAX] = 900

p1->uclamp[UCLAMP_MIN] = 500
p1->uclamp[UCLAMP_MAX] = 500

那么假设 p0 和 p1 都入队到同一个 rq,UCLAMP_MIN 和 UCLAMP_MAX 都会变为

rq->uclamp[UCLAMP_MIN] = max(300, 500) = 500
rq->uclamp[UCLAMP_MAX] = max(900, 500) = 900

正如我们在第 5.1 节中将看到的,这种最大聚合是使用 util clamp 时的局限性之一的原因,特别是当用户空间希望节省功耗时,UCLAMP_MAX 提示。

2.3. 分层聚合

如前所述,util clamp 是系统中每个任务的属性。但是,实际应用(有效)值不仅会受到任务或代表任务的其他参与者(中间件库)提出的请求的影响。

任何任务的有效 util clamp 值都受到以下限制

  1. 通过它所附加的 cgroup CPU 控制器定义的 uclamp 设置(如果有)。

  2. (1) 中的受限值然后进一步受到系统范围的 uclamp 设置的限制。

第 3 节讨论了接口,并将对此进行进一步的阐述。

现在足以说明,如果任务发出请求,其实际有效值必须遵守 cgroup 和系统范围设置施加的一些限制。

即使实际上会超出约束,系统仍会接受该请求,但是一旦任务移动到不同的 cgroup 或系统管理员修改了系统设置,则只有当它在新的约束范围内时,才会满足该请求。

换句话说,当任务更改其 uclamp 值时,此聚合不会导致错误,而是系统可能无法基于这些因素满足请求。

2.4. 范围

Uclamp 性能请求的范围是 0 到 1024(包括 0 和 1024)。

对于 cgroup 接口,使用百分比(即 0 到 100,包括 0 和 100)。就像其他 cgroup 接口一样,您可以使用 “max” 代替 100。

3. 接口

3.1. 每个任务的接口

sched_setattr() 系统调用已扩展为接受两个新字段

  • sched_util_min:请求系统在此任务运行时应运行的最低性能点。或较低的性能界限。

  • sched_util_max:请求系统在此任务运行时应运行的最高性能点。或较高的性能界限。

例如,以下场景具有 40% 到 80% 的利用率约束

attr->sched_util_min = 40% * 1024;
attr->sched_util_max = 80% * 1024;

当任务 @p 运行时,**调度程序应尽最大努力确保它以 40% 的性能水平启动**。如果任务运行足够长的时间,以使其实际利用率超过 80%,则利用率或性能水平将被限制。

特殊值 -1 用于将 uclamp 设置重置为系统默认值。

请注意,使用 -1 将 uclamp 值重置为系统默认值与手动将 uclamp 值设置为系统默认值不同。这种区别很重要,因为正如我们在系统接口中将看到的那样,RT 的默认值可能会更改。SCHED_NORMAL/OTHER 将来也可能会获得类似的旋钮。

3.2. cgroup 接口

CPU cgroup 控制器中有两个与 uclamp 相关的值

  • cpu.uclamp.min

  • cpu.uclamp.max

当任务附加到 CPU 控制器时,其 uclamp 值将受到以下影响

  • cpu.uclamp.min 是一个保护,如cgroup v2 文档的第 3-3 节所述。

    如果任务 uclamp_min 值低于 cpu.uclamp.min,则任务将继承 cgroup cpu.uclamp.min 值。

    在 cgroup 层次结构中,有效的 cpu.uclamp.min 是(子,父)的最大值。

  • cpu.uclamp.max 是一个限制,如cgroup v2 文档的第 3-2 节所述。

    如果任务 uclamp_max 值高于 cpu.uclamp.max,则任务将继承 cgroup cpu.uclamp.max 值。

    在 cgroup 层次结构中,有效的 cpu.uclamp.max 是(子,父)的最小值。

例如,给定以下参数

p0->uclamp[UCLAMP_MIN] = // system default;
p0->uclamp[UCLAMP_MAX] = // system default;

p1->uclamp[UCLAMP_MIN] = 40% * 1024;
p1->uclamp[UCLAMP_MAX] = 50% * 1024;

cgroup0->cpu.uclamp.min = 20% * 1024;
cgroup0->cpu.uclamp.max = 60% * 1024;

cgroup1->cpu.uclamp.min = 60% * 1024;
cgroup1->cpu.uclamp.max = 100% * 1024;

当 p0 和 p1 附加到 cgroup0 时,这些值变为

p0->uclamp[UCLAMP_MIN] = cgroup0->cpu.uclamp.min = 20% * 1024;
p0->uclamp[UCLAMP_MAX] = cgroup0->cpu.uclamp.max = 60% * 1024;

p1->uclamp[UCLAMP_MIN] = 40% * 1024; // intact
p1->uclamp[UCLAMP_MAX] = 50% * 1024; // intact

当 p0 和 p1 附加到 cgroup1 时,这些值变为

p0->uclamp[UCLAMP_MIN] = cgroup1->cpu.uclamp.min = 60% * 1024;
p0->uclamp[UCLAMP_MAX] = cgroup1->cpu.uclamp.max = 100% * 1024;

p1->uclamp[UCLAMP_MIN] = cgroup1->cpu.uclamp.min = 60% * 1024;
p1->uclamp[UCLAMP_MAX] = 50% * 1024; // intact

请注意,cgroup 接口允许 cpu.uclamp.max 值低于 cpu.uclamp.min。其他接口不允许这样做。

3.3. 系统接口

3.3.1 sched_util_clamp_min

允许的 UCLAMP_MIN 范围的系统范围限制。默认情况下,它设置为 1024,这意味着任务允许的有效 UCLAMP_MIN 范围是 [0:1024]。例如,将其更改为 512 会将范围缩小到 [0:512]。这对于限制允许任务获取的提升量很有用。

任务发出高于此旋钮值的请求仍然会成功,但在它大于 p->uclamp[UCLAMP_MIN] 之前不会得到满足。

该值必须小于或等于 sched_util_clamp_max。

3.3.2 sched_util_clamp_max

允许的 UCLAMP_MAX 范围的系统范围限制。默认情况下,它设置为 1024,这意味着任务允许的有效 UCLAMP_MAX 范围是 [0:1024]。

例如,将其更改为 512 会将有效的允许范围缩小到 [0:512]。这意味着没有任务可以运行在 512 以上,这意味着所有 rq 也都受到限制。换句话说,整个系统的性能容量被限制为一半。

这对于限制系统的整体最大性能点很有用。例如,当电池电量不足时,或者当系统希望在空闲状态或屏幕关闭时限制对更多耗能性能级别的访问时,这可能很方便。

任务发出高于此旋钮值的请求仍然会成功,但在它大于 p->uclamp[UCLAMP_MAX] 之前不会得到满足。

该值必须大于或等于 sched_util_clamp_min。

3.4. 默认值

默认情况下,所有 SCHED_NORMAL/SCHED_OTHER 任务都初始化为

p_fair->uclamp[UCLAMP_MIN] = 0
p_fair->uclamp[UCLAMP_MAX] = 1024

也就是说,默认情况下,它们会被提升到以启动或运行时更改的最大性能点运行。尚未提出任何理由说明我们为什么要提供此功能,但将来可以添加此功能。

对于 SCHED_FIFO/SCHED_RR 任务

p_rt->uclamp[UCLAMP_MIN] = 1024
p_rt->uclamp[UCLAMP_MAX] = 1024

也就是说,默认情况下,它们会被提升到以系统的最大性能点运行,这保留了 RT 任务的历史行为。

RT 任务的默认 uclamp_min 值可以在启动或运行时通过 sysctl 修改。请参阅以下部分。

3.4.1 sched_util_clamp_min_rt_default

在最大性能点运行 RT 任务在电池供电设备上成本很高,而且没有必要。为了使系统开发人员能够为这些任务提供良好的性能保证,而无需将其全部推到最大性能点,此 sysctl 旋钮允许调整最佳提升值,以满足系统要求,而无需始终以最大性能点运行而消耗电力。

鼓励应用程序开发人员使用每个任务的 util clamp 接口,以确保它们具有性能和功耗意识。理想情况下,此旋钮应由系统设计人员设置为 0,并将管理性能要求的任务留给应用程序。

4. 如何使用 util clamp

Util clamp 促进了用户空间辅助电源和性能管理的概念。在调度程序级别,不需要任何信息即可做出最佳决策。但是,借助 util clamp,用户空间可以向调度程序提示,以便更好地决策任务放置和频率选择。

最佳结果是通过不对应用程序运行的系统做任何假设,并将其与反馈循环结合使用以动态监视和调整来实现的。最终,这将以更好的性能/瓦特带来更好的用户体验。

对于某些系统和用例,静态设置将有助于获得良好的结果。在这种情况下,可移植性将是一个问题。在 100、200 或 1024 时可以完成多少工作,对于每个系统都是不同的。除非有特定的目标系统,否则应避免静态设置。

有足够多的可能性基于 util clamp 创建整个框架,或者直接使用它的自包含应用程序。

4.1. 提升重要任务和 DVFS 延迟敏感型任务

当 GUI 任务唤醒时,它可能不忙,无法保证将频率驱动到较高水平。但是,它需要在特定的时间窗口内完成其工作,以提供所需的用户体验。它在唤醒时所需的正确频率将取决于系统。在某些功率不足的系统上,它将很高,而在其他功率过剩的系统上,它将很低或为 0。

此任务可以在每次错过截止时间时增加其 UCLAMP_MIN 值,以确保在下次唤醒时以更高的性能点运行。它应该尝试接近最低的 UCLAMP_MIN 值,该值允许在任何特定系统上满足其截止时间,以实现该系统的最佳性能/瓦特。

在异构系统上,此任务在更快的 CPU 上运行可能很重要。

通常,建议将输入视为性能级别或点,这将暗示任务放置和频率选择。.

4.2. 限制后台任务

就像引言中为 Android 案例所解释的那样。任何应用程序都可以降低某些不关心性能但在系统上最终可能很忙并消耗不必要的系统资源的后台任务的 UCLAMP_MAX。

4.3. 省电模式

sched_util_clamp_max 系统范围的接口可用于限制所有任务在通常能源效率较低的较高性能点运行。

这并非 uclamp 独有,因为可以通过降低 cpufreq governor 的最大频率来实现相同的目的。它可以被认为是更方便的替代接口。

4.4. 每个应用的性能限制

中间件/实用程序可以为用户提供一个选项,在每次执行应用程序时设置 UCLAMP_MIN/MAX,以保证最低性能点和/或限制其消耗系统电源,代价是这些应用程序的性能降低。

如果您希望在移动中编译内核时防止笔记本电脑发热,并且愿意牺牲性能来节省电量,但仍然希望保持浏览器的性能不受影响,那么 uclamp 可以实现这一点。

5. 局限性

5.1. 在某些条件下,使用 uclamp_max 限制频率会失败

如果任务 p0 被限制以 512 的频率运行

p0->uclamp[UCLAMP_MAX] = 512

并且它与可以以任何性能点自由运行的 p1 共享 rq

p1->uclamp[UCLAMP_MAX] = 1024

那么由于最大聚合,rq 将被允许达到最大性能点

rq->uclamp[UCLAMP_MAX] = max(512, 1024) = 1024

假设 p0 和 p1 的 UCLAMP_MIN = 0,那么 rq 的频率选择将取决于任务的实际利用率值。

如果 p1 是一个小任务,但 p0 是一个 CPU 密集型任务,那么由于两者都在同一个 rq 上运行,p1 将导致 rq 上的频率限制被取消,即使 p1 被允许以任何性能点运行,实际上并不需要以该频率运行。

5.2. UCLAMP_MAX 可能会破坏 PELT (util_avg) 信号

PELT 假设频率会随着信号的增长而增加,以确保 CPU 上总有一些空闲时间。但是使用 UCLAMP_MAX,频率增加将被阻止,这可能导致在某些情况下没有空闲时间。当没有空闲时间时,任务将陷入繁忙循环,这将导致 util_avg 为 1024。

结合下面描述的问题,当严重限制的任务与一个小型的非限制任务共享 rq 时,这可能会导致不必要的频率尖峰。

例如,如果任务 p 具有

p0->util_avg = 300
p0->uclamp[UCLAMP_MAX] = 0

在空闲 CPU 上唤醒,那么它将以该 CPU 能够达到的最小频率 (Fmin) 运行。最大 CPU 频率 (Fmax) 在这里也很重要,因为它指定了在此 CPU 上完成任务工作的最短计算时间。

rq->uclamp[UCLAMP_MAX] = 0

如果 Fmax/Fmin 的比率为 3,则最大值将为

300 * (Fmax/Fmin) = 900

这表明 CPU 仍然会看到空闲时间,因为 900 < 1024。但是,_实际的_ util_avg 将不是 900,而是在 300 到 900 之间。只要有空闲时间,p->util_avg 的更新就会有一些偏差,但与 Fmax/Fmin 不成比例。

p0->util_avg = 300 + small_error

现在,如果 Fmax/Fmin 的比率为 4,则最大值变为

300 * (Fmax/Fmin) = 1200

这高于 1024,表明 CPU 没有空闲时间。发生这种情况时,_实际的_ util_avg 将变为

p0->util_avg = 1024

如果任务 p1 在此 CPU 上唤醒,该 CPU 具有

p1->util_avg = 200
p1->uclamp[UCLAMP_MAX] = 1024

那么根据最大聚合规则,CPU 的有效 UCLAMP_MAX 将为 1024。但是,由于受限制的 p0 任务正在运行并受到严重限制,因此 rq->util_avg 将为

p0->util_avg = 1024
p1->util_avg = 200

rq->util_avg = 1024
rq->uclamp[UCLAMP_MAX] = 1024

因此导致频率尖峰,因为如果 p0 没有受到限制,我们应该得到

p0->util_avg = 300
p1->util_avg = 200

rq->util_avg = 500

并在该 CPU 的中间性能点附近运行,而不是我们得到的 Fmax。

5.3. Schedutil 响应时间问题

schedutil 有三个限制

  1. 硬件响应任何频率更改请求都需要非零时间。在某些平台上,可能需要几毫秒的时间。

  2. 非快速切换系统需要工作线程截止时间线程唤醒并执行频率更改,这会增加可测量的开销。

  3. schedutil rate_limit_us 会在此 rate_limit_us 窗口期间丢弃任何请求。

如果一个相对较小的任务正在执行关键工作,并且在唤醒并开始运行时需要特定的性能点,那么所有这些限制将阻止它在预期的时间尺度内获得它想要的东西。

此限制不仅在使用 uclamp 时会产生影响,而且随着我们不再逐渐增加或减少频率,它将更加普遍。我们可能会根据任务唤醒的顺序及其各自的 uclamp 值,轻松地在频率之间跳跃。

我们认为这是底层系统本身能力的限制。

改进 schedutil rate_limit_us 的行为还有改进空间,但对于 1 或 2 没有太多可做的。它们被认为是系统的硬性限制。