17个不稳定版本 (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.1.7 2021年3月19日

#500 in 过程宏

Download history 4844/week @ 2024-03-14 5365/week @ 2024-03-21 5209/week @ 2024-03-28 6889/week @ 2024-04-04 4462/week @ 2024-04-11 4583/week @ 2024-04-18 6130/week @ 2024-04-25 3921/week @ 2024-05-02 6789/week @ 2024-05-09 4294/week @ 2024-05-16 3763/week @ 2024-05-23 4351/week @ 2024-05-30 1896/week @ 2024-06-06 1984/week @ 2024-06-13 3385/week @ 2024-06-20 1919/week @ 2024-06-27

9,797 每月下载次数
用于 8 个crate(直接使用3个)

Apache-2.0

175KB
3.5K SLoC

usdt

用USDT探针为您的Rust去尘。

概述

usdt 将静态定义的 DTrace探针 暴露给Rust代码。用户编写一个 提供者 定义,可以是D语言或直接在Rust代码中。然后,提供者的 探针 可以编译成触发探针的Rust代码。这些可以通过 dtrace 命令行工具查看。

将D探针定义转换为Rust有三种机制。

  1. 一个 build.rs 脚本
  2. 一个类似于函数的过程宏,usdt::dtrace_provider
  3. 一个属性宏,usdt::provider

生成的代码在所有情况下都是相同的,尽管第三种比前两种提供了更多灵活性。有关更多详细信息,请参阅下面,但简要地说,第三种形式支持任何实现 serde::Seralize 的类型的探针参数。这些不同版本分别在crate probe-test-{build,macro,attr} 中展示。

注意:此crate使用内联汇编来实现其魔法。请参阅注意事项,以了解在Rust 1.59之前以及macOS上的1.66之前的使用情况。

示例

此包中的 probe-test-build 二进制crate实现了使用构建时代码生成的完整示例。

起始点是一个名为 "test.d" 的 D 脚本。它看起来像这样:

provider my_provider {
	probe start_work(uint8_t);
	probe stop_work(char*, uint8_t);
};

此脚本定义了一个名为 test 的单个提供者,具有两个探针 startstop,具有不同的参数集。(目前支持积分原始类型、积分类型的指针以及 &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 内核模块所必需的。这对于希望对其代码进行仪器化的库开发者来说是一个难题,因为他们的库消费者可能忘记(或选择不)调用此函数。存在解决这个问题(init-sections,其他魔法)的潜在解决方案,但每个都有重大的权衡。因此,当前的推荐做法是

鼓励库开发者重新导出 usdt::register_probes(或调用它的函数),并通知用户应调用此函数以确保探针已注册。

注意

usdt 库需要 内联汇编,该功能在 Rust 1.59 版本中稳定。在此之前,需要 nightly 工具链来导入该功能。出于对旧版本库的方便考虑,该库还包含一个空的、无操作的实现,它生成了所有相同的探测宏,但具有空体(因此不需要内联汇编)。在构建库时,可以通过传递 --no-default-features 标志或使用 default-features = false[dependencies] 中选择此功能。

库开发者可以使用 usdt 作为带有功能的可选依赖项,例如命名为 usdt-probes 或类似名称。此功能将隐含 usdt/asm 功能,但默认情况下可以使用无操作实现。例如,您的 Cargo.toml 可能包含

[dependencies]
usdt = { version = "*", optional = true, default-features = false }

# ... later

[features]
usdt-probes = ["usdt/asm"]

这允许用户在适合的工具链中启用探测,或者在启用了 asm 功能的旧版 nightly 上进行项目。

Rust asm 功能

在 1.59 版本之前的工具链上,如果没有启用功能,内联汇编不可用。这适用于调用探测宏的代码,以及实现它们的 usdt。这些生成的探测宏必须在一个模块中构建,该模块使用 >=1.59 工具链,或者存在 feature(asm) 配置。

工具链版本和 asm_sym 功能

在 macOS(其中链接器参与 USDT 探测创建)上,在 1.66 版本之前的工具链上,需要 asm_sym 功能(在 2021 年 11 月之前的 nightly 工具链中需要 asm;见 此问题)。对于此类工具链,此功能可以仅包括在 macOS 上,例如使用 #![cfg_attr(target_os = "macos", feature(asm_sym))],或在所有平台上无条件使用。

asm_sym 功能的添加带来了一个不幸的问题。现在无法使用在添加该功能之前和之后添加功能之前编译 usdt 库(或任何定义探测的库)。在前一种情况下,如果包含 asm_sym 功能,我们会收到有关未知功能的错误,如果我们省略了功能,我们会收到关于后来编译器功能门背后的功能的错误。

幸运的是,由于 asm_sym 在 1.66 版本中的稳定,使用此库应变得更加简单。

参考文献

依赖项

~3–4.5MB
~87K SLoC