eBPF(extended Berkeley Packet Filter)早已不是字面上的"扩展包过滤器"。从 Linux 3.18 起,它演化为一个运行在内核态的通用虚拟机:程序先被验证器做静态分析,再经 JIT 编译成本地机器码,挂载到 kprobe、tracepoint、XDP、cgroup 等钩子上执行。可观测性、网络加速、安全策略、流量整形,几乎所有现代内核子系统的可编程化都建立在这一层之上。
本文从 BPF 指令集与程序类型出发,剖析 BTF 类型信息与 CO-RE(Compile Once - Run Everywhere)重定位原理,详解 libbpf 的工程骨架与 bpftool gen skeleton 生成流程,对比 ring buffer 与 perf buffer 的取舍,梳理 kprobe、tracepoint、fentry、XDP 等挂载点与 SEC() 段名约定,最后给出生产部署要点与验证器报错排查清单。
一、eBPF 基础回顾
1.1 从 cBPF 到 eBPF 的演进
最初的 BPF(后称 cBPF)诞生于 1992 年,用于 tcpdump 在内核态过滤数据包,只有两个 32 位寄存器 A 与 X。2014 年 Linux 3.18 引入 eBPF,寄存器扩展为 11 个 64 位通用寄存器,并配套了验证器、JIT 与 map 机制:
| 时间点 | 内核版本 | 关键能力 |
|---|---|---|
| 2014 | 3.18 | eBPF 指令集、bpf() 系统调用、JIT |
| 2016 | 4.4 | kprobe 挂载、perf event 输出 |
| 2018 | 4.18 | BTF 类型信息,CO-RE 的前提 |
| 2019 | 5.3 | 有界循环、BPF_PROG_TYPE_TRACING |
| 2020 | 5.8 | ring buffer、CAP_BPF 与 CAP_PERFMON |
| 2021 | 5.11 | map 内存计入 memcg,摆脱 memlock |
1.2 指令集与寄存器约定
eBPF 指令定长 8 字节,格式为 opcode:8 | dst_reg:4 | src_reg:4 | off:16 | imm:32。寄存器约定是编写程序时必须牢记的契约:
- r0:函数返回值,也是程序退出时的返回码
- r1 ~ r5:函数调用参数,跨调用不保证保留
- r6 ~ r9:被调用者保存,可跨 helper 调用存活
- r10:只读栈帧指针,指向 512 字节栈顶,不可写
栈空间上限是 512 字节,这是验证器最常触发的限制之一,任何超过该尺寸的局部数组都会在加载阶段被拒绝。
1.3 程序类型一览
不同挂载点对应不同的程序类型,验证器会据此施加不同的能力约束,包括可调用的 helper 集合、可访问的内存区域与返回值语义:
| 程序类型 | 典型用途 | 典型挂载点 |
|---|---|---|
BPF_PROG_TYPE_KPROBE | 内核函数探针 | kprobe/、kretprobe/ |
BPF_PROG_TYPE_TRACEPOINT | 静态追踪点 | tracepoint/ |
BPF_PROG_TYPE_TRACING | fentry/fexit/tp_btf | fentry/、fexit/ |
BPF_PROG_TYPE_XDP | 网卡驱动层收包 | XDP hook |
BPF_PROG_TYPE_SCHED_CLS | tc 分类器 | tc ingress/egress |
BPF_PROG_TYPE_CGROUP_SKB | 容器网络策略 | cgroup v2 |
BPF_PROG_TYPE_PERF_EVENT | 定时采样 | perf event |
1.4 加载流程与 JIT
所有用户态库最终都落到同一个系统调用上,加载失败时 log_buf 中会写入验证器的人类可读日志:
union bpf_attr attr = {
.prog_type = BPF_PROG_TYPE_KPROBE,
.insns = (unsigned long)insns,
.insn_cnt = insn_cnt,
.license = (unsigned long)"GPL",
.log_buf = (unsigned long)log_buf,
.log_size = log_size,
.log_level = 1,
};
int fd = syscall(__NR_bpf, BPF_PROG_LOAD, &attr, sizeof(attr));
加载的完整链路是编译、验证、JIT、挂载。验证器会做控制流图分析、寄存器状态追踪、内存边界检查与有界循环证明;JIT 则把 eBPF 指令翻译为宿主机指令,x86-64 上寄存器几乎一对一映射,执行开销接近原生代码。
二、CO-RE 与 BTF 原理
2.1 为什么需要 CO-RE
早期 BCC 工具链的痛点是运行时编译:程序在目标机器上现场调用 clang 编译,依赖完整的内核头文件包,编译耗时可观,且无法在容器中轻松分发。更根本的问题是内核结构体布局随版本漂移,struct task_struct 的字段偏移在不同内核上并不相同。
CO-RE 的核心思想是编译一次,到处运行。程序在编译期记录下我要访问哪个类型的哪个字段这类意图,加载期由 libbpf 根据目标内核的真实 BTF 信息,把意图重定位成实际的字节偏移。
2.2 BTF 类型信息
BTF(BPF Type Format)是一种紧凑的类型元数据格式,从 Linux 4.18 起内建,描述了内核中所有结构体、联合体、枚举与函数原型的布局信息:
ls -l /sys/kernel/btf/vmlinux # 需要 CONFIG_DEBUG_INFO_BTF=y
grep CONFIG_DEBUG_INFO_BTF /boot/config-$(uname -r)
# 若内核未内建 BTF,可从 https://github.com/aquasecurity/btfhub 下载归档补齐
2.3 vmlinux.h 生成
有了 BTF,就能反向生成一份只包含类型声明、不含任何函数体的头文件,用它替代庞大的内核头文件依赖:
# 从运行中内核导出,约 12 万行,仅类型定义,无 include
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
# 也可从 BTFHub 归档导出,用于离线场景
bpftool btf dump file ./5.15.0-91-generic.btf format c > vmlinux.h
vmlinux.h 顶部包含 #ifndef __VMLINUX_H__ 保护宏,可直接被 .bpf.c 引入,配合 libbpf 的 bpf_helpers.h 使用。
2.4 preserve_access_index 与重定位
CO-RE 的实现依赖两件事:编译期标记与加载期重定位。
/* vmlinux.h 中每个结构体都被标注了 preserve_access_index,
编译器会为每次字段访问额外生成一条重定位记录 */
struct task_struct {
int __state;
pid_t pid;
} __attribute__((preserve_access_index));
pid_t pid = task->pid; /* 直接访问即可,编译器自动埋点 */
加载时 libbpf 遍历这些记录,按类型名与字段名在当前内核 BTF 中查找真实偏移并改写指令。重定位共有三类:
| 重定位类型 | 触发来源 | 失败后果 |
|---|---|---|
| field-based | 结构体字段访问 | 字段不存在则加载失败 |
| type-based | bpf_core_type_id_* 等宏 | 类型不匹配则加载失败 |
| enum-based | 枚举值引用 | 枚举项缺失则加载失败 |
对多级解引用,推荐使用 BPF_CORE_READ 宏,它会自动处理中间指针的逐级读取与重定位:
#include <bpf/bpf_core_read.h>
pid_t ppid = BPF_CORE_READ(task, real_parent, pid);
/* 需要写入局部指针变量时改用 bpf_core_read */
struct task_struct *parent;
bpf_core_read(&parent, sizeof(parent), &task->real_parent);
三、libbpf 工程骨架
3.1 目录结构
libbpf-bootstrap 定义了事实上的标准布局,每个工具由三个核心文件与一个 Makefile 组成:
myprobe/
├── myprobe.bpf.c # 内核态:BPF 程序
├── myprobe.c # 用户态:加载器与事件消费
├── myprobe.h # 共享的事件结构体定义
├── vmlinux.h # 由 bpftool 生成
├── Makefile
└── .output/
├── myprobe.skel.h # 由 bpftool gen skeleton 生成
└── myprobe.bpf.o
3.2 内核态程序
内核态文件以 SEC() 段名声明挂载点,程序体是一个普通 C 函数:
// myprobe.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include "myprobe.h"
char LICENSE[] SEC("license") = "GPL";
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} events SEC(".maps");
SEC("kprobe/do_unlinkat")
int BPF_KPROBE(handle_unlink, int dfd, struct filename *name)
{
struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
if (!e)
return 0;
e->pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&e->comm, sizeof(e->comm));
bpf_probe_read_kernel_str(&e->fname, sizeof(e->fname),
BPF_CORE_READ(name, name));
bpf_ringbuf_submit(e, 0);
return 0;
}
3.3 用户态加载器
用户态通过 skeleton 暴露的 __open、__load、__attach 三步完成加载,并用 ring_buffer__poll 消费事件:
// myprobe.c
#include <stdio.h>
#include <bpf/libbpf.h>
#include "myprobe.skel.h"
static int on_event(void *ctx, void *data, size_t len)
{
const struct event *e = data;
(void)ctx; (void)len;
printf("%-16s %-8d %s\n", e->comm, e->pid, e->fname);
return 0;
}
int main(void)
{
struct myprobe_bpf *skel = myprobe_bpf__open_and_load();
struct ring_buffer *rb;
if (!skel)
return 1;
if (myprobe_bpf__attach(skel))
return 1;
rb = ring_buffer__new(bpf_map__fd(skel->maps.events), on_event, NULL, NULL);
while (ring_buffer__poll(rb, 100) >= 0)
;
myprobe_bpf__destroy(skel);
return 0;
}
3.4 skeleton 生成与 Makefile
bpftool gen skeleton 把编译好的 .bpf.o 转成一个自包含的 C 头文件,内嵌字节码与所有 map、程序、全局变量的元数据:
BPF_CFLAGS := -g -O2 -target bpf -D__TARGET_ARCH_x86 -I.output
.output/myprobe.bpf.o: myprobe.bpf.c myprobe.h vmlinux.h
@mkdir -p .output
clang $(BPF_CFLAGS) -c $< -o $@
.output/myprobe.skel.h: .output/myprobe.bpf.o
bpftool gen skeleton $< > $@
myprobe: myprobe.c .output/myprobe.skel.h
cc -g -O2 -I.output $< -o $@ -lbpf -lelf -lz
关键点:-target bpf 让 clang 输出 BPF 字节码而非本机码;-g 让 BTF 被写入 .bpf.o;skeleton 头文件使运行时无需任何外部文件,天然适合打包进容器镜像。
四、Map 类型与 ring buffer
4.1 常用 Map 类型
Map 是内核态与用户态共享数据的唯一官方通道,选型直接影响性能与语义:
| Map 类型 | 语义 | 适用场景 |
|---|---|---|
BPF_MAP_TYPE_HASH | 哈希表,自动扩容 | 通用键值统计 |
BPF_MAP_TYPE_ARRAY | 定长数组,索引即键 | 按 CPU 索引的计数器 |
BPF_MAP_TYPE_PERCPU_HASH | 每 CPU 一份副本 | 高频写入,避免锁竞争 |
BPF_MAP_TYPE_LRU_HASH | 容量满时淘汰最久未用 | 跟踪活跃连接 |
BPF_MAP_TYPE_RINGBUF | 无锁环形缓冲 | 事件流输出 |
BPF_MAP_TYPE_PERF_EVENT_ARRAY | perf 事件数组 | 旧式事件输出 |
BPF_MAP_TYPE_PROG_ARRAY | 程序跳转表 | tail call 分发 |
PERCPU_* 的关键价值在于免锁:每个 CPU 写自己的副本,用户态读取时再求和,适合每秒百万级更新的计数器。
4.2 ring buffer 原理
ring buffer 是内核 5.8 引入的单生产者单消费者共享内存环,内核态通过三步 API 使用:
/* 1. 预留空间:环满时返回 NULL,绝不阻塞 */
struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
if (!e)
return 0;
/* 2. 填充数据 */
e->pid = bpf_get_current_pid_tgid() >> 32;
/* 3. 提交使其对用户态可见,也可用 bpf_ringbuf_discard 撤销 */
bpf_ringbuf_submit(e, 0);
用户态消费接口是 ring_buffer__new、ring_buffer__poll 与 ring_buffer__consume,回调在 poll 返回时同步触发。ring buffer 支持多生产者语义,多个 CPU 可同时 reserve,内核用原子操作协调位置。
4.3 ring buffer 与 perf buffer 对比
| 维度 | perf buffer | ring buffer |
|---|---|---|
| 引入版本 | 4.4 | 5.8 |
| 内存模型 | 每 CPU 独立缓冲 | 全局共享环 |
| 事件顺序 | 跨 CPU 无全局顺序 | 全局有序 |
| 内存开销 | CPU 数乘以缓冲大小 | 单份缓冲 |
| 数据拷贝 | 两次 | 一次,走共享内存 |
| 通知机制 | 每事件一次唤醒 | 批量唤醒,可延迟 |
| 推荐度 | 仅兼容旧内核 | 新项目首选 |
五、挂载点与 SEC 段名
5.1 kprobe 与 kretprobe
kprobe 可挂到任意内核函数入口,是兼容性最好的探针,但属于不稳定 ABI,函数可能被内联或改名:
SEC("kprobe/do_unlinkat")
int BPF_KPROBE(handle_enter, int dfd, struct filename *name)
{
return 0;
}
/* 返回探针:用 PT_REGS_RC 宏提取返回值 */
SEC("kretprobe/do_unlinkat")
int BPF_KRETPROBE(handle_exit, int ret)
{
bpf_printk("do_unlinkat returned %d\n", ret);
return 0;
}
BPF_KPROBE 宏依赖 -D__TARGET_ARCH_x86 正确传入,否则寄存器到参数的映射会错位,这是参数全是乱码最常见的根因。
5.2 tracepoint 与 tp_btf
tracepoint 是内核开发者显式埋设的稳定接口,格式由 tracefs 定义,字段名固定不漂移。传统 tracepoint 的字段需要从 ctx 手动读取,而 tp_btf 直接使用内核函数原型,参数即字段:
SEC("tracepoint/syscalls/sys_enter_openat")
int handle_openat(struct trace_event_raw_sys_enter *ctx)
{
const char *fname = (const char *)ctx->args[1];
bpf_printk("openat: %s\n", fname);
return 0;
}
SEC("tp_btf/sched_switch")
int BPF_PROG(handle_switch, bool preempt,
struct task_struct *prev, struct task_struct *next)
{
bpf_printk("switch %d -> %d\n", prev->pid, next->pid);
return 0;
}
可在目标机器上枚举可用 tracepoint 与其字段格式:
cat /sys/kernel/tracing/available_events | grep '^syscalls:sys_enter_open'
cat /sys/kernel/tracing/events/sched/sched_switch/format
5.3 fentry 与 fexit
fentry/fexit 基于 BPF trampoline,从 5.5 起稳定,是 BPF 的原生挂载方式:无需修改内核指令、可同时拿到入参与返回值、性能优于 kprobe:
SEC("fentry/do_unlinkat")
int BPF_PROG(fentry_unlink, int dfd, struct filename *name)
{
bpf_printk("fentry: dfd=%d\n", dfd);
return 0;
}
/* fexit:最后一个参数是返回值 */
SEC("fexit/do_unlinkat")
int BPF_PROG(fexit_unlink, int dfd, struct filename *name, int ret)
{
bpf_printk("fexit: ret=%d\n", ret);
return 0;
}
fentry 要求目标内核导出 BTF,且函数不能是 static 或被完全内联。若加载报 -ENOTSUPP,通常意味着该函数没有 BTF 原型,退回 kprobe 即可。
5.4 XDP 与 tc
XDP 在网卡驱动收包最早阶段执行,返回码决定包的命运:
| 返回码 | 语义 |
|---|---|
XDP_PASS | 交给协议栈正常处理 |
XDP_DROP | 立即丢弃 |
XDP_TX | 从同一网卡原路发回 |
XDP_REDIRECT | 重定向到其他网卡或 CPU |
XDP_ABORTED | 出错路径,等价于丢弃 |
SEC("xdp")
int xdp_drop_icmp(struct xdp_md *ctx)
{
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return XDP_PASS;
if (eth->h_proto != bpf_htons(ETH_P_IP))
return XDP_PASS;
return XDP_PASS;
}
挂载用 ip link set dev eth0 xdp obj prog.o sec xdp,或用 libbpf 的 bpf_xdp_attach。tc 程序段名为 SEC("tc"),返回 TC_ACT_OK 或 TC_ACT_SHOT。
六、用户态与内核态通信
6.1 全局变量与只读配置
libbpf 允许把全局变量直接暴露成 map,用户态写入后内核态立即可见:
const volatile int target_pid = 0; /* 只读配置,用户态可覆写 */
int counter = 0; /* 可写状态,运行时共享 */
用户态通过 skeleton 直接赋值,且必须在 load 之前:
skel = myprobe_bpf__open();
skel->rodata->target_pid = getpid();
myprobe_bpf__load(skel);
.rodata 会被内核映射为只读页,验证器可对其做常量折叠,比 map 查找快得多;.bss 与 .data 则可读写。
6.2 map 查找与更新
内核态操作 map 的 helper 语义需要留意:bpf_map_lookup_elem 返回指向 map 内部的指针,就地修改即生效,无需再 update:
/* counts 为 BPF_MAP_TYPE_HASH,键是 pid,值是计数 */
SEC("kprobe/vfs_read")
int count_reads(void *ctx)
{
__u32 pid = bpf_get_current_pid_tgid() >> 32;
__u64 *val = bpf_map_lookup_elem(&counts, &pid);
if (val) {
__sync_fetch_and_add(val, 1); /* 原子自增 */
} else {
__u64 one = 1;
bpf_map_update_elem(&counts, &pid, &one, BPF_NOEXIST);
}
return 0;
}
用户态用 bpf_map__fd 拿到 fd 后,可用 bpf_map_lookup_elem、bpf_map_update_elem 与 bpf_map_delete_elem 三个 libbpf 封装操作,也可用 bpf_map_get_next_key 遍历。
6.3 常用 helper
| Helper | 作用 | 注意点 |
|---|---|---|
bpf_get_current_pid_tgid | 返回 pid 与 tgid 打包值 | 需右移 32 位取 pid |
bpf_get_current_comm | 写进程名到缓冲 | 上限 16 字节 |
bpf_ktime_get_ns | 单调时钟纳秒 | 适合算差值,不适合绝对时间 |
bpf_probe_read_kernel_str | 安全读内核字符串 | 越界返回负值,不崩溃 |
bpf_probe_read_user_str | 安全读用户态字符串 | 需先确认指针来自用户 |
bpf_get_prandom_u32 | 伪随机数 | 用于采样与哈希扰动 |
bpf_printk | 写 tracefs 调试输出 | 仅调试用,生产禁用 |
bpf_printk 的输出在 /sys/kernel/tracing/trace_pipe,格式限定为 3 个参数,且格式化在用户态完成,开销不小。
七、性能采样与生产部署
7.1 采样策略
高频事件全量记录会压垮 ring buffer 并显著抬升开销,标准做法是按比例采样:
/* 1/64 采样:掩码必须是 2 的幂减一 */
if ((bpf_get_prandom_u32() & 63) != 0)
return 0;
/* 也可只在特定 CPU 上采样,降低缓存争用 */
if (bpf_get_smp_processor_id() != 3)
return 0;
7.2 bpf_perf_event_output
对于 BPF_PROG_TYPE_PERF_EVENT 类型,例如 CPU 周期采样,输出走 perf 通道而非 ring buffer:
/* perf_events 为 BPF_MAP_TYPE_PERF_EVENT_ARRAY 类型的 map */
SEC("perf_event")
int on_sample(struct bpf_perf_event_data *ctx)
{
struct sample s = {};
s.pid = bpf_get_current_pid_tgid() >> 32;
s.ip = PT_REGS_IP(&ctx->regs);
bpf_perf_event_output(ctx, &perf_events, BPF_F_CURRENT_CPU,
&s, sizeof(s));
return 0;
}
用户态用 perf_buffer__new 与 perf_buffer__poll 消费,接口与 ring buffer 类似,但失去了全局有序性。
7.3 CO-RE 跨内核版本
CO-RE 的价值在异构集群中最明显,同一个二进制可覆盖多个内核版本。但仍有边界需要注意:
| 依赖项 | 最低版本 | 说明 |
|---|---|---|
| BTF 内建 | 5.2+ 发行版内核 | 需 CONFIG_DEBUG_INFO_BTF=y |
| fentry/fexit | 5.5 | 需 BTF 函数原型 |
| ring buffer | 5.8 | 低于此版本只能用 perf buffer |
CAP_BPF | 5.8 | 更细粒度的权限模型 |
| map 计入 memcg | 5.11 | 摆脱 RLIMIT_MEMLOCK 限制 |
对老内核(如 4.19 LTS),可借助 BTFHub 提供的外部 BTF 归档补齐类型信息,但 5.8 以下的 ring buffer 与 fentry 无法回填,需在代码中做运行时探测降级。
7.4 权限与资源限制
生产部署前必须核对的清单:
RLIMIT_MEMLOCK:5.11 之前 map 内存受此限制,需ulimit -l unlimited或调用setrlimit,否则加载报-EPERMCAP_BPF与CAP_PERFMON:5.8 起可将CAP_SYS_ADMIN拆分为更小的权限集,容器中优先授予这两个kernel.unprivileged_bpf_disabled:多数发行版默认设为 2,非特权用户完全无法加载- 容器中的
bpffs挂载:bpftool需要/sys/fs/bpf可写才能 pin 对象 /sys/kernel/btf/vmlinux可读:缺失则该机器无法运行 CO-RE 程序
八、验证器限制排查
8.1 获取验证器日志
验证器拒绝程序时,唯一有效的信息来源是 log_buf,libbpf 提供了更友好的封装:
#include <bpf/libbpf.h>
static int log_cb(enum libbpf_print_level lvl, const char *fmt, va_list ap)
{
return lvl == LIBBPF_DEBUG ? 0 : vfprintf(stderr, fmt, ap);
}
/* 注册后任何 open 或 load 失败都会打印完整验证器日志 */
libbpf_set_print(log_cb);
若使用原生 bpf() 系统调用,则显式提供缓冲并设置 log_level 为 1,设为 2 会输出逐指令的寄存器状态,信息量巨大但极慢:
bpftool prog load myprobe.bpf.o /sys/fs/bpf/myprobe \
type kprobe 2>&1 | head -50
8.2 常见错误码与对策
| 错误码 | 含义 | 典型对策 |
|---|---|---|
-EPERM | 权限不足或 memlock 超限 | 检查 CAP_BPF 与 ulimit -l |
-EACCES | 验证器拒绝 | 查看 log_buf,定位具体指令 |
-EINVAL | 参数非法或 helper 不适用 | 核对 helper 白名单与 attr 字段 |
-ENOENT | 挂载目标不存在 | kprobe 函数名拼写错误或被内联 |
-ENOTSUPP | 内核不支持该特性 | 降级到 kprobe 或检查内核版本 |
-EBUSY | 同一探针已被占用 | 清理残留的 pin 对象 |
8.3 验证器常见报错与对策
| 报错片段 | 根因 | 修复方式 |
|---|---|---|
invalid stack off | 栈使用超 512 字节 | 缩小局部数组或改用 map |
back-edge from insn | 无界循环 | 改成有界循环或用 #pragma unroll |
R1 type=scalar expected=fp | 指针类型丢失 | 用 bpf_probe_read 系列安全读取 |
A call to built-in function not supported | 调用了非 inline 函数 | 改用 __always_inline |
8.4 排查清单
遇到加载失败时,按以下顺序逐项排查:
- 先看
log_buf最后一行,验证器日志是倒序的,末尾才是根因 - 确认
SEC()段名与程序类型匹配,fentry/段配 kprobe 类型必然失败 - 检查栈用量,单个局部结构体超过 512 字节是头号杀手
- 确认循环有界,上界必须是编译期常量或已被验证
- 核对 helper 与程序类型的兼容性,
bpf_probe_read_user_str在 XDP 程序中不可用 - 确认 BTF 可用,
ls /sys/kernel/btf/vmlinux,缺失则 fentry 与 CO-RE 全部失效 - 检查 memlock 与 capability,用
ulimit -l与capsh --print核对,并用bpftool prog tracelog区分加载失败与加载成功但不触发
相关阅读
- https://plumephp.com/os-ebpf-observability/ —— eBPF 在内核可观测性体系中的整体定位与典型工具
- https://plumephp.com/os-kernel-tracing-ftrace/ —— ftrace 追踪框架,理解 tracepoint 与 kprobe 的内核侧基础
- https://plumephp.com/os-performance-tools/ —— perf 与系统性能分析工具链,eBPF 工具的搭档
延伸阅读
- Linux Kernel Documentation:
Documentation/bpf/,含btf.rst与 ringbuf 说明 - BPF CO-RE 参考文档:
Documentation/bpf/llvm_reloc.rst与 libbpf 仓库docs/目录 - libbpf-bootstrap 项目模板:https://github.com/libbpf/libbpf-bootstrap
- Brendan Gregg,《BPF Performance Tools》,生产环境 BPF 工具的权威参考
- Andrii Nakryiko 的 BPF CO-RE Reference Guide 与 libbpf overview 系列文章
- bpf(2) 与 bpf-helpers(7) man page
#!/usr/bin/env bash
# 完整可运行示例:从零构建一个 CO-RE 可移植的 eBPF 工具
# 依赖 clang、libbpf-dev、bpftool;内核要求 >= 5.8
set -euo pipefail
mkdir -p demo && cd demo
# 1. 生成 vmlinux.h,需要内核开启 CONFIG_DEBUG_INFO_BTF=y
[ -r /sys/kernel/btf/vmlinux ] || { echo "内核未导出 BTF,无法使用 CO-RE" >&2; exit 1; }
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
# 2. 共享事件结构体
echo 'struct event { __u32 pid; char comm[16]; char fname[128]; };' > demo.h
# 3. 内核态程序
cat > demo.bpf.c <<'EOF'
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include "demo.h"
char LICENSE[] SEC("license") = "GPL";
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} events SEC(".maps");
SEC("kprobe/do_unlinkat")
int BPF_KPROBE(handle_unlink, int dfd, struct filename *name)
{
struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
if (!e)
return 0;
e->pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&e->comm, sizeof(e->comm));
bpf_probe_read_kernel_str(&e->fname, sizeof(e->fname),
BPF_CORE_READ(name, name));
bpf_ringbuf_submit(e, 0);
return 0;
}
EOF
# 4. 编译内核态并生成 skeleton
mkdir -p .output
clang -g -O2 -target bpf -D__TARGET_ARCH_x86 -I. -c demo.bpf.c -o .output/demo.bpf.o
bpftool gen skeleton .output/demo.bpf.o > .output/demo.skel.h
# 5. 用户态加载器
cat > demo.c <<'EOF'
#include <stdio.h>
#include <bpf/libbpf.h>
#include "demo.skel.h"
static int on_event(void *c, void *d, size_t l) {
const struct event *e = d;
(void)c; (void)l;
printf("pid=%u comm=%s file=%s\n", e->pid, e->comm, e->fname);
return 0;
}
int main(void) {
struct demo_bpf *skel = demo_bpf__open();
struct ring_buffer *rb;
if (!skel)
return 1;
if (demo_bpf__load(skel) || demo_bpf__attach(skel))
return 1;
rb = ring_buffer__new(bpf_map__fd(skel->maps.events), on_event, NULL, NULL);
while (ring_buffer__poll(rb, 100) >= 0)
;
demo_bpf__destroy(skel);
return 0;
}
EOF
# 6. 编译并运行,需要 root 或 CAP_BPF 与 CAP_PERFMON
cc -g -O2 -I.output demo.c -o demo -lbpf -lelf -lz
ulimit -l unlimited 2>/dev/null || true
sudo ./demo &
sleep 1; touch /tmp/ebpf-demo && rm -f /tmp/ebpf-demo; sleep 1
sudo pkill -f './demo' || true
echo "=== 完成:上方应输出 unlink 事件 ==="
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。