14个不稳定版本 (4个重大更改)
0.5.0 | 2024年1月24日 |
---|---|
0.4.1 | 2023年12月13日 |
0.3.5 | 2022年12月15日 |
0.3.4 | 2022年11月22日 |
0.2.1 | 2021年11月18日 |
在 过程宏 中排名 1559
每月下载量 9,769
在 6 个crate中使用 (通过 usdt)
145KB
3K SLoC
usdt
用USDT探测给你的Rust项目除灰。
概述
usdt
将静态定义的 DTrace探测 暴露给Rust代码。用户编写一个 provider 定义,可以是D语言或直接在Rust代码中。然后可以将provider的探测编译成触发探测的Rust代码。这些通过 dtrace
命令行工具可见。
将D探测定义转换为Rust有三种机制。
- 一个
build.rs
脚本 - 一个类似函数的过程宏,
usdt::dtrace_provider
。 - 一个属性宏,
usdt::provider
。
生成的代码在所有情况下都是相同的,尽管第三种提供比前两种更多的灵活性。有关详细信息,请参见下面,但简要来说,第三种形式支持任何实现了 serde::Seralize
的类型的探测参数。这些不同版本分别在crate probe-test-{build,macro,attr}
中展示。
注意:此软件包使用内联汇编来实现其魔法。有关在 Rust 1.59 之前和 macOS 上 1.66 之前使用方法的讨论,请参阅注释。
示例
本软件包中的 probe-test-build
二进制软件包实现了一个完整的示例,使用构建时代码生成。
起点是一个名为 "test.d"
的 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)之前,需要夜间功能。请参阅注释以了解讨论。
还可以看到,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 的 JSON 功能 -- 任何可序列化类型都可以用 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-default-features
标志或通过在 [dependencies]
表中使用 default-features = false
来选择此实现。
库开发者可以将 usdt
作为可选依赖项使用,并通过一个功能来控制,例如命名为 usdt-probes
或类似。这个功能将隐含 usdt/asm
功能,但默认情况下可以使用无操作的实现。例如,你的 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
包(或任何定义探针的包)。在前一种情况下,如果我们包含了 asm_sym
功能,我们会得到关于未知功能的错误,如果我们省略了功能,我们会得到关于后续编译器功能门后面功能的功能错误。
幸运的是,随着 asm_sym
在 1.66 中的稳定,使用此包应该会变得更加简单。
参考文献
lib.rs
:
使用属性宏生成USDT探针
依赖项
~3.5–4.5MB
~86K SLoC