usdt
跨平台用户态静态定义跟踪探针。
概述
usdt 将静态定义的 DTrace 探针 暴露给 Rust 代码。用户编写一个 provider
定义,可以使用 D 语言或直接在 Rust 代码中编写。该 provider 的 probes
随后可以被编译到 Rust 代码中,以触发这些探针。这些探针可以通过 dtrace
命令行工具查看,或者在 Linux 平台上通过 bpftrace 查看。
有三种机制将 D 探针定义转换为 Rust。
- 一个
build.rs脚本 - 一个函数式过程宏,
usdt::dtrace_provider。 - 一个属性宏,
usdt::provider。
在所有情况下,生成的代码都是相同的,尽管第三种比前两种提供了更多的灵活性。
有关更多详细信息,请参阅下文,但简而言之,第三种
形式支持任何实现了 serde::Seralize 的类型的探针参数。这些不同的
版本分别展示在 crates probe-test-{build,macro,attr} 中。
注意:此 crate 使用内联汇编来实现其功能。有关在 Rust 1.59 之前以及在 macOS 上 1.66 之前 其使用的讨论,请参阅备注。
示例
此包中的 probe-test-build 二进制 crate 实现了一个完整的示例,使用
构建时代码生成。
起点是一个 D 脚本,名为 "test.d"。它看起来像:
provider my_provider {
probe start_work(uint8_t);
probe stop_work(char*, uint8_t);
};
此脚本定义了一个单一提供者,test,包含两个探针,start 和 stop,
具有不同的参数集。(目前支持整数原始类型、指向整数类型的指针以及 &str。请注意,char* 用于
表示 Rust 风格的 UTF-8 字符串。如果您需要字节数组,请使用 uint8_t*
或 int8_t*。)
此提供者定义必须转换为 Rust 代码,这可以通过一个简单的 构建脚本来完成:
use usdt::Builder;
fn main() {
Builder::new("test.d").build().unwrap();
}
这会在目录 OUT_DIR 中生成一个文件,其中包含用于触发探针的生成的 Rust 宏。除非被更改,否则该文件与 provider 定义文件同名,因此在本例中为 test.rs。
在 Rust 代码中使用探针的示例如下,其位于 probe-test-build/src/main.rs 中。
//! An example using the `usdt` crate, generating the probes via a build script.
use std::thread::sleep;
use std::time::Duration;
use usdt::register_probes;
// Include the Rust implementation generated by the build script.
include!(concat!(env!("OUT_DIR"), "/test.rs"));
fn main() {
let duration = Duration::from_secs(1);
let mut counter: u8 = 0;
// NOTE: One _must_ call this function in order to actually register the probes with DTrace.
// Without this, it won't be possible to list, enable, or see the probes via `dtrace(1)`.
register_probes().unwrap();
loop {
// Call the "start_work" probe which accepts a u8.
my_provider::start_work!(|| (counter));
// Do some work.
sleep(duration);
// Call the "stop_work" probe, which accepts a &str and a u8.
my_provider::stop_work!(|| ("the probe has fired", counter));
counter = counter.wrapping_add(1);
}
}
也可以看到,Rust 代码是通过 include! 宏直接包含的。探针
定义被转换为 Rust 宏,位于由提供者命名的模块中,且宏
由探针命名。在我们的情况下,第一个探针被转换为宏
my_provider::start_work!。
重要提示:需要注意的是,应用程序 必须 调用
usdt::register_probes()才能实际将探针点注册到 DTrace。如果不这样做,不会影响 应用程序的功能,但如果没有此操作,将无法使用dtrace(1)工具列出、启用或 查看这些探针。
我们可以通过运行示例并按名称列出预期的 探针,来看到它与 DTrace 的集成。
$ cargo run
在另一个终端中,使用以下命令列出匹配的探针:
$ sudo dtrace -l -n my_provider*:::
ID PROVIDER MODULE FUNCTION NAME
2865 test14314 probe-test-build _ZN16probe_test_build4main17h906db832bb52ab01E [probe_test_build::main::h906db832bb52ab01] start_work
2866 test14314 probe-test-build _ZN16probe_test_build4main17h906db832bb52ab01E [probe_test_build::main::h906db832bb52ab01] stop_work
探针参数
可以看出,探针宏是通过闭包调用的,而不是直接使用探针参数。这有两个目的。
首先,这表明探针参数可能不会被求值。DTrace 会为已定义的探针生成“是否已启用”探针,这是一种检查探针当前是否已启用的简单方法。只有当探针已启用时,参数才会被解包,因此用户_不得_依赖副作用。闭包有助于表明这一点。
其次,这一点在于效率。同样,如果探针未启用,参数不会被求值。闭包仅在探针_经过_验证已启用后在内部求值,从而避免了在探针禁用时进行参数编组的无用工作。
过程宏版本
本 crate 的过程宏版本可以在 probe-test-macro 示例中看到,它与上述示例几乎相同。但是,没有 build.rs 脚本,因此,在 include! 宏的位置,可以找到过程宏:
dtrace_provider!("test.d");
此宏生成与上文相同的宏,但会在源代码本身编译时执行。对于某些用例,这可能更简单,因为无需构建脚本。 然而,过程宏存在缺点。理解其内部机制可能较为困难,尤其是在出错时。此外,即使 provider 定义未发生变化,该宏也会在每次编译时运行。 对于小型 provider 定义,这可能微不足道,但当定义了许多探针时,用户可能会注意到编译时间明显增加。
可序列化类型
如上所述,定义 provider 的三种形式几乎是等价的。唯一的区别在于对实现了 serde::Serialize 的类型的支持。
这利用了 DTrace 的 JSON 功能 —— 任何可序列化类型都会通过 serde_json::to_string() 序列化为 JSON,
并且该字符串可以在 DTrace 脚本中使用 json 函数进行解包和检查。例如,假设我们有以下类型:
#[derive(serde::Serialize)]
pub struct Arg {
val: u8,
data: Vec<String>,
}
以及一个探针定义:
#[usdt::provider]
mod my_provider {
use crate::Arg;
fn my_probe(_: &Arg) {}
}
类型 Arg 的值可用于生成的探针宏中。在 DTrace 脚本中,可以
像这样查看参数中的数据:
dtrace -n 'my_probe* { printf("%s", json(copyinstr(arg0), "ok.val")); }' # prints `Arg::val`.
The json 函数还支持嵌套对象和数组索引,因此也可以执行以下操作:
dtrace -n 'my_probe* { printf("%s", json(copyinstr(arg0), "ok.data[0]")); }' # prints `Arg::data[0]`.
请参阅 probe-test-attr 示例以获取更多详细信息和用法。
序列化可能会失败
请注意,在上面的示例中,被访问的 JSON 数据块中的第一个键是 "ok"。这是因为
serde_json::to_string 函数可能会失败,并返回一个 Result。这以自然的方式映射
到 JSON 中:
Ok(_) => {"ok": _}Err(_) => {"err": _}
在错误情况下,返回的 Error 会使用其 Display
实现进行格式化。这并非一个理论上的问题。即使对于 #[derive(Serialize)] 的类型,也很容易构建出能够成功
编译,但在运行时序列化失败的类型。有关详细信息,请参阅
此问题。
关于注册的说明
请注意,在上述
示例中,usdt::register_probes() 函数在 main 的顶部被调用。此方法用于实际将探针注册到 DTrace 内核
模块。这给希望对其代码进行插桩的库开发者带来了困境,因为库的使用者可能会忘记(或选择不)调用此函数。
针对此问题存在一些潜在的变通方法(init-sections、其他魔法),但每种方法
都伴随着显著的权衡。因此,当前的建议是:
建议库开发者重新导出
usdt::register_probes(或 调用它的函数),并向用户文档说明应调用此函数, 以确保探针已注册。
支持的平台
截至 v0.6.0,此 crate 支持:
- illumos 和其他 Solaris 衍生系统
- macOS
- FreeBSD
- x86-64 Linux,通过发射 SystemTap v3 探针实现。ARM 支持未经 测试,但可能碰巧可用。