利用率钳制

1. 简介

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

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

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

正确的看待 util clamp 的方式是将其作为一种对性能约束提出请求或提示的机制。它由两个可调参数组成:

  • UCLAMP_MIN,设置下限。

  • UCLAMP_MAX,设置上限。

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

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

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

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

当然,也可以进行静态配置。具体的使用方式将取决于系统、应用程序和期望的结果。

另一个例子是在 Android 中,任务被分类为后台、前台、顶部应用程序等。Util clamp 可以通过限制后台任务可以运行的性能点来限制它们消耗多少资源。这种约束有助于为重要的任务保留资源,例如属于当前活动应用程序(顶部应用程序组)的任务。此外,这还有助于限制它们消耗的电量。这在异构系统中(例如 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 任务请求 MAX。

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

2. 设计

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

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

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

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

为了能够聚合附加到 rq 的所有任务的 util clamp 值,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(含)。

对于 cgroup 接口,使用百分比(即 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,用户空间可以提示调度程序做出有关任务放置和频率选择的更好决策。

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

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

有足够的可能性基于 util clamp 创建整个框架或直接使用它的独立应用程序。

4.1. 提升重要且对 DVFS 延迟敏感的任务

GUI 任务可能不会忙到需要驱动频率升高才能唤醒。但是,它需要在特定的时间范围内完成其工作,才能提供所需的用户体验。它在唤醒时需要的正确频率将取决于系统。在某些功率不足的系统上,它会很高,而在其他功率过大的系统上,它会很低或为 0。

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

在异构系统上,此任务在更快的 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 可以自由地以任何性能点运行

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 上唤醒,它具有

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 没有太多可做的。它们被认为是系统的硬性限制。