libbpf-bootstrap开发指南:静态跟踪点 - UTSD

目录

代码分析

BPF程序分析

功能说明

usdt_auto_attach & usdt_manual_attach

SEC("usdt/libc.so.6:libc:setjmp")

用户态程序分析

功能说明

skel->bss

skel->links

skel->progs

bpf_program__attach_usdt

执行效果

UTSD与uprobe 的性能比较


UTSD (Userland Statically Defined Tracing) 是一种在用户空间应用程序中插入静态跟踪点的技术。这种技术可以让你在程序中插入一些预定义的跟踪点,然后在运行时通过跟踪工具来动态地启用或禁用这些跟踪点。

UTSD 使用了 Linux 内核的 uprobes 功能来实现用户空间的静态跟踪点。uprobes 可以让你在用户空间程序的任意位置设置断点,当程序执行到这个位置时,就会触发 uprobes 并运行你注册的处理函数。这样,你就可以在这个处理函数中插入你的跟踪代码,用于收集你想要的信息。

UTSD 的一个主要应用场景是性能分析和故障诊断。通过 UTSD,你可以在应用程序中插入一些跟踪点,用于记录程序的关键事件,例如函数的调用和返回,或者某个关键变量的修改等。然后,你可以在程序运行时动态地启用或禁用这些跟踪点,并收集这些事件的信息,以帮助你理解程序的行为,找出性能瓶颈或者故障原因。

需要注意的是,UTSD 是一种较为底层的跟踪技术,使用它需要一定的系统编程知识,以及对 Linux 内核的理解。而且,并非所有的 Linux 发行版都支持 uprobes,因此在某些系统上,你可能无法使用 UTSD。

代码分析

BPF程序分析

// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
/* Copyright (c) 2022 Hengqi Chen */
#include 
#include 
#include 
#include 

pid_t my_pid;

SEC("usdt/libc.so.6:libc:setjmp")
int BPF_USDT(usdt_auto_attach, void *arg1, int arg2, void *arg3)
{
	pid_t pid = bpf_get_current_pid_tgid() >> 32;

	if (pid != my_pid)
		return 0;

	bpf_printk("USDT auto attach to libc:setjmp: arg1 = %lx, arg2 = %d, arg3 = %lx", arg1, arg2,
		   arg3);
	return 0;
}

SEC("usdt")
int BPF_USDT(usdt_manual_attach, void *arg1, int arg2, void *arg3)
{
	bpf_printk("USDT manual attach to libc:setjmp: arg1 = %lx, arg2 = %d, arg3 = %lx", arg1,
		   arg2, arg3);
	return 0;
}

char LICENSE[] SEC("license") = "Dual BSD/GPL";
功能说明

程序中定义了两个探针处理函数 usdt_auto_attach 和 usdt_manual_attach,它们都被设计为处理来自 libc:setjmp 函数的探针触发。它们的区别在于,usdt_auto_attach 函数只处理当前进程的探针触发,而 usdt_manual_attach 函数处理所有进程的探针触发。

usdt_auto_attach & usdt_manual_attach
  1. usdt_auto_attach: 当 libc:setjmp 函数被当前进程调用时,这个函数会被触发。它首先获取当前的 PID,并检查是否与全局变量 my_pid 相同。如果不相同,函数就直接返回,不做任何处理。如果相同,它就会打印一条包含 libc:setjmp 的三个参数值的消息。
  2. usdt_manual_attach: 当 libc:setjmp 函数被任何进程调用时,这个函数都会被触发。它会打印一条包含 libc:setjmp 的三个参数值的消息,无论当前进程的 PID 是什么。

SEC("usdt/libc.so.6:libc:setjmp")

libc:setjmp 函数的探针,一种UTSD,我们也可以在自己的代码中设置UTSD,性能会比直接uprobe 一个用户函数要好一些

下面是一个使用 SystemTap 的 DTRACE_PROBE 宏在 C 代码中定义 USDT 探针的例子:

#include 

void my_function() {
    int arg1 = 10;
    int arg2 = 20;

    DTRACE_PROBE2(my_provider, my_probe, arg1, arg2);

    // ...其他代码...
}

USDT (User Statically Defined Tracing) 探针最初是由 DTrace 工具引入的,所以 DTRACE_PROBE 宏通常用于定义这些探针。然而,因为 USDT 探针的概念已经被多个跟踪工具所接受,其他工具也提供了自己的方式来定义这些探针。

例如,Linux 中的 SystemTap 也提供了类似的 STAP_PROBE 宏来定义探针:

#include 

void my_function() {
    int arg1 = 10;
    int arg2 = 20;

    STAP_PROBE2(my_provider, my_probe, arg1, arg2);

    // ...其他代码...
}

这段代码中的 STAP_PROBE2 宏的工作方式和 DTRACE_PROBE2 宏类似。同样,你也可以使用 BPF 或 SystemTap 工具来挂钩这个探针。

用户态程序分析

// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
/* Copyright (c) 2022 Hengqi Chen */
#include 
#include 
#include 
#include 
#include "usdt.skel.h"

static volatile sig_atomic_t exiting;
static jmp_buf env;

static void sig_int(int signo)
{
	exiting = 1;
}

static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
	return vfprintf(stderr, format, args);
}

static void usdt_trigger()
{
	setjmp(env);
}

int main(int argc, char **argv)
{
	struct usdt_bpf *skel;
	int err;

	libbpf_set_print(libbpf_print_fn);

	skel = usdt_bpf__open();
	if (!skel) {
		fprintf(stderr, "Failed to open BPF skeleton\n");
		return 1;
	}

	skel->bss->my_pid = getpid();

	err = usdt_bpf__load(skel);
	if (!skel) {
		fprintf(stderr, "Failed to load BPF skeleton\n");
		return 1;
	}

	/*
	 * Manually attach to libc.so we find.
	 * We specify pid here, so we don't have to do pid filtering in BPF program.
	 */
	skel->links.usdt_manual_attach = bpf_program__attach_usdt(
		skel->progs.usdt_manual_attach, getpid(), "libc.so.6", "libc", "setjmp", NULL);
	if (!skel->links.usdt_manual_attach) {
		err = errno;
		fprintf(stderr, "Failed to attach BPF program `usdt_manual_attach`\n");
		goto cleanup;
	}

	/*
	 * Auto attach by libbpf, libbpf should be able to find libc.so in your system.
	 * By default, auto attach does NOT specify pid, so we do pid filtering in BPF program
	 */
	err = usdt_bpf__attach(skel);
	if (err) {
		fprintf(stderr, "Failed to attach BPF skeleton\n");
		goto cleanup;
	}

	if (signal(SIGINT, sig_int) == SIG_ERR) {
		err = errno;
		fprintf(stderr, "can't set signal handler: %s\n", strerror(errno));
		goto cleanup;
	}

	printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` "
	       "to see output of the BPF programs.\n");

	while (!exiting) {
		/* trigger our BPF programs */
		usdt_trigger();
		fprintf(stderr, ".");
		sleep(1);
	}

cleanup:
	usdt_bpf__destroy(skel);
	return -err;
}
功能说明

它在每秒触发一个 setjmp 调用,并使用 BPF 来跟踪这些调用。

skel->bss

这一行代码是将当前进程的 PID(由 getpid() 函数获取)赋值给 BPF 骨架(skel)的 BSS 段中名为 my_pid 的变量。具体来说,skel->bss->my_pid 是一个指向 BPF 骨架的 BSS 段中 my_pid 变量的指针。

BPF 骨架中的各个段对应于生成的 BPF 程序中的各个段。在 BPF 中,通常有以下几种段:

  • text:这个段包含 BPF 程序的字节码。
  • maps:这个段定义了 BPF 程序使用的所有 map。map 是 BPF 程序用来存储和共享数据的主要手段。
  • data:这个段包含 BPF 程序的只读数据。
  • rodata:这个段包含 BPF 程序的只读数据。与 data 段不同,rodata 段的内容在 BPF 程序加载后不能被修改。
  • bss:这个段包含 BPF 程序的读写数据。与 data 和 rodata 段不同,bss 段的内容在 BPF 程序加载前不需要初始化。在 BPF 程序中,bss 段通常用于存储全局变量。
skel->links

在 BPF 骨架(skeleton)中,skel->links 是一个存储 struct bpf_link 指针的结构。每一个 bpf_link 对象代表一个已经加载并附加(attach)到某个事件(例如一个函数调用,或者一个网络事件)的 BPF 程序。

在你给出的代码中,skel->links.usdt_manual_attach 是一个 struct bpf_link 指针,它指向一个 BPF 程序,该程序已经附加到了 USDT(User Statically Defined Tracing)探针。

如果你在代码中看到 skel->links.some_name,你可以理解为 some_name 是一个已经加载并附加的 BPF 程序。

bpf_link 对象提供了一个对已加载并附加的 BPF 程序的引用,这样你就可以在稍后的代码中使用它,例如在你需要卸载(detach)或者销毁(destroy)这个 BPF 程序的时候。

skel->progs

在 BPF 骨架(skeleton)中,skel->progs 是一个结构体,其中包含了所有的 BPF 程序。每个 BPF 程序都是一个 struct bpf_program 类型的指针,这个结构体包含了 BPF 程序的字节码,以及其他与该程序相关的信息。

在你给出的代码中,skel->progs.usdt_manual_attach 是一个 struct bpf_program 指针,它指向一个名为 usdt_manual_attach 的 BPF 程序。

如果你在代码中看到 skel->progs.some_name,你可以理解为 some_name 是一个 BPF 程序。

bpf_program__attach_usdt

bpf_program__attach_usdt 是 libbpf 库提供的一个函数,用于将 BPF 程序附加到一个 USDT(User Statically Defined Tracing)探针。

struct bpf_link *bpf_program__attach_usdt(struct bpf_program *prog,
                                           pid_t pid,
                                           const char *binary_path,
                                           const char *provider,
                                           const char *probe_name,
                                           const char *fn_name);

这个函数的参数是:

  • prog:要附加的 BPF 程序。
  • pid:要附加探针的进程的 PID。
  • binary_path:包含探针的二进制文件的路径。通常是一个共享库的路径,例如 "libc.so.6"。
  • provider:探针的提供者的名称。
  • probe_name:探针的名称。
  • fn_name:要附加的探针的第二个名称(如果存在)。如果没有第二个名称,这个参数应该为 NULL。

这个函数的返回值是一个 struct bpf_link 指针,代表了 BPF 程序和 USDT 探针之间的链接。如果链接成功,这个指针应该非 NULL。如果链接失败,这个指针将为 NULL,并且可以通过 errno 获取错误信息。

让我们更详细地解析这段代码:

skel->links.usdt_manual_attach = bpf_program__attach_usdt(
		skel->progs.usdt_manual_attach, getpid(), "libc.so.6", "libc", "setjmp", NULL);
	if (!skel->links.usdt_manual_attach) {
		err = errno;
		fprintf(stderr, "Failed to attach BPF program `usdt_manual_attach`\n");
		goto cleanup;
	}
  • skel->progs.usdt_manual_attach:skel->progs 是 BPF 骨架中存储所有 BPF 程序的结构。usdt_manual_attach 是一个 BPF 程序,它的目标是附加到一个 USDT 探针。
  • getpid():这个函数返回当前进程的 PID,也就是我们要将 BPF 程序附加到的进程。
  • "libc.so.6":这是包含目标 USDT 探针的二进制文件的路径。在这个例子中,我们的目标是 libc.so.6,这是标准 C 库的动态链接版本。
  • "libc":这是提供我们要附加的 USDT 探针的提供者的名称。在这个情况下,libc 是提供 setjmp 探针的提供者。
  • "setjmp":这是我们要附加的 USDT 探针的名称。在这个例子中,我们想要附加到 setjmp 函数。
  • NULL:这是一个可选参数,代表附加探针的第二个名称(如果存在)。在这个例子中,我们不需要第二个名称,所以传递 NULL。

bpf_program__attach_usdt 函数将返回一个 struct bpf_link 指针,代表了 BPF 程序和 USDT 探针之间的链接。如果链接成功,这个指针被赋值给 skel->links.usdt_manual_attach,以便后续的使用和管理。如果链接失败,函数将返回 NULL。

执行效果

usdt-56794   [004] d..21 74634.623846: bpf_trace_printk: USDT auto attach to libc:setjmp: arg1 = 55d5e764b240, arg2 = 0, arg3 = 55d5e760836d
usdt-56794   [004] d..21 74634.623848: bpf_trace_printk: USDT manual attach to libc:setjmp: arg1 = 55d5e764b240, arg2 = 0, arg3 = 55d5e760836d
usdt-56794   [004] d..21 74635.623982: bpf_trace_printk: USDT auto attach to libc:setjmp: arg1 = 55d5e764b240, arg2 = 0, arg3 = 55d5e760836d
usdt-56794   [004] d..21 74635.623983: bpf_trace_printk: USDT manual attach to libc:setjmp: arg1 = 55d5e764b240, arg2 = 0, arg3 = 55d5e760836d
usdt-56794   [004] d..21 74636.624120: bpf_trace_printk: USDT auto attach to libc:setjmp: arg1 = 55d5e764b240, arg2 = 0, arg3 = 55d5e760836d
usdt-56794   [004] d..21 74636.624121: bpf_trace_printk: USDT manual attach to libc:setjmp: arg1 = 55d5e764b240, arg2 = 0, arg3 = 55d5e760836d
usdt-56794   [004] d..21 74637.624394: bpf_trace_printk: USDT auto attach to libc:setjmp: arg1 = 55d5e764b240, arg2 = 0, arg3 = 55d5e760836d
usdt-56794   [004] d..21 74637.624396: bpf_trace_printk: USDT manual attach to libc:setjmp: arg1 = 55d5e764b240, arg2 = 0, arg3 = 55d5e760836d
usdt-56794   [004] d..21 74638.624702: bpf_trace_printk: USDT auto attach to libc:setjmp: arg1 = 55d5e764b240, arg2 = 0, arg3 = 55d5e760836d
usdt-56794   [004] d..21 74638.624704: bpf_trace_printk: USDT manual attach to libc:setjmp: arg1 = 55d5e764b240, arg2 = 0, arg3 = 55d5e760836d
usdt-56794   [004] d..21 74639.624842: bpf_trace_printk: USDT auto attach to libc:setjmp: arg1 = 55d5e764b240, arg2 = 0, arg3 = 55d5e760836d
usdt-56794   [004] d..21 74639.624846: bpf_trace_printk: USDT manual attach to libc:setjmp: arg1 = 55d5e764b240, arg2 = 0, arg3 = 55d5e760836d
usdt-56794   [004] d..21 74640.624982: bpf_trace_printk: USDT auto attach to libc:setjmp: arg1 = 55d5e764b240, arg2 = 0, arg3 = 55d5e760836d
usdt-56794   [004] d..21 74640.624985: bpf_trace_printk: USDT manual attach to libc:setjmp: arg1 = 55d5e764b240, arg2 = 0, arg3 = 55d5e760836d
usdt-56794   [004] d..21 74641.625110: bpf_trace_printk: USDT auto attach to libc:setjmp: arg1 = 55d5e764b240, arg2 = 0, arg3 = 55d5e760836d
usdt-56794   [004] d..21 74641.625113: bpf_trace_printk: USDT manual attach to libc:setjmp: arg1 = 55d5e764b240, arg2 = 0, arg3 = 55d5e760836d
usdt-56794   [004] d..21 74642.625246: bpf_trace_printk: USDT auto attach to libc:setjmp: arg1 = 55d5e764b240, arg2 = 0, arg3 = 55d5e760836d
usdt-56794   [004] d..21 74642.625248: bpf_trace_printk: USDT manual attach to libc:setjmp: arg1 = 55d5e764b240, arg2 = 0, arg3 = 55d5e760836d
usdt-56794   [004] d..21 74643.625385: bpf_trace_printk: USDT auto attach to libc:setjmp: arg1 = 55d5e764b240, arg2 = 0, arg3 = 55d5e760836d
usdt-56794   [004] d..21 74643.625387: bpf_trace_printk: USDT manual attach to libc:setjmp: arg1 = 55d5e764b240, arg2 = 0, arg3 = 55d5e760836d
usdt-56794   [004] d..21 74644.625513: bpf_trace_printk: USDT auto attach to libc:setjmp: arg1 = 55d5e764b240, arg2 = 0, arg3 = 55d5e760836d
usdt-56794   [004] d..21 74644.625516: bpf_trace_printk: USDT manual attach to libc:setjmp: arg1 = 55d5e764b240, arg2 = 0, arg3 = 55d5e760836d

UTSD与uprobe 的性能比较

USDT (User Statically Defined Tracing) 和 uprobes 是两种用于在用户空间进行跟踪的机制,它们各自有其优点和适用场景。

  1. USDT:这种机制允许开发者在应用程序中定义静态的跟踪点。这些跟踪点在编译时就已经确定,且通常会在关键的位置和事件处设置(例如函数的入口和出口,或者重要状态的改变)。因为这些跟踪点是预先定义的,所以它们可以提供详细的上下文信息,这对于理解程序的行为非常有帮助。此外,USDT 的开销相对较小,因为它们是在编译时就已经确定的,所以在运行时不需要额外的解析和查找过程。
  2. uprobes:这种机制允许在运行时动态地在用户空间程序中插入跟踪点。这在你需要跟踪没有预先定义跟踪点的程序时非常有用。不过,uprobes 的开销通常比 USDT 要大,因为它们需要在运行时解析和查找程序的符号表,以确定跟踪点的位置。

因此,从性能的角度来看,USDT 通常比 uprobes 有优势,因为 USDT 的开销更小。然而,这并不意味着 USDT 总是比 uprobes 更好。它们各自适应不同的场景:如果你需要跟踪的程序已经定义了 USDT 跟踪点,那么使用 USDT 是最好的选择;如果你需要跟踪的程序没有定义 USDT 跟踪点,或者你需要在运行时动态地插入跟踪点,那么使用 uprobes 是更好的选择。

你可能感兴趣的:(BPF,性能优化)