# DTrace：老式跟踪系统的新扩展

* 原文地址：[DTrace: New Additions to an Old Tracing System](https://freebsdfoundation.org/wp-content/uploads/2023/01/stolfa_dtrace.pdf)
* 作者：**DOMAGOJ STOLFA**

## DTrace 简介

DTrace 是 FreeBSD 内置的软件跟踪框架，允许用户实时检查和修改正在运行的系统。它高度可扩展，最初为 Solaris 设计，但后来被多次移植到其他环境，如 FreeBSD、macOS、Windows 和 Linux。本文将重点介绍在 FreeBSD 上 DTrace 的用法，并提供示例，同时总结 DTrace 在 FreeBSD 领域的最新发展。

## DTrace 概述

操作系统是极其复杂的软件，包含众多组件。单一的跟踪系统若试图支持几乎整个操作系统的跟踪，可能会因其复杂性而变得难以管理。为了解决这一问题并支持未来的扩展，DTrace 引入了 **provider（提供者）** 的概念。

在 FreeBSD 中，**provider** 默认作为内核模块（kernel module）运行，并负责实现特定操作系统组件的跟踪功能。它们提供 **DTrace probes（探测点）**，即操作系统代码中的命名位置，可通过 **D** 语言编写的可脚本化例程进行动态跟踪。

FreeBSD 自带的一些示例 **provider** 包括：

* **函数边界跟踪提供者（fbt.ko）**：负责对 **内核函数** 的入口和退出点进行跟踪。
* **性能分析提供者（profile.ko）**：提供基于脚本指定的固定时间间隔触发的探测点。
* **PID 提供者（fasttrap.ko）**：类似于 fbt，但适用于 **用户进程及其链接的库**。
* **其他多种 provider**。

虽然深入了解 DTrace 并非理解本文的必要前提，但希望深入学习 DTrace 的读者可以参考以下资料：

1. **用户指南**
2. **DTrace 规范**
3. **FreeBSD 手册页面**³
4. **DTrace 白皮书**⁴
5. **DTrace 书籍**⁵
6. **FreeBSD Wiki 页面（包括一些 DTrace 单行命令示例⁶）**。

此外，以往的 FreeBSD Journal 也曾多次发表关于 DTrace 的相关文章⁷⁸⁹。

## 简单示例

**DTrace 探测点** 由 **provider:module:function:name** **四元组** 指定。其中的每个字段可以使用通配符匹配多个值，或留空表示“匹配所有”。

下面是一个简单的 DTrace **监视脚本（snooper script）**，用于检测 **用户正在运行的程序**。我们在执行时使用 `-x quiet` 选项，以避免 DTrace 输出额外的信息。

```sh
# dtrace -x quiet -n 'proc:::exec { printf(“user = %u, gid = %u: %s\n”, uid, gid,
stringof(args[0])); }'
user = 1001, gid = 1001: /usr/sbin/service
user = 1001, gid = 1001: /bin/kenv
user = 1001, gid = 1001: /sbin/sysctl
user = 1001, gid = 1001: /sbin/env
user = 1001, gid = 1001: /bin/env
user = 1001, gid = 1001: /usr/sbin/env
user = 1001, gid = 1001: /usr/bin/env
user = 1001, gid = 1001: /etc/rc.d/sendmail
user = 1001, gid = 1001: /bin/kenv
user = 1001, gid = 1001: /sbin/sysctl
user = 1001, gid = 1001: /bin/ls
```

正如我们所看到的，**D 语言** 在语法上与 **C 语言** 非常相似，但也有一些特定的特殊语法形式。与 C 语言不同，D 语言**不支持循环**，因此任何形式的循环都必须**手动展开**。

在上面的示例中，我们可以通过 **`uid`** 和 **`gid`** 这两个内置变量来访问用户 ID 和组 ID。

此外，DTrace 还支持以多种方式对跟踪结果进行**聚合**。例如，我们可以统计**每个程序执行的系统调用次数**。

```sh
# dtrace -n 'syscall:::entry { @syscall_agg[execname, pid] = count(); }'
dtrace: description 'syscall:::entry ' matched 1148 probes
 sh                                                    46569                7
 sh                                                    46570                7
 syslogd                                                 703               16
 sshd                                                    848               17
 devd                                                    501               20
 ntpd                                                    771               24
 sh                                                    46565               93
 dtrace                                                46568              138
 ps                                                    46570              254
 sshd                                                  46564            27517
 ls                                                    46569            35755
```

在变量前使用 **`@`** 作为前缀，会将其定义为**聚合变量**。 `@syscall_agg` 以两个键进行索引，不过也可以继续添加更多的键。 `@syscall_agg` 的聚合输出应解读为：

```c
execname                                              pid            count
```

我们的最后一个示例涉及**堆栈追踪**。DTrace 允许用户使用 **`stack()`** 和 **`ustack()`** 例程分别收集**内核**和**用户空间**的堆栈追踪。此外，DTrace 还可以通过**语言特定的堆栈展开器**进行扩展。例如，**`jstack()`** 操作可以为用户提供 Java 程序的可读**回溯信息**。在我们的示例中，我们将重点关注 **`stack()`**：

```c
# dtrace -x quiet -n 'io:::start { @[stack()] = count(); }'
             zfs.ko`zio_vdev_io_start+0x2f5           
             zfs.ko`zio_nowait+0x15f        
             zfs.ko`vdev_mirror_io_start+0xfd
             zfs.ko`zio_vdev_io_start+0x1eb   
             zfs.ko`zio_nowait+0x15f                  
             zfs.ko`arc_read+0x14aa                               
             zfs.ko`dbuf_read+0xc84    
             zfs.ko`dmu_tx_check_ioerr+0x84  
             zfs.ko`dmu_tx_count_write+0x191
             zfs.ko`dmu_tx_hold_write_by_dnode+0x64
             zfs.ko`zfs_write+0x500           
             zfs.ko`zfs_freebsd_write+0x39  
             kernel`VOP_WRITE_APV+0x194    
             kernel`vn_write+0x2ce            
             kernel`vn_io_fault_doio+0x43             
             kernel`vn_io_fault1+0x163            
             kernel`vn_io_fault+0x1cc          
             kernel`dofilewrite+0x81
             kernel`sys_writev+0x6e           
             kernel`amd64_syscall+0x12e        
               1                              
                                              
             zfs.ko`zio_vdev_io_start+0x2f5
             zfs.ko`zio_nowait+0x15f       
             zfs.ko`zil_lwb_write_done+0x360
             zfs.ko`zio_done+0x10d6            
             zfs.ko`zio_execute+0xdf                    
             kernel`taskqueue_run_locked+0xaa
             kernel`taskqueue_thread_loop+0xc2
             kernel`fork_exit+0x80           
             kernel`0xffffffff810a35ae      
               1
```

这个 D 脚本统计了**所有导致块设备 I/O 的内核堆栈追踪**。

在此脚本中，我们省略了**聚合名称**，因为它只包含一个聚合，并且**键**是 `stack()`——这是一个 DTrace 内置操作，返回一个**程序计数器数组**，在打印结果时会解析为符号。

此外，DTrace 还可以使用 **profile** 提供者收集 **CPU 上的堆栈追踪**，从而支持**生成火焰图（Flame Graphs）**。

## 新进展

### dwatch

新工具 **dwatch** 由 **Devin Teske（**[**dteske@freebsd.org**](mailto:dteske@freebsd.org)**）** 开发，并在 **FreeBSD 11.2** 中上游合并。 **dwatch** 使得 DTrace 在常见使用场景下比 `dtrace` 命令行工具更易用。回到我们之前的**进程监视**示例，用户只需运行：

```sh
# dwatch execve
```

即可获得**比之前的简单监视器更丰富的信息**，且输出经过**良好过滤**。

```sh
# dwatch execve
INFO Watching 'syscall:freebsd:execve:entry' ...
2022 Nov 24 18:46:53 1001.1001 sh[46565]: sudo ps auxw
2022 Nov 24 18:46:53 0.0 sudo[46920]: ps auxw
2022 Nov 24 18:46:55 1001.1001 sh[46565]: ls
2022 Nov 24 18:47:01 1001.1001 sh[46565]: ls -lapbtr
2022 Nov 24 18:47:09 1001.1001 sh[46924]: kenv -q rc.debug
2022 Nov 24 18:47:09 1001.1001 sh[46924]: /sbin/sysctl -n -q kern.boottrace.enabled
2022 Nov 24 18:47:09 1001.1001 sh[46565]: env -i -L -/daemon HOME=/ PATH=/sbin:/
bin:/usr/sbin:/usr/bin /etc/rc.d/sendmail onestop
2022 Nov 24 18:47:09 1001.1001 env[46565]: /bin/sh /etc/rc.d/sendmail onestop
2022 Nov 24 18:47:09 1001.1001 sh[46924]: kenv -q rc.debug
2022 Nov 24 18:47:09 1001.1001 sh[46924]: /sbin/sysctl -n -q kern.boottrace.enabled
```

此外，**dwatch** 支持基于 **jail**、用户组、进程等多种条件进行过滤，即便是经验丰富的 **DTrace** 用户也值得学习这款工具。演讲 *All along the dwatch tower* 介绍了 **dwatch** 并详细讲解了其功能。此外，**FreeBSD** 的 **dwatch(1)** 手册页也提供了许多优秀的示例，适合感兴趣的用户尝试。

## CTFv3

**紧凑 C 类型格式（CTF）** 是一种用于在 **FreeBSD ELF** 二进制文件中编码 **C** 类型信息的格式。它使 **DTrace** 能够了解目标二进制文件（进程或内核）的 **C** 类型布局，以便用户编写的脚本可以引用和探索这些类型。

过去，由于 **CTFv2** 的实现方式，**DTrace** 在单个二进制文件中最多仅支持 **2^15** 种 **C** 类型。这一限制导致了 **FreeBSD** 中许多与 **DTrace** 相关的错误报告。今年 **3 月**，**Mark Johnston（**[**markj@freebsd.org**](mailto:markj@freebsd.org)**）** 提交了相关更改，使 **DTrace** 切换为 **CTFv3**，这不仅提高了 **DTrace** 可处理的 **C** 类型数量，同时还扩展了 **CTF** 的其他多个限制。

## kinst —— 用于指令级跟踪的全新 DTrace 提供程序

在 **2022 年 Google 夏季编程大赛（GSoC）** 中，**Christos Margiolis（**[**christos@freebsd.org**](mailto:christos@freebsd.org)**）** 在 **Mark Johnston（**[**markj@freebsd.org**](mailto:markj@freebsd.org)**）** 的指导下成功完成了一项项目，并将 **指令级跟踪（Instruction-level Tracing）** 功能合并到 **FreeBSD**。实现该功能的提供程序被称为 **kinst**。

**kinst** 复用了 **fbt** 机制的部分内容，并扩展了它，使其能够对 **内核函数的任意位置** 进行插桩（Instrumentation），而不仅仅是入口和出口点。对于熟悉 **内核开发** 的读者而言，**kinst** 在分析某些分支的调用栈时所带来的潜力不言而喻。因此，**kinst** 可以帮助更快地发现和修复 **FreeBSD** 中的 **bug** 及 **性能问题**。

以下是一个类似 **C 语言** 伪代码的示例场景：

```c
if (__predict_false(rarely_true)) {
return (slow_operation());
} else {
return (get_from_cache());
}
```

在这个示例中，我们聚焦于 **FreeBSD** 内核中的一个特定函数，其行为类似于此。该函数的简化版如下：

```c
void
_thread_lock(struct thread *td)
{
 ...
 if (__predict_false(LOCKSTAT_PROFILE_ENABLED(spin__acquire)))
 goto slowpath_noirq;
 spinlock_enter();
 ...
 if (__predict_false(m == &blocked_lock))
 goto slowpath_unlocked;
 if (__predict_false(!_mtx_obtain_lock(m, tid)))
 goto slowpath_unlocked;
 ...
 _mtx_release_lock_quick(m);
slowpath_unlocked:
 spinlock_exit();
slowpath_noirq:
 thread_lock_flags_(td, 0, 0, 0);
}
```

可以立即注意到有两个慢路径：`slowpath_unlocked` 和 `slowpath_noirq`。在这两个慢路径中，分别调用了 `spinlock_exit()` 或 `thread_lock_flags_()`，而 `_mtx_release_lock_quick()` 则只是在 amd64 上执行原子比较和交换指令。为了使用 `kinst` 来识别最终进入慢路径的调用堆栈，我们首先需要以某种方式反汇编该函数。一种可能的方法是在 **FreeBSD** 中使用 **kgdb**（通过 `pkg install gdb` 安装）：

```c
# kgdb
(kgdb) disas /r _thread_lock
Dump of assembler code for function _thread_lock:
...
0xffffffff80bc7dcc <+124>: 5d pop %rbp
 0xffffffff80bc7dcd <+125>: e9 4e 72 09 00 jmp 0xffffffff80c5f020
<witness_lock>
 0xffffffff80bc7dd2 <+130>: 48 c7 43 18 00 00 00 00 movq $0x0,0x18(%rbx)
 0xffffffff80bc7dda <+138>: e8 e1 43 4e 00 call 0xffffffff810ac1c0
<spinlock_exit>
 0xffffffff80bc7ddf <+143>: 8b 75 d4 mov -0x2c(%rbp),%esi
...
 0xffffffff80bc7df2 <+162>: 41 5d pop %r13
 0xffffffff80bc7df4 <+164>: 41 5e pop %r14
 0xffffffff80bc7df6 <+166>: 41 5f pop %r15
 0xffffffff80bc7df8 <+168>: 5d pop %rbp
 0xffffffff80bc7df9 <+169>: e9 82 00 00 00 jmp 0xffffffff80bc7e80
<thread_lock_flags_>
```

在这种情况下，我们可以取偏移量 +138 和 +169 处的指令，这些指令分别是对 `spinlock_exit()` 和 `thread_lock_flags_()` 的函数调用。使用这些偏移量，我们现在可以编写我们的 DTrace 脚本：

```c
# dtrace -n 'kinst::_thread_lock:138,kinst::_thread_lock:169 { @[stack(),
probename] = count(); }'
...
 0xcf566bb0
 kernel`ipi_bitmap_handler+0x87
 kernel`0xffffffff810a48b3
 kernel`vm_fault_trap+0x71
 kernel`trap_pfault+0x22d
 kernel`trap+0x48c
 kernel`0xffffffff810a2548
 138 8
```

熟悉 DTrace 的人可能会注意到，这本可以通过使用推测性追踪而不需要使用 kinst 来轻松实现。然而，人们可以很容易地想象出一些场景，其中“慢路径”或其等效物并不是一个简单的函数调用，或者相同的函数调用可能出现在所有的分支中。kinst 对 FreeBSD 上的 DTrace 生态系统也有其他影响。历史上，使用 fbt 对内联函数的内核进行插桩存在一个问题。用于实现 kinst 的机制可以帮助扩展 fbt，以支持可靠地追踪内联函数。

## 正在进行的工作

### DTrace 和 eBPF – 比较

Mateusz Piotrowski (<0mp@FreeBSD.org>) 一直在研究 FreeBSD 上 DTrace 的性能分析，以及它与 Linux 上 eBPF 的比较。今年，他在 EuroBSDcon 2022 上展示了部分结果。这项工作可能会得出有趣的结果，可以作为进一步优化 DTrace 的基础。这将使在性能关键的系统上启用插桩变得更少干扰。

### HyperTrace

HyperTrace 是建立在 DTrace 之上的一个框架，允许用户使用 D 编程语言应用类似 DTrace 的追踪技术来追踪虚拟机。它源自英国剑桥大学的 CADETS 项目。作为一个简单的例子，我们来看一下最初的 snooper 脚本，并扩展它以使用 HyperTrace：

```c
# dtrace -x quiet -En 'FreeBSD-14*:proc:::exec { printf(“%s: user = %u, gid = %u:
%s\n”, vmname, uid, gid, stringof(args[0])); }'
scylla1-webserver-0: user = 0, gid = 0: /usr/sbin/dtrace
scylla1-webserver-0: user = 0, gid = 0: /sbin/ls
scylla1-webserver-0: user = 0, gid = 0: /bin/ls
scylla1-client-0: user = 0, gid = 0: /usr/sbin/sshd
scylla1-client-0: user = 0, gid = 0: /bin/csh
scylla1-client-0: user = 0, gid = 0: /usr/bin/resizewin
scylla1-client-0: user = 0, gid = 0: /usr/sbin/iperf
scylla1-client-0: user = 0, gid = 0: /usr/bin/iperf
scylla1-client-0: user = 0, gid = 0: /usr/local/bin/iperf
host: user = 0, gid = 0: /bin/sh
host: user = 0, gid = 0: /usr/libexec/atrun
scylla1-client-0: user = 0, gid = 0: /bin/sh
scylla1-client-0: user = 0, gid = 0: /usr/libexec/atrun
scylla1-client-1: user = 0, gid = 0: /bin/sh
scylla1-client-1: user = 0, gid = 0: /usr/libexec/atrun
scylla1-client-2: user = 0, gid = 0: /bin/sh
scylla1-client-2: user = 0, gid = 0: /usr/libexec/atrun
scylla1-client-3: user = 0, gid = 0: /bin/sh
scylla1-client-3: user = 0, gid = 0: /usr/libexec/atrun
scylla1-webserver-0: user = 0, gid = 0: /bin/sh
scylla1-webserver-0: user = 0, gid = 0: /usr/libexec/atrun
```

我们修改了脚本，以实现两个新的功能：使用内建变量 `vmname` 指定每个进程执行的位置前缀，以及在探针规范中添加第五个元组条目：`FreeBSD-14*`。这允许用户指定要插桩的目标机器（虚拟机），并可以通过命令行标志来控制，支持通过操作系统版本或机器的主机名进行名称解析。类似的修改也可以应用到我们的块 I/O 示例中：

```c
# dtrace -x quiet -En 'scylla1-*:io:::start { @[vmname, immstack()] = count(); }'
...
 scylla1-webserver-0
 devstat_start_transacti+0x90
 g_disk_start+0x316
 g_io_request+0x2d7
 g_part_start+0x289
g_io_request+0x2d7
 g_io_request+0x2d7
 ufs_strategy+0x83
 VOP_STRATEGY_APV+0xd2
 bufstrategy+0x3e
 bufwriteL+0x80
 vfs_bio_awrite+0x24f
 flushbufqueues+0x52a
 buf_daemon+0x1f1
 fork_exit+0x80
 fork_trampoline+0xe
 aio_process_rw+0x10c
 aio_daemon+0x285
 fork_exit+0x80
 fork_trampoline+0xe
 fork_trampoline+0xe
 10598
 scylla1-client-0
 g_disk_start+0x316
 g_io_request+0x2d7
 g_part_start+0x289
 g_io_request+0x2d7
 g_io_request+0x2d7
 ufs_strategy+0x83
 VOP_STRATEGY_APV+0x9e
 bufstrategy+0x3e
 bufwriteL+0x3e
 vfs_bio_awrite+0x24f
 flushbufqueues+0x52a
 buf_daemon+0x1f1
 fork_exit+0x80
 fork_trampoline+0xe
 fork_trampoline+0xe
 aio_process_rw+0x10c
 aio_daemon+0x285
 fork_exit+0x80
 fork_trampoline+0xe
 fork_trampoline+0xe
 10605
```

在这里，使用了一个新的 DTrace 动作 `immstack()`，它与 `stack()` 类似，但符号解析发生在内核中，而不是在打印输出时。HyperTrace 的工作原理是旨在在主机内核上执行整个 D 脚本，而不是在客户端内部运行 DTrace，同时每个客户机负责自行插桩，并在客户机执行探针时向主机发出同步的超调用（类似于操作系统中的系统调用）。这种设计使得能够在一个地方保持跨所有客户机和主机的全局状态——提高了在追踪虚拟机时 D 的整体表现力。该工作仍在进行中，可以在 GitHub 上查看。

## 进一步阅读

1. <https://illumos.org/books/dtrace/preface.html#preface>
2. <https://github.com/opendtrace>
3. <https://docs.freebsd.org/en/books/handbook/dtrace/>
4. <https://www.cs.princeton.edu/courses/archive/fall05/cos518/papers/dtrace.pdf>
5. <https://www.brendangregg.com/dtracebook/>
6. <https://wiki.freebsd.org/DTrace/One-Liners>
7. <https://freebsdfoundation.org/wp-content/uploads/2014/05/DTrace.pdf>
8. <https://issue.freebsdfoundation.org/publication/?m=29305&i=417423&p=14&ver=html5>
9. <http://www.onlinedigeditions.com/publication/?m=29305&i=536657&p=4&ver=html5>
10. <https://www.brendangregg.com/FlameGraphs/cpuflamegraphs.html>
11. <https://papers.freebsd.org/2018/bsdcan/teske-all_along_the_dwatch_tower/>
12. <https://github.com/freebsd/freebsd-papers/pull/112>
13. <https://github.com/cadets/freebsd>

***

**DOMAGOJ STOLFA** 是剑桥大学的研究助理，专注于虚拟化系统的动态追踪。他自 2016 年起就一直在 FreeBSD 上与 bhyve 和 DTrace 合作，贡献补丁。Domagoj 还是剑桥大学高级操作系统课程的教学助理，教授使用 FreeBSD、DTrace 和 PMC 的操作系统概念。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://freebsd-journal-cn.bsdcn.org/20221112-ke-guan-ce-xing-he-heng-liang-biao-zhun/dtrace.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
