RFC-0123:CPU 性能信息系统调用

RFC-0123:CPU 性能信息系统调用
状态已接受
区域
  • 内核
说明

用于与内核通信以了解 CPU 性能的接口

Gerrit 更改
作者
审核人
提交日期(年-月-日)2021-07-20
审核日期(年-月-日)2021-08-18

摘要

此 RFC 提出了一种机制,通过该机制,用户空间代理可以与内核就 CPU 性能进行交互,既可以更新内核调度程序使用的性能缩放比例,也可以查询其状态。

设计初衷

为了在 big.LITTLE 等异构架构中有效地调度 CPU 工作,Zircon 内核调度程序会对 CPU 的相对性能进行建模。在撰写本文时,描述这些相对性能的性能比例是静态的,由 ZBI 中的数据提供。

对 big.LITTLE 系统执行 CPU 温控调频时,大核和小核的频率通常不会按相同的系数进行调节,因此它们的相对性能会动态变化。与大多数其他操作系统不同,在 Fuchsia 中,对核心频率的修改是在用户空间中进行的,并且必须跨内核边界通知调度程序相对 CPU 性能的变化。这种通信需要新的系统调用。

设计

效果比例

概念

在考虑提议的系统调用之前,最好先了解内核调度器中已有的性能缩放概念。性能比例是指 CPU 以当前速度运行时的性能与系统相关参考性能的比率,其中性能可以使用任何合适的指标(例如 DMIPS)来衡量。在撰写本文时(但未来不一定如此),参考性能是指最强大的 CPU 以最大速度运行时的性能,因此 1.0 是最大性能缩放值。通常,供应商会提供每个 CPU 以标称速度运行时的性能值,并假设性能随 CPU 频率线性变化。

例如,在 big.LITTLE 系统上,供应商可能会提供性能数据,表明以最大速度运行的大核心的 DMIPS 是以最大速度运行的小核心的两倍。如果参考性能对应于以最大速度运行的大核心,则该运行条件对应于性能比例 1.0,而以最大速度运行的小核心的性能比例为 0.5。将大核心的速度降低 25% 会使其新的性能比例为 0.75,而将小核心的速度降低 25% 会使其性能比例变为 0.375。

更准确地说,如果 fref 是具有已知性能缩放比例 sref 的参考频率,那么频率 fnew 的性能缩放比例为 snew=sreffnew/fref。一般来说,系统中的每种不同的 CPU 架构都需要一个参考频率。

通常,给定系统仅支持固定数量的频次组合。例如,同一集群中的 CPU 通常必须具有相同的频率,并且每个集群仅支持相对较少的不同频率。不过,内核无法跟踪哪些性能缩放比例是有效的。因此,内核信任用户空间会提供实际值,并将尽可能使用通过建议的 API 提供的值。

定点数表示法

为避免使用浮点数,性能缩放比例使用定点数表示,由结构体指定

  typedef struct zx_cpu_performance_scale {
    uint32_t integral_part;
    uint32_t fractional_part;  // Increments of 2**-32
  } zx_cpu_performance_scale_t;

integral_partfractional_part 分别描述整数部分和小数部分,其中 fractional_part 指定 2-32 的增量。 实数表示法和定点表示法之间的转换应根据以下函数进行:

zx_status_t ToFixedPoint(double real, zx_cpu_performance_scale_t* scale) {
  double integer;
  double fraction = std::modf(real, &integer);

  // Converting from double to fixed point should fail if the input's integer
  // part is too large.
  if (integer > static_cast<double>(UINT32_MAX)) {
    return ZX_ERR_INVALID_ARGS;
  }

  scale->integral_part = static_cast<uint32_t>(integer);

  // Rounding down the fractional part is suggested but should not matter
  // much in practice. A difference of 1 in the output is a difference of only
  // 2**-32 in the corresponding real value.
  scale->fractional_part = static_cast<uint32_t>(std::ldexp(fraction, 32));

  return ZX_OK;
}

double FromFixedPoint(zx_cpu_performance_scale_t scale) {
  return static_cast<double>(scale.integral_part)
    + std::ldexp(scale.fractional_part, -32);
}

系统调用 1:zx_system_set_performance_info

第一个系统调用允许用户空间代理设置内核调度程序使用的性能缩放比例:

zx_status_t zx_system_set_performance_info(
    zx_handle_t resource,
    uint32_t topic,
    const void* new_info,
    size_t info_count
);

其参数如下:

  • resource:向此调用授予权限的资源。必须为 ZX_RSRC_SYSTEM_CPU_BASE(专门为此 API 引入的新资源),否则调用将失败。

  • topic:相应调用所引用的表演类型。必须为 ZX_CPU_PERF_SCALE,将在提案实施后定义。

  • new_info:有效的 zx_cpu_performance_info_t[],其元素由以下内容指定

    typedef struct zx_cpu_performance_info {
        uint32_t logical_cpu_number;
        zx_cpu_performance_scale_t performance_scale;
    } zx_cpu_performance_info_t;
    

    其中,zx_cpu_performance_t上文定义的

    logical_cpu_number 指定了相应结构体描述的 CPU,使用的编号方案与内核使用的方案相同。每个 logical_cpu_number 都必须是有效的 CPU 标识符。new_info 的元素必须按严格递增的 logical_cpu_number 顺序排序(因此,每个 logical_cpu_number 只能出现一次)。

    performance_scale 表示指定 CPU 的新性能规模,应与 CPU 的新频率相对应,如前文所述。不过,内核不会根据支持的 CPU 频率验证输入;任何正值都可以作为输入。

    输入缩放比例 {.integral_part = 0, .fractional_part = 0} 无效,以免与离线核心的请求混淆,离线核心是一个具有独特机制的程序,预计未来会有不同的 API。

    内核可能会在内部将有效输入替换为调度程序可以使用的最接近的值。例如,在撰写本文时,支持的最大性能比例为 1.0。因此,如果 performance_scale 表示的值大于 1.0,内核会在内部将其限制为 {.integral_part = 1, .fractional_part = 0}

    如果对 zx_system_set_performance_info 的调用失败,内核不会采取任何操作,并且 new_info 不会产生任何影响。

    如果调用成功,内核调度程序将从下一次重新调度操作开始,利用与 new_info 对应的修改后的性能缩放比例,这通常发生在调用返回后的某个时间。对于 new_info 中未引用的 CPU,内核不会修改其性能缩放比例。

    通过此调用进行的更改将一直存在,直到重新启动或通过进一步使用此 API 将其覆盖为止。

  • info_countnew_info 中的元素数量。必须为正数,且不得大于系统中的 CPU 数量。

错误情况

ZX_ERR_BAD_HANDLE

  • resource”不是有效的句柄。

ZX_ERR_WRONG_TYPE

  • resource 不是有效的资源句柄,或者不是 ZX_RSRC_KIND_SYSTEM 类型。

ZX_ERR_INVALID_ARGS

  • topic 不是 ZX_CPU_PERF_SCALE
  • new_info 是无效的指针。
  • new_info 未按严格递增的 logical_cpu_number 排序。

ZX_ERR_OUT_OF_RANGE

  • resource 的类型为 ZX_RSRC_KIND_SYSTEM,但不等于 ZX_RSRC_SYSTEM_CPU_BASE
  • info_count0 或超过 CPU 数量。
  • logical_cpu_number 无效。
  • 输入 performance_scale{.integral_part = 0, .fractional_part = 0}

预期用途

每当 CPU 频率发生变化时,都应使用 zx_system_set_performance_info 通知内核 CPU 性能的变化。该 API 仅支持为部分 CPU 指定性能缩放比例,因为不同的 CPU 可能由不同的实体控制。

如果 CPU 的频率要降低,建议在频率更改发生之前调用 zx_system_set_performance_info。这样做可让内核调度程序有机会在 CPU 容量降低之前减少该 CPU 上的负载。(调度程序应能快速响应,无需进一步协调;一旦实现支持,就会确认此预期。)

相反,如果要增加 CPU 的频率,建议在频率更改发生后调用 zx_system_set_performance_info,仅在新的容量可用时通知调度程序。

无论哪种情况,如果 CPU 频率更新失败,调用方都必须根据生成的 CPU 状态更新内核调度程序。调用方应尝试确定故障后的 CPU 频率,并使用该频率来通知对 zx_system_set_performance_info 的单独调用。如果无法确定频率(例如,如果关联的驱动程序完全失败),调用方应悲观地(低)猜测最终的 CPU 速度。此建议可能会随着进一步考虑而发生变化;例如,请参阅 https://fxbug.dev/42165500

新 API 最终将由待开发的“CPU 管理器”组件使用,该组件将负责 CPU 的用户空间管理。希望修改 CPU 频率的代理将向 CPU 管理器注册请求,而不是直接与 CPU 驱动程序交互。CPU 管理器将按照此提案中的描述,协调频率更改与内核更新。

CPU 管理器还将从电源管理器接管 CPU 的热节流责任(此提案的动机用例)。

系统调用 2:zx_system_get_performance_info

第二个系统调用会检索所有 CPU 的性能信息:

zx_status_t zx_system_get_performance_info(
    zx_handle_t resource,
    uint32_t topic,
    void* info,
    size_t info_count
    size_t* output_count
);

其参数如下:

  • resource:向此调用授予权限的资源。必须为 ZX_RSRC_SYSTEM_CPU_BASE

  • topicZX_CPU_PERF_SCALEZX_CPU_DEFAULT_PERF_SCALE,将在提案实施后定义。主题决定了写入 info 的内容,如下所述。

  • info:一个有效的 zx_cpu_performance_info_t[],其长度等于系统中的 CPU 数量。

    如果调用失败,info 将保持不变。

    如果调用成功,则返回时 info 包含每个 CPU 的一个元素,并按 logical_cpu_number 递增的顺序排列。每个元素的 performance_scale 都会根据 topic 进行填充:

    • ZX_CPU_PERF_SCALEperformance_scale 存储指定 CPU 的内核当前性能缩放比例。提供的值反映了最近一次对 zx_system_set_performance_info 的调用,即使下一次重新安排操作尚未发生也是如此。

    • ZX_CPU_DEFAULT_PERF_SCALEperformance_scale 存储内核在启动时用于指定 CPU 的默认性能缩放比例。

  • info_countinfo 数组的长度;必须等于系统中的 CPU 数量。

  • output_count:如果调用成功,此参数将包含写入 info 的元素数量。如果调用失败,则其值未指定。

错误情况

ZX_ERR_BAD_HANDLE

  • resource”不是有效的句柄。

ZX_ERR_WRONG_TYPE

  • resource 不是有效的资源句柄,或者不是 ZX_RSRC_KIND_SYSTEM 类型。

ZX_ERR_INVALID_ARGS

  • topic 不是 ZX_CPU_PERF_SCALEZX_CPU_DEFAULT_PERF_SCALE
  • info 是无效的指针。

ZX_ERR_OUT_OF_RANGE

  • resource 的类型为 ZX_RSRC_KIND_SYSTEM,但不等于 ZX_RSRC_SYSTEM_CPU_BASE
  • info_count 不等于系统中的 CPU 总数。

预期用途

ZX_CPU_PERF_SCALE 下的行为允许用户空间代理查询性能缩放,以用于诊断。例如,这可能有助于代理在首次启动时评估系统状态,或作为崩溃报告的信号。

ZX_CPU_DEFAULT_PERF_SCALE 下的行为允许代理确认其配置的性能规模与内核使用的性能规模是否一致。

实现

内核

  • 必须实现新的系统调用,并由新的资源 ZX_RSRC_SYSTEM_CPU_BASE 进行门控。

  • 必须修改内核调度程序以支持动态性能缩放,更新它们以使用 zx_system_set_performance_info 提供的最新值,并额外向 zx_system_get_performance_info 公开其当前使用的性能缩放和默认性能缩放。

组件管理器

必须定义新的协议 CpuResource,并且必须由组件管理器实现该协议,以提供 ZX_RSRC_SYSTEM_CPU_BASE 资源。这遵循了用于控制系统调用的资源的现有模式。

性能

新系统调用的执行时间非常短,因为它们只会触及少量与 CPU 数量成正比的数据。

使用 zx_cpu_set_performance_info 会导致调度程序以不同的方式分配工作,将工作转移到性能相对于所有性能比例之和有所提高的核心,并从性能比例类似地有所降低的核心转移工作。重新安排流程本身不会给调度程序带来大量负载。

重新安排会导致系统性能发生预期变化。对这些更改的测试相当于测试调度程序的功能正确性,这在测试中进行了说明。

安全注意事项

这两个新系统调用都受新资源句柄 ZX_RSRC_SYSTEM_CPU_BASE 的限制。对于 zx_system_set_performance_info,此保护措施可解决恶意干扰调度程序的明显问题。对于 zx_system_get_performance_info,还有一个更微妙的数据泄露问题;不应信任不受信任的实体来了解内核的性能扩展,这通常会提供有关系统支持的 P 状态的信息。

隐私注意事项

此提案对隐私权没有有意义的影响。

测试

  • 将添加核心测试,以测试基本成功和失败条件。
  • 将添加单元测试来验证调度程序对更新后的性能比例的处理。他们将验证,如果截止时间线程固定到某个 CPU,并且该 CPU 的性能规模按系数 α 进行修改,那么分配给该线程的实际时间将乘以 1/α。

文档

Zircon 系统调用文档将更新,以包含新的 API。

缺点、替代方案和未知因素

通用性

我们考虑了更通用的接口,例如最终可以处理内核和 CPU 之间其他交互(例如离线)的 zx_set_cpu_properties 系统调用。最终,我们选择了窄接口,因为预计该接口的客户很少,这样可以使未来对所提议接口的更改成本相对较低。目前,对更通用的接口提出的要求很大程度上是猜测。

替代通话结构

除了 zx_system_set_performance_info 的仅设置操作之外,还考虑了返回性能比例已修改的 CPU 的先前性能比例的组合式获取/设置操作。此方法旨在确保调用方能够还原性能规模更改,以防关联的频率更改在较低级别执行时失败。

不过,进一步考虑后发现,简单地还原更改是不够的。这导致了一组更复杂的故障处理建议,并最终回到了更简单的仅设置操作。

最后,需要 zx_system_get_performance_info 来支持封闭式测试,在这种情况下,直接还原更改合适的,并且支持诊断用例。

替代 CPU 索引

我们曾考虑使用其他 CPU 索引方案,例如按物理 CPU 编号引用 CPU。不过,由于内核没有其他需要此类方案的情况,因此最符合 Zircon 的有限范围的做法是让 API 使用内核现有的逻辑 CPU 编号。这些数字在给定系统上是一致的,客户端可以维护静态的每主板配置来引用它们,也可以从 ZBI 访问它们的配置数据。

效果规模的替代方案

我们认为,新的 API 可能不会直接引用性能规模,而是利用调度程序将应用于给定 CPU 的基本性能规模的“速度因子”。这样做会减少客户端需要了解的特定于上下文的信息量;客户端无需了解 CPU 之间的相对性能,只需了解 CPU 的新频与标称频之间的比率。

我们之所以选择不采用这种方法,是因为性能缩放旨在以基本方式用于异构系统上的 CPU 热节流,因此该 API 的预期客户端不会因使用速度系数而获得任何有意义的好处。与此同时,我们还需要承担定义新概念和修改调度器以利用新概念的成本。

性能上限

此提案最初使用 uint32_t 表示性能缩放比例,该值表示 [0.0, 1.0] 中的实数值。特别是,这允许表示最大值 1.0。

虽然 1.0 是撰写本文时 Zircon 调度程序支持的最大性能缩放比例,但我们决定允许输入表示大于 1.0 的值,以支持未来的使用情形,例如涡轮模式。此外,之前的表示形式不是定点,因此导致了调度程序无法直接使用的值。

performance_scale 的表示法

performance_scale 最初是一个 uint64_t,其中高 32 位保存整数部分,低 32 位保存小数部分。这会在 zx_cpu_performance_info_t 中的字段之间产生 32 位的填充,从而引入潜在的泄漏向量。新的表示法避免了这一陷阱。

performance_scale 的允许值

我们仔细考虑了 zx_system_set_performance_info 应允许哪些值作为 performance_scale 的输入。表示 0.0 的值被认为太容易与离线 CPU 的指令混淆,而 Zircon 目前不支持离线 CPU,但预计未来会使用不同的 API 支持。因此,系统将表示 0.0 的值判定为错误。

极小的值也需要特别注意。例如,输入 {.integral_part = 0, .fractional_part = 1} 表示 2-32,可以合理地将其视为 0.0,从而有效地使相应核心离线。虽然可以通过强制执行最小允许值来解决此问题,但任何此类阈值目前都是任意的,并且会进一步复杂化内核与用户空间之间的合约。我们认为,将新 API 视为提示机制是最直接的方法,这样一来,内核就可以自由地替换输入(如果需要),而无需公开与此类选择相关的内部细节。

未来的工作

配置管理

理想情况下,用户空间代理会使用 ZBI 来共享内核调度程序所用的完全相同的 CPU 配置数据。目前尚不清楚这样做是否可行。

此外,还必须注意确保内核和用户空间代理将默认性能缩放与相同的标称频率相关联。

性能缩放的下限

原则上,调度器可以根据当前截止时间线程和 CPU 负载确定系统应保持的最低性能规模。这些边界的动态版本将成为用户空间代理的重要输入,该代理尝试利用较低的 CPU 频率来提高能效。zx_system_get_performance_info 的一个额外选项将提供一种自然的方式来公开这些信息。

CPU 归因

应建立某种方法,将线程的归因 CPU 时间与调度该线程的 CPU 的性能相关联。这种关联性对于建立对大核心与小核心上的调度具有鲁棒性的性能指标已经非常重要,随着我们开发围绕频率修改的机制(如本提案中所示),这种关联性变得更加重要。

保证执行节流代理

执行温控调频时 CPU 频率的降低可能会导致 CPU 饥饿,进而可能导致调频代理的进程不太可能及时调度。应以适当的方式优先执行节流代理。

在先技术和参考资料

将 CPU 频率控制责任委托给用户空间对于操作系统来说是不寻常的,因此无法获得有关此主题的现有技术。