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

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

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

问题
  • 67477
Gerrit 更改
  • 485181
作者
审核人
提交日期(年-月-日)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 会保证每个时间段内,系统会在每个时间段开始的截止期限内为线程分配最多容量 CPU。请看以下示例:

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

在接下来的 5 毫秒内,每 10 毫秒就会为线程分配 2 毫秒的 CPU。有两种方式可错过截止日期:

  1. 此任务已安排延迟。例如,如果下一个周期从时间 T 开始,但任务直到时间 T+4 毫秒才被调度,则该任务不可能达到其截止时间(它的调度时间本应不晚于 T+3 毫秒)。内核可以检测到这种错过的截止时间,但在实践中,除非调度程序有错误或超额订阅,否则绝不会发生这种情况。

  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()。本实现不会涵盖自旋锁定。虽然自旋锁定可以争用,但我们目前会忽略自旋锁定,因为测量自旋锁定的争用成本可能非常高昂。

为了最大限度地减少开销,我们会将这些时长记录为 tick 计数,并在 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 的访问权限。但是,这会极大地限制此功能的实用性:我们通常无法在开发环境中重现性能错误。我们需要一个可在正式版 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 可以强制内核遇到锁争用或触发页面错误(进程终止分段错误除外)。