DTrace:老式跟踪系统的新扩展

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 输出额外的信息。

# 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 语言不支持循环,因此任何形式的循环都必须手动展开

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

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

# 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 的聚合输出应解读为:

execname                                              pid            count

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

# 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

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

# dwatch execve

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

# 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 并详细讲解了其功能。此外,FreeBSDdwatch(1) 手册页也提供了许多优秀的示例,适合感兴趣的用户尝试。

CTFv3

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

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

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

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

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

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

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

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

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_unlockedslowpath_noirq。在这两个慢路径中,分别调用了 spinlock_exit()thread_lock_flags_(),而 _mtx_release_lock_quick() 则只是在 amd64 上执行原子比较和交换指令。为了使用 kinst 来识别最终进入慢路径的调用堆栈,我们首先需要以某种方式反汇编该函数。一种可能的方法是在 FreeBSD 中使用 kgdb(通过 pkg install gdb 安装):

# 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 脚本:

# 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:

# 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 示例中:

# 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 上查看。

进一步阅读


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

最后更新于

这有帮助吗?