为什么近几年大家都在谈 eBPF?一句话:在不改内核源码的前提下,把“小而安全”的代码段挂到内核关键路径里运行,从而获得接近内核旁路的性能,同时保留 Linux 内核栈的安全与可维护性。
eBPF 是什么?
能挂到哪里?常见 Hook 一览
- XDP(eXpress Data Path):网卡驱动最前面的收包路径,极低延迟,典型用于丢弃、重定向、负载均衡等。
- TC(Traffic Control):在
ingress/egress
对 skb
处理,能访问更丰富的协议栈信息。
- 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 程序之间共享状态。
- 常见类型:
HASH
、LRU_HASH
、ARRAY
、PERCPU_ARRAY
、RINGBUF
、PROG_ARRAY
、SOCKHASH
/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
| #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_ARRAY
且 max_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
)。
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
上手快但运行时依赖较重。
- 建议路径:
- 本机或 VM 安装较新内核与
bpftool/clang
;
- 用
xdp-tutorial
或 libbpf-bootstrap
起步;
- 先做可观测性(风险低),再做网络路径改写(逐步放行/灰度);
- 为 map 启用
pinning
,为服务编写最小的“控制面”管理进程;
- 加上可观测性(统计、延迟、错误码)与回滚开关。
什么是 JIT(Just-In-Time)
与内核旁路的比较(DPDK/RDMA)
- eBPF:保留内核网络栈生态与安全边界,延迟/吞吐优异(非极限),工程可运维性强。
- DPDK/RDMA:极致性能与控制,但需要专用驱动/巨页/轮询,系统集成成本更高。
- 折中理念:将“高频快路径”放 eBPF,“复杂慢路径”留在用户态或完整协议栈。
常见坑与排查
- Verifier 不通过:缩短函数/循环,给出边界检查与上界,使用
__always_inline
;善用 bpftool prog load
的日志。
- 性能不达标:
- XDP 程序中避免复杂解析与多次访问内存;
- 合理使用
XDP_REDIRECT
到目标队列或 AF_XDP
;
- 批量读取 ringbuf,减少用户态唤醒开销。
- 热更新中断:确保对象
pin
到 bpffs,并采用“双程序切换”减少流量抖动。
运行与验证(示例流程)
- 编译与加载(以 libbpf 为例):
- 编译 eBPF 程序为
*.o
;
bpftool prog load
加载,bpftool net attach xdp dev <iface> pinned <path>
附着;
bpftool map dump pinned <path>
查看计数;
- 用
ping/iperf/wrk
压测并对比 XDP_PASS/XDP_DROP
行为。
- 回滚:
- 临时卸载:
bpftool net detach xdp dev <iface>
;
- 恢复策略:保留旧版本对象于 bpffs,切换符号链接并原子替换。
📝 小结
- 一句话:eBPF 让我们在不改内核的前提下,将小而高效的逻辑“内联”到内核关键路径,既快又安全。
- 入门三步:选 hook → 写最小程序 + map → 用 bpftool 验证与观测。
- 进阶方向:编排与热更新、fast-path/slow-path 分层、复杂状态一致性与可观测性。