2. eBPF 基础回顾:eBPF 程序类型、eBPF Map、eBPF Helper 函数与内核探针
在深入资源配额校准之前,必须牢固掌握 eBPF 的核心基础组件。本章将系统回顾 eBPF 程序类型、eBPF Map、Helper 函数以及内核探针(kprobe/tracepoint)这四大支柱,为后续的故障排查场景打下坚实的技术地基。
2.1 eBPF 程序类型:挂载点决定观测视角
eBPF 程序类型决定了程序可以挂载到内核的哪个子系统,以及能够访问哪些上下文数据。不同的程序类型对应不同的观测场景。
| 程序类型 | 挂载点 / 触发事件 | 典型应用场景 |
|---|---|---|
| BPF_PROG_TYPE_KPROBE | 内核函数入口/返回 (kprobe/kretprobe) | 动态追踪任意内核函数,如 tcp_connect、__alloc_skb |
| BPF_PROG_TYPE_TRACEPOINT | 内核静态 tracepoint | 稳定追踪系统调用、网络事件、调度事件 |
| BPF_PROG_TYPE_XDP | 网卡驱动层(最早 hook) | 高性能 DDoS 防护、负载均衡、包过滤 |
| BPF_PROG_TYPE_SCHED_CLS | TC(Traffic Control)入/出方向 | 流量整形、QoS、网络策略 |
| BPF_PROG_TYPE_SOCKET_FILTER | socket 层(AF_PACKET) | socket 级别的包过滤与监控 |
| BPF_PROG_TYPE_CGROUP_SKB | cgroup 网络入口/出口 | 容器网络带宽限制与审计 |
故障排查提示: 在资源配额超限场景中,
BPF_PROG_TYPE_KPROBE 和 BPF_PROG_TYPE_TRACEPOINT 是最常用的两种类型。前者用于动态探测内核函数调用栈,后者用于稳定捕获系统调用级别的配额检查事件。
2.2 eBPF Map:内核与用户态的数据桥梁
eBPF Map 是内核态 eBPF 程序与用户态控制面之间共享数据的关键结构。Map 以键值对形式存储,支持多种数据类型,是实现状态聚合、计数、配置下发的基础。
- 哈希表 (BPF_MAP_TYPE_HASH):基于键的快速查找,适合存储连接跟踪表、进程 PID 映射。
- 数组 (BPF_MAP_TYPE_ARRAY):预分配固定大小的连续内存,适合存储 CPU 核心统计、per-CPU 计数器。
- Per-CPU 数组/哈希 (BPF_MAP_TYPE_PERCPU_ARRAY / PERCPU_HASH):每个 CPU 核心拥有独立副本,避免锁竞争,适合高频计数场景。
- 环形缓冲区 (BPF_MAP_TYPE_RINGBUF):高效的内核到用户态数据传输通道,适合事件日志流。
- 栈跟踪 (BPF_MAP_TYPE_STACK_TRACE):专门存储内核调用栈,与 kprobe 配合使用。
关键概念: Map 的生命周期与 eBPF 程序分离。即使挂载 eBPF 程序的文件描述符被关闭,只要存在引用 Map 的 pin 路径(
/sys/fs/bpf/),Map 数据就会持久保留。这在故障现场保留关键计数数据时非常有用。
2.3 eBPF Helper 函数:安全的内核能力调用
eBPF 程序不能直接调用内核函数,必须通过预定义的 Helper 函数来安全地访问内核数据或执行特定操作。Helper 函数是 eBPF 虚拟机与内核交互的唯一合法通道。
| Helper 函数 | 功能描述 | 资源配额场景用途 |
|---|---|---|
bpf_get_current_pid_tgid() |
获取当前进程的 PID 和 TGID | 识别触发配额检查的进程 |
bpf_get_current_comm() |
获取当前进程名称 | 定位异常进程名称 |
bpf_map_lookup_elem() |
从 Map 中查找键对应的值 | 读取当前资源使用计数 |
bpf_map_update_elem() |
更新 Map 中的键值对 | 更新资源使用计数或标记超限状态 |
bpf_perf_event_output() |
将数据发送到用户态 perf 缓冲区 | 输出配额超限告警事件 |
bpf_ktime_get_ns() |
获取当前内核时间戳(纳秒) | 记录配额超限发生的时间点 |
2.4 内核探针:动态与静态观测点
内核探针是 eBPF 程序挂载到内核执行路径上的“钩子”。主要分为动态探针(kprobe)和静态探针(tracepoint)。
- kprobe / kretprobe:动态插桩,可以挂载到任意内核函数入口或返回点。灵活性极高,但可能因内核版本变化导致函数签名变更。
- tracepoint:内核开发者预定义的静态事件点,接口稳定,适合生产环境长期监控。
稳定性警告: 在资源配额校准场景中,优先使用 tracepoint(如
syscalls:sys_enter_write、cgroup:cg_mem_charge)进行监控。仅在 tracepoint 无法覆盖所需内核函数时,才谨慎使用 kprobe,并做好内核版本兼容性测试。
2.5 综合示例:使用 kprobe 监控文件写入配额
以下是一个完整的 eBPF 程序片段,演示如何通过 kprobe 挂载到 vfs_write 函数,结合 Map 和 Helper 函数,实现对进程写入字节数的实时计数。
// 定义 Map:进程 PID -> 累计写入字节数
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1024);
__type(key, u32);
__type(value, u64);
} write_bytes_map SEC(".maps");
SEC("kprobe/vfs_write")
int trace_vfs_write(struct pt_regs *ctx)
{
u32 pid = bpf_get_current_pid_tgid() >> 32;
u64 *count, new_count = 0;
// 从 Map 中查找当前进程的累计写入字节数
count = bpf_map_lookup_elem(&write_bytes_map, &pid);
if (count) {
new_count = *count;
}
// 获取写入的字节数(第二个参数)
// 注意:x86_64 架构下,第二个参数在寄存器 si 中
size_t bytes_written = (size_t)PT_REGS_PARM2(ctx);
new_count += bytes_written;
// 更新 Map
bpf_map_update_elem(&write_bytes_map, &pid, &new_count, BPF_ANY);
// 检查是否超过配额(例如 1MB)
if (new_count > 1048576) {
// 输出告警事件到用户态
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU,
&pid, sizeof(pid));
}
return 0;
}
故障排查要点: 上述代码中,
bpf_map_lookup_elem 和 bpf_map_update_elem 是资源配额校准的核心 Helper 函数。通过 Map 的原子操作,可以安全地在多个 CPU 核心上并发更新计数,而无需担心数据竞争。