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 解析器实现

Download history 6072/week @ 2024-04-06 4628/week @ 2024-04-13 5747/week @ 2024-04-20 4556/week @ 2024-04-27 3977/week @ 2024-05-04 6803/week @ 2024-05-11 4682/week @ 2024-05-18 3953/week @ 2024-05-25 2913/week @ 2024-06-01 2071/week @ 2024-06-08 2135/week @ 2024-06-15 3290/week @ 2024-06-22 1984/week @ 2024-06-29 3358/week @ 2024-07-06 3945/week @ 2024-07-13 2430/week @ 2024-07-20

12,251 每月下载量
9 仓库中使用(直接使用 4 个)

Apache-2.0

28KB
675

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实现了使用构建时代码生成的一个完整示例。

起点是一个D脚本,称为"test.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之前)需要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