From 6e8427badf2105bb9602bc541b36e78291be602b Mon Sep 17 00:00:00 2001 From: yunwei37 <1067852565@qq.com> Date: Sun, 7 May 2023 01:59:08 +0800 Subject: [PATCH] update perf --- src/10-hardirqs/README.md | 2 +- src/11-bootstrap/Makefile | 4 +- src/11-bootstrap/README.md | 2 +- src/12-profile/README.md | 384 +++++++++++++++++++++++++++++-------- 4 files changed, 306 insertions(+), 86 deletions(-) diff --git a/src/10-hardirqs/README.md b/src/10-hardirqs/README.md index 8aad03e..cfdbcd5 100644 --- a/src/10-hardirqs/README.md +++ b/src/10-hardirqs/README.md @@ -149,7 +149,7 @@ int BPF_PROG(irq_handler_exit, int irq, struct irqaction *action) char LICENSE[] SEC("license") = "GPL"; ``` -这是一个 BPF(Berkeley Packet Filter)程序。BPF 程序是小型程序,可以直接在 Linux 内核中运行,用于过滤和操纵网络流量。这个特定的程序似乎旨在收集内核中中断处理程序的统计信息。它定义了一些地图(可以在 BPF 程序和内核的其他部分之间共享的数据结构)和两个函数:handle_entry 和 handle_exit。当内核进入和退出中断处理程序时,分别执行这些函数。handle_entry 函数用于跟踪中断处理程序被执行的次数,而 handle_exit 则用于测量中断处理程序中花费的时间。 +这是一个 BPF(Berkeley Packet Filter)程序。BPF 程序是小型程序,可以直接在 Linux 内核中运行,用于过滤和操纵网络流量。这个特定的程序似乎旨在收集内核中中断处理程序的统计信息。它定义了一些 maps (可以在 BPF 程序和内核的其他部分之间共享的数据结构)和两个函数:handle_entry 和 handle_exit。当内核进入和退出中断处理程序时,分别执行这些函数。handle_entry 函数用于跟踪中断处理程序被执行的次数,而 handle_exit 则用于测量中断处理程序中花费的时间。 ## 运行代码 diff --git a/src/11-bootstrap/Makefile b/src/11-bootstrap/Makefile index fa2df33..4fb616c 100644 --- a/src/11-bootstrap/Makefile +++ b/src/11-bootstrap/Makefile @@ -24,13 +24,13 @@ INCLUDES := -I$(OUTPUT) -I../../libbpf/include/uapi -I$(dir $(VMLINUX)) CFLAGS := -g -Wall ALL_LDFLAGS := $(LDFLAGS) $(EXTRA_LDFLAGS) -APPS = # minimal minimal_legacy bootstrap uprobe kprobe fentry usdt sockfilter tc ksyscall +APPS = bootstrap # minimal minimal_legacy uprobe kprobe fentry usdt sockfilter tc ksyscall CARGO ?= $(shell which cargo) ifeq ($(strip $(CARGO)),) BZS_APPS := else -BZS_APPS := profile +BZS_APPS := # profile APPS += $(BZS_APPS) # Required by libblazesym ALL_LDFLAGS += -lrt -ldl -lpthread -lm diff --git a/src/11-bootstrap/README.md b/src/11-bootstrap/README.md index cbad360..b72789a 100644 --- a/src/11-bootstrap/README.md +++ b/src/11-bootstrap/README.md @@ -180,6 +180,6 @@ Runing eBPF program... ## 总结 -这是一个使用BPF的C程序,用于跟踪进程的启动和退出事件,并显示有关这些事件的信息。它通过使用argp API来解析命令行参数,并使用BPF地图存储进程的信息,包括进程的PID和执行文件的文件名。程序还使用了SEC函数来附加BPF程序,以监视进程的执行和退出事件。最后,程序在终端中打印出启动和退出的进程信息。 +这是一个使用BPF的C程序,用于跟踪进程的启动和退出事件,并显示有关这些事件的信息。它通过使用argp API来解析命令行参数,并使用BPF maps 存储进程的信息,包括进程的PID和执行文件的文件名。程序还使用了SEC函数来附加BPF程序,以监视进程的执行和退出事件。最后,程序在终端中打印出启动和退出的进程信息。 编译这个程序可以使用 ecc 工具,运行时可以使用 ecli 命令。更多的例子和详细的开发指南,请参考 eunomia-bpf 的官方文档:https://github.com/eunomia-bpf/eunomia-bpf diff --git a/src/12-profile/README.md b/src/12-profile/README.md index aef12de..276d975 100644 --- a/src/12-profile/README.md +++ b/src/12-profile/README.md @@ -1,93 +1,26 @@ -# eBPF 入门实践教程:使用 eBPF 程序 profile 进行性能分析 +# eBPF 入门实践教程十二:使用 eBPF 程序 profile 进行性能分析 -## 背景 +本教程将指导您使用 libbpf 和 eBPF 程序进行性能分析。我们将利用内核中的 perf 机制,学习如何捕获函数的执行时间以及如何查看性能数据。 -`profile` 是一款用户追踪程序执行调用流程的工具,类似于perf中的 -g 指令。但是相较于perf而言, -`profile`的功能更为细化,它可以选择用户需要追踪的层面,比如在用户态层面进行追踪,或是在内核态进行追踪。 +libbpf 是一个用于与 eBPF 交互的 C 库。它提供了创建、加载和使用 eBPF 程序所需的基本功能。本教程中,我们将主要使用 libbpf 完成开发工作。perf 是 Linux 内核中的性能分析工具,允许用户测量和分析内核及用户空间程序的性能,以及获取对应的调用堆栈。它利用内核中的硬件计数器和软件事件来收集性能数据。 -## 实现原理 +## eBPF 工具:profile 性能分析示例 -`profile` 的实现依赖于linux中的perf_event。在注入ebpf程序前,`profile` 工具会先将 perf_event -注册好。 +`profile` 工具基于 eBPF 实现,利用 Linux 内核中的 perf 事件进行性能分析。`profile` 工具会定期对每个处理器进行采样,以便捕获内核函数和用户空间函数的执行。它可以显示栈回溯的以下信息: -```c -static int open_and_attach_perf_event(int freq, struct bpf_program *prog, - struct bpf_link *links[]) -{ - struct perf_event_attr attr = { - .type = PERF_TYPE_SOFTWARE, - .freq = env.freq, - .sample_freq = env.sample_freq, - .config = PERF_COUNT_SW_CPU_CLOCK, - }; - int i, fd; +- 地址:函数调用的内存地址 +- 符号:函数名称 +- 文件名:源代码文件名称 +- 行号:源代码中的行号 - for (i = 0; i < nr_cpus; i++) { - if (env.cpu != -1 && env.cpu != i) - continue; +这些信息有助于开发人员定位性能瓶颈和优化代码。更进一步,可以通过这些对应的信息生成火焰图,以便更直观的查看性能数据。 - fd = syscall(__NR_perf_event_open, &attr, -1, i, -1, 0); - if (fd < 0) { - /* Ignore CPU that is offline */ - if (errno == ENODEV) - continue; - fprintf(stderr, "failed to init perf sampling: %s\n", - strerror(errno)); - return -1; - } - links[i] = bpf_program__attach_perf_event(prog, fd); - if (!links[i]) { - fprintf(stderr, "failed to attach perf event on cpu: " - "%d\n", i); - links[i] = NULL; - close(fd); - return -1; - } - } - - return 0; -} -``` - -其ebpf程序实现逻辑是对程序的堆栈进行定时采样,从而捕获程序的执行流程。 - -```c -SEC("perf_event") -int profile(void *ctx) -{ - int pid = bpf_get_current_pid_tgid() >> 32; - int cpu_id = bpf_get_smp_processor_id(); - struct stacktrace_event *event; - int cp; - - event = bpf_ringbuf_reserve(&events, sizeof(*event), 0); - if (!event) - return 1; - - event->pid = pid; - event->cpu_id = cpu_id; - - if (bpf_get_current_comm(event->comm, sizeof(event->comm))) - event->comm[0] = 0; - - event->kstack_sz = bpf_get_stack(ctx, event->kstack, sizeof(event->kstack), 0); - - event->ustack_sz = bpf_get_stack(ctx, event->ustack, sizeof(event->ustack), BPF_F_USER_STACK); - - bpf_ringbuf_submit(event, 0); - - return 0; -} -``` - -通过这种方式,它可以根据用户指令,简单的决定追踪用户态层面的执行流程或是内核态层面的执行流程。 - -## 编译运行 +在本示例中,可以通过 libbpf 库编译运行它(以 Ubuntu/Debian 为例): ```console -$ git clone https://github.com/libbpf/libbpf-bootstrap.git --recurse-submodules -$ cd examples/c -$ make profile +$ git submodule update --init --recursive +$ sudo apt install clang libelf1 libelf-dev zlib1g-dev +$ make $ sudo ./profile COMM: chronyd (pid=156) @ CPU 1 Kernel: @@ -110,6 +43,293 @@ Userspace: 1 [<0000556dec34cad0>] ``` +## 实现原理 + +profile 工具由两个部分组成,内核态中的 eBPF 程序和用户态中的 `profile` 符号处理程序。`profile` 符号处理程序负责加载 eBPF 程序,以及处理 eBPF 程序输出的数据。 + +### 内核态部分: + +内核态 eBPF 程序的实现逻辑主要是借助 perf event,对程序的堆栈进行定时采样,从而捕获程序的执行流程。 + +```c +// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause +/* Copyright (c) 2022 Meta Platforms, Inc. */ +#include "vmlinux.h" +#include +#include +#include + +#include "profile.h" + +char LICENSE[] SEC("license") = "Dual BSD/GPL"; + +struct { + __uint(type, BPF_MAP_TYPE_RINGBUF); + __uint(max_entries, 256 * 1024); +} events SEC(".maps"); + +SEC("perf_event") +int profile(void *ctx) +{ + int pid = bpf_get_current_pid_tgid() >> 32; + int cpu_id = bpf_get_smp_processor_id(); + struct stacktrace_event *event; + int cp; + + event = bpf_ringbuf_reserve(&events, sizeof(*event), 0); + if (!event) + return 1; + + event->pid = pid; + event->cpu_id = cpu_id; + + if (bpf_get_current_comm(event->comm, sizeof(event->comm))) + event->comm[0] = 0; + + event->kstack_sz = bpf_get_stack(ctx, event->kstack, sizeof(event->kstack), 0); + + event->ustack_sz = bpf_get_stack(ctx, event->ustack, sizeof(event->ustack), BPF_F_USER_STACK); + + bpf_ringbuf_submit(event, 0); + + return 0; +} +``` + +接下来,我们将重点讲解内核态代码的关键部分。 + +1. 定义 eBPF maps `events`: + +```c + +struct { + __uint(type, BPF_MAP_TYPE_RINGBUF); + __uint(max_entries, 256 * 1024); +} events SEC(".maps"); +``` + +这里定义了一个类型为 `BPF_MAP_TYPE_RINGBUF` 的 eBPF maps 。Ring Buffer 是一种高性能的循环缓冲区,用于在内核和用户空间之间传输数据。`max_entries` 设置了 Ring Buffer 的最大大小。 + +2. 定义 `perf_event` eBPF 程序: + +```c +SEC("perf_event") +int profile(void *ctx) +``` + + + +这里定义了一个名为 `profile` 的 eBPF 程序,它将在 perf 事件触发时执行。 + +3. 获取进程 ID 和 CPU ID: + +```c +int pid = bpf_get_current_pid_tgid() >> 32; +int cpu_id = bpf_get_smp_processor_id(); +``` + +`bpf_get_current_pid_tgid()` 函数返回当前进程的 PID 和 TID,通过右移 32 位,我们得到 PID。`bpf_get_smp_processor_id()` 函数返回当前 CPU 的 ID。 + +4. 预留 Ring Buffer 空间: + +```c +event = bpf_ringbuf_reserve(&events, sizeof(*event), 0); +if (!event) + return 1; +``` + +通过 `bpf_ringbuf_reserve()` 函数预留 Ring Buffer 空间,用于存储采集的栈信息。若预留失败,返回错误. + +5. 获取当前进程名: + +```c + +if (bpf_get_current_comm(event->comm, sizeof(event->comm))) + event->comm[0] = 0; +``` + +使用 `bpf_get_current_comm()` 函数获取当前进程名并将其存储到 `event->comm`。 + +6. 获取内核栈信息: + +```c + +event->kstack_sz = bpf_get_stack(ctx, event->kstack, sizeof(event->kstack), 0); +``` + +使用 `bpf_get_stack()` 函数获取内核栈信息。将结果存储在 `event->kstack`,并将其大小存储在 `event->kstack_sz`。 + +7. 获取用户空间栈信息: + +```c +event->ustack_sz = bpf_get_stack(ctx, event->ustack, sizeof(event->ustack), BPF_F_USER_STACK); +``` + +同样使用 `bpf_get_stack()` 函数,但传递 `BPF_F_USER_STACK` 标志以获取用户空间栈信息。将结果存储在 `event->ustack`,并将其大小存储在 `event->ustack_sz`。 + +8. 将事件提交到 Ring Buffer: + +```c +bpf_ringbuf_submit(event, 0); +``` + +最后,使用 `bpf_ringbuf_submit()` 函数将事件提交到 Ring Buffer,以便用户空间程序可以读取和处理。 + +这个内核态 eBPF 程序通过定期采样程序的内核栈和用户空间栈来捕获程序的执行流程。这些数据将存储在 Ring Buffer 中,以便用户态的 `profile` 程序能读取 + +### 用户态部分 + +这段代码主要负责为每个在线 CPU 设置 perf event 并附加 eBPF 程序: + +```c +static long perf_event_open(struct perf_event_attr *hw_event, pid_t pid, + int cpu, int group_fd, unsigned long flags) +{ + int ret; + + ret = syscall(__NR_perf_event_open, hw_event, pid, cpu, group_fd, flags); + return ret; +} + +int main(){ + ... + for (cpu = 0; cpu < num_cpus; cpu++) { + /* skip offline/not present CPUs */ + if (cpu >= num_online_cpus || !online_mask[cpu]) + continue; + + /* Set up performance monitoring on a CPU/Core */ + pefd = perf_event_open(&attr, pid, cpu, -1, PERF_FLAG_FD_CLOEXEC); + if (pefd < 0) { + fprintf(stderr, "Fail to set up performance monitor on a CPU/Core\n"); + err = -1; + goto cleanup; + } + pefds[cpu] = pefd; + + /* Attach a BPF program on a CPU */ + links[cpu] = bpf_program__attach_perf_event(skel->progs.profile, pefd); + if (!links[cpu]) { + err = -1; + goto cleanup; + } + } + ... +} +``` + +`perf_event_open` 这个函数是一个对 perf_event_open 系统调用的封装。它接收一个 perf_event_attr 结构体指针,用于指定 perf event 的类型和属性。pid 参数用于指定要监控的进程 ID(-1 表示监控所有进程),cpu 参数用于指定要监控的 CPU。group_fd 参数用于将 perf event 分组,这里我们使用 -1,表示不需要分组。flags 参数用于设置一些标志,这里我们使用 PERF_FLAG_FD_CLOEXEC 以确保在执行 exec 系列系统调用时关闭文件描述符。 + +在 main 函数中: + +```c +for (cpu = 0; cpu < num_cpus; cpu++) { + // ... +} +``` + +这个循环针对每个在线 CPU 设置 perf event 并附加 eBPF 程序。首先,它会检查当前 CPU 是否在线,如果不在线则跳过。然后,使用 perf_event_open() 函数为当前 CPU 设置 perf event,并将返回的文件描述符存储在 pefds 数组中。最后,使用 bpf_program__attach_perf_event() 函数将 eBPF 程序附加到 perf event。links 数组用于存储每个 CPU 上的 BPF 链接,以便在程序结束时销毁它们。 + +通过这种方式,用户态程序为每个在线 CPU 设置 perf event,并将 eBPF 程序附加到这些 perf event 上,从而实现对系统中所有在线 CPU 的监控。 + +以下这两个函数分别用于显示栈回溯和处理从 ring buffer 接收到的事件: + +```c +static void show_stack_trace(__u64 *stack, int stack_sz, pid_t pid) +{ + const struct blazesym_result *result; + const struct blazesym_csym *sym; + sym_src_cfg src; + int i, j; + + if (pid) { + src.src_type = SRC_T_PROCESS; + src.params.process.pid = pid; + } else { + src.src_type = SRC_T_KERNEL; + src.params.kernel.kallsyms = NULL; + src.params.kernel.kernel_image = NULL; + } + + result = blazesym_symbolize(symbolizer, &src, 1, (const uint64_t *)stack, stack_sz); + + for (i = 0; i < stack_sz; i++) { + if (!result || result->size <= i || !result->entries[i].size) { + printf(" %d [<%016llx>]\n", i, stack[i]); + continue; + } + + if (result->entries[i].size == 1) { + sym = &result->entries[i].syms[0]; + if (sym->path && sym->path[0]) { + printf(" %d [<%016llx>] %s+0x%llx %s:%ld\n", + i, stack[i], sym->symbol, + stack[i] - sym->start_address, + sym->path, sym->line_no); + } else { + printf(" %d [<%016llx>] %s+0x%llx\n", + i, stack[i], sym->symbol, + stack[i] - sym->start_address); + } + continue; + } + + printf(" %d [<%016llx>]\n", i, stack[i]); + for (j = 0; j < result->entries[i].size; j++) { + sym = &result->entries[i].syms[j]; + if (sym->path && sym->path[0]) { + printf(" %s+0x%llx %s:%ld\n", + sym->symbol, stack[i] - sym->start_address, + sym->path, sym->line_no); + } else { + printf(" %s+0x%llx\n", sym->symbol, + stack[i] - sym->start_address); + } + } + } + + blazesym_result_free(result); +} + +/* Receive events from the ring buffer. */ +static int event_handler(void *_ctx, void *data, size_t size) +{ + struct stacktrace_event *event = data; + + if (event->kstack_sz <= 0 && event->ustack_sz <= 0) + return 1; + + printf("COMM: %s (pid=%d) @ CPU %d\n", event->comm, event->pid, event->cpu_id); + + if (event->kstack_sz > 0) { + printf("Kernel:\n"); + show_stack_trace(event->kstack, event->kstack_sz / sizeof(__u64), 0); + } else { + printf("No Kernel Stack\n"); + } + + if (event->ustack_sz > 0) { + printf("Userspace:\n"); + show_stack_trace(event->ustack, event->ustack_sz / sizeof(__u64), event->pid); + } else { + printf("No Userspace Stack\n"); + } + + printf("\n"); + return 0; +} +``` + +`show_stack_trace()` 函数用于显示内核或用户空间的栈回溯。它接收一个 stack 参数,是一个指向内核或用户空间栈的指针,stack_sz 参数表示栈的大小,pid 参数表示要显示的进程的 ID(当显示内核栈时,设置为 0)。函数中首先根据 pid 参数确定栈的来源(内核或用户空间),然后调用 blazesym_symbolize() 函数将栈中的地址解析为符号名和源代码位置。最后,遍历解析结果,输出符号名和源代码位置信息。 + +`event_handler()` 函数用于处理从 ring buffer 接收到的事件。它接收一个 data 参数,指向 ring buffer 中的数据,size 参数表示数据的大小。函数首先将 data 指针转换为 stacktrace_event 结构体指针,然后检查内核和用户空间栈的大小。如果栈为空,则直接返回。接下来,函数输出进程名称、进程 ID 和 CPU ID 信息。然后分别显示内核栈和用户空间栈的回溯。调用 show_stack_trace() 函数时,分别传入内核栈和用户空间栈的地址、大小和进程 ID。 + +这两个函数作为 eBPF profile 工具的一部分,用于显示和处理 eBPF 程序收集到的栈回溯信息,帮助用户了解程序的运行情况和性能瓶颈。 + ### 总结 -`profile` 实现了对程序执行流程的分析,在debug等操作中可以极大的帮助开发者提高效率。 +通过本篇 eBPF 入门实践教程,我们学习了如何使用 eBPF 程序进行性能分析。在这个过程中,我们详细讲解了如何创建 eBPF 程序,监控进程的性能,并从 ring buffer 中获取数据以分析栈回溯。我们还学习了如何使用 perf_event_open() 函数设置性能监控,并将 BPF 程序附加到性能事件上。在本教程中,我们还展示了如何编写 eBPF 程序来捕获进程的内核和用户空间栈信息,进而分析程序性能瓶颈。通过这个例子,您可以了解到 eBPF 在性能分析方面的强大功能。 + +如果您希望学习更多关于 eBPF 的知识和实践,请查阅 eunomia-bpf 的官方文档:https://github.com/eunomia-bpf/eunomia-bpf。您还可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 以获取更多示例和完整的教程。 + +接下来的教程将进一步探讨 eBPF 的高级特性,我们会继续分享更多有关 eBPF 开发实践的内容,帮助您更好地理解和掌握 eBPF 技术,希望这些内容对您在 eBPF 开发道路上的学习和实践有所帮助。