4. eBPF 程序编写入门:Hello World 程序、事件挂载点、数据采集与输出

本章是 eBPF 技术的实战起点。我们将从最经典的 Hello World 程序入手,逐步拆解 eBPF 程序的完整生命周期:编写、编译、加载、挂载到内核事件点,以及从内核空间向用户空间输出数据。掌握这些基础后,你就能理解 eBPF 如何在不修改内核源码或加载内核模块的前提下,安全、高效地观测和干预系统行为。

4.1 eBPF 程序的基本结构

一个完整的 eBPF 程序通常由两部分组成:内核态 BPF 字节码(运行在内核沙箱中)和 用户态加载/控制程序(负责加载、挂载、读取数据)。

组成部分 运行位置 主要职责 常见实现方式
BPF 程序(内核态) 内核沙箱 挂载到事件点,采集数据或执行策略 C 语言编写,由 LLVM/Clang 编译为 BPF 字节码
加载器(用户态) 用户空间 加载 BPF 字节码、创建 Map、挂载事件、读取输出 libbpf (C)、bcc (Python)、cilium/ebpf (Go)
数据通道 内核 ↔ 用户 传递事件数据或统计信息 BPF Map、perf_event 环形缓冲区、BPF 尾部调用
关键概念: eBPF 程序并非独立运行,而是挂载到内核预定义的事件挂载点(Hook Points)上。当该事件发生时,内核自动触发 BPF 程序执行。这种机制保证了 eBPF 的事件驱动特性,避免了轮询带来的性能开销。

4.2 Hello World 实战:基于 libbpf 的最小程序

我们使用 libbpf(C 语言库)编写一个最简单的 eBPF 程序:当进程执行 execve 系统调用时,在内核中打印一条消息,并通过 BPF Map 将进程 PID 和命令名传递到用户空间。

4.2.1 内核态 BPF 程序(hello.bpf.c)

// hello.bpf.c
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>

char LICENSE[] SEC("license") = "GPL";

// 定义一个 BPF Map,用于向用户空间传递数据
struct {
    __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
    __uint(key_size, sizeof(int));
    __uint(value_size, sizeof(__u32));
    __uint(max_entries, 64);
} my_map SEC(".maps");

// 定义要传递的数据结构
struct event {
    int pid;
    char comm[16];
};

SEC("tracepoint/syscalls/sys_enter_execve")
int hello_execve(struct trace_event_raw_sys_enter *ctx) {
    struct event e = {};
    e.pid = bpf_get_current_pid_tgid() >> 32;
    bpf_get_current_comm(&e.comm, sizeof(e.comm));
    bpf_perf_event_output(ctx, &my_map, BPF_F_CURRENT_CPU, &e, sizeof(e));
    return 0;
}

4.2.2 用户态加载程序(hello.c)

// hello.c
#include <stdio.h>
#include <unistd.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
#include "hello.skel.h"  // 由 bpftool 生成的骨架头文件

// perf_event 回调函数
void handle_event(void *ctx, int cpu, void *data, __u32 size) {
    struct event *e = data;
    printf("PID %d 执行了命令: %s\n", e->pid, e->comm);
}

int main() {
    struct hello_bpf *skel = hello_bpf__open_and_load();
    if (!skel) { fprintf(stderr, "加载 BPF 程序失败\n"); return 1; }

    // 挂载到 tracepoint
    hello_bpf__attach(skel);

    // 设置 perf_buffer 并轮询
    struct perf_buffer *pb = perf_buffer__new(
        bpf_map__fd(skel->maps.my_map), 8, handle_event, NULL, NULL, NULL);
    if (!pb) { fprintf(stderr, "创建 perf_buffer 失败\n"); return 1; }

    printf("开始监听 execve 系统调用...\n");
    while (1) { perf_buffer__poll(pb, 100); }

    perf_buffer__free(pb);
    hello_bpf__destroy(skel);
    return 0;
}
编译提示: 使用 clang -O2 -target bpf -c hello.bpf.c -o hello.bpf.o 编译内核态程序;使用 bpftool gen skeleton hello.bpf.o > hello.skel.h 生成骨架头文件;用户态程序使用 gcc -o hello hello.c -lbpf -lelf -lz 编译。

4.3 事件挂载点:eBPF 的“钩子”体系

eBPF 程序必须挂载到内核的特定事件点才能被触发执行。这些挂载点覆盖了系统调用、内核函数、网络事件、跟踪点等多个维度。

4.3.1 主要挂载点类型

挂载点类型 SEC 宏定义 触发时机 典型用途
Tracepoint SEC("tracepoint/...") 内核静态跟踪点 系统调用监控、文件操作审计
Kprobe SEC("kprobe/...") 内核函数入口 动态内核函数观测
Kretprobe SEC("kretprobe/...") 内核函数返回 获取函数返回值、延迟分析
XDP SEC("xdp") 网卡驱动层收到数据包 高性能网络过滤、DDoS 防护
TC (Traffic Control) SEC("tc") 内核网络协议栈处理数据包 容器网络策略、带宽管理
Socket Filter SEC("socket") Socket 层数据收发 应用层协议解析、socket 监控
挂载点选择原则:
  • 优先使用 Tracepoint:稳定、文档齐全、ABI 保证,适合生产环境。
  • Kprobe 用于动态探测:当没有现成 Tracepoint 时使用,但需注意内核函数名可能随版本变化。
  • XDP/TC 用于网络数据面:处理数据包转发、过滤、封装等高性能场景。

4.4 数据采集与输出:从内核到用户空间的通道

eBPF 程序在内核中执行,采集到的数据必须通过特定通道传递到用户空间。常用的数据输出方式有三种:BPF Map、Perf Event 环形缓冲区、BPF 尾部调用。

4.4.1 三种输出方式对比

方式 数据传输方向 性能 适用场景
BPF Map(共享内存) 双向 高(无拷贝) 计数器、配置下发、状态共享
Perf Event 环形缓冲区 内核 → 用户(异步) 高(批量、无锁) 高频事件日志、系统调用追踪
BPF 尾部调用 内核 → 内核 极高(跳转表) 复杂网络策略链、多阶段处理

4.4.2 Perf Event 环形缓冲区详解

Perf Event 环形缓冲区是 eBPF 最常用的数据输出通道。内核 BPF 程序通过 bpf_perf_event_output() 将数据写入缓冲区,用户空间通过 perf_buffer__poll() 异步读取。

// 内核端:写入数据
bpf_perf_event_output(ctx, &my_map, BPF_F_CURRENT_CPU, &event_data, sizeof(event_data));

// 用户端:注册回调并轮询
void handle_event(void *ctx, int cpu, void *data, __u32 size) {
    // data 指向内核传递过来的数据
    process_event(data, size);
}
struct perf_buffer *pb = perf_buffer__new(map_fd, 8, handle_event, NULL, NULL, NULL);
while (1) { perf_buffer__poll(pb, timeout_ms); }
性能要点: Perf Event 缓冲区采用无锁环形队列设计,支持批量写入和批量读取。在 eBPF 程序中,应尽量将多个小事件合并为一个大事件写入,减少缓冲区操作次数,提升吞吐量。
eBPF 程序数据流架构:从事件挂载到用户空间输出 eBPF 程序数据流架构 内核空间 事件挂载点 Tracepoint / Kprobe / XDP BPF 程序 字节码沙箱执行 数据输出通道 Perf Event / Map 触发执行 写入数据 用户空间 用户态加载器 libbpf / bcc / Go 数据消费 perf_buffer__poll() 应用 策略/日志 读取事件数据 传递 输出

4.5 本章小结

  • eBPF 程序结构:由内核态 BPF 字节码和用户态加载器组成,通过 BPF Map 或 Perf Event 环形缓冲区通信。
  • 事件挂载点:Tracepoint、Kprobe、XDP、TC 等,决定了 BPF 程序的触发时机和适用范围。
  • 数据采集与输出:Perf Event 环形缓冲区是最常用的高性能输出通道,支持异步、批量、无锁的数据传递。
  • Hello World 实战:通过 libbpf 编写并运行了第一个 eBPF 程序,验证了从事件触发到数据输出的完整链路。