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 的频率,建议在发生频率更改后调用 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 的热节流控制(此提案的动机用例)的责任。

系统调用 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:长度等于系统中 CPU 数量的有效 zx_cpu_performance_info_t[]

    如果调用失败,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。

缺点、替代方案和未知情况

通用性

我们考虑过更通用的接口,例如 zx_set_cpu_properties 系统调用,它最终可以处理内核和 CPU 之间的其他互动,例如离线。最终,我们选择了窄接口,因为预计此接口的客户非常少,这使得日后对建议接口进行更改的成本相对较小。目前,对更通用接口的要求在很大程度上是猜测性的。

备选调用结构

作为 zx_system_set_performance_info 的仅设置操作的替代方案,我们考虑了一种组合 get/set 操作,该操作会针对修改了规模的 CPU 返回之前的性能规模。这是为了确保,如果相关频率更改的低级别执行失败,调用方能够还原性能缩放更改。

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

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

备选 CPU 编制索引

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

效果规模的替代方案

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

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

最大性能缩放

此提案最初使用表示 [0.0, 1.0] 范围内实值的 uint32_t 来表示性能规模。具体而言,此表示法允许的最大值为 1.0。

虽然在撰写本文时,Zircon 调度程序支持的最大性能规模为 1.0,但我们决定允许输入表示大于 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 频率控制责任委托给用户空间对操作系统来说并不常见,因此无法找到与此主题相关的先前技术。