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,并做好内核版本兼容性测试。
eBPF 核心组件架构与数据流 eBPF 核心组件架构与数据流 用户态控制面 (bpftool / libbpf / 自定义应用) 读取/写入 Map 读取事件流 内核态 eBPF 虚拟机 (JIT 编译 & 验证器) 调用 Helper 函数 Helper 函数 (bpf_map_*, bpf_perf_event_*, bpf_ktime_*) 操作 Map eBPF Map (Hash / Array / Ringbuf) 附加到探针 内核探针 (kprobe / tracepoint) 挂载到内核事件 Linux 内核 (网络栈 / 内存管理 / 调度器)

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 核心上并发更新计数,而无需担心数据竞争。