8 个版本
0.2.0 | 2024年1月24日 |
---|---|
0.1.15 | 2023年12月13日 |
0.1.14 | 2022年11月22日 |
0.1.12 | 2021年11月5日 |
0.1.9 | 2021年3月19日 |
#621 in 解析器实现
12,251 每月下载量
在 9 个 仓库中使用(直接使用 4 个)
28KB
675 行
usdt
用 USDT 探针给你的 Rust 擦拭尘埃。
概述
usdt
将静态定义的 DTrace 探针 暴露给 Rust 代码。用户编写一个 提供者 定义,可以是 D 语言或直接在 Rust 代码中。提供者的 探针 可以编译成触发探针的 Rust 代码。这些通过 dtrace
命令行工具可见。
将 D 探针定义转换为 Rust 有三种机制。
- 一个
build.rs
脚本 - 一个类似于函数的过程宏,
usdt::dtrace_provider
。 - 一个属性宏,
usdt::provider
。
在所有情况下生成的代码都是相同的,尽管第三个比前两个提供更多一些灵活性。有关详细信息,请参阅下面,但简要来说,第三种形式支持任何实现了serde::Seralize
的类型的探测参数。这些不同版本分别显示在crate 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宏。除非更改,否则此文件与提供者定义文件同名,因此在本例中为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);
}
}
注意:在1.59之前(以及在macOS上的1.66之前)需要nightly功能。请参阅说明以了解讨论。
还可以看到,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为已定义的探测生成“is-enabled”探测,这是一种简单的方式来检查探测是否当前已启用。只有当探测启用时才会解包参数,因此用户必须不依赖副作用。闭包有助于表明这一点。
第二个点是效率。再次强调,如果探测器未启用,则不会评估这些参数。闭包仅在探测器被验证为启用后内部评估,这样如果在探测器禁用时可以避免参数打包的不必要工作。
过程宏版本
此crate的过程宏版本可以在probe-test-macro
示例中看到,该示例与上面的示例几乎相同。然而,没有build.rs脚本,因此用过程宏include!
来代替。
dtrace_provider!("test.d");
此宏生成与上面相同的宏,但在编译源文件时进行。对于某些用例来说,这可能更容易,因为没有构建脚本。然而,过程宏也有缺点。理解它们的内部结构可能很困难,尤其是在出错时。此外,即使在提供者定义未更改的情况下,宏也会在每次编译时运行。对于小的提供者定义,这可能微不足道,但当定义了许多探测器时,用户可能会注意到编译时间的显著增加。
序列化类型
如上所述,定义提供者的三种形式几乎是等效的。唯一的区别是支持实现serde::Serialize
的类型的支持。这使用了DTrace的serde_json::to_string
函数将任何可序列化类型序列化为JSON,并且可以使用json
函数在DTrace脚本中解包和检查字符串。例如,假设我们有以下类型
#[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`.
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 内核模块所必需的。这对希望对其代码进行仪表化的库开发者来说是一个难题,因为他们的库使用者可能会忘记(或选择不)调用此函数。此问题存在潜在的解决方案(初始化部分,其他魔法),但每种方法都有显著的权衡。因此,目前的建议是
鼓励库开发者重新导出
usdt::register_probes
(或调用它的函数),并告知用户应调用此函数以保证探测器已注册。
注意
usdt
库需要 内联汇编,这是 Rust 1.59 中稳定的功能。在此版本之前,需要使用夜间构建工具链来导入此功能。出于对旧版便利性的考虑,该库还包含一个空的、无操作(no-op)实现,它生成所有相同的探测器宏,但具有空体(因此不需要内联汇编)。这可以通过在构建库时传递 --no-default-features
标志或使用 default-features = false
在 [dependencies]
表中 选择。
库开发者可以将 usdt
作为具有功能的可选依赖项使用,例如命名为 usdt-probes
或类似。此功能意味着 usdt/asm
功能,但 usdt
库可以默认使用无操作实现。例如,您的 Cargo.toml
可能包含
[dependencies]
usdt = { version = "*", optional = true, default-features = false }
# ... later
[features]
usdt-probes = ["usdt/asm"]
这允许用户在如果他们使用适合的工具链,或者启用了 asm
功能的较旧夜间版本时选择加入探测器。
Rust asm
功能
在 1.59 之前的工具链上,没有启用功能的情况下,内联汇编不可用。这适用于调用探测器宏的代码,包括 usdt
实现它们的代码。生成的探测器宏必须位于一个模块中,该模块使用 >=1.59 工具链构建,或者存在 feature(asm)
配置。
工具链版本和 asm_sym
功能
在 macOS(其中链接器参与 USDT 探测器创建)上,在 1.66 之前的工具链中,需要 asm_sym
功能(在 2021 年 11 月之前的夜间工具链中需要 asm
;见 此问题)。对于此类工具链,此功能可以仅包括在 macOS 上,例如,使用 #![cfg_attr(target_os = "macos", feature(asm_sym))]
,或者无条件地适用于所有平台。
添加asm_sym
功能带来了一个不幸的问题。现在无法使用添加该功能之前和之后的工具链来编译usdt
crate(或任何定义探针的crate)。在前一种情况下,如果我们包含了asm_sym
功能,我们会遇到关于未知功能的错误;如果我们省略了该功能,我们会遇到来自后续编译器的关于功能门后面的功能的错误。
幸运的是,随着asm_sym
在1.66版本中的稳定,使用这个crate应该会变得简单得多。
参考
lib.rs
:
一个用于解析DTrace提供者文件的库。
依赖项
约2.2-3MB
约59K SLoC