ITADN
oxidecomputer/usdt · 文件 下载 ZIP
文件最后提交记录最后更新时间
README.md
以下内容由 AI 翻译,如有问题请点此提交 issue 反馈

usdt

跨平台用户态静态定义跟踪探针。

概述

usdt 将静态定义的 DTrace 探针 暴露给 Rust 代码。用户编写一个 provider 定义,可以使用 D 语言或直接在 Rust 代码中编写。该 provider 的 probes 随后可以被编译到 Rust 代码中,以触发这些探针。这些探针可以通过 dtrace 命令行工具查看,或者在 Linux 平台上通过 bpftrace 查看。

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

  1. 一个 build.rs 脚本
  2. 一个函数式过程宏,usdt::dtrace_provider
  3. 一个属性宏,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,包含两个探针,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 宏。除非被更改,否则该文件与 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 支持未经 测试,但可能碰巧可用。

参考资料