diff --git a/1-helloworld/index.html b/1-helloworld/index.html index 157b323..91eb375 100644 --- a/1-helloworld/index.html +++ b/1-helloworld/index.html @@ -236,15 +236,15 @@ typedef unsigned int u32; typedef int pid_t; const pid_t pid_filter = 0; -char LICENSE[] SEC("license") = "Dual BSD/GPL"; +char LICENSE[] SEC("license") = "Dual BSD/GPL"; -SEC("tp/syscalls/sys_enter_write") +SEC("tp/syscalls/sys_enter_write") int handle_tp(void *ctx) { pid_t pid = bpf_get_current_pid_tgid() >> 32; if (pid_filter && pid != pid_filter) return 0; - bpf_printk("BPF triggered sys_enter_write from PID %d.\n", pid); + bpf_printk("BPF triggered sys_enter_write from PID %d.\n", pid); return 0; } @@ -270,7 +270,7 @@ Packing ebpf object and config into package.json... Runing eBPF program...

运行这段程序后,可以通过查看 /sys/kernel/debug/tracing/trace_pipe 文件来查看 eBPF 程序的输出:

-
$ sudo cat /sys/kernel/debug/tracing/trace_pipe | grep "BPF triggered sys_enter_write"
+
$ sudo cat /sys/kernel/debug/tracing/trace_pipe | grep "BPF triggered sys_enter_write"
            <...>-3840345 [010] d... 3220701.101143: bpf_trace_printk: write system call from PID 3840345.
            <...>-3840345 [010] d... 3220701.101143: bpf_trace_printk: write system call from PID 3840345.
 
@@ -283,7 +283,7 @@ Runing eBPF program...

如上所述, eBPF 程序的基本框架包括:

这段代码是一个 eBPF 程序,用于捕获和分析内核中硬件中断处理程序(hardirqs)的执行信息。程序的主要目的是获取中断处理程序的名称、执行次数和执行时间,并以直方图的形式展示执行时间的分布。让我们一步步分析这段代码。

    @@ -336,9 +336,9 @@ char LICENSE[] SEC("license") = "GPL"; #include <bpf/bpf_core_read.h> #include <bpf/bpf_helpers.h> #include <bpf/bpf_tracing.h> -#include "hardirqs.h" -#include "bits.bpf.h" -#include "maps.bpf.h" +#include "hardirqs.h" +#include "bits.bpf.h" +#include "maps.bpf.h"

    该程序包含了 eBPF 开发所需的标准头文件,以及用于定义数据结构和映射的自定义头文件。

    @@ -363,25 +363,25 @@ const volatile bool do_count = false;
  1. 定义 eBPF 程序的入口点:

    
    -SEC("tp_btf/irq_handler_entry")
    +SEC("tp_btf/irq_handler_entry")
     int BPF_PROG(irq_handler_entry_btf, int irq, struct irqaction *action)
     {
     return handle_entry(irq, action);
     }
     
    -SEC("tp_btf/irq_handler_exit")
    +SEC("tp_btf/irq_handler_exit")
     int BPF_PROG(irq_handler_exit_btf, int irq, struct irqaction *action)
     {
     return handle_exit(irq, action);
     }
     
    -SEC("raw_tp/irq_handler_entry")
    +SEC("raw_tp/irq_handler_entry")
     int BPF_PROG(irq_handler_entry, int irq, struct irqaction *action)
     {
     return handle_entry(irq, action);
     }
     
    -SEC("raw_tp/irq_handler_exit")
    +SEC("raw_tp/irq_handler_exit")
     int BPF_PROG(irq_handler_exit, int irq, struct irqaction *action)
     {
     return handle_exit(irq, action);
    diff --git a/11-bootstrap/index.html b/11-bootstrap/index.html
    index 279d99f..bbfe787 100644
    --- a/11-bootstrap/index.html
    +++ b/11-bootstrap/index.html
    @@ -192,7 +192,7 @@
     
     

    结合 libbpf 和 BTF,eBPF 程序可以在各种不同版本的内核上运行,而无需为每个内核版本单独编译。这极大地提高了 eBPF 生态系统的可移植性和兼容性,降低了开发和维护的难度。

    什么是 bootstrap

    -

    Bootstrap 是一个使用 libbpf 的完整应用,它利用 eBPF 程序来跟踪内核中的 exec() 系统调用(通过 SEC("tp/sched/sched_process_exec") handle_exec BPF 程序),这主要对应于新进程的创建(不包括 fork() 部分)。此外,它还跟踪进程的 exit() 系统调用(通过 SEC("tp/sched/sched_process_exit") handle_exit BPF 程序),以了解每个进程何时退出。

    +

    Bootstrap 是一个使用 libbpf 的完整应用,它利用 eBPF 程序来跟踪内核中的 exec() 系统调用(通过 SEC("tp/sched/sched_process_exec") handle_exec BPF 程序),这主要对应于新进程的创建(不包括 fork() 部分)。此外,它还跟踪进程的 exit() 系统调用(通过 SEC("tp/sched/sched_process_exit") handle_exit BPF 程序),以了解每个进程何时退出。

    这两个 BPF 程序共同工作,允许捕获关于新进程的有趣信息,例如二进制文件的文件名,以及测量进程的生命周期,并在进程结束时收集有趣的统计信息,例如退出代码或消耗的资源量等。这是深入了解内核内部并观察事物如何真正运作的良好起点。

    Bootstrap 还使用 argp API(libc 的一部分)进行命令行参数解析,使得用户可以通过命令行选项配置应用行为。这种方式提供了灵活性,让用户能够根据实际需求自定义程序行为。虽然这些功能使用 eunomia-bpf 工具也可以实现,但是这里我们使用 libbpf 可以在用户态提供更高的可扩展性,不过也带来了不少额外的复杂度。

    Bootstrap

    @@ -200,29 +200,29 @@

    内核态 eBPF 程序 bootstrap.bpf.c

    // SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
     /* Copyright (c) 2020 Facebook */
    -#include "vmlinux.h"
    +#include "vmlinux.h"
     #include <bpf/bpf_helpers.h>
     #include <bpf/bpf_tracing.h>
     #include <bpf/bpf_core_read.h>
    -#include "bootstrap.h"
    +#include "bootstrap.h"
     
    -char LICENSE[] SEC("license") = "Dual BSD/GPL";
    +char LICENSE[] SEC("license") = "Dual BSD/GPL";
     
     struct {
         __uint(type, BPF_MAP_TYPE_HASH);
         __uint(max_entries, 8192);
         __type(key, pid_t);
         __type(value, u64);
    -} exec_start SEC(".maps");
    +} exec_start SEC(".maps");
     
     struct {
         __uint(type, BPF_MAP_TYPE_RINGBUF);
         __uint(max_entries, 256 * 1024);
    -} rb SEC(".maps");
    +} rb SEC(".maps");
     
     const volatile unsigned long long min_duration_ns = 0;
     
    -SEC("tp/sched/sched_process_exec")
    +SEC("tp/sched/sched_process_exec")
     int handle_exec(struct trace_event_raw_sched_process_exec *ctx)
     {
         struct task_struct *task;
    @@ -261,7 +261,7 @@ int handle_exec(struct trace_event_raw_sched_process_exec *ctx)
         return 0;
     }
     
    -SEC("tp/sched/sched_process_exit")
    +SEC("tp/sched/sched_process_exit")
     int handle_exit(struct trace_event_raw_sched_process_template* ctx)
     {
         struct task_struct *task;
    @@ -312,30 +312,30 @@ int handle_exit(struct trace_event_raw_sched_process_template* ctx)
     

    这段代码是一个内核态 eBPF 程序(bootstrap.bpf.c),主要用于跟踪 exec() 和 exit() 系统调用。它通过 eBPF 程序捕获进程的创建和退出事件,并将相关信息发送到用户态程序进行处理。下面是对代码的详细解释。

    首先,我们引入所需的头文件,定义 eBPF 程序的许可证以及两个 eBPF maps:exec_start 和 rb。exec_start 是一个哈希类型的 eBPF map,用于存储进程开始执行时的时间戳。rb 是一个环形缓冲区类型的 eBPF map,用于存储捕获的事件数据,并将其发送到用户态程序。

    -
    #include "vmlinux.h"
    +
    #include "vmlinux.h"
     #include <bpf/bpf_helpers.h>
     #include <bpf/bpf_tracing.h>
     #include <bpf/bpf_core_read.h>
    -#include "bootstrap.h"
    +#include "bootstrap.h"
     
    -char LICENSE[] SEC("license") = "Dual BSD/GPL";
    +char LICENSE[] SEC("license") = "Dual BSD/GPL";
     
     struct {
         __uint(type, BPF_MAP_TYPE_HASH);
         __uint(max_entries, 8192);
         __type(key, pid_t);
         __type(value, u64);
    -} exec_start SEC(".maps");
    +} exec_start SEC(".maps");
     
     struct {
         __uint(type, BPF_MAP_TYPE_RINGBUF);
         __uint(max_entries, 256 * 1024);
    -} rb SEC(".maps");
    +} rb SEC(".maps");
     
     const volatile unsigned long long min_duration_ns = 0;
     

    接下来,我们定义了一个名为 handle_exec 的 eBPF 程序,它会在进程执行 exec() 系统调用时触发。首先,我们从当前进程中获取 PID,记录进程开始执行的时间戳,然后将其存储在 exec_start map 中。

    -
    SEC("tp/sched/sched_process_exec")
    +
    SEC("tp/sched/sched_process_exec")
     int handle_exec(struct trace_event_raw_sched_process_exec *ctx)
     {
         // ...
    @@ -368,7 +368,7 @@ int handle_exec(struct trace_event_raw_sched_process_exec *ctx)
         return 0;
     

    最后,我们定义了一个名为 handle_exit 的 eBPF 程序,它会在进程执行 exit() 系统调用时触发。首先,我们从当前进程中获取 PID 和 TID(线程 ID)。如果 PID 和 TID 不相等,说明这是一个线程退出,我们将忽略此事件。

    -
    SEC("tp/sched/sched_process_exit")
    +
    SEC("tp/sched/sched_process_exit")
     int handle_exit(struct trace_event_raw_sched_process_template* ctx)
     {
         // ...
    @@ -448,27 +448,27 @@ struct event {
     #include <time.h>
     #include <sys/resource.h>
     #include <bpf/libbpf.h>
    -#include "bootstrap.h"
    -#include "bootstrap.skel.h"
    +#include "bootstrap.h"
    +#include "bootstrap.skel.h"
     
     static struct env {
         bool verbose;
         long min_duration_ms;
     } env;
     
    -const char *argp_program_version = "bootstrap 0.0";
    -const char *argp_program_bug_address = "<bpf@vger.kernel.org>";
    +const char *argp_program_version = "bootstrap 0.0";
    +const char *argp_program_bug_address = "<bpf@vger.kernel.org>";
     const char argp_program_doc[] =
    -"BPF bootstrap demo application.\n"
    -"\n"
    -"It traces process start and exits and shows associated \n"
    -"information (filename, process duration, PID and PPID, etc).\n"
    -"\n"
    -"USAGE: ./bootstrap [-d <min-duration-ms>] [-v]\n";
    +"BPF bootstrap demo application.\n"
    +"\n"
    +"It traces process start and exits and shows associated \n"
    +"information (filename, process duration, PID and PPID, etc).\n"
    +"\n"
    +"USAGE: ./bootstrap [-d <min-duration-ms>] [-v]\n";
     
     static const struct argp_option opts[] = {
    -    { "verbose", 'v', NULL, 0, "Verbose debug output" },
    -    { "duration", 'd', "DURATION-MS", 0, "Minimum process duration (ms) to report" },
    +    { "verbose", 'v', NULL, 0, "Verbose debug output" },
    +    { "duration", 'd', "DURATION-MS", 0, "Minimum process duration (ms) to report" },
         {},
     };
     
    @@ -482,7 +482,7 @@ static error_t parse_arg(int key, char *arg, struct argp_state *state)
             errno = 0;
             env.min_duration_ms = strtol(arg, NULL, 10);
             if (errno || env.min_duration_ms <= 0) {
    -            fprintf(stderr, "Invalid duration: %s\n", arg);
    +            fprintf(stderr, "Invalid duration: %s\n", arg);
                 argp_usage(state);
             }
             break;
    @@ -524,17 +524,17 @@ static int handle_event(void *ctx, void *data, size_t data_sz)
     
         time(&t);
         tm = localtime(&t);
    -    strftime(ts, sizeof(ts), "%H:%M:%S", tm);
    +    strftime(ts, sizeof(ts), "%H:%M:%S", tm);
     
         if (e->exit_event) {
    -        printf("%-8s %-5s %-16s %-7d %-7d [%u]",
    -               ts, "EXIT", e->comm, e->pid, e->ppid, e->exit_code);
    +        printf("%-8s %-5s %-16s %-7d %-7d [%u]",
    +               ts, "EXIT", e->comm, e->pid, e->ppid, e->exit_code);
             if (e->duration_ns)
    -            printf(" (%llums)", e->duration_ns / 1000000);
    -        printf("\n");
    +            printf(" (%llums)", e->duration_ns / 1000000);
    +        printf("\n");
         } else {
    -        printf("%-8s %-5s %-16s %-7d %-7d %s\n",
    -               ts, "EXEC", e->comm, e->pid, e->ppid, e->filename);
    +        printf("%-8s %-5s %-16s %-7d %-7d %s\n",
    +               ts, "EXEC", e->comm, e->pid, e->ppid, e->filename);
         }
     
         return 0;
    @@ -561,7 +561,7 @@ int main(int argc, char **argv)
         /* Load and verify BPF application */
         skel = bootstrap_bpf__open();
         if (!skel) {
    -        fprintf(stderr, "Failed to open and load BPF skeleton\n");
    +        fprintf(stderr, "Failed to open and load BPF skeleton\n");
             return 1;
         }
     
    @@ -571,14 +571,14 @@ int main(int argc, char **argv)
         /* Load & verify BPF programs */
         err = bootstrap_bpf__load(skel);
         if (err) {
    -        fprintf(stderr, "Failed to load and verify BPF skeleton\n");
    +        fprintf(stderr, "Failed to load and verify BPF skeleton\n");
             goto cleanup;
         }
     
         /* Attach tracepoints */
         err = bootstrap_bpf__attach(skel);
         if (err) {
    -        fprintf(stderr, "Failed to attach BPF skeleton\n");
    +        fprintf(stderr, "Failed to attach BPF skeleton\n");
             goto cleanup;
         }
     
    @@ -586,13 +586,13 @@ int main(int argc, char **argv)
         rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL);
         if (!rb) {
             err = -1;
    -        fprintf(stderr, "Failed to create ring buffer\n");
    +        fprintf(stderr, "Failed to create ring buffer\n");
             goto cleanup;
         }
     
         /* Process events */
    -    printf("%-8s %-5s %-16s %-7s %-7s %s\n",
    -           "TIME", "EVENT", "COMM", "PID", "PPID", "FILENAME/EXIT CODE");
    +    printf("%-8s %-5s %-16s %-7s %-7s %s\n",
    +           "TIME", "EVENT", "COMM", "PID", "PPID", "FILENAME/EXIT CODE");
         while (!exiting) {
             err = ring_buffer__poll(rb, 100 /* timeout, ms */);
             /* Ctrl-C will cause -EINTR */
    @@ -601,7 +601,7 @@ int main(int argc, char **argv)
                 break;
             }
             if (err < 0) {
    -            printf("Error polling perf buffer: %d\n", err);
    +            printf("Error polling perf buffer: %d\n", err);
                 break;
             }
         }
    @@ -623,8 +623,8 @@ cleanup:
     

    接下来,我们使用 argp 库来解析命令行参数:

    static const struct argp_option opts[] = {
    -    { "verbose", 'v', NULL, 0, "Verbose debug output" },
    -    { "duration", 'd', "DURATION-MS", 0, "Minimum process duration (ms) to report" },
    +    { "verbose", 'v', NULL, 0, "Verbose debug output" },
    +    { "duration", 'd', "DURATION-MS", 0, "Minimum process duration (ms) to report" },
         {},
     };
     
    @@ -649,7 +649,7 @@ libbpf_set_print(libbpf_print_fn);
     

    接下来,我们打开 eBPF 脚手架(skeleton)文件,将最小持续时间参数传递给 eBPF 程序,并加载和附加 eBPF 程序:

    skel = bootstrap_bpf__open();
     if (!skel) {
    -    fprintf(stderr, "Failed to open and load BPF skeleton\n");
    +    fprintf(stderr, "Failed to open and load BPF skeleton\n");
         return 1;
     }
     
    @@ -657,13 +657,13 @@ skel->rodata->min_duration_ns = env.min_duration_ms * 1000000ULL;
     
     err = bootstrap_bpf__load(skel);
     if (err) {
    -    fprintf(stderr, "Failed to load and verify BPF skeleton\n");
    +    fprintf(stderr, "Failed to load and verify BPF skeleton\n");
         goto cleanup;
     }
     
     err = bootstrap_bpf__attach(skel);
     if (err) {
    -    fprintf(stderr, "Failed to attach BPF skeleton\n");
    +    fprintf(stderr, "Failed to attach BPF skeleton\n");
         goto cleanup;
     }
     
    @@ -671,7 +671,7 @@ if (err) {
    rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL);
     if (!rb) {
         err = -1;
    -    fprintf(stderr, "Failed to create ring buffer\n");
    +    fprintf(stderr, "Failed to create ring buffer\n");
         goto cleanup;
     }
     
    diff --git a/12-profile/index.html b/12-profile/index.html index 37b39e1..7e0c216 100644 --- a/12-profile/index.html +++ b/12-profile/index.html @@ -217,21 +217,21 @@ Userspace:

    内核态 eBPF 程序的实现逻辑主要是借助 perf event,对程序的堆栈进行定时采样,从而捕获程序的执行流程。

    // SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
     /* Copyright (c) 2022 Meta Platforms, Inc. */
    -#include "vmlinux.h"
    +#include "vmlinux.h"
     #include <bpf/bpf_helpers.h>
     #include <bpf/bpf_tracing.h>
     #include <bpf/bpf_core_read.h>
     
    -#include "profile.h"
    +#include "profile.h"
     
    -char LICENSE[] SEC("license") = "Dual BSD/GPL";
    +char LICENSE[] SEC("license") = "Dual BSD/GPL";
     
     struct {
         __uint(type, BPF_MAP_TYPE_RINGBUF);
         __uint(max_entries, 256 * 1024);
    -} events SEC(".maps");
    +} events SEC(".maps");
     
    -SEC("perf_event")
    +SEC("perf_event")
     int profile(void *ctx)
     {
         int pid = bpf_get_current_pid_tgid() >> 32;
    @@ -266,13 +266,13 @@ int profile(void *ctx)
     struct {
         __uint(type, BPF_MAP_TYPE_RINGBUF);
         __uint(max_entries, 256 * 1024);
    -} events SEC(".maps");
    +} events SEC(".maps");
     

    这里定义了一个类型为 BPF_MAP_TYPE_RINGBUF 的 eBPF maps 。Ring Buffer 是一种高性能的循环缓冲区,用于在内核和用户空间之间传输数据。max_entries 设置了 Ring Buffer 的最大大小。

  2. 定义 perf_event eBPF 程序:

    -
    SEC("perf_event")
    +
    SEC("perf_event")
     int profile(void *ctx)
     

    这里定义了一个名为 profile 的 eBPF 程序,它将在 perf 事件触发时执行。

    @@ -342,7 +342,7 @@ int main(){ /* 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"); + fprintf(stderr, "Fail to set up performance monitor on a CPU/Core\n"); err = -1; goto cleanup; } @@ -387,34 +387,34 @@ int main(){ for (i = 0; i < stack_sz; i++) { if (!result || result->size <= i || !result->entries[i].size) { - printf(" %d [<%016llx>]\n", i, stack[i]); + 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", + 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", + 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]); + 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", + 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, + printf(" %s+0x%llx\n", sym->symbol, stack[i] - sym->start_address); } } @@ -431,23 +431,23 @@ static int event_handler(void *_ctx, void *data, size_t size) 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); + printf("COMM: %s (pid=%d) @ CPU %d\n", event->comm, event->pid, event->cpu_id); if (event->kstack_sz > 0) { - printf("Kernel:\n"); + printf("Kernel:\n"); show_stack_trace(event->kstack, event->kstack_sz / sizeof(__u64), 0); } else { - printf("No Kernel Stack\n"); + printf("No Kernel Stack\n"); } if (event->ustack_sz > 0) { - printf("Userspace:\n"); + printf("Userspace:\n"); show_stack_trace(event->ustack, event->ustack_sz / sizeof(__u64), event->pid); } else { - printf("No Userspace Stack\n"); + printf("No Userspace Stack\n"); } - printf("\n"); + printf("\n"); return 0; }
    diff --git a/13-tcpconnlat/index.html b/13-tcpconnlat/index.html index 522905a..fca9f85 100644 --- a/13-tcpconnlat/index.html +++ b/13-tcpconnlat/index.html @@ -205,19 +205,19 @@

    理解了这两个队列的用途,我们就可以开始探究 tcpconnlat 的具体实现。tcpconnlat 的实现可以分为内核态和用户态两个部分,其中包括了几个主要的跟踪点:tcp_v4_connect, tcp_v6_connecttcp_rcv_state_process

    这些跟踪点主要位于内核中的 TCP/IP 网络栈。当执行相关的系统调用或内核函数时,这些跟踪点会被激活,从而触发 eBPF 程序的执行。这使我们能够捕获和测量 TCP 连接建立的整个过程。

    让我们先来看一下这些挂载点的源代码:

    -
    SEC("kprobe/tcp_v4_connect")
    +
    SEC("kprobe/tcp_v4_connect")
     int BPF_KPROBE(tcp_v4_connect, struct sock *sk)
     {
      return trace_connect(sk);
     }
     
    -SEC("kprobe/tcp_v6_connect")
    +SEC("kprobe/tcp_v6_connect")
     int BPF_KPROBE(tcp_v6_connect, struct sock *sk)
     {
      return trace_connect(sk);
     }
     
    -SEC("kprobe/tcp_rcv_state_process")
    +SEC("kprobe/tcp_rcv_state_process")
     int BPF_KPROBE(tcp_rcv_state_process, struct sock *sk)
     {
      return handle_tcp_rcv_state_process(ctx, sk);
    @@ -401,7 +401,7 @@ if (inet_opt && inet_opt->opt.srr) {
     #include <bpf/bpf_helpers.h>
     #include <bpf/bpf_core_read.h>
     #include <bpf/bpf_tracing.h>
    -#include "tcpconnlat.h"
    +#include "tcpconnlat.h"
     
     #define AF_INET    2
     #define AF_INET6   10
    @@ -420,13 +420,13 @@ struct {
       __uint(max_entries, 4096);
       __type(key, struct sock *);
       __type(value, struct piddata);
    -} start SEC(".maps");
    +} start SEC(".maps");
     
     struct {
       __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
       __uint(key_size, sizeof(u32));
       __uint(value_size, sizeof(u32));
    -} events SEC(".maps");
    +} events SEC(".maps");
     
     static int trace_connect(struct sock *sk)
     {
    @@ -489,43 +489,43 @@ cleanup:
       return 0;
     }
     
    -SEC("kprobe/tcp_v4_connect")
    +SEC("kprobe/tcp_v4_connect")
     int BPF_KPROBE(tcp_v4_connect, struct sock *sk)
     {
       return trace_connect(sk);
     }
     
    -SEC("kprobe/tcp_v6_connect")
    +SEC("kprobe/tcp_v6_connect")
     int BPF_KPROBE(tcp_v6_connect, struct sock *sk)
     {
       return trace_connect(sk);
     }
     
    -SEC("kprobe/tcp_rcv_state_process")
    +SEC("kprobe/tcp_rcv_state_process")
     int BPF_KPROBE(tcp_rcv_state_process, struct sock *sk)
     {
       return handle_tcp_rcv_state_process(ctx, sk);
     }
     
    -SEC("fentry/tcp_v4_connect")
    +SEC("fentry/tcp_v4_connect")
     int BPF_PROG(fentry_tcp_v4_connect, struct sock *sk)
     {
       return trace_connect(sk);
     }
     
    -SEC("fentry/tcp_v6_connect")
    +SEC("fentry/tcp_v6_connect")
     int BPF_PROG(fentry_tcp_v6_connect, struct sock *sk)
     {
       return trace_connect(sk);
     }
     
    -SEC("fentry/tcp_rcv_state_process")
    +SEC("fentry/tcp_rcv_state_process")
     int BPF_PROG(fentry_tcp_rcv_state_process, struct sock *sk)
     {
       return handle_tcp_rcv_state_process(ctx, sk);
     }
     
    -char LICENSE[] SEC("license") = "GPL";
    +char LICENSE[] SEC("license") = "GPL";
     

    这个eBPF(Extended Berkeley Packet Filter)程序主要用来监控并收集TCP连接的建立时间,即从发起TCP连接请求(connect系统调用)到连接建立完成(SYN-ACK握手过程完成)的时间间隔。这对于监测网络延迟、服务性能分析等方面非常有用。

    首先,定义了两个eBPF maps:starteventsstart是一个哈希表,用于存储发起连接请求的进程信息和时间戳,而events是一个PERF_EVENT_ARRAY类型的map,用于将事件数据传输到用户态。

    @@ -534,13 +534,13 @@ char LICENSE[] SEC("license") = "GPL"; __uint(max_entries, 4096); __type(key, struct sock *); __type(value, struct piddata); -} start SEC(".maps"); +} start SEC(".maps"); struct { __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); __uint(key_size, sizeof(u32)); __uint(value_size, sizeof(u32)); -} events SEC(".maps"); +} events SEC(".maps");

    tcp_v4_connecttcp_v6_connect的kprobe处理函数trace_connect中,会记录下发起连接请求的进程信息(进程名、进程ID和当前时间戳),并以socket结构作为key,存储到start这个map中。

    static int trace_connect(struct sock *sk)
    @@ -621,7 +621,7 @@ cleanup:
         while (!exiting) {
             err = perf_buffer__poll(pb, PERF_POLL_TIMEOUT_MS);
             if (err < 0 && err != -EINTR) {
    -            fprintf(stderr, "error polling perf buffer: %s\n", strerror(-err));
    +            fprintf(stderr, "error polling perf buffer: %s\n", strerror(-err));
                 goto cleanup;
             }
             /* reset err to return 0 if exiting */
    @@ -643,7 +643,7 @@ cleanup:
         if (env.timestamp) {
             if (start_ts == 0)
                 start_ts = e->ts_us;
    -        printf("%-9.3f ", (e->ts_us - start_ts) / 1000000.0);
    +        printf("%-9.3f ", (e->ts_us - start_ts) / 1000000.0);
         }
         if (e->af == AF_INET) {
             s.x4.s_addr = e->saddr_v4;
    @@ -652,18 +652,18 @@ cleanup:
             memcpy(&s.x6.s6_addr, e->saddr_v6, sizeof(s.x6.s6_addr));
             memcpy(&d.x6.s6_addr, e->daddr_v6, sizeof(d.x6.s6_addr));
         } else {
    -        fprintf(stderr, "broken event: event->af=%d", e->af);
    +        fprintf(stderr, "broken event: event->af=%d", e->af);
             return;
         }
     
         if (env.lport) {
    -        printf("%-6d %-12.12s %-2d %-16s %-6d %-16s %-5d %.2f\n", e->tgid,
    +        printf("%-6d %-12.12s %-2d %-16s %-6d %-16s %-5d %.2f\n", e->tgid,
                    e->comm, e->af == AF_INET ? 4 : 6,
                    inet_ntop(e->af, &s, src, sizeof(src)), e->lport,
                    inet_ntop(e->af, &d, dst, sizeof(dst)), ntohs(e->dport),
                    e->delta_us / 1000.0);
         } else {
    -        printf("%-6d %-12.12s %-2d %-16s %-16s %-5d %.2f\n", e->tgid, e->comm,
    +        printf("%-6d %-12.12s %-2d %-16s %-16s %-5d %.2f\n", e->tgid, e->comm,
                    e->af == AF_INET ? 4 : 6, inet_ntop(e->af, &s, src, sizeof(src)),
                    inet_ntop(e->af, &d, dst, sizeof(dst)), ntohs(e->dport),
                    e->delta_us / 1000.0);
    diff --git a/14-tcpstates/index.html b/14-tcpstates/index.html
    index d911f33..32651b4 100644
    --- a/14-tcpstates/index.html
    +++ b/14-tcpstates/index.html
    @@ -200,29 +200,29 @@ struct {
         __uint(max_entries, MAX_ENTRIES);
         __type(key, __u16);
         __type(value, __u16);
    -} sports SEC(".maps");
    +} sports SEC(".maps");
     
     struct {
         __uint(type, BPF_MAP_TYPE_HASH);
         __uint(max_entries, MAX_ENTRIES);
         __type(key, __u16);
         __type(value, __u16);
    -} dports SEC(".maps");
    +} dports SEC(".maps");
     
     struct {
         __uint(type, BPF_MAP_TYPE_HASH);
         __uint(max_entries, MAX_ENTRIES);
         __type(key, struct sock *);
         __type(value, __u64);
    -} timestamps SEC(".maps");
    +} timestamps SEC(".maps");
     
     struct {
         __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
         __uint(key_size, sizeof(__u32));
         __uint(value_size, sizeof(__u32));
    -} events SEC(".maps");
    +} events SEC(".maps");
     
    -SEC("tracepoint/sock/inet_sock_set_state")
    +SEC("tracepoint/sock/inet_sock_set_state")
     int handle_set_state(struct trace_event_raw_inet_sock_set_state *ctx)
     {
         struct sock *sk = (struct sock *)ctx->skaddr;
    @@ -299,8 +299,8 @@ int handle_set_state(struct trace_event_raw_inet_sock_set_state *ctx)
         if (emit_timestamp) {
             time(&t);
             tm = localtime(&t);
    -        strftime(ts, sizeof(ts), "%H:%M:%S", tm);
    -        printf("%8s ", ts);
    +        strftime(ts, sizeof(ts), "%H:%M:%S", tm);
    +        printf("%8s ", ts);
         }
     
         inet_ntop(e->family, &e->saddr, saddr, sizeof(saddr));
    @@ -308,14 +308,14 @@ int handle_set_state(struct trace_event_raw_inet_sock_set_state *ctx)
         if (wide_output) {
             family = e->family == AF_INET ? 4 : 6;
             printf(
    -            "%-16llx %-7d %-16s %-2d %-26s %-5d %-26s %-5d %-11s -> %-11s "
    -            "%.3f\n",
    +            "%-16llx %-7d %-16s %-2d %-26s %-5d %-26s %-5d %-11s -> %-11s "
    +            "%.3f\n",
                 e->skaddr, e->pid, e->task, family, saddr, e->sport, daddr,
                 e->dport, tcp_states[e->oldstate], tcp_states[e->newstate],
                 (double)e->delta_us / 1000);
         } else {
             printf(
    -            "%-16llx %-7d %-10.10s %-15s %-5d %-15s %-5d %-11s -> %-11s %.3f\n",
    +            "%-16llx %-7d %-10.10s %-15s %-5d %-15s %-5d %-11s -> %-11s %.3f\n",
                 e->skaddr, e->pid, e->task, saddr, e->sport, daddr, e->dport,
                 tcp_states[e->oldstate], tcp_states[e->newstate],
                 (double)e->delta_us / 1000);
    @@ -335,17 +335,17 @@ int handle_set_state(struct trace_event_raw_inet_sock_set_state *ctx)
     

    tcprtt

    在本章节中,我们将分析tcprtt eBPF 程序的内核态代码。tcprtt是一个用于测量 TCP 往返时间(Round Trip Time, RTT)的程序,它将 RTT 的信息统计到一个 histogram 中。

    
    -/// @sample {"interval": 1000, "type" : "log2_hist"}
    +/// @sample {"interval": 1000, "type" : "log2_hist"}
     struct {
         __uint(type, BPF_MAP_TYPE_HASH);
         __uint(max_entries, MAX_ENTRIES);
         __type(key, u64);
         __type(value, struct hist);
    -} hists SEC(".maps");
    +} hists SEC(".maps");
     
     static struct hist zero;
     
    -SEC("fentry/tcp_rcv_established")
    +SEC("fentry/tcp_rcv_established")
     int BPF_PROG(tcp_rcv, struct sock *sk)
     {
         const struct inet_sock *inet = (struct inet_sock *)(sk);
    diff --git a/15-javagc/index.html b/15-javagc/index.html
    index dde5feb..1129d12 100644
    --- a/15-javagc/index.html
    +++ b/15-javagc/index.html
    @@ -182,8 +182,8 @@
     

    在用户层面进行动态跟踪,即用户级动态跟踪(User-Level Dynamic Tracing)允许我们对任何用户级别的代码进行插桩。比如,我们可以通过在 MySQL 服务器的 dispatch_command() 函数上进行插桩,来跟踪服务器的查询请求:

    # ./uprobe 'p:cmd /opt/bin/mysqld:_Z16dispatch_command19enum_server_commandP3THDPcj +0(%dx):string'
     Tracing uprobe cmd (p:cmd /opt/bin/mysqld:0x2dbd40 +0(%dx):string). Ctrl-C to end.
    -  mysqld-2855  [001] d... 19957757.590926: cmd: (0x6dbd40) arg1="show tables"
    -  mysqld-2855  [001] d... 19957759.703497: cmd: (0x6dbd40) arg1="SELECT * FROM numbers"
    +  mysqld-2855  [001] d... 19957757.590926: cmd: (0x6dbd40) arg1="show tables"
    +  mysqld-2855  [001] d... 19957759.703497: cmd: (0x6dbd40) arg1="SELECT * FROM numbers"
     [...]
     

    这里我们使用了 uprobe 工具,它利用了 Linux 的内置功能:ftrace(跟踪器)和 uprobes(用户级动态跟踪,需要较新的 Linux 版本,例如 4.0 左右)。其他的跟踪器,如 perf_events 和 SystemTap,也可以实现此功能。

    @@ -200,7 +200,7 @@ adjust_time_range

    这有 21,000 个函数。我们也可以跟踪库函数,甚至是单个的指令偏移。

    用户级动态跟踪的能力是非常强大的,它可以解决无数的问题。然而,使用它也有一些困难:需要确定需要跟踪的代码,处理函数参数,以及应对代码的更改。

    -

    用户级静态定义跟踪(User-level Statically Defined Tracing, USDT)则可以在某种程度上解决这些问题。USDT 探针(或者称为用户级 "marker")是开发者在代码的关键位置插入的跟踪宏,提供稳定且已经过文档说明的 API。这使得跟踪工作变得更加简单。

    +

    用户级静态定义跟踪(User-level Statically Defined Tracing, USDT)则可以在某种程度上解决这些问题。USDT 探针(或者称为用户级 "marker")是开发者在代码的关键位置插入的跟踪宏,提供稳定且已经过文档说明的 API。这使得跟踪工作变得更加简单。

    使用 USDT,我们可以简单地跟踪一个名为 mysql:query__start 的探针,而不是去跟踪那个名为 _Z16dispatch_command19enum_server_commandP3THDPcj 的 C++ 符号,也就是 dispatch_command() 函数。当然,我们仍然可以在需要的时候去跟踪 dispatch_command() 以及其他 21,000 个 mysqld 函数,但只有当 USDT 探针无法解决问题的时候我们才需要这么做。

    在 Linux 中的 USDT,无论是哪种形式的静态跟踪点,其实都已经存在了几十年。它最近由于 Sun 的 DTrace 工具的流行而再次受到关注,这使得许多常见的应用程序,包括 MySQL、PostgreSQL、Node.js、Java 等都加入了 USDT。SystemTap 则开发了一种可以消费这些 DTrace 探针的方式。

    你可能正在运行一个已经包含了 USDT 探针的 Linux 应用程序,或者可能需要重新编译(通常是 --enable-dtrace)。你可以使用 readelf 来进行检查,例如对于 Node.js:

    @@ -221,7 +221,7 @@ Notes at offset 0x00c43058 with length 0x00000494: Arguments: 8@%rax 8@%rdx 8@-136(%rbp) -4@-140(%rbp) 8@-72(%rbp) 8@-80(%rbp) -4@-144(%rbp) [...]
    -

    这就是使用 --enable-dtrace 重新编译的 node,以及安装了提供 "dtrace" 功能来构建 USDT 支持的 systemtap-sdt-dev 包。这里显示了两个探针:node:gc__start(开始进行垃圾回收)和 node:http__client__request。

    +

    这就是使用 --enable-dtrace 重新编译的 node,以及安装了提供 "dtrace" 功能来构建 USDT 支持的 systemtap-sdt-dev 包。这里显示了两个探针:node:gc__start(开始进行垃圾回收)和 node:http__client__request。

    在这一点上,你可以使用 SystemTap 或者 LTTng 来跟踪这些探针。然而,内置的 Linux 跟踪器,比如 ftrace 和 perf_events,目前还无法做到这一点(尽管 perf_events 的支持正在开发中)。

    USDT 在内核态 eBPF 运行时,也可能产生比较大的性能开销,这时候也可以考虑使用用户态 eBPF 运行时,例如 bpftime。bpftime 是一个基于 LLVM JIT/AOT 的用户态 eBPF 运行时,它可以在用户态运行 eBPF 程序,和内核态的 eBPF 兼容,避免了内核态和用户态之间的上下文切换,从而提高了 eBPF 程序的执行效率。对于 uprobe 而言,bpftime 的性能开销比 kernel 小一个数量级。

    Java GC 介绍

    @@ -236,20 +236,20 @@ Notes at offset 0x00c43058 with length 0x00000494: #include <bpf/bpf_helpers.h> #include <bpf/bpf_core_read.h> #include <bpf/usdt.bpf.h> -#include "javagc.h" +#include "javagc.h" struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 100); __type(key, uint32_t); __type(value, struct data_t); -} data_map SEC(".maps"); +} data_map SEC(".maps"); struct { __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); __type(key, int); __type(value, int); -} perf_map SEC(".maps"); +} perf_map SEC(".maps"); __u32 time; @@ -286,38 +286,38 @@ static int gc_end(struct pt_regs *ctx) return 0; } -SEC("usdt") +SEC("usdt") int handle_gc_start(struct pt_regs *ctx) { return gc_start(ctx); } -SEC("usdt") +SEC("usdt") int handle_gc_end(struct pt_regs *ctx) { return gc_end(ctx); } -SEC("usdt") +SEC("usdt") int handle_mem_pool_gc_start(struct pt_regs *ctx) { return gc_start(ctx); } -SEC("usdt") +SEC("usdt") int handle_mem_pool_gc_end(struct pt_regs *ctx) { return gc_end(ctx); } -char LICENSE[] SEC("license") = "Dual BSD/GPL"; +char LICENSE[] SEC("license") = "Dual BSD/GPL";

    首先,我们定义了两个映射(map):

    • data_map:这个 hashmap 存储每个进程 ID 的垃圾收集开始时间。data_t 结构体包含进程 ID、CPU ID 和时间戳。
    • perf_map:这是一个 perf event array,用于将数据发送回用户态程序。
    -

    然后,我们有四个处理函数:gc_startgc_end 和两个 USDT 处理函数 handle_mem_pool_gc_starthandle_mem_pool_gc_end。这些函数都用 BPF 的 SEC("usdt") 宏注解,以便在 Java 进程中捕获到与垃圾收集相关的 USDT 事件。

    +

    然后,我们有四个处理函数:gc_startgc_end 和两个 USDT 处理函数 handle_mem_pool_gc_starthandle_mem_pool_gc_end。这些函数都用 BPF 的 SEC("usdt") 宏注解,以便在 Java 进程中捕获到与垃圾收集相关的 USDT 事件。

    gc_start 函数在垃圾收集开始时被调用。它首先获取当前的 CPU ID、进程 ID 和时间戳,然后将这些数据存入 data_map

    gc_end 函数在垃圾收集结束时被调用。它执行与 gc_start 类似的操作,但是它还从 data_map 中检索开始时间,并计算垃圾收集的持续时间。如果持续时间超过了设定的阈值(变量 time),那么它将数据发送回用户态程序。

    handle_gc_starthandle_gc_end 是针对垃圾收集开始和结束事件的处理函数,它们分别调用了 gc_startgc_end

    @@ -333,17 +333,17 @@ char LICENSE[] SEC("license") = "Dual BSD/GPL"; FILE *f; int i = 0; - sprintf(buf, "/proc/%d/maps", env.pid); - f = fopen(buf, "r"); + sprintf(buf, "/proc/%d/maps", env.pid); + f = fopen(buf, "r"); if (!f) return -1; - while (fscanf(f, "%zx-%zx %s %zx %*s %*d%[^\n]\n", + while (fscanf(f, "%zx-%zx %s %zx %*s %*d%[^\n]\n", &seg_start, &seg_end, mode, &seg_off, line) == 5) { i = 0; while (isblank(line[i])) i++; - if (strstr(line + i, "libjvm.so")) { + if (strstr(line + i, "libjvm.so")) { break; } } @@ -356,34 +356,34 @@ char LICENSE[] SEC("license") = "Dual BSD/GPL";

    接下来,我们看到的是将 eBPF 程序(函数 handle_gc_starthandle_gc_end)附加到Java进程的相关USDT探针上。每个程序都通过调用 bpf_program__attach_usdt 函数来实现这一点,该函数的参数包括BPF程序、进程ID、二进制路径以及探针的提供者和名称。如果探针挂载成功,bpf_program__attach_usdt 将返回一个链接对象,该对象将存储在skeleton的链接成员中。如果挂载失败,程序将打印错误消息并进行清理。

        skel->links.handle_mem_pool_gc_start = bpf_program__attach_usdt(skel->progs.handle_gc_start, env.pid,
    -                                    binary_path, "hotspot", "mem__pool__gc__begin", NULL);
    +                                    binary_path, "hotspot", "mem__pool__gc__begin", NULL);
         if (!skel->links.handle_mem_pool_gc_start) {
             err = errno;
    -        fprintf(stderr, "attach usdt mem__pool__gc__begin failed: %s\n", strerror(err));
    +        fprintf(stderr, "attach usdt mem__pool__gc__begin failed: %s\n", strerror(err));
             goto cleanup;
         }
     
         skel->links.handle_mem_pool_gc_end = bpf_program__attach_usdt(skel->progs.handle_gc_end, env.pid,
    -                                binary_path, "hotspot", "mem__pool__gc__end", NULL);
    +                                binary_path, "hotspot", "mem__pool__gc__end", NULL);
         if (!skel->links.handle_mem_pool_gc_end) {
             err = errno;
    -        fprintf(stderr, "attach usdt mem__pool__gc__end failed: %s\n", strerror(err));
    +        fprintf(stderr, "attach usdt mem__pool__gc__end failed: %s\n", strerror(err));
             goto cleanup;
         }
     
         skel->links.handle_gc_start = bpf_program__attach_usdt(skel->progs.handle_gc_start, env.pid,
    -                                    binary_path, "hotspot", "gc__begin", NULL);
    +                                    binary_path, "hotspot", "gc__begin", NULL);
         if (!skel->links.handle_gc_start) {
             err = errno;
    -        fprintf(stderr, "attach usdt gc__begin failed: %s\n", strerror(err));
    +        fprintf(stderr, "attach usdt gc__begin failed: %s\n", strerror(err));
             goto cleanup;
         }
     
         skel->links.handle_gc_end = bpf_program__attach_usdt(skel->progs.handle_gc_end, env.pid,
    -                binary_path, "hotspot", "gc__end", NULL);
    +                binary_path, "hotspot", "gc__end", NULL);
         if (!skel->links.handle_gc_end) {
             err = errno;
    -        fprintf(stderr, "attach usdt gc__end failed: %s\n", strerror(err));
    +        fprintf(stderr, "attach usdt gc__end failed: %s\n", strerror(err));
             goto cleanup;
         }
     
    @@ -397,8 +397,8 @@ char LICENSE[] SEC("license") = "Dual BSD/GPL"; time(&t); tm = localtime(&t); - strftime(ts, sizeof(ts), "%H:%M:%S", tm); - printf("%-8s %-7d %-7d %-7lld\n", ts, e->cpu, e->pid, e->ts/1000); + strftime(ts, sizeof(ts), "%H:%M:%S", tm); + printf("%-8s %-7d %-7d %-7lld\n", ts, e->cpu, e->pid, e->ts/1000); }

    安装依赖

    diff --git a/16-memleak/index.html b/16-memleak/index.html index 9965305..e63c516 100644 --- a/16-memleak/index.html +++ b/16-memleak/index.html @@ -231,7 +231,7 @@ union combined_alloc_info {

    这里定义了两个主要的数据结构:alloc_infocombined_alloc_info

    alloc_info 结构体包含了一个内存分配的基本信息,包括分配的内存大小 size、分配发生时的时间戳 timestamp_ns,以及触发分配的调用堆栈 ID stack_id

    combined_alloc_info 是一个联合体(union),它包含一个嵌入的结构体和一个 __u64 类型的位图表示 bits。嵌入的结构体有两个成员:total_sizenumber_of_allocs,分别代表所有未释放分配的总大小和总次数。其中 40 和 24 分别表示 total_size 和 number_of_allocs这两个成员变量所占用的位数,用来限制其大小。通过这样的位数限制,可以节省combined_alloc_info结构的存储空间。同时,由于total_size和number_of_allocs在存储时是共用一个unsigned long long类型的变量bits,因此可以通过在成员变量bits上进行位运算来访问和修改total_size和number_of_allocs,从而避免了在程序中定义额外的变量和函数的复杂性。

    -

    接下来,memleak 定义了一系列用于保存内存分配信息和分析结果的 eBPF 映射(maps)。这些映射都以 SEC(".maps") 的形式定义,表示它们属于 eBPF 程序的映射部分。

    +

    接下来,memleak 定义了一系列用于保存内存分配信息和分析结果的 eBPF 映射(maps)。这些映射都以 SEC(".maps") 的形式定义,表示它们属于 eBPF 程序的映射部分。

    const volatile size_t min_size = 0;
     const volatile size_t max_size = -1;
     const volatile size_t page_size = 4096;
    @@ -245,33 +245,33 @@ struct {
         __type(key, pid_t);
         __type(value, u64);
         __uint(max_entries, 10240);
    -} sizes SEC(".maps");
    +} sizes SEC(".maps");
     
     struct {
         __uint(type, BPF_MAP_TYPE_HASH);
         __type(key, u64); /* address */
         __type(value, struct alloc_info);
         __uint(max_entries, ALLOCS_MAX_ENTRIES);
    -} allocs SEC(".maps");
    +} allocs SEC(".maps");
     
     struct {
         __uint(type, BPF_MAP_TYPE_HASH);
         __type(key, u64); /* stack id */
         __type(value, union combined_alloc_info);
         __uint(max_entries, COMBINED_ALLOCS_MAX_ENTRIES);
    -} combined_allocs SEC(".maps");
    +} combined_allocs SEC(".maps");
     
     struct {
         __uint(type, BPF_MAP_TYPE_HASH);
         __type(key, u64);
         __type(value, u64);
         __uint(max_entries, 10240);
    -} memptrs SEC(".maps");
    +} memptrs SEC(".maps");
     
     struct {
         __uint(type, BPF_MAP_TYPE_STACK_TRACE);
         __type(key, u32);
    -} stack_traces SEC(".maps");
    +} stack_traces SEC(".maps");
     
     static union combined_alloc_info initial_cinfo;
     
    @@ -285,24 +285,24 @@ static union combined_alloc_info initial_cinfo;
  3. stack_traces:这是一个堆栈追踪类型的映射,键为 u32 类型,用于存储堆栈 ID。

以用户态的内存分配追踪部分为例,主要是挂钩内存相关的函数调用,如 malloc, free, calloc, realloc, mmapmunmap,以便在调用这些函数时进行数据记录。在用户态,memleak 主要使用了 uprobes 技术进行挂载。

-

每个函数调用被分为 "enter" 和 "exit" 两部分。"enter" 部分记录的是函数调用的参数,如分配的大小或者释放的地址。"exit" 部分则主要用于获取函数的返回值,如分配得到的内存地址。

+

每个函数调用被分为 "enter" 和 "exit" 两部分。"enter" 部分记录的是函数调用的参数,如分配的大小或者释放的地址。"exit" 部分则主要用于获取函数的返回值,如分配得到的内存地址。

这里,gen_alloc_enter, gen_alloc_exit, gen_free_enter 是实现记录行为的函数,他们分别用于记录分配开始、分配结束和释放开始的相关信息。

函数原型示例如下:

-
SEC("uprobe")
+
SEC("uprobe")
 int BPF_KPROBE(malloc_enter, size_t size)
 {
     // 记录分配开始的相关信息
     return gen_alloc_enter(size);
 }
 
-SEC("uretprobe")
+SEC("uretprobe")
 int BPF_KRETPROBE(malloc_exit)
 {
     // 记录分配结束的相关信息
     return gen_alloc_exit(ctx);
 }
 
-SEC("uprobe")
+SEC("uprobe")
 int BPF_KPROBE(free_enter, void *address)
 {
     // 记录释放开始的相关信息
@@ -326,12 +326,12 @@ int BPF_KPROBE(free_enter, void *address)
     bpf_map_update_elem(&sizes, &pid, &size, BPF_ANY);
 
     if (trace_all)
-        bpf_printk("alloc entered, size = %lu\n", size);
+        bpf_printk("alloc entered, size = %lu\n", size);
 
     return 0;
 }
 
-SEC("uprobe")
+SEC("uprobe")
 int BPF_KPROBE(malloc_enter, size_t size)
 {
     return gen_alloc_enter(size);
@@ -370,7 +370,7 @@ int BPF_KPROBE(malloc_enter, size_t size)
     }
 
     if (trace_all) {
-        bpf_printk("alloc exited, size = %lu, result = %lx\n",
+        bpf_printk("alloc exited, size = %lu, result = %lx\n",
                 info.size, address);
     }
 
@@ -381,7 +381,7 @@ static int gen_alloc_exit(struct pt_regs *ctx)
     return gen_alloc_exit2(ctx, PT_REGS_RC(ctx));
 }
 
-SEC("uretprobe")
+SEC("uretprobe")
 int BPF_KRETPROBE(malloc_exit)
 {
     return gen_alloc_exit(ctx);
@@ -421,7 +421,7 @@ int BPF_KRETPROBE(malloc_exit)
 
     existing_cinfo = bpf_map_lookup_elem(&combined_allocs, &stack_id);
     if (!existing_cinfo) {
-        bpf_printk("failed to lookup combined allocs\n");
+        bpf_printk("failed to lookup combined allocs\n");
         return;
     }
 
@@ -446,14 +446,14 @@ int BPF_KRETPROBE(malloc_exit)
     update_statistics_del(info->stack_id, info->size);
 
     if (trace_all) {
-        bpf_printk("free entered, address = %lx, size = %lu\n",
+        bpf_printk("free entered, address = %lx, size = %lu\n",
                 address, info->size);
     }
 
     return 0;
 }
 
-SEC("uprobe")
+SEC("uprobe")
 int BPF_KPROBE(free_enter, void *address)
 {
     return gen_free_enter(address);
@@ -461,7 +461,7 @@ int BPF_KPROBE(free_enter, void *address)
 

接下来看 gen_free_enter 函数。它接收一个地址作为参数,这个地址是内存分配的结果,也就是将要释放的内存的起始地址。函数首先在 allocs 这个 map 中使用这个地址作为键来查找对应的 alloc_info 结构体。如果找不到,那么就直接返回,因为这意味着这个地址并没有被分配过。如果找到了,那么就删除这个元素,并且调用 update_statistics_del 函数来更新统计数据。最后,如果启用了全局追踪,那么还会输出一条信息,包括这个地址以及它的大小。 在我们追踪和统计内存分配的同时,我们也需要对内核态的内存分配和释放进行追踪。在Linux内核中,kmem_cache_alloc函数和kfree函数分别用于内核态的内存分配和释放。

-
SEC("tracepoint/kmem/kfree")
+
SEC("tracepoint/kmem/kfree")
 int memleak__kfree(void *ctx)
 {
     const void *ptr;
@@ -478,7 +478,7 @@ int memleak__kfree(void *ctx)
 }
 

上述代码片段定义了一个函数memleak__kfree,这是一个bpf程序,会在内核调用kfree函数时执行。首先,该函数检查是否存在kfree函数。如果存在,则会读取传递给kfree函数的参数(即要释放的内存块的地址),并保存到变量ptr中;否则,会读取传递给kmem_free函数的参数(即要释放的内存块的地址),并保存到变量ptr中。接着,该函数会调用之前定义的gen_free_enter函数来处理该内存块的释放。

-
SEC("tracepoint/kmem/kmem_cache_alloc")
+
SEC("tracepoint/kmem/kmem_cache_alloc")
 int memleak__kmem_cache_alloc(struct trace_event_raw_kmem_alloc *ctx)
 {
     if (wa_missing_free)
diff --git a/17-biopattern/index.html b/17-biopattern/index.html
index c698dd5..30c704c 100644
--- a/17-biopattern/index.html
+++ b/17-biopattern/index.html
@@ -227,9 +227,9 @@ sda         0   100        1          4
 
#include <vmlinux.h>
 #include <bpf/bpf_helpers.h>
 #include <bpf/bpf_tracing.h>
-#include "biopattern.h"
-#include "maps.bpf.h"
-#include "core_fixes.bpf.h"
+#include "biopattern.h"
+#include "maps.bpf.h"
+#include "core_fixes.bpf.h"
 
 const volatile bool filter_dev = false;
 const volatile __u32 targ_dev = 0;
@@ -239,9 +239,9 @@ struct {
     __uint(max_entries, 64);
     __type(key, u32);
     __type(value, struct counter);
-} counters SEC(".maps");
+} counters SEC(".maps");
 
-SEC("tracepoint/block/block_rq_complete")
+SEC("tracepoint/block/block_rq_complete")
 int handle__block_rq_complete(void *args)
 {
     struct counter *counterp, zero = {};
@@ -278,7 +278,7 @@ int handle__block_rq_complete(void *args)
     return 0;
 }
 
-char LICENSE[] SEC("license") = "GPL";
+char LICENSE[] SEC("license") = "GPL";
 
  1. 全局变量定义
  2. @@ -293,11 +293,11 @@ char LICENSE[] SEC("license") = "GPL"; __uint(max_entries, 64); __type(key, u32); __type(value, struct counter); - } counters SEC(".maps"); + } counters SEC(".maps");

这部分代码定义了一个 BPF map,类型为哈希表。该映射的键是设备的标识符,而值是一个 counter 结构体,用于存储设备的 I/O 统计信息。

追踪点函数:

-
    SEC("tracepoint/block/block_rq_complete")
+
    SEC("tracepoint/block/block_rq_complete")
     int handle__block_rq_complete(void *args)
     {
         struct counter *counterp, zero = {};
@@ -407,7 +407,7 @@ char LICENSE[] SEC("license") = "GPL";
         while (!bpf_map_get_next_key(fd, &lookup_key, &next_key)) {
             err = bpf_map_lookup_elem(fd, &next_key, &counter);
             if (err < 0) {
-                fprintf(stderr, "failed to lookup counters: %d\n", err);
+                fprintf(stderr, "failed to lookup counters: %d\n", err);
                 return -1;
             }
             lookup_key = next_key;
@@ -417,12 +417,12 @@ char LICENSE[] SEC("license") = "GPL";
             if (env.timestamp) {
                 time(&t);
                 tm = localtime(&t);
-                strftime(ts, sizeof(ts), "%H:%M:%S", tm);
-                printf("%-9s ", ts);
+                strftime(ts, sizeof(ts), "%H:%M:%S", tm);
+                printf("%-9s ", ts);
             }
             partition = partitions__get_by_dev(partitions, next_key);
-            printf("%-7s %5ld %5ld %8d %10lld\n",
-                partition ? partition->name : "Unknown",
+            printf("%-7s %5ld %5ld %8d %10lld\n",
+                partition ? partition->name : "Unknown",
                 counter.random * 100L / total,
                 counter.sequential * 100L / total, total,
                 counter.bytes / 1024);
@@ -432,7 +432,7 @@ char LICENSE[] SEC("license") = "GPL";
         while (!bpf_map_get_next_key(fd, &lookup_key, &next_key)) {
             err = bpf_map_delete_elem(fd, &next_key);
             if (err < 0) {
-                fprintf(stderr, "failed to cleanup counters: %d\n", err);
+                fprintf(stderr, "failed to cleanup counters: %d\n", err);
                 return -1;
             }
             lookup_key = next_key;
diff --git a/18-further-reading/index.html b/18-further-reading/index.html
index 65d1269..38e6826 100644
--- a/18-further-reading/index.html
+++ b/18-further-reading/index.html
@@ -197,7 +197,7 @@
 

随着微秒级 NVMe 存储设备的出现,Linux 内核存储堆栈开销变得显著,几乎使访问时间翻倍。我们介绍了 XRP,一个框架,允许应用程序从 eBPF 在 NVMe 驱动程序中的钩子执行用户定义的存储功能,如索引查找或聚合,安全地绕过大部分内核的存储堆栈。为了保持文件系统的语义,XRP 将少量的内核状态传播到其 NVMe 驱动程序钩子,在那里调用用户注册的 eBPF 函数。我们展示了如何利用 XRP 显著提高两个键值存储,BPF-KV,一个简单的 B+ 树键值存储,和 WiredTiger,一个流行的日志结构合并树存储引擎的吞吐量和延迟。

OSDI '22 最佳论文: https://www.usenix.org/conference/osdi22/presentation/zhong

Specification and verification in the field: Applying formal methods to BPF just-in-time compilers in the Linux kernel

-

本文描述了我们将形式方法应用于 Linux 内核中的一个关键组件,即 Berkeley 数据包过滤器 (BPF) 虚拟机的即时编译器 ("JIT") 的经验。我们使用 Jitterbug 验证这些 JIT,这是第一个提供 JIT 正确性的精确规范的框架,能够排除实际错误,并提供一个自动化的证明策略,该策略可以扩展到实际实现。使用 Jitterbug,我们设计、实施并验证了一个新的针对 32 位 RISC-V 的 BPF JIT,在五个其他部署的 JIT 中找到并修复了 16 个之前未知的错误,并开发了新的 JIT 优化;所有这些更改都已上传到 Linux 内核。结果表明,在一个大型的、未经验证的系统中,通过仔细设计规范和证明策略,可以构建一个经过验证的组件。

+

本文描述了我们将形式方法应用于 Linux 内核中的一个关键组件,即 Berkeley 数据包过滤器 (BPF) 虚拟机的即时编译器 ("JIT") 的经验。我们使用 Jitterbug 验证这些 JIT,这是第一个提供 JIT 正确性的精确规范的框架,能够排除实际错误,并提供一个自动化的证明策略,该策略可以扩展到实际实现。使用 Jitterbug,我们设计、实施并验证了一个新的针对 32 位 RISC-V 的 BPF JIT,在五个其他部署的 JIT 中找到并修复了 16 个之前未知的错误,并开发了新的 JIT 优化;所有这些更改都已上传到 Linux 内核。结果表明,在一个大型的、未经验证的系统中,通过仔细设计规范和证明策略,可以构建一个经过验证的组件。

OSDI 20: https://www.usenix.org/conference/osdi20/presentation/nelson

λ-IO: A Unified IO Stack for Computational Storage

新兴的计算存储设备为存储内计算提供了一个机会。它减少了主机与设备之间的数据移动开销,从而加速了数据密集型应用程序。在这篇文章中,我们介绍 λ-IO,一个统一的 IO 堆栈,跨主机和设备管理计算和存储资源。我们提出了一套设计 - 接口、运行时和调度 - 来解决三个关键问题。我们在全堆栈软件和硬件环境中实施了 λ-IO,并使用合成和实际应用程序对其

@@ -205,7 +205,7 @@

FAST23: https://www.usenix.org/conference/fast23/presentation/yang-zhe

Extension Framework for File Systems in User space

用户文件系统相对于其内核实现提供了许多优势,例如开发的简易性和更好的系统可靠性。然而,它们会导致重大的性能损失。我们观察到现有的用户文件系统框架非常通用;它们由一个位于内核中的最小干预层组成,该层简单地将所有低级请求转发到用户空间。虽然这种设计提供了灵活性,但由于频繁的内核-用户上下文切换,它也严重降低了性能。

-

这项工作介绍了 ExtFUSE,一个用于开发可扩展用户文件系统的框架,该框架还允许应用程序在内核中注册"薄"的专用请求处理程序,以满足其特定的操作需求,同时在用户空间中保留复杂的功能。我们使用两个 FUSE 文件系统对 ExtFUSE 进行评估,结果表明 ExtFUSE 可以通过平均不到几百行的改动来提高用户文件系统的性能。ExtFUSE 可在 GitHub 上找到。

+

这项工作介绍了 ExtFUSE,一个用于开发可扩展用户文件系统的框架,该框架还允许应用程序在内核中注册"薄"的专用请求处理程序,以满足其特定的操作需求,同时在用户空间中保留复杂的功能。我们使用两个 FUSE 文件系统对 ExtFUSE 进行评估,结果表明 ExtFUSE 可以通过平均不到几百行的改动来提高用户文件系统的性能。ExtFUSE 可在 GitHub 上找到。

ATC 19: https://www.usenix.org/conference/atc19/presentation/bijlani

Electrode: Accelerating Distributed Protocols with eBPF

在标准的Linux内核网络栈下实现分布式协议可以享受到负载感知的CPU缩放、高兼容性以及强大的安全性和隔离性。但由于过多的用户-内核切换和内核网络栈遍历,其性能较低。我们介绍了Electrode,这是一套为分布式协议设计的基于eBPF的性能优化。这些优化在网络栈之前在内核中执行,但实现了与用户空间中实现的相似功能(例如,消息广播,收集ack的仲裁),从而避免了用户-内核切换和内核网络栈遍历所带来的开销。我们展示,当应用于经典的Multi-Paxos状态机复制协议时,Electrode可以提高其吞吐量高达128.4%,并将延迟降低高达41.7%。

@@ -227,9 +227,9 @@

在这项工作中,我们研究如何在 eBPF 中开发高性能的网络测量。我们以绘图为案例研究,因为它们具有支持广泛任务的能力,同时提供低内存占用和准确性保证。我们实现了 NitroSketch,一个用于用户空间网络的最先进的绘图,并表明用户空间网络的最佳实践不能直接应用于 eBPF,因为它的性能特点不同。通过应用我们学到的经验教训,我们将其性能提高了40%,与初级实现相比。

SIGCOMM 23: https://dl.acm.org/doi/abs/10.1145/3594255.3594256

SPRIGHT: extracting the server from serverless computing! high-performance eBPF-based event-driven, shared-memory processing

-

无服务器计算在云环境中承诺提供高效、低成本的计算能力。然而,现有的解决方案,如Knative这样的开源平台,包含了繁重的组件,破坏了无服务器计算的目标。此外,这种无服务器平台缺乏数据平面优化,无法实现高效的、高性能的功能链,这也是流行的微服务开发范式的设施。它们为构建功能链使用的不必要的复杂和重复的功能严重降低了性能。"冷启动"延迟是另一个威慑因素。

-

我们描述了 SPRIGHT,一个轻量级、高性能、响应式的无服务器框架。SPRIGHT 利用共享内存处理显著提高了数据平面的可伸缩性,通过避免不必要的协议处理和序列化-反序列化开销。SPRIGHT 大量利用扩展的伯克利数据包过滤器 (eBPF) 进行事件驱动处理。我们创造性地使用 eBPF 的套接字消息机制支持共享内存处理,其开销严格与负载成正比。与常驻、基于轮询的DPDK相比,SPRIGHT 在真实工作负载下实现了相同的数据平面性能,但 CPU 使用率降低了10倍。此外,eBPF 为 SPRIGHT 带来了好处,替换了繁重的无服务器组件,使我们能够以微不足道的代价保持函数处于"暖"状态。

-

我们的初步实验结果显示,与 Knative 相比,SPRIGHT 在吞吐量和延迟方面实现了一个数量级的提高,同时大大减少了 CPU 使用,并消除了 "冷启动"的需要。

+

无服务器计算在云环境中承诺提供高效、低成本的计算能力。然而,现有的解决方案,如Knative这样的开源平台,包含了繁重的组件,破坏了无服务器计算的目标。此外,这种无服务器平台缺乏数据平面优化,无法实现高效的、高性能的功能链,这也是流行的微服务开发范式的设施。它们为构建功能链使用的不必要的复杂和重复的功能严重降低了性能。"冷启动"延迟是另一个威慑因素。

+

我们描述了 SPRIGHT,一个轻量级、高性能、响应式的无服务器框架。SPRIGHT 利用共享内存处理显著提高了数据平面的可伸缩性,通过避免不必要的协议处理和序列化-反序列化开销。SPRIGHT 大量利用扩展的伯克利数据包过滤器 (eBPF) 进行事件驱动处理。我们创造性地使用 eBPF 的套接字消息机制支持共享内存处理,其开销严格与负载成正比。与常驻、基于轮询的DPDK相比,SPRIGHT 在真实工作负载下实现了相同的数据平面性能,但 CPU 使用率降低了10倍。此外,eBPF 为 SPRIGHT 带来了好处,替换了繁重的无服务器组件,使我们能够以微不足道的代价保持函数处于"暖"状态。

+

我们的初步实验结果显示,与 Knative 相比,SPRIGHT 在吞吐量和延迟方面实现了一个数量级的提高,同时大大减少了 CPU 使用,并消除了 "冷启动"的需要。

https://dl.acm.org/doi/10.1145/3544216.3544259

Programmable System Call Security with eBPF

利用 eBPF 进行可编程的系统调用安全

diff --git a/19-lsm-connect/index.html b/19-lsm-connect/index.html index 6ea151e..9197b50 100644 --- a/19-lsm-connect/index.html +++ b/19-lsm-connect/index.html @@ -198,27 +198,27 @@ CONFIG_BPF_LSM=y ndlock,lockdown,yama,integrity,apparmor

查看输出是否包含 bpf 选项,如果输出不包含(像上面的例子),可以通过修改 /etc/default/grub

-
GRUB_CMDLINE_LINUX="lsm=ndlock,lockdown,yama,integrity,apparmor,bpf"
+
GRUB_CMDLINE_LINUX="lsm=ndlock,lockdown,yama,integrity,apparmor,bpf"
 

并通过 update-grub2 命令更新 grub 配置(不同系统的对应命令可能不同),然后重启系统。

编写 eBPF 程序

// lsm-connect.bpf.c
-#include "vmlinux.h"
+#include "vmlinux.h"
 #include <bpf/bpf_core_read.h>
 #include <bpf/bpf_helpers.h>
 #include <bpf/bpf_tracing.h>
 
-char LICENSE[] SEC("license") = "GPL";
+char LICENSE[] SEC("license") = "GPL";
 
 #define EPERM 1
 #define AF_INET 2
 
 const __u32 blockme = 16843009; // 1.1.1.1 -> int
 
-SEC("lsm/socket_connect")
+SEC("lsm/socket_connect")
 int BPF_PROG(restrict_connect, struct socket *sock, struct sockaddr *address, int addrlen, int ret)
 {
-    // Satisfying "cannot override a denial" rule
+    // Satisfying "cannot override a denial" rule
     if (ret != 0)
     {
         return ret;
@@ -235,11 +235,11 @@ int BPF_PROG(restrict_connect, struct socket *sock, struct sockaddr *address, in
 
     // Where do you want to go?
     __u32 dest = addr->sin_addr.s_addr;
-    bpf_printk("lsm: found connect to %d", dest);
+    bpf_printk("lsm: found connect to %d", dest);
 
     if (dest == blockme)
     {
-        bpf_printk("lsm: blocking %d", dest);
+        bpf_printk("lsm: blocking %d", dest);
         return -EPERM;
     }
     return 0;
@@ -248,7 +248,7 @@ int BPF_PROG(restrict_connect, struct socket *sock, struct sockaddr *address, in
 

这是一段 C 实现的 eBPF 内核侧代码,它会阻碍所有试图通过 socket 对 1.1.1.1 的连接操作,其中:

    -
  • SEC("lsm/socket_connect") 宏指出该程序期望的挂载点;
  • +
  • SEC("lsm/socket_connect") 宏指出该程序期望的挂载点;
  • 程序通过 BPF_PROG 宏定义(详情可查看 tools/lib/bpf/bpf_tracing.h);
  • restrict_connectBPF_PROG 宏要求的程序名;
  • ret 是该挂载点上(潜在的)当前函数之前的 LSM 检查程序的返回值;
  • @@ -293,7 +293,7 @@ Retrying.

完整源代码:https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/19-lsm-connect

总结

-

本文介绍了如何使用 BPF LSM 来限制通过 socket 对特定 IPv4 地址的访问。我们可以通过修改 GRUB 配置文件来开启 LSM 的 BPF 挂载点。在 eBPF 程序中,我们通过 BPF_PROG 宏定义函数,并通过 SEC 宏指定挂载点;在函数实现上,遵循 LSM 安全检查模块中 "cannot override a denial" 的原则,并根据 socket 连接请求的目的地址对该请求进行限制。

+

本文介绍了如何使用 BPF LSM 来限制通过 socket 对特定 IPv4 地址的访问。我们可以通过修改 GRUB 配置文件来开启 LSM 的 BPF 挂载点。在 eBPF 程序中,我们通过 BPF_PROG 宏定义函数,并通过 SEC 宏指定挂载点;在函数实现上,遵循 LSM 安全检查模块中 "cannot override a denial" 的原则,并根据 socket 连接请求的目的地址对该请求进行限制。

如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。

参考