BPF 是内核可扩展性的强大工具
灵活、安全、低开销的方式让用户代码在内核中运行。
MIT 6.1810 · 第 22 课 · nz
BPF(Berkeley Packet Filter)提供了一种灵活、安全、低开销的方式让用户代码在内核中运行。本节课探讨 BPF 的设计、实现与性能优化。
用户空间进程需要检查传入的网络数据包:
指定内核应该复制或丢弃什么数据包——在内核中基于指定规则检查,避免不必要的复制。
# tcpdump -n -i wlan0 udp
# tcpdump -n -i wlan0 udp and port 123
性能!限制使得高性能执行成为可能,并启用新的用例。
// 解释器循环
for (int i = 0; i < nins; i++) {
switch (pc->opcode) {
case BPF_LD: ... break;
case BPF_JMP: ... break;
case BPF_RET: ... break;
default: return 0; // 非法操作码
}
}
因此,整个 BPF 程序的执行会"快速"完成。
$ time ./run_interp ../seccomp.bpf
bpf_run: 0x7fff0000
real 0m2.392s
user 0m2.384s
sys 0m0.002s
// 每次调用约 24 纳秒
// 对比:切换到用户空间(如 getpid)约 100 纳秒
关键洞察:解释器慢的部分原因是执行检查。但这些检查与我们要过滤的数据包无关!
解决方案:预先验证 BPF 程序:bpf_prepare()
$ time ./run_fast_interp ../seccomp.bpf
bpf_run: 0x7fff0000
real 0m1.650s
// 每次调用约 16 纳秒(vs 24 纳秒)
x86-64 调用约定:
%rdi = 第一个函数参数 → 存储数据包指针
%eax = 函数返回值 → 存储 BPF 的 "A" 寄存器
%eax 是调用者保存的(函数可以修改它)
$ time ./run_jit ../seccomp.bpf
bpf_run: 0x7fff0000
real 0m0.380s
// 每次调用约 3.8 纳秒(vs 16 纳秒解释器)
| 方案 | 每次调用时间 | 相对速度 |
|---|---|---|
| 解释器(带检查) | 24 ns | 1x |
| 快速解释器(预验证) | 16 ns | 1.5x |
| JIT 编译 | 3.8 ns | 6.3x |
Linux 支持更复杂的 BPF 版本,称为 eBPF:
# bpftrace -e 'tracepoint:syscalls:sys_enter_mkdir {
printf("PID %d calling mkdir(%s)\n", pid, str(args.pathname));
}'
// drop_icmp.c
SEC("xdp")
int drop_icmp(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
if (eth + 1 > data_end) return XDP_PASS;
if (eth->h_proto == htons(ETH_P_IP)) {
struct iphdr *ip = data + sizeof(*eth);
if (ip + 1 > data_end) return XDP_PASS;
if (ip->protocol == IPPROTO_ICMP)
return XDP_DROP;
}
return XDP_PASS;
}
eBPF 验证器非常活跃的研究和内核开发领域:
灵活、安全、低开销的方式让用户代码在内核中运行。
将检查从关键路径移到程序加载时,解释器性能提升 50%。
将 BPF 字节码翻译为本地 CPU 指令,比解释器快 6 倍。
用于跟踪、网络、安全等众多领域。
Q1:BPF 解释器如何确保程序会终止?
Q2:为什么预验证能提高解释器性能?
Q3:JIT 编译为什么比解释器快?