深入理解eBPF与可观测性
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.1.1 Linux的跟踪与诊断技术简介

在eBPF出现之前,Linux已经提供了多种跟踪与诊断基础功能模块,包括kprobe/kretprobe、uprobe/uretprobe、fentry/fexit以及tracepoint。这些功能模块为跟踪和诊断提供了丰富的支持。目前,大部分eBPF的跟踪与诊断功能都是基于这些功能模块实现的。接下来,我们将分别介绍这些功能的工作原理,以便更好地理解eBPF的工作原理。

1)kprobe/kretprobe:kprobe用于在函数的入口处插入代码进行跟踪和调试;kretprobe是kprobe的扩展,用于在函数的返回处插入代码进行跟踪和调试。

kprobe/kretprobe的工作原理如图1-1所示。首先,被探测的函数入口指令会被替换成int 3指令。当被跟踪的函数执行时,将引发int 3异常中断,然后int 3异常处理程序将被触发,进而调用相应的kprobe处理函数。kprobe的处理函数有两种:一种是执行用户所注册的kprobe函数;另一种是执行通过kretprobe机制注册的kprobe函数。值得注意的是,通过kretprobe注册的kprobe处理函数会更改函数的返回地址,将该地址替换为kprobe处理函数的地址。因此,当程序执行ret指令并返回时,将直接跳转至kretprobe的处理函数处,并执行用户所注册的kretprobe函数。

图1-1 kprobe/kretprobe工作原理

2)uprobe/uretprobe:与kprobe/kretprobe的运作机制基本相似,但uprobe/uretprobe跟踪的是用户态函数。uretprobe作为uprobe的补充功能,当部署uretprobe函数时,会同时部署uprobe处理函数,以便将函数的返回地址更改为uretprobe处理函数的地址。这样,当函数执行完毕并准备返回时,将直接跳转至uretprobe处理函数并继续执行相应的操作。

3)fentry/fexit:相较于kprobe/kretprobe通过int 3指令来触发执行kprobe处理程序,fentry/fexit的实现方式略有不同。在内核编译时,它会通过GCC(GNU Compiler Collection,一个开源的编译器系统)的编译选项-mfentry在每个函数的入口处加入NOP指令[1]。当用户插入具体的fentry/fexit处理函数时,这些NOP指令会被替换成调用相应的处理函数指令。尽管ftrace早期就使用了fentry/fexit功能,但直到Linux内核5.5版本,用户才能直接使用该功能。值得一提的是,fexit的实现原理与kretprobe和uretprobe相同,也是在fentry处理函数内将函数的返回地址更改成fexit处理函数的地址。

4)tracepoint:tracepoint是在内核中定义的一系列预定义的事件跟踪点。这些跟踪点位于内核代码中的关键位置,允许开发人员插入自定义的跟踪代码,以捕获特定事件的信息。我们可以在/sys/kernel/debug/tracing/events/目录看到内核支持的跟踪点。它与fentry/fexit具有一样高的性能。

尽管kprobe/kretprobe、uprobe/uretprobe、fentry/fexit和tracepoint都具有跟踪诊断功能,但各自具有独特的意义。kprobe和uprobe虽然性能相对较差,但具有高度的灵活性,可以在内核中的任意位置进行跟踪。相比之下,fentry/fexit和tracepoint性能最佳,但灵活性稍差,只能跟踪特定的内核位置。我们还从灵活性、性能和支持eBPF的内核版本等关键指标进行了总结和比较,如表1-1所示。

表1-1 关键指标对比