RFC-0274:内核辅助 CPU 分析 | |
---|---|
状态 | 已接受 |
区域 |
|
说明 | 一种基于时间、单会话的 CPU 分析器,利用帧指针从用户线程获取回溯 |
问题 | |
Gerrit 更改 | |
作者 | |
审核人 | |
提交日期(年-月-日) | 2025-05-29 |
审核日期(年-月-日) | 2025-09-15 |
问题陈述
Fuchsia 开发者需要有效的性能分析工具,包括 CPU 分析。目前,Fuchsia 缺少用于 CPU 分析的稳定 API,而这对于分析原生 Fuchsia 组件和在 Starnix 容器中运行的二进制文件的性能至关重要。我们需要此 API 来了解代码中的热点。
摘要
本文档提出了 Fuchsia 中 CPU 分析的设计。具体而言,它概述了一个基于时间的单会话 CPU 分析器,该分析器利用帧指针从用户线程获取回溯。熟悉 Fuchsia 上的分析的读者可能会注意到,所提出的设计与目前已签入的实验性分析器非常相似,但确实包含一些关键差异。
利益相关方
辅导员:
- davemoore@google.com
审核者:
- eieio@google.com
- mcgrathr@google.com
- wilkinsonclay@google.com
已咨询:
- adamperry@google.com
- tq-performance@google.com
- maniscalco@google.com
共同化:
此 RFC 已在 zircon-discuss 论坛上发布。
要求
本文提出的分析器 API 旨在尽可能精简,以支持快速开发。此 API 必须:
- 能够使用帧指针对用户空间线程的调用堆栈进行采样。
- 在 eng build 和 userdebug build 中均可用。
- 能够收集基于帧指针的 Fuchsia 程序(包括 Starnix)回溯。
- 能够收集在 Starnix 容器中运行的 Linux 程序的帧指针回溯。
- 支持基于时间的采样,采样率最高可达 4000Hz。
并且,它在执行所有这些操作时,对系统性能的影响应尽可能小。
明确的非目标和未来工作
我们明确希望避免以下情况,因为这些情况会偏离保持 Zircon 运行时无关性的目标:
- 在内核中添加对独特语言运行时(例如 Go 运行时)的感知。
此外,在制定此提案时,我们考虑了许多分析功能,但为了加快进度,这些功能已被声明为超出范围。这些功能与设计兼容,但会作为可能的后期附加项单独列出。
- 硬件辅助分析(例如 Intel LBR 或 ARM 性能计数器)。
- 在用户空间初始化之前和可以进行系统调用之前,对早期系统启动进行分析。
- 分析内核调用堆栈。
- 处理动态链接库在能够从中加载符号之前重复使用地址(通过 dlopen/dlclose)的情况。
- 在支持影子调用堆栈的架构上支持影子调用堆栈。
- 支持多个并发的分析会话。
- 能够配置要分析哪些任务(线程、进程或作业)。
如需进一步了解,请参阅未来工作。
设计
内核设计
图 1 - 内核结构
上图概述了我们计划实现的系统。它利用单个全局抽样会话来提供对基于时间的抽样的访问权限。
一般流程如下:
- 用户发出系统调用,以根据配置创建采样器。在此系统调用期间:
- 内核会分配、映射并固定用于存储采样数据的每个 CPU 缓冲区。
- 为每个 CPU 创建一个单调计时器,以下称为“采样计时器”。
- 系统会向用户返回一个句柄,用户可以通过该句柄请求控制平面操作以及读取数据。
- 然后,用户发出系统调用以开始采样,该调用首先会设置每个 CPU 的采样计时器。
- 每次抽样计时器触发时,我们都会检查当前线程。如果应抽样线程,我们会抽取一个样本并将其写入当前 CPU 的缓冲区。
- 用户可以随时读取抽样数据。
- 用户可以继续采样,直到想要停止为止。此时,他们会调用系统调用来停止采样,从缓冲区读取剩余数据,然后关闭采样器的句柄。
主要逻辑将在 ThreadSampler(一个包含会话所需状态的全局单例)中实现。用户可以通过下一部分中介绍的 zx_sampler_*
系统调用与之互动。
系统调用 API
以下系统调用 API 将用于与采样器进行交互。请注意,我们考虑了多种不同的替代方案;“考虑的替代方案”部分讨论了这些替代方案及其优缺点。
zx_sampler_create
以下系统调用将创建采样器并返回其句柄:
// The sampler_config_t sets the properties of a sampler.
//
// Note that there is no version number on this struct; the options argument in
// zx_sampler_create can be used as one to evolve this struct.
struct zx_sampler_config_t {
// How often a sample should be taken.
zx_duration_mono_t sample_period;
// The maximum number of stack frames to collect in each sample.
uint16_t max_stack_depth;
};
// zx_sampler_create will create the sampler if it has not already been created
// and then return a handle to it.
//
// Arguments:
// * sampler_resource: A handle to the sampling resource.
// * options: Must be zero.
// * config: The configuration to sample with.
// * sampler_out: An out parameter that will contain a handle to a
// sampler on success.
zx_status_t zx_sampler_create(
zx_handle_t sampler_resource,
uint64_t options,
const zx_sampler_config_t* config,
zx_handle_t* sampler_out);
sampler_resource
是一种新的系统资源,将用于控制采样器的创建。
如果用户空间通过 zx_handle_close
关闭此系统调用返回的句柄,则会销毁会话并停止对所有目标的抽样,就像调用了 zx_sampler_stop
一样。
在此 RFC 中,我们计划实现具有单个全局单例采样器的采样器,并且一次只允许创建一个采样器。如果全局会话正在使用,zx_sampler_create
将返回 ZX_ERR_ALREADY_EXISTS
,直到现有句柄关闭为止。请参阅未来工作中的多会话。
zx_sampler_[start|stop]
以下系统调用将用于启动和停止抽样:
// zx_sampler_start will start sampling, and zx_sampler_stop will stop sampling.
//
// Arguments (for both syscalls):
// * sampler: A handle to the global sampler.
// * options: Must be zero for now.
zx_status_t zx_sampler_start(zx_handle_t sampler, uint64_t options);
zx_status_t zx_sampler_stop(zx_handle_t sampler, uint64_t options);
请注意,在采样器已运行时调用 zx_sampler_start
是无效的,并且会返回 ZX_ERR_BAD_STATE
。同样,在采样器未运行时调用 zx_sampler_stop
也是无效的,也会返回 ZX_ERR_BAD_STATE
。
zx_sampler_read
以下系统调用将用于读取采样器数据:
// zx_sampler_read reads sampler data.
//
// Arguments:
// * sampler: A handle to the global sampler.
// * options: Must be zero.
// * buf: The user buffer to read data into.
// * buf_len: The size of buf.
// * actual: An out parameter that will contain the amount of data that was
// actually read on success.
zx_status_t zx_sampler_read(
zx_handle_t sampler,
uint64_t options,
void* buf,
size_t buf_len,
size_t* actual);
此函数会将所有可用的抽样数据读入所提供的缓冲区。如果缓冲区太小,无法包含所有这些数据,则返回 ZX_ERR_INVALID_ARGS
。
在抽样未进行时(即在调用 zx_sampler_stop
之后)调用此系统调用是有效的。在这种情况下,系统会读取并返回抽样期间生成的所有数据。
录制选段
当计时器触发时,我们会检查当前 CPU。如果我们已迁移,则立即返回。然后,我们检查当前 CPU 上运行的线程。如果我们不想对相应线程进行采样(例如,该线程是内核线程),那么我们只需记录时间戳并返回,因为内核堆栈采样不在本提案的范围内。
即使当前线程是用户线程,我们也无法立即从计时器回调中对调用堆栈进行采样。这是因为我们无法从中断上下文中安全地读取用户内存,因为这可能需要发生页面错误。而是设置 THREAD_SIGNAL_SAMPLE_STACK
线程信号并返回。
稍后,当线程即将退出内核时,它会调用 ProcessPendingSignals,该函数会检查 THREAD_SIGNAL_SAMPLE_STACK
。如果存在信号,则对堆栈进行采样。我们可以在此处安全地尝试读取用户内存,因为:
- 我们不再处于中断上下文中,因此可以根据需要发生缺页中断
- 我们已解除我们持有的大部分锁定
- 内核堆栈相对较浅,因此我们可以将可能较深的调用堆栈读取到内核堆栈中。
实验性 API 遇到的一个问题是,如果给定内核行程的计时器触发多次,此方法可能会导致丢失样本,因为我们每次行程只获取一个堆栈样本。我们改进了实验性采样器,使其能够立即发出包含时间戳和其他辅助数据的部分记录,然后稍后发出包含堆栈轨迹的延续记录。一个或多个部分记录可能引用同一堆栈轨迹延续记录(请参阅数据格式)。格式(详情)。由于部分记录不会读取用户内存,并且其写入的缓冲区已映射并固定,因此在中断上下文中发出是安全的。
来自用户空间的调用堆栈采样
采样可以在用户空间中完全低效地完成,并且目前是分析的一种选择。不过,这样做需要多次调用 zx_process_read_memory
,并且每个样本大约需要 2-3 毫秒。即使以 100Hz 的频率进行采样,也会占用 20-30% 的 CPU,这会造成无法承受的开销。内核中的类似操作需要 2-3 微秒,因此我们可以在低开销的情况下实现超过 4000Hz 的采样率。
使用帧指针展开调用栈
堆栈将通过步行帧指针读取。抽样需要启用帧指针的 build。Fuchsia 目前默认在用户产品和内核中启用帧指针。
由于用户程序可以控制自己的堆栈,因此内核在读取用户内存时需要采取一些预防措施。它必须小心谨慎,仅跟踪指向采样目标有效内存的指针,并且可能需要限制所采样堆栈的深度。用户堆栈可能非常深,超过 30 个堆栈帧,因此内核将提供可配置的最大堆栈采样深度,如系统调用 API 部分中所述。
之所以使用帧指针而不是其他方法,是因为帧指针既能提供良好的跨平台使用体验,又能限制复制的内存量。我们确实打算在未来探索和实施其他选项。
符号化
为了让用户程序能够象征性地表示其接收到的指令指针的调用堆栈,它需要有关目标程序的其他信息。它需要知道哪些库和可执行文件映射到了哪里。用户空间负责获取对接收到的调用堆栈进行符号化所需的相应信息。
符号化要求我们首先将调用堆栈上的指令指针转换为相应 ELF 二进制文件中的偏移量。如今,用户空间通过以下方式实现此目的:
- 使用
zx_object_get_info(ZX_INFO_PROCESS_MAPS).
检索当前进程中的内存映射 - 遍历返回的
zx_info_maps_t[]
,以确定 ELF 二进制文件的映射以及它们已加载到的基地址。 - 在每个基地址上使用
zx_process_read_memory
读取 ELF 标头,这样我们就可以获取与映射的二进制文件对应的 build-ID。 - 然后,系统会使用 ELF build-ID 和基址以及调试信息数据库,在会话完成后对指令指针进行符号化。
由于重复查询成本高昂,用户空间 CPU 分析器目前仅扫描两次映射。第一种情况是,当它拦截线程的调试启动通知时,该通知由 ld.so 在链接线程的库后直接触发。第二个是在性能分析会话结束后立即执行。 第二次传递旨在捕获可能在第一个动态链接之后加载的任何动态加载的库(例如通过 dlopen)。
虽然在实践中,这涵盖了现有 Fuchsia 程序使用的大多数映射,但在动态加载的库方面并不完全正确:
- 它不会处理在进程退出之前取消映射/卸载/关闭的映射,为不同的库重复使用相同的地址可能会导致符号损坏
- 在会话结束前退出的进程不会再次扫描动态加载的库,并且只会包含动态关联的库的符号。
我们将在未来迭代中处理 dlopen/dlclose,因为支持它们需要在内核、动态链接器和加载器、组件管理器及其加载器服务实现之间进行额外协调,可能还需要文件系统来协调符号化动态加载文件所需的数据。
从内核流式传输数据
采样器分析器将生成大量需要从内核高效流式传输的数据。幸运的是,Fuchsia 最近添加了对内核跟踪的流式支持,我们计划采用类似的方法。
简而言之,我们计划使用每个 CPU 的单读取器单写入器环形缓冲区来存储采样器数据。每个缓冲区的写入器无需获取任何锁即可继续操作,而读取器和其他控制操作则需要使用锁。当缓冲区已满时,我们尝试写入缓冲区的所有新记录都会被舍弃,这与现有内核 FXT 写入库的支持行为一致。
数据格式
数据以 FXT blob 记录的形式写入到每个 CPU 映射的固定缓冲区,该缓冲区采用与内核跟踪相同的方法。由于缓冲区已映射并固定,因此在中断上下文中写入缓冲区是安全的,不会有发生页面错误的风险。
如果用户空间组件无法足够快地读取数据,并且我们尝试写入会使缓冲区溢出的记录,则会舍弃该记录,并记录舍弃的记录数。当用户空间释放缓冲区空间时,我们会发出一条记录,告知用户有多少记录被丢弃。
发出的记录
我们将以下 FXT Blob 记录写入包含以下信息的缓冲区
示例记录
FXT Blob Record {
u64 fxt_header, // Blob_type SAMPLE
u64 fields_bitflags,
// Each of the following optional fields are only included if the
// corresponding `fields_bitflags` bit is set
Optional<u64> continuation,
Optional<u64> continuation_completion,
Optional<u64> pid,
Optional<u64> tid,
Optional<u64> boot_ts,
Optional<u64, [u64]> user_fp_call_stack,
}
样本以 FXT blob 记录的形式写入。Blob 中的第一个字段是一个 u64,其中包含每个纳入字段的位字段。只有当位标志中的相应位已设置时,这些字段才会显示,并且会按所述顺序显示。位标志中的位分配如下:
- 1:继续(u64),如果后面还有更多样本,则设置此值
- 1:continuation_completion (u64),如果此记录会向之前的记录添加样本,则设置此值
- 1:pid(koid/u64)
- 1:tid(koid/u64)
- 1:boot_ts (u64)
- 1:user_fp_call_stack(长度前缀为 u64 的数组)
- 48:预留
延续
记录可以通过设置“继续”位并提供 u64 ID 来指示后续还有其他信息。如果后续记录的“continuation_completion”位设置为 1 且具有匹配的 ID,则应将其内容附加到之前的记录中。多个记录可能会使用相同的延续 ID,在这种情况下,与这些 ID 匹配的 continuation_completion 记录的样本会被复制并附加到每个记录中。
每个 CPU 都会维护一个单调递增的计数器(在 CPU 数量之间划分 u64 空间),并在发出每个完成记录后递增该计数器。
https://fxrev.dev/1252454 中提供了一个示例实现,展示了如何从中断上下文中发出这些延续记录。
CPU 事件
内核可以修改 CPU 的电源状态,这可能会影响采样行为。此处介绍了这些与电源相关的 CPU 操作和采样之间的互动。
CPU 热插拔
缓冲区由各个 CPU 之外的数据结构拥有。当 CPU 离线时,缓冲区会保留,但在 CPU 离线期间不会写入缓冲区。读取器可以随时读取数据,并且在会话句柄关闭时,缓冲区将被销毁。
中止
此设计的初始实现将利用单调计时器来触发调用堆栈的收集。因此,在系统暂停期间(尤其是挂起到空闲状态期间),不会收集任何样本。我们认为这是一个可接受的解决方案,因为在挂起到空闲状态期间不会安排任何线程,这意味着即使我们使用启动计时器,也不会对任何调用堆栈进行采样。
用户空间设计
用户空间主要通过组件“cpu_profiler.cm”与分析进行交互。用户空间组件负责:
- 通过过滤
zx_sampler_create
中的数据,转换更高级别的用户空间配置(例如,按 tid/pid/测试/组件 moniker/网址附加) - 服务缓冲区和渗漏数据
- 读取目标的 ELF 标头以获取符号化数据
- 与 ffx、starnix 或其他调用方通信
实现
实现将从现有的实验性采样器 API 开始,使其与本文档中所述的内容保持一致,并根据本文档的更改情况进行更新。
在咨询安全性、隐私权和 API 委员会并解决提出的问题后,这些 API 将被放入下一个 vdso 中,并继续进行开发和迭代,直到获得批准。
提案 API 与实验性 API 之间的差异
与上述提案相比,缺少现有的内核采样支持:
- 支持每次进入内核时进行多次采样。
- 内核流式传输支持
性能
采样 API 应允许以 4000Hz 的频率进行采样,开销低于 10%,或者每个 250us 采样周期的运行时时间少于 25us。这可以通过检查启用和未启用分析功能时对基准的影响,以及测量启用和未启用分析功能时的 CPU、内存和其他资源使用情况来衡量。
工效学设计
通过允许调用方在采样会话期间避免调用 zx_process_read_memory
,所提议的 API 可显著简化调用栈采样的实现。
向后兼容性
此 API 是对 Zircon 公开的现有 API 的补充。由于我们打算在未来对该 API 进行迭代,因此我们添加了一个部分,介绍了未来的工作以及如何扩展所提议的 API 来支持这些工作。我们保留了预留字段和其他机制,以便以向后兼容的方式发展此 API。
安全注意事项
我们引入了一种新的“采样”资源,类似于现有的内核跟踪资源。该请求由组件管理器路由到单个用户空间分析组件,该组件负责将更高级别的用户空间配置(例如按 tid/pid/测试/moniker/网址附加)转换为 zx_sampler_create
数据上的过滤器。
安全审核摘要
- 通过以下方式对客户端访问进行严格控制:使用特定的抽样资源、为 cpu_profiler 组件的客户端提供有限的许可名单、限制为 eng/userdebug build,以及调试 syscall 命令行标志(最后一个可能需要在某个时间点放宽对 userdebug 的限制;请参阅下文)。
- 通过控制最大堆栈深度和基本指针验证,可限制恶意被跟踪进程堆栈带来的风险。有人可能会想到一种复杂的攻击,即使用旁路通道读取伪造的帧指针指向的特权内存,但这不太可能成为任何实际问题。内核中的代码已尽可能减少,尤其避免在那里进行 ELF 解析。
- 可能需要进一步讨论(超出此初始设计范围),以允许在 userdebug build 上进行采样器 syscall 访问,而无需启用所有其他调试 syscall。
隐私注意事项
在以下三种主要情况下,系统会获取个人资料并从 Fuchsia 设备中移除数据。
1) 本地开发者在 eng 设备上进行性能诊断/问题排查。 2) 基于基础架构的端到端 CUJ 性能测试。Infra 会自动使用测试账号在预先确定的关键用户历程中运行测试。我们启用跟踪和分析来监控性能特征。 3) 快照和字段跟踪。此工作流会定期记录测试设备上的配置文件,因此需要额外的隐私权控制措施。此方法收集的数据由 Perfetto 完成,该工具具有现有的隐私权控制功能,例如轨迹过滤和编辑。添加其他收集字段配置文件需通过额外的隐私权审核。
对敏感数据的访问权限和数据持久性
调用堆栈和执行轨迹虽然对性能分析很有用,但可能会泄露敏感数据。调用堆栈中的函数名称、参数,甚至数据本身的部分内容都可能会泄露正在处理的数据类型或其他私密信息。
此类敏感数据预计会在开发者设备和基础架构测试设备上公开,并且不会被分析堆栈的更高级别层进行编辑。因此,对于使用情形 (1) 和 (2),系统仅在收到请求时才进行分析,并将数据立即以文件的形式返回给请求者,而不会存储或上传到任何其他位置。
访问权限限制
我们使用以下组合来防止敏感信息被意外披露: - 路由到组件的编译时允许列表的系统资源 - 仅限于组件的编译时允许列表的 FIDL API - 针对用户产品的编译时停用更高级别的许可组件将负责编辑和过滤数据(如果它们希望存储数据)。在没有上述隐私控制的情况下,系统绝不会在用户设备上自动获取个人资料。
测试
当前的 CPU 分析器实现具有内核和用户空间级别的现有单元测试、集成测试和端到端测试,这些测试用于测试当前基于 zx_process_read_memory 的实现(以及启用时的当前实验性 API)。
这些测试将继续测试相关流程,并且我们将通过测试来扩展这些流程,以测试此处提出的新增功能,例如流式传输和新的记录类型。
文档
我们需要更新 //docs/development/profiling/profiling-cpu-usage.md 中的 CPU 分析器使用情况文档,以反映不需要构建标志。
我们还需要更新和完善 zx_sampler_* 系统调用的系统调用文档。
缺点、替代方案和未知因素
在制定本设计文档时,我们考虑了许多替代方案。其中一些替代方案因有更好的选择而被舍弃,但另一些则仅仅是因为产品目前不需要而被排除。因此,未来对该设计进行迭代时,可能会纳入本部分中的某些项。
替代系统调用 API
基于任务
或者,任务是指可以从中探测信息的事物。一种方法是配置任务,以开始对某个输出缓冲区的配置进行采样。可以通过设置属性来处理控制平面。
zx_status_t zx_task_probe(zx_handle_t profiling_resource,
zx_handle_t task,
zx_sample_config_t config,
zx_handle_t* ep0_out // Buffer to write to)
zx_set_property(ep0, ZX_PROBE_STATE, ZX_PROBE_STATE_START);
优点
- 不需要新的调度程序,数据存储在任务调度程序和 iob 上
缺点
- 没有明显的方法来请求对内核线程进行抽样,我们无法获取它们的句柄
- 没有明显的方法来指定系统范围的抽样。也许是在配置中指定了 root-job + kernel_threads。
- 对两个不相关的线程/进程进行采样需要多个会话(或需要额外的逻辑来将输出合并到单个缓冲区)
基于任务配置文件的
与基于任务的类似,但我们也利用了线程配置文件基础架构。在此方法中,我们创建了一个包含配置和输出缓冲区的 probe_profile,并可将其应用于任务。
zx_profile_create(zx_resource_t, zx_sample_config_t config, &ep0,
&profile);
zx_task_set_profile(zx_handle_t task, zx_handle_t profile, ...);
zx_set_property(zx_handle_t task, ZX_PROBE_STATE, ZX_PROBE_STATE_START);
优点
- 重用现有内核基础架构
- 关注点高度分离:创建配置文件需要权限,线程可以决定是否应应用配置文件,控制需要不同于创建的权限,读取和控制都可以单独处理。
缺点
- 系统调用分布在三个不同的
zx_
前缀中,这使得 API 难以跟踪和发现。
未融合的系统调用
我们可以改为向用户空间提供一对系统调用:
- 等待所请求的事件发生的能力
- 一种内核辅助的数据收集形式,可高效收集有关目标的数据
优点:
- 缓冲区分配和写入完全可以在用户空间中完成
- 配置复杂性可以分布在多个调用中
缺点:
- 添加了开销,以 16000Hz 的速率(4 个 CPU 上以 4000Hz 的速率进行采样)唤醒用户空间分析器,但该分析器会立即调用内核,然后再次进入休眠状态。
- 4 个 CPU 会在唤醒/通知单个用户进程时发生争用
- 当用户空间将抽样数据复制到自己的缓冲区时,会产生额外的副本
- 目标进程需要暂停,直到性能分析器被唤醒并完成其采样。
这是一个很棒的问题分解,但我们(承认有点毫无根据地)认为,这会造成不必要的开销,而且很难实现高效。标准优化方法:
- 提供包含事件的示例数据,而无需额外的系统调用来获取该数据
- 允许指定将事件写入何处,以避免额外的复制
- 允许对读取事件进行批处理,以避免唤醒
导致我们重新推导上述变体。
内核辅助符号化
Linux 用于跟踪映射的策略是在为附加进程映射可执行内存时发出记录:
struct {
struct perf_event_header header;
u32 pid, tid;
u64 addr; // Mapping address in target u64
usize len; // Length of mapping
u64 pgoff; // Page offset of mapping
char filename[]; // Location of backing memory
};
这在 Fuchsia 中更难实现,因为我们没有类似的概念来读取正在加载的库。共享对象作为每个组件软件包的一部分进行分发,外部组件很难(或不一定需要)访问它们。
虽然给定映射的地址和长度,我们可以像在符号化中那样使用 elf-search 来获取所需信息,但我们遇到了同样的限制,即只有在附加进程处于活动状态时,此方法才有效。我们最终仍然会遇到竞态条件,即在收到 mmap 通知后,如果我们没有及时处理该通知,进程可能会退出。
除了使用文件名之外,其他方法可能包括:
- ELF build ID,这需要教导内核从用户内存解析 ELF;或者,
- 相关映射的 koid,这需要用户空间维护某种 koid -> build-id 服务。
如果我们发现使用 ZX_INFO_PROCESS_MAPS
获取基本地址和使用 zx_process_read_memory
获取用户空间中的 build ID 的方法性能不足,可能需要更认真地考虑这些选项。
未来工作
用户可能需要许多功能。本部分收集了本文档中提及或未提及的各种功能,这些功能会影响未来的理想状态。
多次会话
多会话是一项所需的功能,它允许同时对 FIDL 客户端和服务器进行分析,而无需进行系统范围的分析。不过,由于它会引入许多额外的复杂性,因此该提案将其排除在范围之外。我们建议使用以下 API:这些 API 表面上可像多会话样式 API 一样使用,但如果创建了多个会话,则会出错。我们将多个会话支持推迟到后续版本中。
前期启动分析
如果像当前设计那样需要用户空间组件并依赖于组件管理器来实现 realm 查询 API,那么我们可以在多早开始分析方面会受到一些限制。更具体地说,在组件管理器准备就绪并可以启动用户分析组件之前,无法进行分析。
后续可能需要通过启动实参(如 ktrace)启用早期启动性能分析。启动实参描述了缓冲区大小和配置。此功能会在内核准备就绪后自动开启内核中的分析功能,并允许用户在空闲时从缓冲区中读取数据,而不会覆盖缓冲区。这可以在内核或 userboot 中完成。然后,我们可以在启动时向 component_manager 交付一个采样器句柄。
对内核堆栈进行采样
在处理请求内核堆栈的事件时,我们能够立即遍历内核的帧指针链,因为这不需要用户复制,并写出记录,以便稍后可能继续处理用户空间调用堆栈。
Zircon 和停用中断
由于我们依赖于计时器中断,并且某些内核工作是在中断停用的情况下完成的,因此我们无法了解这些工作。例如,zx_futex_wake
在启用中断的情况下执行基本句柄验证,但遍历 futex 列表和唤醒等待者都是在停用中断的情况下完成的。这会限制内核堆栈的粒度,因为采样中断仅在重新启用中断后触发。在这种情况下,我们只会看到 CPU 时间归因于重新启用中断的位置,通常是 Guard::~Guard
析构函数。
CPU 性能计数器
主要 CPU 架构提供了一组可设置为统计事件(例如执行的指令、缓存未命中或分支未命中)的每个 CPU 寄存器/计数器。这些计数器可以在溢出时生成中断,从而让我们有机会重置它们并记录数据。通常,这些功能只能由特权模式代码访问和配置,并且可以选择性地向非特权代码公开部分功能。
软件事件
操作系统操作(如缺页中断、系统调用或上下文切换)中的软件事件可让用户程序诊断其代码中导致这些事件发生的热点。虽然有些事件可以使用用户空间封装容器进行插桩,但内核提供了一个高效的真实来源,用于记录这些事件发生的时间信息。
基于任务或上下文切换的感知事件
由于硬件计数器是每个 CPU 的资源,因此需要与被采样的线程一起保存和恢复。否则,每次上下文切换后的第一个样本实际上是随机的,具体取决于之前调度的进程。同样,了解上下文切换有助于我们更好地利用缓冲区空间,因为我们只需从关注的任务中进行采样,而无需在后期进行过滤。
堆栈采样的替代方法
当前设计利用帧指针来遍历调用栈。这是一个不错的起点,但我们可能还需要支持其他更具架构特异性的方法,例如在 ARM 上使用影子调用堆栈或在 Intel 上使用 LBR。我们可能最终还希望支持允许用户空间程序指定内核可用于生成调用堆栈的二进制文件(类似于 DTrace 或 ebpf)。
在先技术和参考资料
什么是 CPU 分析?
本文档讨论了基于样本的 CPU 分析。对于不熟悉此方法的用户,这是一种可观测性方法,可帮助回答“我的代码的热点在哪里?”这一问题。与跟踪相比,它不需要添加编译时跟踪点,但数据是随机抽样的。基于抽样的分析的主要前提是,每次目标发生某个“x”时,记录一些“y”数据。“x”可以是“每 250 微秒”或“每 1000 次 L1 缓存未命中”,而“y”通常是调用堆栈和时间戳。结果通常以火焰图的形式呈现,而不是时间序列。
虽然 Brenan Gregg 的文章侧重于 Linux,但出色地介绍了这种方法所支持的分析类型。
其他操作系统采用的方法
Linux
Linux 将硬件计数器、计时器或软件事件等“性能事件”表示为一种在触发时将数据写入文件描述符的事物。为了获取这些 fd,Linux 提供了一个系统调用“perf_event_open”。它会创建一个文件描述符,当发生请求的事件时,该描述符中会写入可配置的数据量。对于低频事件,可以直接读取文件描述符;对于高频事件,可以像环形缓冲区一样映射和读取文件描述符。
Windows
Windows 提供了一个通用的事件跟踪框架。作为收集的事件的一部分,调用方可以请求使用帧指针检索的调用堆栈。
macOS 和 Dtrace
macOS 提供了 DTrace。DTrace 没有特定的“获取某些配置文件”系统调用,而是公开了一个通用脚本语言接口,允许在发生某些事件时定义可自定义的行为。例如,
dtrace -x ustackframes=100 -n 'profile-99 /execname == "starnix_runner.cm" && \
arg1/ { @[ustack()] = count(); } tick-60s { exit(0); }' -o out.stacks
将以 99Hz 的频率对名为“starnix_runner.cm”的任务进行堆栈帧采样。
有些操作系统将 DTrace 用作主要的分析方法(例如,macOS 通过 Instruments.app 和其他 BSD),或者将其作为可选的附加组件提供(例如,DTrace 在 Linux 中实现为可加载的内核模块,它还通过 SystemTap 公开类似的功能)。