RFC-0084:向 zx_info_task_runtime_t 添加更多指标

RFC-0084:向 zx_info_task_runtime_t 添加更多指标
状态已接受
区域
  • 内核
说明

向 zx_info_task_runtime_t 添加了其他指标,以调试媒体性能问题。

问题
Gerrit 更改
作者
审核人
提交日期(年-月-日)2021-03-09
审核日期(年-月-日)2021-04-06

摘要

ZX_INFO_TASK_RUNTIME 主题提供了一种检索任务在 CPU 上运行或排队等待运行所花费的总时间的方法。为了诊断调度瓶颈,我们建议添加更多精细的运行时信息,特别是等待页面故障所花费的时间和等待内核互斥量所花费的时间。

设计初衷

实时任务有截止期限。有时,任务会因为在内核中被阻塞的时间过长而错过截止期限。如需调试这些情况,了解任务被阻塞的原因会很有帮助。例如,如果某个任务的截止时间为 10 毫秒,但在等待页面故障时花费了 11 毫秒,我们可以得出结论,由于页面故障速度过慢,因此未能满足截止时间。

具体而言,我们希望改进由媒体子系统(例如 audio_core)生成的诊断信息。在 audio_core 中,每个混音器任务都必须在 10 毫秒内完成。如果任务耗时超过 10 毫秒,用户会听到音频中断。最近,我们发现由于页面故障速度缓慢(需要将 audio_core 可执行页面分页回来)和内核堆互斥量争用,导致错过了截止期限。这些问题无法通过快照进行诊断,我们必须记录轨迹,这是一个繁琐的过程,有时会因无法在本地重现问题而受阻(因为 bug 可能仅在特定环境中运行特定应用时触发)。这些是优先级较高的问题,会对媒体效果产生严重的负面影响。

我们的目标是从内核中导出足够的诊断信息,以便我们能够从快照中诊断这些问题,而无需深入研究执行轨迹。本文档建议向 zx_info_task_runtime_t 添加新统计信息,以更全面地回答“为什么此任务不可运行?”这一问题。

Zircon 线程截止期限的背景信息

Zircon 截止时间配置文件由三个部分组成:时段、容量和截止时间。为线程分配截止时间配置文件后,Zircon 会保证在每个周期的开始截止时间内,为线程分配最多 capacity CPU。请看以下示例:

  • 周期 = 10 毫秒
  • 容量 = 2 毫秒
  • 截止时间 = 5 毫秒

每隔 10 毫秒,系统会在接下来的 5 毫秒内为该线程分配 2 毫秒的 CPU 时间。有两种方式会导致错过截止期限:

  1. 任务的安排时间较晚。例如,如果下一个时间段从时间 T 开始,但任务要到时间 T+4 毫秒才会安排,那么该任务就不可能在截止时间之前完成(该任务应在 T+3 毫秒之前安排)。内核可以检测此类错过的截止期限,但在实践中,除非调度程序存在 bug 或过度订阅,否则这种情况绝不应发生。

  2. 任务完成时间超过 2 毫秒。内核无法知道何时会发生这种情况,因为它不了解任务边界。例如,如果任务在 T+1 毫秒时调度,总共运行 1 毫秒,然后阻塞 9 毫秒,内核无法知道任务是否错过了截止期限(因为它因某种原因而被阻塞),或者任务请求了 2 毫秒,但只需要 1 毫秒即可完成(然后休眠 9 毫秒以等待下一个周期)。

我们的目标是帮助诊断第二种错过截止日期的情况。目前,如果任务运行时间过长,则可以查询 ZX_INFO_TASK_RUNTIME 以了解在用户空间中在 CPU 上运行所花费的时间。如果 cpu_time 大于任务的预期运行时间,则任务会知道它只是运行时间过长。但是,如果 cpu_time 只占任务总时间的一小部分,则表示任务在内核中花费了大部分时间,并且可能处于阻塞状态且不可运行。此 RFC 的目标是帮助了解这些时间的用途。

设计空间

我们的目标是回答“为什么此任务不可运行?”这一问题。我们必须做出一些决定:

  1. 我们的回答应该多详细?具体而言,我们应该列出任务被阻塞的所有原因,还是只列出一些看似重要的原因?

  2. 我们应该以什么粒度来回答这个问题?例如,我们应该仅报告用户级别可以理解的简单事件(例如“阻塞在 zx_channel_read 上”),还是应该添加特定于 Zircon 当前实现的更低级别事件?

  3. 如果我们报告 N 个统计数据,我们是否应要求这 N 个统计数据不重叠,还是应允许它们重叠(或许以任意方式重叠)?

最简单、最直接的方法是枚举我们关注的一些事件,并为这些事件生成统计信息。鉴于媒体子系统存在页面故障和内核锁争用问题,我们可能会生成“在页面故障中花费的时间”和“在内核锁上阻塞的时间”统计信息。

可能还存在其他问题,这两个统计数据无法捕获。因此,完整的解决方案很有吸引力。一种方法是枚举线程可以进入内核的所有方式。这包括 N 个硬件中断(计时器和设备中断)和 K 个软件中断(系统调用和故障)。然后,我们会生成 N+K 个统计信息,每种中断对应一个统计信息。当线程进入内核时,计时器会启动,并在控制权返回到用户空间线程时停止。不过,用户级别已经可以计算“在系统调用 X 中花费的时间”,因此让内核计算此信息是多余的。

另一种方法是生成“在内核模式下在 CPU 上运行所花的时间”和“在 X 上阻塞所花的时间”统计信息,其中 X 是一组内核基元,例如“内核锁”或“通道”。由于一组内核基元会随时间而变化,因此这种做法可能会导致膨胀和频繁更改。

我们真正想要的是从实际设备中提取轨迹。理想情况下,我们会将轨迹持续记录到循环缓冲区,并在上传 TRACE_ALERT 后上传该缓冲区。

设计

虽然完全完整的解决方案是理想之选,但其设计和构建可能需要很长时间。我们需要立即诊断实际环境中的性能回归问题。因此,我建议我们添加两个有针对性的指标来解决当前的问题:等待页面错误所花费的时间,以及等待内核锁所花费的时间。

关于上述设计空间问题:

  1. 我们不会力求完整。

  2. 粒度是任意的(我们会记录我们认为需要的所有内容)

  3. 统计数据可能会以任意方式重叠

内核变更

// This struct contains a partial breakdown of time spent by this task since
// creation. The breakdown is not complete and individual fields may overlap:
// there is no expectation that these fields should sum to an equivalent
// "wall time".
typedef struct zx_info_task_runtime {
  // Existing fields
  zx_duration_t cpu_time;
  zx_duration_t queue_time;

  // New fields below here

  // The total amount of time this task and its children spent handling page faults.
  zx_duration_t page_fault_time;

  // The total amount of time this task and its children spent waiting on contended
  // kernel locks.
  zx_duration_t lock_contention_time;

} zx_info_task_runtime_t;

系统会按线程计算这两个字段,然后对进程和作业进行求和,就像目前对 cpu_timequeue_time 所做的那样。请注意,媒体子系统不需要按进程和按作业进行汇总,但为了与 zx_info_task_runtime_t 中的现有字段保持一致,此处还是包含了这些字段。

页面错误有多种类型,内核锁定也有多种类型。page_fault_time 表示处理所有类型的页面故障所花费的总时间。通过涵盖所有页面故障,我们避免了需要说明涵盖了哪一部分页面故障的问题,因为随着实现随时间推移而发生变化以及支持新架构,内核可能会添加或移除某些类型的页面故障,这可能会很难解释。

lock_contention_time 涵盖所有争用锁。不过,我们有意将“争用”一词的定义留得较为宽泛,以便内核能够随着时间的推移不断改进其实现,从而平衡衡量争用情况的成本与报告争用时间的好处。如需了解详情,请参阅“实现”部分(见下文)。

用户空间如何诊断错过的截止期限

有了这些新字段,用户空间可以使用以下代码来诊断错过的截止期限:

for (;;) {
  zx_object_get_info(current_thread, ZX_TASK_RUNTIME_INFO, &start_info, ...)
  deadline_task()
  if (current_time() > deadline) {
    zx_object_get_info(current_thread, ZX_TASK_RUNTIME_INFO, &end_info, ...)
    // ...
    // report stats from (end_info - start_info)
    // ...
  }
}

实现

page_fault_time 将计算所有页面故障处理程序所用的时间总和。在当前实现中,这包括 vmm_page_fault_handlervmm_accessed_fault_handler

lock_contention_time 将计算 Mutex::AcquireContendedMutexBrwLock::Block 所花费的总时间。此方法已可以访问当前的 Threadcurrent_ticks()。此实现不涵盖自旋锁。虽然旋转锁可能会发生争用,但我们目前会忽略旋转锁,因为衡量旋转锁的争用可能非常昂贵。

为了最大限度地减少开销,我们将以计数器计数的形式记录这些时长,并在 zx_object_get_info 系统调用期间转换为 zx_duration_t。实现的其他详细信息将遵循 cpu_timequeue_time 使用的现有模式。原型实现可在 fxrev.dev/469818 上找到。

性能

我们将运行 Zircon 互斥量基准测试,以验证是否没有出现回归问题。我们将在原始硬件 (x86 和 ARM) 上运行这些基准测试。此外,为了验证虚拟化环境中没有回归问题,我们将在 QEMU(x86 和 ARM)上运行这些基准测试。

向后兼容性

zx_info_task_runtime_t 结构体将采用版本控制,与对其他 zx_info_* 结构体所做的类似(如需查看示例,请参阅 fxrev.dev/406754)。

安全注意事项

ZX_INFO_TASK_RUNTIME 是一个可能泄露受检任务相关信息的旁道。例如,page_fault_time 可用于衡量任务的内存访问模式。为了缓解此类泄露,ZX_INFO_TASK_RUNTIME 主题已要求使用 ZX_RIGHT_INSPECT。系统会假定拥有该权限的任何用户都可以访问任务的私密数据。

ZX_INFO_TASK_RUNTIME 还可能会泄露与其他任务相关的间接信息。例如,如果某个任务知道自己的 page_fault_time,则可能能够推断出其他任务的内存访问模式。同样,如果某个任务知道自己在等待争用内核锁的时间,则可能能够推断出其他任务如何使用共享内核资源。未来,我们可能会使用低分辨率计时器构建 zx_info_task_runtime_t。这并不一定能防止时间攻击,但可以限制其有效性。

另一种防范措施是限制 zx_info_task_runtime_t 对特殊开发者 build 的访问权限。不过,这会大大限制此功能的实用性:我们通常很难在开发环境中重现性能 bug。我们需要在正式版 build 中启用解决方案。

为了完全避免此边信道,我们需要将指标报告和指标检查分为两项功能。例如,如果我们将轨迹持续记录到循环缓冲区中,并在命中 TRACE_ALERT 后将该缓冲区上传到特殊通道或端口,则无需向触发 TRACE_ALERT 的任务授予对轨迹的访问权限,从而消除了侧信道。如前所述,此类解决方案需要很长时间才能设计和构建,而我们现在需要立即解决问题。

隐私注意事项

无。

文档

需要更新 Zircon 系统调用文档,以添加新的 zx_info_task_runtime_t 字段。

在先技术和参考文档

在 Linux 中,最相关的先前技术是 getrusage,它会报告用户和系统 CPU 时间以及页面错误、I/O 操作和上下文切换的计数。Windows 具有 GetThreadTimes,用于报告用户和系统 CPU 时间。硬件性能计数器(例如 x86 上的 RDPMC)提供类似的信息,并且存在类似的安全问题

测试

我们将通过 audio_core 记录新的运行时信息来手动进行测试(我们已经为我的原型实现完成了此操作:请参阅 fxrev.dev/469819)。

我们将更新 cpu_timequeue_time 的现有测试,以测试旧版 zx_info_task_runtime_t(将命名为 zx_info_task_runtime_v1_t)。此外,Zircon 的 abi_type_validator.h 将更新为验证旧版和新版 ABI。这将确保保留 ABI 向后兼容性。

为此功能添加集成测试并不容易,因为例如,没有任何 API 可强制内核发生锁争用或触发页面故障(除了进程终止分段故障之外)。