| 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 会保证在每个周期内,线程在每个周期开始的截止时间内最多可分配到容量 CPU。不妨考虑以下示例:
- 周期 = 10 毫秒
- 容量 = 2 毫秒
- 截止时间 = 5 毫秒
每隔 10 毫秒,线程会在接下来的 5 毫秒内分配到 2 毫秒的 CPU 时间。以下两种情况会导致错过截止日期:
任务安排在截止时间之后。例如,如果下一个周期从时间 T 开始,但任务直到时间 T+4ms 才被调度,那么任务就无法在截止时间之前完成(它应该在不晚于 T+3ms 的时间被调度)。内核可以检测到这种错过的截止期限,但在实践中,除非调度器存在 bug 或超额订阅,否则这种情况永远不会发生。
任务完成时间超过 2 毫秒。内核无法知道何时发生这种情况,因为它不了解任务边界。例如,如果任务在 T+1 毫秒时被调度,总共运行 1 毫秒,然后阻塞 9 毫秒,内核无法知道该任务是否错过了截止时间(因为它被阻塞了),或者该任务是否请求了 2 毫秒,但只需要 1 毫秒即可完成(然后休眠 9 毫秒以等待下一个周期)。
我们的目标是帮助诊断第二种错过截止期限的情况。目前,如果某项任务运行时间过长,它可以查询 ZX_INFO_TASK_RUNTIME,以了解在用户空间中在 CPU 上运行所花费的时间。如果该 cpu_time 大于任务的预期运行时长,则任务知道自己运行的时间过长。不过,如果 cpu_time 仅占总任务时间的一小部分,则表示任务的大部分时间都花费在内核中,很可能处于阻塞状态且无法运行。此 RFC 的目标是帮助了解该时间花费在何处。
设计空间
我们的目标是回答“为什么此任务无法运行?”这一问题。我们必须做出一些决定:
我们的回答应有多完整?具体来说,我们是应该列举任务被阻塞的所有原因,还是只列举一些看似重要的原因?
我们应该以何种粒度回答此问题?例如,我们是应该只报告用户级可理解的简单事件(例如“blocked on zx_channel_read”),还是应该包含特定于 Zircon 当前实现的较低级别事件?
如果我们报告 N 个统计信息,是否应要求这 N 个统计信息不重叠,还是应允许它们重叠(可能以任意方式重叠)?
最简单、最直接的方法是列出我们关注的几个事件,并生成这些事件的统计信息。鉴于媒体子系统在页面错误和内核锁争用方面存在问题,我们可能会生成“花费在页面错误上的时间”和“花费在内核锁上的阻塞时间”的统计信息。
可能还潜藏着其他问题,而这两个统计数据无法反映这些问题。因此,完整的解决方案很有吸引力。一种思路是列举线程进入内核的所有方式。这包括 N 个硬件中断(计时器和设备中断)和 K 个软件中断(系统调用和故障)。然后,我们生成 N+K 统计信息,每种中断各一个。当线程进入内核时,计时器会开始计时;当控制权返回到用户空间线程时,计时器会停止计时。不过,用户级已经可以计算“在系统调用 X 中花费的时间”,因此让内核计算此信息是多余的。
另一种方法是生成“在内核模式下在 CPU 上运行所花费的时间”和“在 X 上阻塞所花费的时间”统计信息,其中 X 是一组内核原语,例如“内核锁定”或“渠道”。随着内核原语集随时间变化,这种想法可能会导致膨胀和频繁更改。
从宏观角度来看,我们真正想要的是从实际设备中提取轨迹。理想情况下,我们会持续将轨迹记录到循环缓冲区中,并在遇到 TRACE_ALERT 后上传该缓冲区。
设计
虽然理想的解决方案是完全完整的,但设计和构建可能需要很长时间。我们迫切需要诊断实际环境中的性能回归。因此,我建议添加两个有针对性的指标来解决我们当前的问题:等待缺页中断的时间量和等待内核锁的时间量。
关于上述设计空间问题:
我们不会追求完整性。
粒度是任意的(我们会记录我们认为需要记录的任何内容)
统计数据可能会以任意方式重叠
内核变更
// 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_time 和 queue_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_handler 和 vmm_accessed_fault_handler。
lock_contention_time 将计算 Mutex::AcquireContendedMutex 和 BrwLock::Block 所用的总时间。此方法已可访问当前的 Thread 和 current_ticks()。该实现不会涵盖自旋锁。虽然自旋锁可能会发生争用,但我们暂时忽略自旋锁,因为衡量自旋锁的争用可能会非常耗费资源。
为了最大限度地减少开销,我们将这些时长记录为时钟周期数,并在 zx_object_get_info 系统调用期间转换为 zx_duration_t。实现的其余细节将遵循 cpu_time 和 queue_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。这不一定会阻止时序攻击,但可以限制其有效性。
另一种防御措施是限制对特殊开发者 build 的 zx_info_task_runtime_t 访问权限。不过,这会大大限制此功能的实用性:我们经常难以在开发环境中重现性能 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_time 和 queue_time 的现有测试,以测试旧版 zx_info_task_runtime_t,该版本将命名为 zx_info_task_runtime_v1_t。此外,Zircon 的 abi_type_validator.h 将更新为验证旧 ABI 和新 ABI。这样可确保保留 ABI 向后兼容性。
为该功能添加集成测试并不容易,因为例如,没有 API 可以强制内核遭受锁争用或触发缺页中断(除了进程终止分段错误)。