retsnoop 是什么?
retsnoop 是一个基于 BPF 的工具,用于非侵入式地大规模跟踪 Linux 内核内部情况。
retsnoop 的主要目标是提供一种灵活且人性化的方式,从内核中提取对用户有用的准确信息。在任何给定时刻,运行中的内核都在不同的子系统上、不同的核心上做许多不同的事情。提取和审查整个内核中的各种日志、调用栈、跟踪点等可能是一项非常耗时和繁重的任务。同样,反复添加 printk() 语句需要长时间的重新编译、重新启动和重新运行测试用例的迭代周期。另一方面,retsnoop 允许用户通过指定他们想要监控的特定内核函数子集以及从这些函数中收集的信息类型来实现更高的信噪比,而无需对内核进行任何更改。retsnoop 现在还可以捕获函数参数,进一步实现了简单灵活地为用户提供相关和有用信息的承诺。
retsnoop 通过低开销的非侵入式跟踪一组内核函数,拦截它们的入口和出口来实现其目标。retsnoop 的核心概念是用户指定的感兴趣的内核函数集。这允许 retsnoop 通过让用户灵活控制相关的内核函数子集来捕获高相关性的数据。所有其他内核函数都被忽略,不会用无关信息污染捕获的数据。
retsnoop 还支持一组额外的过滤器,用于进一步限制捕获跟踪数据的上下文和条件,允许根据 PID 或进程名进行过滤,选择函数返回的可接受错误子集,或根据函数延迟进行过滤。
retsnoop 支持三种不同且互补的模式。
默认的栈跟踪模式简洁地指出满足用户条件的最深函数调用栈(例如,从系统调用返回的错误)。它显示函数调用序列、栈每一层对应的源代码位置,并输出延迟和返回结果:
$ sudo ./retsnoop -e '*sys_bpf' -a ':kernel/bpf/*.c'
Receiving data...
20:19:36.372607 -> 20:19:36.372682 TID/PID 8346/8346 (simfail/simfail):
entry_SYSCALL_64_after_hwframe+0x63 (arch/x86/entry/entry_64.S:120:0)
do_syscall_64+0x35 (arch/x86/entry/common.c:80:7)
. do_syscall_x64 (arch/x86/entry/common.c:50:12)
73us [-ENOMEM] __x64_sys_bpf+0x1a (kernel/bpf/syscall.c:5067:1)
70us [-ENOMEM] __sys_bpf+0x38b (kernel/bpf/syscall.c:4947:9)
. map_create (kernel/bpf/syscall.c:1106:8)
. find_and_alloc_map (kernel/bpf/syscall.c:132:5)
! 50us [-ENOMEM] array_map_alloc
!* 2us [NULL] bpf_map_alloc_percpu
^C
Detaching... DONE in 251 ms.
函数调用跟踪模式(-T)额外提供了给定函数集的详细控制流跟踪,使我们能更全面地理解内核行为:
FUNCTION CALL TRACE RESULT DURATION
----------------------------------------------- -------------------- ---------
→ bpf_prog_load
→ bpf_prog_alloc
↔ bpf_prog_alloc_no_stats [0xffffc9000031e000] 5.539us
← bpf_prog_alloc [0xffffc9000031e000] 10.265us
[...]
→ bpf_prog_kallsyms_add
↔ bpf_ksym_add [void] 2.046us
← bpf_prog_kallsyms_add [void] 6.104us
← bpf_prog_load [5] 374.697us
最后但同样重要的是,LBR 模式(Last Branch Records)允许用户"回顾"并更深入地查看单个函数的内部,跟踪"不可见的"内联函数,并将问题精确定位到单个 C 语句。这种模式在跟踪不熟悉的内核部分而没有很好的想法要查找什么时特别有用。它支持迭代发现过程,而无需太多了解要查看的位置和相关函数:
$ sudo ./retsnoop -e '*sys_bpf' -a 'array_map_alloc_check' --lbr=any
Receiving data...
20:29:17.844718 -> 20:29:17.844749 TID/PID 2385333/2385333 (simfail/simfail):
...
[#22] ftrace_trampoline+0x14c -> array_map_alloc_check+0x5 (kernel/bpf/arraymap.c:53:20)
[#21] array_map_alloc_check+0x13 (kernel/bpf/arraymap.c:54:18) -> array_map_alloc_check+0x75 (kernel/bpf/arraymap.c:54:18)
. bpf_map_attr_numa_node (include/linux/bpf.h:1735:19) . bpf_map_attr_numa_node (include/linux/bpf.h:1735:19)
[#20] array_map_alloc_check+0x7a (kernel/bpf/arraymap.c:54:18) -> array_map_alloc_check+0x18 (kernel/bpf/arraymap.c:57:5)
. bpf_map_attr_numa_node (include/linux/bpf.h:1735:19)
[#19] array_map_alloc_check+0x1d (kernel/bpf/arraymap.c:57:5) -> array_map_alloc_check+0x6f (kernel/bpf/arraymap.c:62:10)
[#18] array_map_alloc_check+0x74 (kernel/bpf/arraymap.c:79:1) -> __kretprobe_trampoline+0x0
...
请也查看配套的博客文章"使用 retsnoop 跟踪 Linux 内核",其中详细介绍了每种模式,并演示了在一些受实际问题启发的例子上使用 retsnoop。
注意: Retsnoop 利用了 BPF 技术的强大功能,因此需要 root 权限,或足够级别的能力(CAP_BPF 和 CAP_PERFMON)。还要注意,尽管 BPF 技术有各种安全保障和谨慎的实现,内核跟踪本质上是棘手的,可能会干扰生产工作负载,因此始终建议尽可能在非生产系统上测试您要尝试的操作。典型的用户上下文内核代码(这是 Linux 内核代码的大部分)不会造成任何问题,但跟踪在特殊内核上下文(例如,硬 IRQ、NMI 等)中运行的非常低级的内部结构可能需要一些注意。Linux 内核本身通常会保护自己不跟踪这些危险和敏感的部分,但内核错误有时会溜进来,所以请使用您最好的判断(如果可能的话进行测试)。
注意: Retsnoop 依赖 BPF CO-RE 技术,因此请确保您的 Linux 内核是使用 CONFIG_DEBUG_INFO_BTF=y 内核配置构建的。如果没有此配置,retsnoop 将拒绝启动。
使用 retsnoop
本节以参考风格描述了各种 retsnoop 概念和功能,适合那些想要充分利用 retsnoop 全部功能的人。如果您是 retsnoop 新手,可以考虑查看"使用 retsnoop 跟踪 Linux 内核"博客文章,以熟悉输出格式并了解如何在实践中使用该工具。
指定跟踪的函数
Retsnoop 的操作以跟踪多个函数为中心。函数分为两类:入口函数和非入口(辅助)函数。入口函数定义了一组激活 retsnoop 记录逻辑的函数。当任何入口函数首次被调用时,retsnoop 开始跟踪和记录所有后续函数调用,直到触发记录的入口函数返回。一旦记录被激活,入口和非入口函数都以完全相同的方式被记录,彼此之间没有区别。每个线程内的函数调用都是完全独立跟踪的,因此在多 CPU 系统上可能同时进行多个记录。
简而言之,入口函数是记录触发器,而非入口函数是通过额外的内部函数调用来增强记录的数据,但只有在从激活的入口函数调用时才会这样做。如果在调用入口函数之前调用了某个非入口函数,这样的调用会被retsnoop
忽略。这种划分避免了低信号噪音,比如在入口函数有趣上下文之外发生的常见辅助函数的记录。
函数集通过以下方式指定:
-e
(--entry
)参数,用于指定一组入口函数。-a
(--allow
)参数,用于指定一组非入口函数。如果非入口集中的任何函数与入口集重叠,入口集优先。-d
(--deny
)参数,用于从入口和非入口集中排除指定的函数。被拒绝的函数会从入口和非入口子集中过滤掉。拒绝列表始终优先。
-e
、-a
和-d
每个都期望一个值,可以是以下两种形式之一:
- 函数名通配符,类似于shell的文件通配符。例如,
*bpf*
将匹配名称中包含"bpf"子串的任何函数。只支持*
和?
通配符,可以多次指定。*
通配符匹配任何字符序列(或无),?
匹配恰好一个字符。所以foo??
会匹配foo10
,但不会匹配foo1
。你还可以选择添加内核模块通配符,格式为<func-glob> [<module-glob>]
,以将函数搜索范围缩小到仅匹配指定通配符模式的内核模块。例如,*_read_* [*kvm*]
会匹配kvm_intel
模块中的vmx_read_guest_seg_ar
,或kvm
模块中的segmented_read_std
。因此,指定* [kvm]
可能是发现kvm
模块中所有可跟踪函数的最简单方法。 - 源代码路径通配符,以':'为前缀(例如,
:kernel/bpf/*.c
)。任何在匹配指定文件路径通配符的源文件中定义的函数都会被添加到匹配中。源代码位置必须相对于内核仓库根目录。因此,:kernel/bpf/*.c
将匹配Linux的kernel/bpf
目录下*.c
文件中定义的任何函数。当然,这一切都依赖于内核映像文件(vmlinux
)在标准位置之一可用,并且其中包含DWARF调试信息。请注意,retsnoop
不会分析源代码本身,也不期望源代码存在于任何地方。所有这些信息都应该记录在DWARF调试信息中。
-e
、-a
和-d
可以多次指定,匹配的函数在各自的类别内连接。这允许灵活匹配无法通过一个简单通配符表达的不同函数子集。也支持混合使用函数名通配符和源代码路径通配符。
所有匹配的函数还会与内核在/sys/kernel/tracing/available_filter_functions
文件中报告的可跟踪函数列表进行额外检查。如果你没有在那里看到预期的函数,那很可能是由于以下几个常见原因之一:
- 函数被内联,因此无法直接跟踪;尝试跟踪调用所需函数或被其调用的非内联函数;
- 函数在某种程度上是特殊的,内核本身不允许跟踪(通常是在受限内核上下文中执行的一些低级函数);
- 由于内核配置,函数可能被编译出去,或者在跟踪时未加载相关内核模块;
- 有时函数由于某些编译器优化而被重命名,添加了类似
.isra.0
的后缀;在这种情况下,在末尾添加'*'以匹配这些后缀。
如果不确定是否指定了正确的函数集,使用--dry-run -v
参数进行详细的预运行,retsnoop
将报告它发现并尝试附加的所有函数,但不会实际附加它们。这是在不影响系统工作负载的情况下验证一切的最佳方法。
操作模式
默认堆栈跟踪模式
如上所述,默认情况下,retsnoop
捕获堆栈跟踪,包含满足条件的最深嵌套函数调用。例如,如果没有指定自定义错误过滤器,retsnoop
将尝试捕获导致错误返回的最深函数调用链的堆栈跟踪。有关更多详细信息,请参阅配套博客文章中的堆栈跟踪模式示例。
Retsnoop始终捕获并输出堆栈跟踪。其他模式(函数调用跟踪和LBR,下面描述)是对默认堆栈跟踪模式的补充,彼此之间也是互补的。
函数调用跟踪模式
提供-T
(--trace
)标志启用函数调用跟踪模式。在此模式下,retsnoop
将跟踪每个入口和非入口函数之间的详细调用序列。与堆栈跟踪模式一样,只有在命中入口函数并满足任何附加过滤条件时才激活此记录。
如上所述,此模式是对默认堆栈跟踪模式的补充,如果启用,也是对LBR模式的补充。它提供了捕获的函数调用序列的不同视图。鉴于根据特定工作负载和感兴趣的函数集,记录可能相当冗长和昂贵,因此需要使用-T
参数显式选择加入。
这种模式非常适合详细了解内核行为,特别是不熟悉的部分。有关更多详细信息,请参阅配套博客文章中的函数调用跟踪模式示例。
LBR(最后分支记录)模式
LBR(最后分支记录)是Intel CPU的一项功能,允许用户指示CPU持续记录最后N次调用/返回/跳转(具体捕获内容是可配置的),且无开销。捕获的记录数量取决于CPU的代代,通常在8到32之间。在最新的Linux内核(v5.16+)中,可以从BPF程序中即时捕获这些LBR,这被retsnoop
在LBR模式中利用。一些非Intel CPU具有类似的功能,这些功能被内核的perf子系统抽象化,所以你不一定需要Intel CPU就能在retsnoop
中利用它。
那么LBR模式何时有用?有几种典型场景。
其中之一是当你在调查内核返回的一些通用错误时。内核可能在各种不同情况下返回错误,而且不清楚是哪种情况。例如,bpf()
系统调用经常出现这种情况,其中像-EINVAL
这样的错误可能由于数十种不同的错误条件而返回,而且一堆这样的条件可能在同一个大函数内检查。调试究竟是哪个错误条件被触发有时会令人抓狂。LBR模式允许用户洞察函数内哪个确切的if条件返回了错误。
另一个常见场景是当你试图跟踪完全不熟悉的Linux内核代码部分时。你可能知道入口函数,但不知道它调用了哪些其他函数,以及这些函数在源代码中的定义位置。例如,一个常见的情况是入口函数调用通用回调,而不清楚回调函数实际上在哪里定义。在这种情况下,很难知道要指定哪些函数名通配符或源代码路径通配符,因为这些回调可能有很多可能的实现。LBR模式在这里可以帮助,因为它不需要跟踪相关函数就能发现它们。
无论什么情况需要LBR模式,都可以使用-R
(--lbr
)参数激活它。与函数调用跟踪模式类似,LBR模式独立于默认堆栈跟踪和函数调用跟踪模式,并对它们进行补充。当满足正确条件时,retsnoop
除了捕获堆栈跟踪和其他信息外,还捕获LBR数据,然后以其自己的格式输出数据。
有关更多详细信息,请参阅配套博客文章中的LBR模式示例。
这里我们只需注意,LBR模式允许自定义指示CPU记录的记录类型。它可以是以下值之一:any
、any_call
、any_return
(默认)、cond
、call
、ind_call
、ind_jump
、call_stack
、abort_tx
、in_tx
、no_tx
。有关每种模式的简要描述,请参阅Linux的perf_event.h UAPI头文件及其enum perf_branch_sample_type_shift
的注释。
默认情况下,retsnoop
采用any_return
LBR配置,记录最后N个函数返回。这对于第二类场景非常有用,当我们想知道调用了哪些函数而无需深入研究代码时。LBR堆栈会指向在retsnoop
显式追踪的最深函数被调用之前调用的函数,即使它们不属于入口/非入口函数集。因此,您可以通过这种方式继续使用retsnoop
并扩展入口/非入口函数集。
对于第一类场景(具有多个错误条件的复杂函数),LBR配置any
可能更合适。它记录任何函数调用、返回和跳转,包括条件和无条件跳转。它可能有助于在单个大函数内精确定位返回错误的具体语句,因为它不受函数边界的限制。
请注意,LBR是一项棘手的技术,它可能无法捕获足够的相关细节,有时可能会捕获无关的细节。如果出现后者情况,您可以使用--lbr-max-count
参数来限制输出指定数量的最相关条目。
函数参数捕获
注意: 函数参数捕获功能目前仅支持x86-64(amd64)架构。很可能会添加arm64支持,如果您对此感兴趣,请通过Github问题提出请求。
retsnoop
可以捕获所有被追踪函数的参数值。这对默认的堆栈跟踪模式和函数调用跟踪模式都有效。BTF信息用于确定所有输入参数的名称、确切类型和大小,但对于没有BTF信息的函数,retsnoop
将退而捕获所有按值传递寄存器的原始值,这符合平台的调用约定ABI。
对于指针参数,不仅捕获指针值,还捕获指向的类型数据!retsnoop
还能理解字符串并捕获和呈现字符串内容。这使得该功能在实践中对通过指针传递的值和字符串更加有用。
由于潜在的大型结构类型,渲染捕获的参数可能比较棘手,retsnoop
支持三种渲染模式,可以使用-C args.fmt-mode=<mode>
配置设置来选择。
在默认的compact
模式下,所有函数捕获的参数将在函数调用跟踪或堆栈跟踪数据中相应函数条目旁的单行(可能相当长)中呈现。每个参数输出默认会被截断为120个字符,但可以使用args.fmt-max-arg-width
设置进行调整,有效值在0到250之间,0表示不进行截断。
以下是一个示例,每个参数的截断长度被覆盖为仅40个字符(显示了堆栈跟踪和函数调用跟踪模式)。请注意,它相当宽,但除此之外并不真正干扰常规跟踪信息的呈现和视觉连贯性。
$ sudo ./retsnoop -e '*sys_bpf' -TAS -C args.fmt-max-arg-width=40
...
FUNCTION CALLS RESULT DURATION ARGS
--------------- ------ -------- ----
→ __x64_sys_bpf regs=&{.r15=0x959998935,.r14=0x171abb7677,.r…
↔ __sys_bpf [0] 3.366us cmd=1 uattr={{.kernel=0x7f1c73665120,.user=0x7f1c73… size=32
← __x64_sys_bpf [0] 4.506us
do_syscall_64+0x3d (arch/x86/entry/common.c:80)
. do_syscall_x64 (arch/x86/entry/common.c:50)
4us [0] __x64_sys_bpf+0x18 (kernel/bpf/syscall.c:5672) regs=&{.r15=0x959998935,.r14=0x171abb7677,.r…
. __se_sys_bpf (kernel/bpf/syscall.c:5672)
. __do_sys_bpf (kernel/bpf/syscall.c:5674)
! 3us [0] __sys_bpf cmd=1 uattr={{.kernel=0x7f1c73665120,.user=0x7f1c73… size=32
对于更加宽度受限的数据呈现,可以使用multiline
模式(即-C args.fmt-mode=multiline
),其中每个参数将在单独的行上呈现(在相应的函数条目下),但参数值本身(即使是相当大的结构)将在单行上紧凑呈现。与默认的compact
模式一样,multiline
模式也应用相同的每参数截断处理。同样可以使用-C args.fmt-max-arg-width=N
设置进行调整。以下是与上面相同的示例,但这次使用multiline
模式,截断限制设置为80个字符:
$ sudo ./retsnoop -e '*sys_bpf' -TAS -C args.fmt-mode=multiline -C args.fmt-max-arg-width=80
...
FUNCTION CALLS RESULT DURATION
--------------- ------ --------
→ __x64_sys_bpf
› regs=&{.r15=0x7ffc86cfd05c,.r14=0x7ffc86cfd048,.r13=0x7f5ae9a55180,.r12=0x7fffffffff…
↔ __sys_bpf [0] 0.842us
› cmd=1
› uattr={{.kernel=0x7ffc86cfcf90,.user=0x7ffc86cfcf90}}
› size=32
← __x64_sys_bpf [0] 2.105us
entry_SYSCALL_64_after_hwframe+0x46 (entry_SYSCALL_64 @ arch/x86/entry/entry_64.S:120)
do_syscall_64+0x3d (arch/x86/entry/common.c:80)
. do_syscall_x64 (arch/x86/entry/common.c:50)
2us [0] __x64_sys_bpf+0x18 (kernel/bpf/syscall.c:5672)
› regs=&{.r15=0x7ffc86cfd05c,.r14=0x7ffc86cfd048,.r13=0x7f5ae9a55180,.r12=0x7fffffffff…
. __se_sys_bpf (kernel/bpf/syscall.c:5672)
. __do_sys_bpf (kernel/bpf/syscall.c:5674)
! 0us [0] __sys_bpf
› cmd=1
› uattr={{.kernel=0x7ffc86cfcf90,.user=0x7ffc86cfcf90}}
› size=32
它在视觉上稍微有些干扰,但函数调用跟踪流程仍然很容易跟随,同时参数数据更容易查看。参数通常与函数返回值输出对齐(对于函数调用跟踪和堆栈跟踪输出都是如此)。
最后,终极的verbose
格式化模式可以毫无妥协地呈现所有捕获的数据。这适用于检查数据是最高优先级,而函数调用呈现的视觉连贯性是次要考虑的情况。在verbose
模式下没有截断,每个参数值可以占用任意多行(这主要适用于呈现结构体/联合体和数组)。注意,如果retsnoop
无法捕获足够的数据字节,数据仍可能被截断,请参阅下面解释默认和最大限制的段落。如果默认值不能满足要求,请适当调整它们。以下是示例:
$ sudo ./retsnoop -e '*sys_bpf' -TAS -C args.fmt-mode=verbose
...
FUNCTION CALLS RESULT DURATION
--------------- ------ --------
→ __x64_sys_bpf
› regs = &{
.r15 = 0x7ffc86cfd05c,
.r14 = 0x7ffc86cfd048,
.r13 = 0x7f5ae9a55180,
.r12 = 0x7fffffffffffffff,
.bp = 0x7ffc86cfd030,
.r11 = 582,
.r10 = 70,
.r9 = 70,
.r8 = 70,
.ax = 0xffffffffffffffda,
.cx = 0x7f5aefb20e39,
.dx = 32,
.si = 0x7ffc86cfcf90,
.di = 1,
.orig_ax = 321,
.ip = 0x7f5aefb20e39,
.cs = 51,
.flags = 582,
.sp = 0x7ffc86cfcf88,
.ss = 43
}
↔ __sys_bpf [0] 0.793us
› cmd = 1
› uattr = {
{
.kernel = 0x7ffc86cfcf90,
.user = 0x7ffc86cfcf90
}
}
› size = 32
← __x64_sys_bpf [0] 1.982us
entry_SYSCALL_64_after_hwframe+0x46 (entry_SYSCALL_64 @ arch/x86/entry/entry_64.S:120)
do_syscall_64+0x3d (arch/x86/entry/common.c:80)
. do_syscall_x64 (arch/x86/entry/common.c:50)
1us [0] __x64_sys_bpf+0x18 (kernel/bpf/syscall.c:5672)
› regs = &{
.r15 = 0x7ffc86cfd05c,
.r14 = 0x7ffc86cfd048,
.r13 = 0x7f5ae9a55180,
.r12 = 0x7fffffffffffffff,
.bp = 0x7ffc86cfd030,
.r11 = 582,
.r10 = 70,
.r9 = 70,
.r8 = 70,
.ax = 0xffffffffffffffda,
.cx = 0x7f5aefb20e39,
.dx = 32,
.si = 0x7ffc86cfcf90,
.di = 1,
.orig_ax = 321,
.ip = 0x7f5aefb20e39,
.cs = 51,
.flags = 582,
.sp = 0x7ffc86cfcf88,
.ss = 43
}
. __se_sys_bpf (kernel/bpf/syscall.c:5672)
. __do_sys_bpf (kernel/bpf/syscall.c:5674)
! 0us [0] __sys_bpf
› cmd = 1
› uattr = {
{
.kernel = 0x7ffc86cfcf90,
.user = 0x7ffc86cfcf90
}
}
› size = 32
这确实非常详细,但会渲染所有捕获的数据以便于检查。
数据捕获大小限制
默认情况下,`retsnoop`会为任何单个函数调用捕获最多3KB的数据,每个参数最多256字节,无论是固定大小的整数、枚举、按值传递的结构体等,还是可变长度的字符串参数。所有这些限制都可以通过`args.*`组中的额外设置进行调整,请参阅[额外设置](#extra_settings)部分。您可以请求捕获每个函数调用总计最多64KB的数据,每个参数最多16KB。
渲染全零值
`retsnoop`在渲染捕获的参数数据时,试图实现高信噪比,因此它会跳过打印全零值。也就是说,如果捕获的结构体中某些字段是零整数值,或者是另一个所有字段都为零的嵌入式结构体,这样的字段和值就不会被打印。当然,如果参数本身是零整数参数,它仍然会被打印。同样,如果按值或按指针传递的结构体参数的数据完全为零,它们仍然会被渲染为`arg={}`或`arg=&{}`。
附加过滤器
默认情况下,`retsnoop`记录任何导致触发的入口函数返回错误的函数调用跟踪(基于入口和非入口函数集)。"错误返回"是通过启发式方法定义的,对于返回指针的函数是`NULL`,对于返回整数的函数是小的负错误值`-Exxx`。这可以进一步调整,如下所述。但是这样的函数跟踪会在任何进程中全局收集,这可能会不方便并导致输出中出现无关的"垃圾信息"。
幸运的是,`retsnoop`允许用户调整捕获跟踪的条件。
错误过滤器
`-S`(`--success-stacks`)强制`retsnoop`捕获任何函数跟踪,禁用默认的只捕获错误返回情况的逻辑。如果您没有看到`retsnoop`捕获堆栈跟踪或记录您确定正在内核中发生的函数调用跟踪,请检查是否需要使用`-S`启用成功堆栈跟踪捕获。
`retsnoop`还允许用户通过`-x`(`--allow-errors`)和`-X`(`--deny-errors`)参数微调哪些返回结果被认为是错误的。它们期望`Exxx`/`-Exxx`符号错误代码(您可以在[这个表格](https://github.com/anakryiko/retsnoop/blob/master/src/utils.c#L10-L50)中找到`retsnoop`识别的所有错误)或`NULL`。
这些参数可以多次指定,所有错误都会连接成一个列表。错误拒绝列表(`-X`)优先,所以如果`-x`和`-X`之间有重叠,`-X`会胜出,即使指定的错误被`-x`参数允许,也会被忽略。
如果没有提供`-x`,则假定所有支持的错误都是允许的,除了那些被`-X`拒绝的。
进程过滤
`retsnoop`允许用户缩小将在其上下文中捕获数据的进程集。这些过滤器在繁忙的生产主机上非常有用,这些主机同时进行很多操作,但您需要调查只在一小部分进程中发生的事情。
`-p`(`--pid`)和`-P`(`--no-pid`)允许基于进程ID(PID)进行过滤。就像大多数其他类似的参数一样,您可以多次指定它们来组合多个PID过滤器。
`-n`(`--comm`)和`-N`(`--no-comm`)允许用户通过进程/线程名称进行过滤,可以与PID过滤一起使用或代替PID过滤。同样也可以多次指定。
持续时间过滤器
`-L`(`--longer`)允许用户指定触发入口函数执行的最小持续时间,只有超过这个时间的才会被捕获和报告,跳过所有更快完成的。这个参数应该是一个以毫秒为单位的正数。
如果您需要调查延迟问题,这个过滤器允许用户忽略不相关的快速完成的函数调用,而是更有针对性地跟踪慢速的函数调用。
其他设置
详细程度、空运行、版本和功能检测
`retsnoop`支持各种级别的"详细程度"。默认情况下,它不会输出任何关于它正在做什么以及它将要跟踪哪些函数的额外信息。默认的详细级别(`-v`)提供了一个高信号的详细输出列表,以了解幕后发生的事情。使用`-v`,retsnoop会报告发现的函数列表、内核镜像的位置等。`retsnoop`还有更详细的级别(`-vv`和`-vvv`),但它们可能只对`retsnoop`的开发人员和调试有用。
默认的详细输出与空运行模式结合使用非常有用,可以通过`--dry-run`参数激活。在此模式下,`retsnoop`将报告它将要执行的所有操作(包括列出它发现并将要跟踪的函数),但不会实际激活任何操作。这样甚至没有机会干扰生产工作负载,是一种安全的预先检查方式。
`-V`(`--version`)将打印`retsnoop`的版本。如果与`-v`结合使用,`retsnoop`还将输出它依赖的所有检测到的内核功能。您需要以root身份或具有`CAP_BPF`和`CAP_PERFMON`能力运行`retsnoop`,功能检测才能工作。您应该看到类似以下内容:
$ sudo ./retsnoop -Vv retsnoop v0.9.1 功能检测: 支持BPF环形缓冲区映射:是 支持bpf_get_func_ip():是 支持bpf_get_branch_snapshot():是 支持BPF cookie:是 支持多附加kprobe:否 功能校准: kretprobe IP偏移:8 fexit睡眠修复:是 fentry重入保护:是
符号化设置
`retsnoop`试图提供尽可能准确和完整的函数和堆栈跟踪信息。如果在系统的标准位置可以找到Linux内核镜像,并且它包含DWARF类型信息,这将用于增强捕获的堆栈跟踪,提供有关源代码位置和内联函数的信息。使用DWARF信息会增加一些额外的CPU开销,因此可以调整这种行为和相关参数。
如果`retsnoop`无法在标准位置找到内核镜像,可以通过`-k`(`--kernel`)参数指向自定义位置。
`-s`(`--symbolize`)允许用户调整堆栈符号化行为。指定`-sn`以禁用额外的基于DWARF的符号化。在这种情况下,`retsnoop`将坚持使用基于`/proc/kallsyms`数据的基本符号化。单独的`-s`将尝试获取源代码位置信息,但不会尝试符号化内联函数。`-ss`允许同时获取内联函数和源代码信息。如果找到带有DWARF信息的内核,这是默认模式,因为在大多数情况下它非常有用。
请注意,DWARF类型信息对于源代码路径通配符的工作也是必要的。
额外设置
随着`retsnoop`功能的增加,它也获得了各种高级用户可能想根据特定使用场景调整的旋钮和额外设置。为了不使主CLI界面过于复杂,所有这些不常用的设置都放在单独的`--config`参数后面,该参数接受`<组>.<键>=<值>`形式的配置参数。例如,要请求`retsnoop`在堆栈跟踪中打印原始地址,可以使用`--config stacks.emit-addrs=true`参数调用`retsnoop`。
使用`--config-help`参数可以请求所有支持的高级设置的列表和相应描述:
```shell
$ retsnoop --config-help
可以自定义retsnoop的各种内部实现细节。
这可以通过使用--config 键=值 CLI参数指定一个或多个额外参数来完成。
支持的配置参数:
bpf.ringbuf-size - BPF环形缓冲区大小(字节)。
如果遇到数据丢失,请增加此值。默认设置为8MB。
bpf.sessions-size - BPF会话映射容量(默认为4096)
stacks.symb-mode - 堆栈符号化模式。
确定堆栈符号化的处理程度以及堆栈跟踪中包含的额外信息类型:
none - 无源代码信息,无内联函数;
linenum - 源代码信息(文件:行),无内联函数;
inlines - 源代码信息和内联函数。
stacks.emit-addrs - 发出捕获的原始堆栈跟踪/LBR地址(除符号外)
stacks.dec-offs - 以十进制发出堆栈跟踪/LBR函数偏移(默认为十六进制)
stacks.unfiltered - 发出所有堆栈跟踪/LBR条目(关闭相关性过滤)
args.max-total-args-size - 每个函数调用捕获的参数数据字节总量上限
args.max-sized-arg-size - 任何固定大小参数捕获的数据字节上限
args.max-str-arg-size - 任何字符串参数捕获的数据字节上限
args.fmt-mode - 函数参数格式化模式(compact、multiline、verbose)
args.fmt-max-arg-width - 单个参数输出占用的最大水平空间。
仅适用于compact和multiline模式。
如果设置为零,则不进行截断。
fmt.lbr-max-count - 限制打印的LBR数量为N
与大多数retsnoop
参数一样,可以同时提供多个--config
/-C
参数。
获取retsnoop
下载预构建的x86-64二进制文件
每个发布版本都有一个预构建的retsnoop二进制文件,适用于x86-64(amd64)和arm64架构,可以直接下载使用。前往"Releases"页面下载最新的二进制文件。
从源代码构建retsnoop
从源代码构建retsnoop
也相当简单。retsnoop
的大多数依赖项已包含在内:
- libbpf作为子模块签出,由
retsnoop
的Makefile自动构建并静态链接; - 唯一的运行时库(除
libc
外)是libelf
和zlib
,你还需要它们的开发版本(用于API头文件)来编译libbpf
; retsnoop
预打包了必要工具的x86-64版本(bpftool在构建过程中需要,但如果有兴趣在非x86架构上使用retsnoop
,可以改进这一点(请开一个issue提出请求);- 最大的外部依赖是支持
bpf
目标的Clang编译器。尽量使用至少Clang 11+,但能获得的最新Clang版本越好。
一旦满足依赖条件,剩下的就很简单了:
$ make -C src
你将在src/
文件夹下得到retsnoop
二进制文件。你可以将它复制到生产服务器上运行。除了主要的retsnoop
可执行文件外,不需要分发任何额外文件。
发行版可用性
Retsnoop已开始被发行版打包。下表将指出哪些发行版打包了retsnoop以及版本情况。