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 系统上,供应商可以提供性能数据,表明以其最大速度运行的大核心在最大速度下的表现是小核心以自身最大速度运行的小核心的两倍。如果参考性能对应于以最大速度运行的大核心,则该运行条件对应于性能规模为 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}

预期用途

zx_system_set_performance_info 应该用于在 CPU 频率发生变化时通知内核 CPU 性能发生变化。该 API 仅支持为一部分 CPU 指定性能规模规格,因为不同的 CPU 可能由不同的实体控制。

如果要降低 CPU 的频率,建议在发生频率变化之前调用 zx_system_set_performance_info。这样做可让内核调度程序在减少容量之前减少该 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 管理器还将通过 Power Manager 来负责 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 的性能等级是否按系数 alpha 进行了修改,那么分配给该线程的实际时间将乘以 1/alpha。

文档

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

缺点、替代方案和问题

通用性

考虑了更通用的接口,例如 zx_set_cpu_properties 系统调用,它可以最终处理内核与 CPU 之间的其他交互(例如离线使用)。最终,我们选择了窄接口,因为预计此接口的客户端非常少,这使得未来对提议的接口进行更改所付出的成本相对较小。针对更通用的接口提出的要求此时在很大程度上都是凭空猜测的。

其他调用结构

作为 zx_system_set_performance_info 的“仅限集合”操作的替代方案,考虑了组合的 get/set 操作,该操作会返回已修改的 CPU 的先前性能指标。这是为了确保调用方能够在相关频率更改的执行失败时还原性能规模更改。

不过,经过进一步的考虑发现,简单的更改还原是不够的。这导致出现了一组更复杂的故障处理建议,并导致了更简单的“仅限设置”操作。

最后,您需要使用 zx_system_get_performance_info 来支持封闭测试(在这种情况下,直接还原更改是合适的做法,并且可支持诊断用例)。

备用 CPU 索引编制

我们考虑使用替代方案来为 CPU 编制索引,例如通过物理 CPU 编号来引用 CPU。不过,由于内核对这种方案没有其他需求,因此让 API 使用内核现有的逻辑 CPU 编号与 Zircon 的有限范围最一致。这些数字在给定系统中是一致的,客户端可以维护一个静态单板配置来引用它们,也可以从 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 目前不支持这种操作,但将来使用其他 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 频率控制的责任委派给用户空间是不常见的,因此有关本主题的前沿技术不可用。