3 个不稳定版本
0.2.1 | 2024年3月27日 |
---|---|
0.2.0 | 2024年3月27日 |
0.1.0 | 2020年7月6日 |
#30 in 性能分析
每月下载 195 次
在 uhyve 中使用
49KB
534 行
rftrace - Rust 函数跟踪器
rftrace 是一个基于 Rust 的函数跟踪器。它提供后端和前端,后端负责实际的跟踪,前端负责将跟踪结果写入磁盘。后端设计为独立运行,不与系统交互。因此,它可以用于部分跟踪像 Hermit 内核 这样的内核,通过操作系统、中断、stdlib 和应用程序。它支持多线程。它也适用于普通的 Rust 和 C 应用程序,尽管对于这些用例存在更好的工具。
需要最新的 nightly rust 编译器(截至 2020 年 6 月 28 日)。
目录
设计
我需要一个函数跟踪器,它可以在内核和用户空间中工作,以跟踪一个 Hermit 应用程序。最好是无需手动注释源代码,作为即插即用解决方案。由于 Hermit 还有一个 gcc 工具链,因此它应该可以与使用 rustc 和 gcc 仪器化的应用程序一起工作。
完成这项工作的最佳方式是使用编译器提供的函数探针功能,其中它们在每个函数的序言中插入 mcount()
调用。在gcc中,可以通过 -pg
标志来实现,而在rustc中,则通过新添加的 -Z instrument-mcount
标志来实现。该机制在例如 uftrace 中也被成功使用,它已经提供了 Rust 支持。
这个跟踪器分为两个部分:后端和前端。
后端是一个静态库,它提供了所谓的 mcount()
调用,并负责将每个函数的进入和退出记录到缓冲区中。它是用 Rust 编写的,但使用了 no_std
和甚至无分配。与 uftrace 不同,它不依赖于与外部软件(例如,操作系统用于线程-id)的任何通信。但它确实需要线程局部存储。
由于它被编译为一个独立的静态库,我们甚至可以使用不同的目标架构。这需要将库轻松嵌入到我们的应用程序中,例如允许使用 SSE 寄存器。这些寄存器如果在内核中错误使用的情况下将导致崩溃!通过将 staticlib 与内核目标编译,我们可以避免这个问题,并可以同时跟踪内核和用户空间。这种子编译的另一个原因是不像 gcc,rust 还没有提供选择性地禁用探针的机制。我们无法对 mcount
函数本身进行探针,否则会导致无限递归。
前端通过几个函数调用来与后端交互。它为后端提供一个事件缓冲区(由于后端是无分配的,所以需要),并在完成后负责保存跟踪结果。理论上它很容易被替换,但 API 尚未完善。
依赖项
被跟踪应用程序的函数序言必须用 mcount
探针。这可以通过 rustc 编译器选项 -Z instrument-mcount
或 gcc 的 -pg
标志来完成。
后端隐式假设了 System-V ABI。这会影响每个函数进入和退出时需要保存和恢复的寄存器,以及函数退出挂钩的方式。如果您使用不同的约定,请检查 mcount()
和 mcount_return_trampoline()
是否正确处理了正确的寄存器。
对于调用点和函数退出的记录,需要帧指针,所以请确保您的编译器不会因为优化而省略它们。
对于在一个跟踪中跟踪内核+应用程序,需要一个单地址空间的操作系统,如 HermitCore。目前并非所有函数都可以被挂钩。裸函数有些问题。挂钩中断也坏了,会导致间歇性崩溃。不幸的是,Rust 编译器没有提供一种机制来选择性地禁用特定函数的 mcount
探针,因此您必须注意只在允许的上下文中启用 rftrace。目前只有在恰好有一个 CPU 内核可用的情况下才能干净地运行。
没有其他依赖项需要记录跟踪。输出格式与uftrace使用的格式相同,因此您需要它来查看和转换。在/tools
中有可以合并来自多个不同来源跟踪的(目前过时的)脚本,这些脚本需要python3
。
当跟踪自定义内核时,它需要提供将文件写入目录的能力,否则我们无法保存跟踪。它还需要支持线程局部存储,因为我们将其用作影子返回堆栈和线程ID分配。
使用方法
在/examples
中有4个用法示例:Rust和C,都在正常的Linux x64和Hermit上。这些是唯一测试过的架构。
将 rftrace 添加到您的应用程序中
Linux Rust 应用程序
要使用rftrace,请将后端和前端添加到您的依赖项中。
[dependencies]
rftrace = "0.2"
rftrace-frontend = "0.2"
确保生成帧指针!调试构建似乎总是启用它们。
通过设置环境变量RUSTFLAGS="-Z instrument-mcount"
或将其包含在.cargo/config
中,启用-Z instrument-mcount
。
[build]
rustflags=["-Z", "instrument-mcount"]
当使用vscode时,可以通过修改您的编译任务以包含以下内容轻松完成此操作
"options": {
"env": {
"RUSTFLAGS": "-Z instrument-mcount",
}
},
实际上进行跟踪时,您还需要在您的crate中添加一些代码,类似于以下内容
fn main() {
let events = rftrace::init(1000000, true);
rftrace::enable();
run_tests();
rftrace::dump_full_uftrace(events, "/trace", "binaryname", false)
.expect("Saving trace failed");
}
Hermit
当跟踪Hermit时,后端直接链接到内核。这是通过hermit
crate的instrument
功能启用的。因此,我们只需要在应用程序中使用前端。通过使用instrument功能,内核始终被仪器化。要记录应用程序的函数调用,请设置如上所述的instrument-mcount
rustflag。
我进一步建议至少使用opt-level 2,否则stdlib会创建很多无用的杂乱。我们在这里用-Z build-std=std,...
自己构建它,因此它受到instrument rustflag的影响!)
在/examples/hermitrust
中提供了一个带有makefile的示例,该示例执行所有必要的跟踪收集、时间转换和kvm-event合并以获得良好的跟踪,可以用make runkvm
编译和运行。
[dependencies]
hermit = { version = "0.8", default-features = false, features = ["instrument"] }
rftrace = "0.2"
任何其他内核
可能感兴趣的后端功能有
interruptsafe
- 在函数退出时将安全地恢复更多寄存器,以确保中断不会破坏它们。可能只需要在中断被仪器化时使用。可以出于性能原因禁用。
输出格式
前端输出一个与uftrace兼容的跟踪文件夹:[uftrace的数据格式](https://github.com/namhyung/uftrace/wiki/Data-Format)。
请注意,时间将错误,因为我们以原始TSC计数输出,而不是纳秒。您可以通过确定TSC频率并使用merge.py来转换它。另请参阅:[时间对齐 Guest <-> Host](#readme-time-alignment-guest---host)。
请注意,TID不是由主机分配的。后端没有任何依赖项,不查询TID,而是分配它自己的。它看到的第一个线程将获得TID 1,第二个将获得2...
完整的跟踪由5+个文件组成,4个用于元数据,每个TID包含实际跟踪的1个文件。
/<TID>.dat
:包含线程TID的跟踪信息。如果是多线程,可能会有多个。/info
:有关cpu、内存、cmdline、版本的一般信息。/task.txt
:包含PID、TID、SID到程序名的映射。/sid-<SID>.map
:包含地址到程序名的映射。默认情况下,内存映射是伪造的。您可以选择启用linux模式,在这种情况下,/proc/self/maps
会被复制。/<exename>.sym
:包含程序符号,如nm -n
的输出(必须排序!)。符号永远不会生成,必须手动完成。
Chrome 跟踪查看器
使用Chrome跟踪查看器可视化跟踪是一个非常好的方法。它可以显示自定义的json跟踪,类似于火焰图,但具有交互性。uftrace可以使用uftrace dump --chrome > trace.json
将其转换为这种格式。
- “旧版”界面:打开Chrome,转到
chrome://tracing
。这会打开一个名为catapult的界面。 - “现代”界面:Perfetto。看起来更美观,但缩放级别有限。
- 对于两者,我建议使用WASD进行导航!
- 跟踪格式文档
同时跟踪主机应用程序
此跟踪器的主要目标是对客户端和主机端记录的跟踪进行对齐。这是可能的,因为我们使用相同的时间源,并随后对齐跟踪。我发现这样做最简单的方法是将uftrace
打补丁以在记录主机跟踪之前使用TSC计数器。
当同时有一个官方安装的uftrace和一个打补丁的uftrace时,您必须在每次调用中指定uftrace-library-path。
uftrace record -L $CUSTOM_PATCHED_UFTRACE APPLICATION
跟踪 virtiofsd
特别地,virtiofsd的跟踪很烦人,因为它将其进程沙盒化。因此,无法使用共享内存。共享内存由/dev/shm
中的文件引用。仅仅在共享文件夹内挂载/dev
并不完全有效,一些跟踪信息丢失。为了使其工作,完全禁用命名空间使用补丁。这并不安全!如果客户端想要,它可能会修改整个文件系统。
现在我们可以按照以下方式在virtiofsd上运行uftrace
sudo uftrace record -L $CUSTOM_PATCHED_UFTRACE ./virtiofsd --thread-pool-size=1 --socket-path=/tmp/vhostqemu -o source=$(pwd)/testdir -o log_level=debug
跟踪 kvm 事件
还有对齐的kvm事件也很有用。内核为此公开了73个不同的跟踪点。通常我会使用perf来处理这个问题,但结果证明它使用它自己的非对齐时间戳。最好使用trace-cmd
,其中我们可以指定时钟源为TSC。
sudo perf record -e 'kvm:*' -a sleep 1h
sudo trace-cmd record -e 'kvm:*' -C x86-tsc
合并来自不同来源的跟踪
有一个小的merge.py python 脚本用于合并跟踪。请参阅它的帮助以获取使用说明。
在examples/multi
中给出了一个从主机+客户端收集事件、对齐所有事件并将其合并的示例makefile。
可视化跟踪
有一个名为Tracy的出色的跟踪记录器和可视化工具。它提供了用于chromium跟踪格式的导入器,因此合并的跟踪可以在那里进行可视化。不过,转换器相当内存低效,所以它不能处理大型跟踪。在我的测试中,大约是15倍的跟踪文件大小内存使用,以及每个100MB的chromium-json-trace文件大约1分钟的转换时间。
其他跟踪器
在编写这个crate之前,考虑了很多替代方案。以下是一个选项列表以及为什么没有选择它们的原因。
perf
perf 更像一个采样器而不是跟踪器,尽管这仍然很有用。它甚至支持跟踪KVM虚拟机!不幸的是,它只支持指令指针,没有回溯。这仅在内核源代码中有文档记录。
Perf使用内核的事件框架。在arch/x86/events/core.c:perf_callchain_user(..)中有一个注释:/* TODO: We don't support guest os callchain now */
。
对于跟踪KVM,它使用一个名为perf_guest_info_callbacks
的函数指针结构,它只包含is_in_guest、
is_user_mode、
get_guest_ip、
handle_intel_pt_intr
。
仅记录IP的简单情况使用sudo perf kvm --guest record
完成。
uftrace
uftrace 是一个很好的用户空间程序跟踪工具,对于已插入本机Rust二进制的跟踪效果很好。
由于它有很多功能,包含了很多代码。对我们来说最相关的部分是libmcount
。这是一个与LD_PRELOAD
一起使用的库,它提供了对插入程序的mcount()
调用的支持。尽管libmcount可以无依赖构建,但还有很多可选的依赖项,默认情况下都会使用。我只需要它的一小部分。
由于libmcount旨在用于用户空间跟踪,而我想要将其嵌入Hermit内核,因此出现了一些问题。
- 使用共享内存,通过文件'mounted'来在mcount和uftrace之间通信跟踪结果,这尚未在Hermit中实现。
- 所有参数都通过环境变量传递,这在跟踪Hermit时设置起来很烦人。
- 没有方便的开关。我们不能跟踪一切,特别是早期引导。
- 用C编写 -> 总是需要gcc工具链。
由于它的跟踪格式相当简单,这个crate生成一个兼容的格式,这样我们就可以使用围绕uftrace的工具来查看和进一步转换它。
穷人版分析器
Poor Mans Profiler 是一个简单的bash脚本,它循环调用gdb。我们可以使用它来暂停支持gdb良好的qemu,打印完整的回溯,然后立即退出。如果我们经常这样做,我们就有一个相当慢但还不错的采样分析器。
这种调用需要约0.15秒。但大多数时间都花在了gdb启动上。我已经对其进行了一些优化,并使用gdb-python文件来执行相同的操作而无需每次都重启gdb。这将暂停时间减少到约15ms,提高了10倍。但这仍然相当慢。这些not_quite_as_poor_mans_profiler
脚本可以在/tools中找到。
不幸的是,virtiofsd包含一个竞争条件,在我们暂停写入/读取文件时会导致它与qemu死锁。修复它并不简单。错误报告:当qemu停止调试时,virtiofsd发生死锁。由于这正是我们想要基准测试的情况,因此它在这里不适用。
gsingh93 的跟踪
gsingh93的trace 是一个针对需要不稳定的 -Z 仪器-mcount
的不错的解决方案:它使用 proc_macro 递归抽象语法树 (AST),并在每个函数入口(和潜在退出)处添加跟踪调用。它目前调用 println!()
或 log::trace!()
,但可以轻松地更改为其他调用。
这里的问题是 Rust 不支持 'proc_macro 中的非内联模块',这意味着我们无法轻松地注释一个 crate 中的所有内容。大多数 mod
都必须单独注释,并且它不会递归到依赖中。这个对宏的限制在 Rust 测试 中进行了测试,并在 过程宏和“卫生 2.0”的跟踪问题 中进行了跟踪。
这个 crate 有一个 旧实现,仍然使用编译器插件接口。问题:内部 AST 非常不稳定,crates 代码需要经常适应以保持同步。
si_trace_print
si_trace_print 是“堆栈缩进的跟踪打印”。这是一个 rust 库,它需要宏语句来打印消息。消息会缩进到堆栈深度,并且可以可选地以函数名开头。
si_trace_print
的目的是成为一个简单的“入门级”跟踪库,以帮助开发人员手动审查单个程序运行。
hawktracer-rust
hawktracer-rust (GitHub) 为亚马逊的 hawktracer 提供了 rust 绑定。这需要使用跟踪点注释您的代码,但如果这是您想要的,这似乎是一个很好的解决方案。
flamegraph-rs
flamegraph-rs 是 perf 的简单前端,因此不能用于我的应用程序。但是,它有一个很棒的 readme,其中涵盖了大量的跟踪背景。
bpftrace
内核虚拟机称为 eBPF 可以做很多事情。它甚至可以用于执行基于样本的剖析
- bpftrace
- Linux 扩展 BPF (eBPF) 跟踪工具
- 它从内核中提取堆栈跟踪,就像 perf 一样(在这种情况下是通过内存映射
BPF_STACK_TRACE
和bpf_get_stack()
) - 我们也可以使用 eBPF 手动遍历堆栈:Linux eBPF 堆栈跟踪黑客
- 据我所知,没有方法可以通过 ebpf 与 KVM 进行接口!
虚拟机内省
有一组针对 kvm-vmi 的补丁,它允许轻松地进行 Xen/KVM 客户端的调试、监控、分析和模糊测试。不幸的是,这还不是上游的,需要修补内核和 qemu。
不过,正在努力将其合并到主线中:幻灯片:利用 KVM 作为调试平台
内部机制
mcount
有关 mcount() 实现的一些说明。如果您感兴趣,请查看 backend,它有一些注释。一些有趣的观点
由于对 mcount()
的调用仅在函数开头插入,我们检查父函数的返回地址,将其保存在影子栈中,并用跳转函数覆盖它。这个跳转函数将然后从栈中弹出正确的地址并恢复它,同时记录函数已退出。
当仅挂钩 Rust 函数时,LLVM 插入 mcount()
调用,在调用 mcount() 之前保存所有需要的寄存器,并在之后恢复。由于它始终在函数的开头插入,这只会影响函数参数。
尽管如此,我们仍然备份应用程序的潜在参数寄存器,以防万一。至少有一个情况,(诚然是不正确的) 不安全 Rust 代码可以破坏它:使用 asm!
,它通过名称访问寄存器而不是依赖于 LLVM 将 Rust 参数名称转换为寄存器。
在编译时实现禁用这种保存的功能相当简单,这样在支持的代码库上跟踪会更快。
在挂钩中断时,我们需要特别小心,因为 mcount 可能现在会在另一个函数的中间被调用,并且不能破坏任何状态。《interruptsafe》功能旨在以较小的运行时成本提供这种额外的安全性。
有关设计函数跟踪器的进一步阅读,请参阅 内核 ftrace 设计。您还可以参考 uftrace 的 libmcount。
时间对齐 客户端 <-> 主机
为了同时从客户机和主机获取跟踪,我们需要一个非常精确且在客户机和主机上恒定的时钟。x86 处理器包含一个时间戳计数器 (TSC)。最初,这个计数器在每个时钟周期增加一次。由于这在具有变化频率和睡眠状态的现代处理器中难以处理,因此引入了两个名为 constant_tsc
和 nonstop_tsc
的功能。有了它们,TSC 具有固定频率,Linux 内核将其称为 tsc_khz
。由于操作系统允许写入此计数器,因此 KVM 需要虚拟化它。过去这通过 kvmclock 完成,并在写入或读取 tsc 时触发虚拟机管理程序异常。现代处理器通过偏移和缩放 '寄存器' 实现硬件虚拟化 tsc_offset
/tsc_scaling
(您可以在 /proc/cpuinfo
中检查您的 CPU 是否支持它)。
KVM 利用这些功能在启动时将我们的虚拟机的 TSC 设置为零。这种行为是不可配置的。幸运的是,我们有两种方法可以确定我们的虚拟机运行的精确偏移:内核跟踪点和 debugfs。
在 /sys/kernel/debug/kvm/33587-20/vcpu0/tsc-offset
可以读取具有 ID 33587-20
的虚拟机的当前 tsc-offset。由于必须在 vm 运行时执行此操作,并且可能无法捕获此值的所有变化,因此我们使用第二种方法。
内核跟踪点 kvm_write_tsc_offset
,在以下 补丁 中引入。要注册此跟踪点,可以使用一个小脚本来注册: setup_kvm_tracing.sh。
通常,TSC 运行的频率(tzc_khz
)未公开。您可以直接从内核内存中运行以下 gdb 命令作为 root 读取它
gdb /dev/null /proc/kcore -ex 'x/uw 0x'$(grep '\<tsc_khz\>' /proc/kallsyms | cut -d' ' -f1) -batch 2>/dev/null | tail -n 1 | cut -f2
StackOverflow: 在 x86 内核中获取 TSC 速率
未来工作
- 创建一个前端,可以将跟踪输出到网络,因此不需要文件访问
- 存在一个名为 '
no_instrument_function
' 的 LLVM 属性,尽管 Rust 编译器没有暴露它。在这里添加该属性可能很容易。参见 让将 LLVM 属性附加到 Rust 函数变得容易。这将消除静态库的需求,但仅在我们针对同一目标编译的情况下(不是内核)。 - 添加禁用返回钩子的选项!
- 修复中断
- 修复多核行为
许可证
许可协议为以下之一:
- Apache License,版本 2.0 (LICENSE-APACHE 或 https://apache.ac.cn/licenses/LICENSE-2.0)
- MIT 许可证 (LICENSE-MIT 或 http://opensource.org/licenses/MIT)
任选其一。
贡献
除非您明确声明,否则您根据 Apache-2.0 许可证定义的工作中的任何有意提交的贡献,将按上述方式双许可,不附加任何其他条款或条件。