2. eBPF 基础概念:程序类型、Map、Helper 函数与验证器
eBPF(extended Berkeley Packet Filter)是一个在内核中运行沙箱化程序的技术,它允许用户在不修改内核源码或加载内核模块的情况下,安全、高效地扩展内核功能。理解其核心组件——程序类型、Map、Helper 函数和验证器——是掌握 eBPF 流量可视化的基石。
2.1 eBPF 程序类型:挂载点决定能力
eBPF 程序必须挂载到特定的内核事件上才能执行。不同的程序类型决定了程序可以访问的数据、触发的时机以及返回值的含义。在流量可视化场景中,最常用的类型如下:
| 程序类型 | 挂载点/事件 | 核心能力 | 流量可视化用途 |
|---|---|---|---|
| XDP | 网络驱动层(最早) | 直接操作原始数据包,性能极高 | DDoS 防御、早期包过滤、流量采样 |
| TC (ingress/egress) | 内核协议栈流量控制层 | 可修改数据包,支持重定向和整形 | 流量镜像、QoS、连接跟踪 |
| Kprobe/Uprobe | 内核/用户态函数入口或出口 | 动态插桩,几乎可探测任何函数 | 跟踪 tcp_v4_connect、sendmsg 等 |
| Tracepoint | 内核预定义的静态跟踪点 | 稳定 ABI,参数结构固定 | 跟踪 net:netif_receive_skb 等 |
| Socket Filter | 单个 socket 上 | 过滤特定 socket 的数据包 | 按进程/容器粒度抓取流量 |
选型建议: 在配额校准场景中,通常组合使用 TC(用于捕获经过网桥/虚拟网卡的流量)和 Kprobe(用于关联进程与 socket),以实现高精度、低开销的流量计量。
2.2 eBPF Map:内核与用户态的数据桥梁
Map 是 eBPF 程序与用户态程序之间共享数据的关键结构。它是一系列键值对存储,支持多种数据类型。eBPF 程序在内核中读写 Map,用户态程序通过 bpf() 系统调用读取或更新 Map 内容。
- 哈希表 (BPF_MAP_TYPE_HASH): 通用 KV 存储,适合按五元组、进程 PID 等索引查询。
- 数组 (BPF_MAP_TYPE_ARRAY): 固定大小,索引访问,适合存储预分配的计数器或配置。
- 环形缓冲区 (BPF_MAP_TYPE_RINGBUF): 高性能、有序的数据流,适合将捕获的数据包元数据批量推送到用户态。
- Per-CPU 数组 (BPF_MAP_TYPE_PERCPU_ARRAY): 每个 CPU 核心一份副本,避免锁竞争,适合高并发计数器。
流量可视化关键模式: 使用 哈希表 存储连接跟踪状态(源/目的 IP、端口、协议),使用 Per-CPU 数组 累加每个 CPU 上的流量字节数,最后通过 环形缓冲区 将聚合后的流量快照异步发送给用户态校准服务。
2.3 eBPF Helper 函数:安全的内核 API
eBPF 程序不能直接调用内核函数。Helper 函数是内核提供的一组经过验证的、稳定的 API,eBPF 程序只能通过这些函数与内核交互。它们封装了常见的操作,如数据包读取、Map 操作、获取当前时间等。
| Helper 函数 | 功能 | 典型应用 |
|---|---|---|
bpf_map_lookup_elem |
在 Map 中查找键对应的值 | 查询连接是否已跟踪 |
bpf_map_update_elem |
更新或插入 Map 条目 | 记录新连接的五元组 |
bpf_ktime_get_ns |
获取当前内核时间戳(纳秒) | 计算数据包延迟 |
bpf_skb_load_bytes |
从 socket buffer 中安全读取指定偏移的数据 | 解析 TCP/UDP 头部 |
bpf_redirect |
将数据包重定向到指定网络设备 | 流量镜像到分析端口 |
bpf_perf_event_output |
将数据发送到 Perf 事件环形缓冲区 | 将流量元数据上报用户态 |
2.4 eBPF 验证器:安全性的守护者
在 eBPF 程序加载到内核之前,必须通过验证器的严格检查。验证器确保程序不会导致内核崩溃、死锁或信息泄露。这是 eBPF 安全性的核心。
- 控制流完整性: 验证器模拟程序执行,确保所有路径都可达,且没有循环(除非有界循环被编译器展开)。
- 指针安全: 检查指针解引用是否在合法范围内,禁止空指针或越界访问。
- 栈大小限制: eBPF 程序的栈空间固定为 512 字节,验证器会检查栈使用是否溢出。
- Helper 函数参数校验: 确保传递给 Helper 函数的参数类型和大小正确。
调优注意: 验证器对程序复杂度有严格限制(指令数上限 100 万条)。在编写流量处理逻辑时,应避免复杂的循环或过深的函数调用链。如果程序被验证器拒绝,通常需要简化逻辑或使用 BPF 尾调用 将程序拆分为多个片段。
2.5 核心概念关系总结
这四个概念构成了 eBPF 程序的完整生命周期:
- 程序类型 决定了 eBPF 程序“在哪里”以及“为什么”被触发。
- Helper 函数 提供了 eBPF 程序“能做什么”的安全工具箱。
- Map 解决了 eBPF 程序“如何存储和共享”数据的问题。
- 验证器 确保了整个执行过程“不会破坏”内核稳定性。