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 程序中,应尽量将多个小事件合并为一个大事件写入,减少缓冲区操作次数,提升吞吐量。
4.5 本章小结
- eBPF 程序结构:由内核态 BPF 字节码和用户态加载器组成,通过 BPF Map 或 Perf Event 环形缓冲区通信。
- 事件挂载点:Tracepoint、Kprobe、XDP、TC 等,决定了 BPF 程序的触发时机和适用范围。
- 数据采集与输出:Perf Event 环形缓冲区是最常用的高性能输出通道,支持异步、批量、无锁的数据传递。
- Hello World 实战:通过 libbpf 编写并运行了第一个 eBPF 程序,验证了从事件触发到数据输出的完整链路。