eBPF 入门:原理、程序类型与上手实践

为什么近几年大家都在谈 eBPF?一句话:在不改内核源码的前提下,把“小而安全”的代码段挂到内核关键路径里运行,从而获得接近内核旁路的性能,同时保留 Linux 内核栈的安全与可维护性。

eBPF 是什么?

  • 定义:eBPF(extended Berkeley Packet Filter)是一种在 Linux 内核中安全运行的通用字节码与运行环境。
  • 工作方式:用户态加载 eBPF 字节码 → 内核验证器(Verifier)进行安全检查 → 通过后 JIT 编译并附着在特定钩子(hook)上执行。
  • 核心优势
    • 性能:在内核态早路径执行,减少用户/内核切换与协议栈开销。
    • 安全:Verifier 强校验(边界、类型、调用、上界循环等)避免内核崩溃。
    • 可编程:通过不同 hook 点覆盖网络、跟踪、系统调用、调度等多场景。
  • 原理:把一小段经过严格安全检查的“字节码程序”挂在内核关键路径的钩子上,事件一来就执行;程序只做受限且安全的操作,状态放在可共享的 Map 里,必要时由 JIT 变成本机机器码以提升性能。

  • 它是怎么跑起来的
    1. 写 C/Rust 小程序并编译为 eBPF 字节码;
    2. 用户态通过 bpf() 系统调用加载;
    3. Verifier 逐条检查安全与终止性;
    4. 通过后将程序附着到某个 hook(XDP/TC/Kprobe/Tracepoint/LSM 等);
    5. 事件触发时内核调用你的 eBPF 程序;
    6. 程序通过内核 helper 做受控操作,并用 Map 读写共享状态;
    7. 为提速可由 JIT 将字节码编译为本机机器码。
  • 关键组件
    • 字节码与运行时:跨版本、可验证;
    • Verifier:安全门卫;
    • Hook:把程序接到正确时机与位置;
    • Helper:白名单式内核能力(解析包头、重定向、时间等);
    • Map:共享状态与内核/用户态通信;
    • JIT:将字节码转为机器码以提速。
  • 为何又快又安全
    • 快:处于内核早路径,少用户/内核切换;JIT 后是本机机器码;
    • 安全:受限指令集 + 强验证 + 只能通过 helper 间接触达敏感资源。
  • 对比内核模块(LKM)
    • eBPF:热插拔、受限且强校验,出错不易拖垮内核;
    • LKM:能力无限但易带来稳定性风险,开发调试成本更高。

能挂到哪里?常见 Hook 一览

  • XDP(eXpress Data Path):网卡驱动最前面的收包路径,极低延迟,典型用于丢弃、重定向、负载均衡等。
  • TC(Traffic Control):在 ingress/egressskb 处理,能访问更丰富的协议栈信息。
  • Socket/CGroups:基于 socket/cgroup 的细粒度策略,如重定向、QoS、准入控制。
  • Kprobe/Kretprobe/Tracepoint/USDT:内核/用户态可观测性与性能分析。
  • LSM(Linux Security Module):基于 eBPF 的细粒度安全策略(需要内核支持)。

eBPF 程序类型与典型用途

  • XDP 程序:包过滤、负载均衡、DoS 缓解、快速 ACK。
  • TC BPF:深度包处理、QoS/整形、服务网格加速。
  • Tracing BPF:火焰图、系统调用/调度/文件 IO 观测。
  • LSM BPF:进程/文件访问控制、细粒度安全策略。

eBPF Maps:状态与通信的基石

  • 作用:在内核 eBPF 与用户态之间共享数据,也用于多个 eBPF 程序之间共享状态。
  • 常见类型HASHLRU_HASHARRAYPERCPU_ARRAYRINGBUFPROG_ARRAYSOCKHASH/SOCKMAP 等。
  • 实践要点
    • 小对象热数据放 ARRAY/PERCPU_ARRAY,高并发键值对用 LRU_HASH
    • 大量事件传输用 RINGBUF,低开销批量消费。
    • 使用 pinning 将 map 固定到 bpffs,支持热更新与进程重启后的复用。

一图看懂网络路径中的 eBPF 挂载点



flowchart LR
    NIC["网卡(RX 队列)"] --> XDP["XDP 钩子(驱动早路径)"]
    XDP --> TC_ING["TC Ingress(进入协议栈前)"]
    TC_ING --> IPSTACK["内核网络栈(路由/防火墙/套接字)"]
    IPSTACK --> TC_EGR["TC Egress(离开协议栈)"]
    TC_EGR --> NIC_TX["网卡(TX 队列)"]
    IPSTACK --> SOCK["Socket/CGroups eBPF"]

最小 XDP 示例:丢弃非期望包,并计数

目标:在 XDP 处直接丢弃不需要的流量,并通过 MAP 暴露计数供用户态读取(示例为入门直觉,不含全部错误处理)。

  • 内核端(eBPF C)核心思路
    • 定义 BPF_MAP_TYPE_ARRAY 计数器。
    • 在 XDP 回调里匹配条件(如以太类型/UDP 端口等),命中则递增计数并 XDP_DROP,否则 XDP_PASS
  • 用户态(libbpf/BCC 或者 bpf2go 等)
    • 加载/验证/附着程序到指定网卡。
    • 周期性读取 map 中的计数。

代码(xdp_drop.c)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// minimal XDP: 丢弃所有包并做计数
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>

struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, __u64);
} drop_cnt SEC(".maps");

SEC("xdp")
int xdp_drop_all(struct xdp_md *ctx) {
__u32 key = 0;
__u64 *val = bpf_map_lookup_elem(&drop_cnt, &key);
if (val) __sync_fetch_and_add(val, 1);
return XDP_DROP; // 示例:全部丢弃;按需替换为条件判定
}

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

逐行讲解

  • 引入头文件:<linux/bpf.h><bpf/bpf_helpers.h> 提供 eBPF 所需类型与辅助宏。
  • 定义 drop_cnt map:BPF_MAP_TYPE_ARRAYmax_entries=1,只用索引 0 存一个 64 位计数;SEC(".maps") 表示把它放在 maps 段,便于内核识别与加载。
  • SEC("xdp"):把后面的函数标记为 XDP 程序入口,会被附着到 XDP 钩子。
  • int xdp_drop_all(struct xdp_md *ctx):XDP 回调;每个到来的包都会调用一次;ctx 是该包上下文。
  • __u32 key = 0;:仅使用数组 map 的第 0 项作为计数槽位。
  • bpf_map_lookup_elem(&drop_cnt, &key):取出计数槽位的地址。
  • __sync_fetch_and_add(val, 1):原子加 1,避免多核并发更新丢计数。
  • return XDP_DROP;:丢弃当前包(不再进入协议栈)。若只想“统计不丢包”,可改为 XDP_PASS
  • LICENSE = "GPL";:声明 GPL 许可,部分 helper 需要 GPL 才可用。

编译(生成 eBPF 对象文件)

1
clang -O2 -g -target bpf -c xdp_drop.c -o build/xdp_drop.o
  • 参数说明
    • -O2:优化等级 2,生成更高质量字节码(更易通过 Verifier,运行更快)。
    • -g:带调试信息,便于排查与 bpftool prog dump jited/llvm 对照。
    • -target bpf:指定编译到 eBPF 目标架构。
    • -c xdp_drop.c:只编译不链接,生成目标文件。
    • -o build/xdp_drop.o:输出到目标路径(需先 mkdir -p build)。

加载与附着(使用 bpftool,并同时固定 map)

1
2
3
4
sudo mount -t bpf bpf /sys/fs/bpf || true
sudo bpftool prog load build/xdp_drop.o /sys/fs/bpf/xdp_drop type xdp \
map name drop_cnt pinned /sys/fs/bpf/drop_cnt
sudo bpftool net attach xdp pinned /sys/fs/bpf/xdp_drop dev <iface>
  • 参数说明
    • mount -t bpf bpf /sys/fs/bpf:挂载 bpffs(BPF 虚拟文件系统),用于固定程序/Map。
    • bpftool prog load … type xdp:把对象文件加载为 XDP 类型程序,并指定固定路径 /sys/fs/bpf/xdp_drop
    • map name drop_cnt pinned …:在加载时将名为 drop_cnt 的 map 固定到给定路径,便于后续读取与热更新。
    • bpftool net attach xdp pinned … dev <iface>:把已固定的程序附着到指定网卡 <iface>(如 eth0)。

验证(查看计数并制造流量)

1
sudo bpftool map dump pinned /sys/fs/bpf/drop_cnt | cat
  • 参数说明
    • map dump pinned <path>:读取固定在 <path> 的 map 内容;| cat 防止分页器拦截,完整输出到终端。
    • 预期看到 key: 0 value: <N>,随着入站流量增长而递增。

卸载与清理

1
2
sudo bpftool net detach xdp dev <iface>
sudo rm -f /sys/fs/bpf/xdp_drop /sys/fs/bpf/drop_cnt
  • 参数说明
    • net detach xdp dev <iface>:从网卡卸载 XDP 程序。
    • rm -f …:删除固定的程序与 map 对象,释放 bpffs 资源。

工具链与开发路线

  • 核心工具
    • clang/llvm:将 C 编译为 eBPF 字节码。
    • bpftool:检查/加载/查看 map 与 prog,配合 bpffs 管理对象。
    • libbpf/libbpf-rs/bcc:三种常见开发栈;前两者偏生产可部署,bcc 上手快但运行时依赖较重。
  • 建议路径
    1. 本机或 VM 安装较新内核与 bpftool/clang
    2. xdp-tutoriallibbpf-bootstrap 起步;
    3. 先做可观测性(风险低),再做网络路径改写(逐步放行/灰度);
    4. 为 map 启用 pinning,为服务编写最小的“控制面”管理进程;
    5. 加上可观测性(统计、延迟、错误码)与回滚开关。

什么是 JIT(Just-In-Time)

  • 定义:JIT(即时编译)在运行时把中间表示/字节码编译成本机机器码执行,相比纯解释更快,相比 AOT(预编译全部)启动更灵活。

  • 对比直觉
    • 解释执行:逐条解析,启动快,运行慢。
    • AOT:运行前全部编译,运行快,首次构建/启动稍慢。
    • JIT:运行中按需编译,折中并可做运行时优化。
  • 在 eBPF 中的工作流
    • 用户态加载 BPF 字节码 → Verifier 安全/终止性检查 → 通过后 JIT 为当前架构机器码(x86-64/ARM64) → 挂载到对应 hook 执行。
    • 若 JIT 被禁用或不支持,则回退为解释器执行;若 Verifier 未通过,直接拒绝加载。
  • 与 JVM/JS 的 JIT 差异
    • eBPF 的 JIT 通常是“一次性编译”,不做热点探测/去优化;优化更偏向架构相关的轻量指令选择(peephole、寄存器分配)。

与内核旁路的比较(DPDK/RDMA)

  • eBPF:保留内核网络栈生态与安全边界,延迟/吞吐优异(非极限),工程可运维性强。
  • DPDK/RDMA:极致性能与控制,但需要专用驱动/巨页/轮询,系统集成成本更高。
  • 折中理念:将“高频快路径”放 eBPF,“复杂慢路径”留在用户态或完整协议栈。

常见坑与排查

  • Verifier 不通过:缩短函数/循环,给出边界检查与上界,使用 __always_inline;善用 bpftool prog load 的日志。
  • 性能不达标
    • XDP 程序中避免复杂解析与多次访问内存;
    • 合理使用 XDP_REDIRECT 到目标队列或 AF_XDP
    • 批量读取 ringbuf,减少用户态唤醒开销。
  • 热更新中断:确保对象 pin 到 bpffs,并采用“双程序切换”减少流量抖动。

运行与验证(示例流程)

  • 编译与加载(以 libbpf 为例):
    1. 编译 eBPF 程序为 *.o
    2. bpftool prog load 加载,bpftool net attach xdp dev <iface> pinned <path> 附着;
    3. bpftool map dump pinned <path> 查看计数;
    4. ping/iperf/wrk 压测并对比 XDP_PASS/XDP_DROP 行为。
  • 回滚
    • 临时卸载:bpftool net detach xdp dev <iface>
    • 恢复策略:保留旧版本对象于 bpffs,切换符号链接并原子替换。

📝 小结

  • 一句话:eBPF 让我们在不改内核的前提下,将小而高效的逻辑“内联”到内核关键路径,既快又安全。
  • 入门三步:选 hook → 写最小程序 + map → 用 bpftool 验证与观测。
  • 进阶方向:编排与热更新、fast-path/slow-path 分层、复杂状态一致性与可观测性。