| 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+4 毫秒才安排,则任务不可能在截止时间之前完成(任务应不晚于 T+3 毫秒安排)。内核可以检测到这种错过截止时间的情况,但在实践中,除非调度器有 bug 或超额订阅,否则这种情况绝不应该发生。
任务完成时间超过 2 毫秒。内核无法知道何时发生这种情况,因为它不了解任务边界。例如,如果任务在 T+1 毫秒安排,总共运行 1 毫秒,然后阻塞 9 毫秒,内核无法知道任务是否错过了截止时间(因为它被阻塞了),或者任务是否请求了 2 毫秒,但只需要 1 毫秒即可完成(然后休眠 9 毫秒以等待下一个周期)。
我们的目标是帮助诊断第二种错过截止时间的情况。目前,如果任务运行时间过长,它可以查询 ZX_INFO_TASK_RUNTIME,了解在用户空间中运行 CPU 所花费的时间。如果 cpu_time 大于任务的预期运行时,则任务知道它只是运行时间过长。但是,如果 cpu_time 仅占任务总时间的一小部分,则任务的大部分时间都花在了内核中,很可能被阻塞且无法运行。此 RFC 的目标是帮助了解这些时间都花在了哪里。
设计空间
我们的目标是回答“为什么此任务无法运行?”这个问题。我们必须做出一些决定:
我们的答案应该有多完整?具体来说,我们应该列出任务被阻塞的所有原因,还是只列出一些看起来很重要的原因?
我们应该以什么粒度回答这个问题?例如,我们应该只报告用户级别可以理解的简单事件,例如“被 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()。该实现不会涵盖自旋锁。虽然自旋锁可能会发生争用,但我们暂时忽略自旋锁,因为衡量自旋锁上的争用可能非常昂贵。
为了最大限度地减少开销,我们会将这些时长记录为 tick 数,并在 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。这不一定会阻止计时攻击,但可以限制其有效性。
另一种防御措施是将 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_time 和 queue_time 的现有测试,以测试旧版本的 zx_info_task_runtime_t,该版本将命名为 zx_info_task_runtime_v1_t。此外,Zircon 的
abi_type_validator.h
将更新,以验证旧 ABI 和新 ABI。这将确保保留 ABI 向后兼容性。
为此功能添加集成测试并不容易,因为例如,没有 API 可以强制内核遭受锁争用或触发页面错误(除了进程终止分段错误)。