MIT 6.1810 · 第 22 课 · nz

内核可扩展性 (BPF)

BPF(Berkeley Packet Filter)提供了一种灵活、安全、低开销的方式让用户代码在内核中运行。本节课探讨 BPF 的设计、实现与性能优化。

授课教师:nz

年份:2025 秋季

难度:中等

论文:The BSD Packet Filter (McCanne, Jacobson)

动机:监控原始网络数据包

用户空间进程需要检查传入的网络数据包:

性能挑战

目标

指定内核应该复制或丢弃什么数据包——在内核中基于指定规则检查,避免不必要的复制。

论文思想:将过滤器指定为字节码程序

示例

# tcpdump -n -i wlan0 udp
# tcpdump -n -i wlan0 udp and port 123

BPF 是内核可扩展性的强大方法

与进程相比的限制

限制带来的好处

性能!限制使得高性能执行成为可能,并启用新的用例。

技术挑战

  1. 如何确保过滤器程序不崩溃(不损坏内存)?
  2. 如何确保过滤器程序不泄漏(不读取其他数据)?
  3. 如何确保过滤器程序不会永远运行?
  4. 如何高性能执行过滤器程序?

方案 1:编写解释器

// 解释器循环
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 纳秒

方案 2:更快的解释器(避免关键路径上的检查)

关键洞察:解释器慢的部分原因是执行检查。但这些检查与我们要过滤的数据包无关!

解决方案:预先验证 BPF 程序:bpf_prepare()

验证规则

性能

$ time ./run_fast_interp ../seccomp.bpf
bpf_run: 0x7fff0000
real    0m1.650s

// 每次调用约 16 纳秒(vs 24 纳秒)

方案 3:将 BPF 字节码翻译为硬件指令(JIT)

寄存器分配

x86-64 调用约定:
%rdi = 第一个函数参数 → 存储数据包指针
%eax = 函数返回值 → 存储 BPF 的 "A" 寄存器
%eax 是调用者保存的(函数可以修改它)

JIT 需要做什么

性能

$ time ./run_jit ../seccomp.bpf
bpf_run: 0x7fff0000
real    0m0.380s

// 每次调用约 3.8 纳秒(vs 16 纳秒解释器)

性能对比

方案每次调用时间相对速度
解释器(带检查)24 ns1x
快速解释器(预验证)16 ns1.5x
JIT 编译3.8 ns6.3x

Linux eBPF

Linux 支持更复杂的 BPF 版本,称为 eBPF:

eBPF 用例

示例:bpftrace

# bpftrace -e 'tracepoint:syscalls:sys_enter_mkdir {
    printf("PID %d calling mkdir(%s)\n", pid, str(args.pathname));
}'

示例:XDP 丢弃 ICMP

// 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 验证器

eBPF 验证器非常活跃的研究和内核开发领域:

要点总结

BPF 是内核可扩展性的强大工具

灵活、安全、低开销的方式让用户代码在内核中运行。

预验证是关键优化

将检查从关键路径移到程序加载时,解释器性能提升 50%。

JIT 编译提供数量级提升

将 BPF 字节码翻译为本地 CPU 指令,比解释器快 6 倍。

eBPF 是现代 Linux 的核心技术

用于跟踪、网络、安全等众多领域。

测验

Q1:BPF 解释器如何确保程序会终止?

Q2:为什么预验证能提高解释器性能?

Q3:JIT 编译为什么比解释器快?

延伸阅读