4. eBPF 程序类型详解:kprobe、tracepoint、perf_event、xdp、tc、socket filter 等程序类型及其应用场景

eBPF 的强大之处在于其多样化的程序类型,每种类型都绑定到特定的内核事件或网络路径。理解这些程序类型及其挂载点,是编写高效 eBPF 程序的基础。本章将深入剖析最常用的几种 eBPF 程序类型,包括它们的触发机制、数据上下文以及典型应用场景。

4.1 程序类型概览与分类

eBPF 程序类型决定了程序可以挂载到内核的哪个位置,以及可以访问哪些内核数据结构。根据功能领域,主要分为以下几类:

分类 程序类型 触发点 典型应用
内核动态插桩 BPF_PROG_TYPE_KPROBE 任意内核函数入口/出口 函数调用追踪、参数/返回值分析、性能瓶颈定位
内核静态插桩 BPF_PROG_TYPE_TRACEPOINT 内核预定义的静态 tracepoint 系统调用监控、文件系统操作、调度事件追踪
性能监控 BPF_PROG_TYPE_PERF_EVENT 硬件 PMU 事件、软件事件、tracepoint CPU 周期计数、缓存命中率、采样分析
网络数据包处理 BPF_PROG_TYPE_XDP 网卡驱动层(最早网络包处理点) DDoS 防御、负载均衡、高速包过滤
网络流量控制 BPF_PROG_TYPE_SCHED_CLS (TC) 内核协议栈的 TC 层(ingress/egress) 流量整形、QoS、容器网络策略
套接字过滤 BPF_PROG_TYPE_SOCKET_FILTER 套接字层(socket) 抓包过滤、应用层协议分析、socket 监控
选择指南: 如果需要监控一个内核函数,但该函数没有 tracepoint,使用 kprobe。如果内核已经提供了稳定的 tracepoint(如 sys_enter_open),优先使用 tracepoint,因为它具有更稳定的 API。对于高性能网络处理,XDP 是最佳选择,而 TC 则更适合需要与内核协议栈交互的场景。

4.2 内核动态插桩:kprobe 与 kretprobe

kprobe 允许你在几乎任何内核指令地址处动态插入探测点。kretprobe 是其变体,专门用于捕获函数的返回值。

  • kprobe:在函数入口处触发,可以访问函数参数(通过 pt_regs 寄存器)。
  • kretprobe:在函数返回时触发,可以访问函数返回值(通过 pt_regs 中的返回值寄存器)。
应用场景: 调试内核驱动、追踪特定内核函数的调用频率和延迟、监控文件系统操作(如 vfs_write)。
// kprobe 示例:监控 do_sys_open 函数的调用
SEC("kprobe/do_sys_open")
int kprobe__do_sys_open(struct pt_regs *ctx)
{
    // 从寄存器中读取第一个参数(文件名指针)
    char __user *filename = (char *)PT_REGS_PARM1(ctx);
    bpf_printk("open syscall called, filename: %s\n", filename);
    return 0;
}

// kretprobe 示例:捕获 do_sys_open 的返回值(文件描述符)
SEC("kretprobe/do_sys_open")
int kretprobe__do_sys_open(struct pt_regs *ctx)
{
    int ret = PT_REGS_RC(ctx);
    bpf_printk("open syscall returned: %d\n", ret);
    return 0;
}
kprobe / kretprobe 执行流程 用户空间程序 系统调用 内核函数 (do_sys_open) kprobe (入口) kretprobe (出口) 返回 关键点 • kprobe 在函数入口触发 • kretprobe 在函数返回触发 • 通过 pt_regs 访问参数/返回值 • 对内核版本变化敏感 • 生产环境需谨慎使用

4.3 内核静态插桩:tracepoint

tracepoint 是内核开发者预定义的静态探测点,它们位于内核代码的关键路径上,如系统调用入口/出口、调度器事件、文件系统操作等。与 kprobe 相比,tracepoint 具有更稳定的 API,因为其参数结构是固定的。

  • 稳定性:tracepoint 的签名(参数类型和数量)在不同内核版本间保持稳定。
  • 安全性:由于是静态定义的,tracepoint 不会因为内核函数内联或重命名而失效。
  • 性能:tracepoint 的开销通常低于 kprobe,因为它们是内核预定义的检查点。
注意: 虽然 tracepoint 更稳定,但并非所有内核函数都有对应的 tracepoint。当需要监控的函数没有 tracepoint 时,kprobe 是唯一的选择。
// tracepoint 示例:监控系统调用 open 的入口
SEC("tracepoint/syscalls/sys_enter_openat")
int tracepoint__syscalls__sys_enter_openat(struct trace_event_raw_sys_enter *ctx)
{
    // 访问系统调用参数
    const char __user *filename = (const char __user *)ctx->args[1];
    bpf_printk("openat called, filename: %s\n", filename);
    return 0;
}
tracepoint 与 kprobe 对比 kprobe • 动态插桩,任意函数 • 依赖内核函数名 • 可能因内联/重命名失效 • 适合调试和临时分析 • 性能开销相对较高 tracepoint • 静态插桩,预定义位置 • 稳定的 API 签名 • 跨内核版本兼容性好 • 适合生产环境监控 • 性能开销较低 优先选择

4.4 性能监控:perf_event

BPF_PROG_TYPE_PERF_EVENT 程序类型与 Linux 性能监控子系统(perf)深度集成。它可以附加到硬件 PMU(Performance Monitoring Unit)事件、软件事件或 tracepoint 上,用于收集细粒度的性能数据。

  • 硬件事件:CPU 周期、指令数、缓存未命中、分支预测错误等。
  • 软件事件:上下文切换、页面错误、CPU 迁移等。
  • 采样模式:可以配置为周期性采样(如每 10000 个周期触发一次),用于生成火焰图。
典型用例: 使用 perf_event 程序监控特定进程的 CPU 使用率,或分析数据库查询时的缓存行为。
// perf_event 示例:监控 CPU 周期事件
SEC("perf_event")
int perf_event_handler(struct bpf_perf_event_data *ctx)
{
    // 获取当前 CPU 的周期计数
    u64 cycles = bpf_perf_event_read(&ctx->regs);
    bpf_printk("CPU cycles: %llu\n", cycles);
    return 0;
}

4.5 网络数据包处理:XDP 与 TC

XDP(eXpress Data Path)和 TC(Traffic Control)是 eBPF 在网络领域的两个核心程序类型,它们工作在网络协议栈的不同层次。

特性 XDP TC (cls_bpf)
处理位置 网卡驱动层,协议栈之前 内核协议栈内部
性能 极高(可达到线速) 高(但低于 XDP)
可访问数据 原始网络数据包(以太网帧) sk_buff 结构体(包含协议头信息)
返回动作 XDP_DROP, XDP_PASS, XDP_TX, XDP_REDIRECT TC_ACT_OK, TC_ACT_SHOT, TC_ACT_PIPE, TC_ACT_REDIRECT
典型场景 DDoS 防护、负载均衡、包过滤 流量整形、QoS、容器网络策略
// XDP 程序示例:丢弃所有 UDP 包
SEC("xdp")
int xdp_drop_udp(struct xdp_md *ctx)
{
    void *data = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;

    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return XDP_PASS;

    // 检查 IP 协议
    if (eth->h_proto != __constant_htons(ETH_P_IP))
        return XDP_PASS;

    struct iphdr *ip = data + sizeof(struct ethhdr);
    if ((void *)(ip + 1) > data_end)
        return XDP_PASS;

    // 丢弃 UDP 包
    if (ip->protocol == IPPROTO_UDP)
        return XDP_DROP;

    return XDP_PASS;
}

// TC 程序示例:统计特定端口的流量
SEC("tc")
int tc_count_port(struct __sk_buff *skb)
{
    // 解析 skb 中的传输层头
    // 统计目的端口为 8080 的包
    // ...
    return TC_ACT_OK;
}
XDP 与 TC 在网络协议栈中的位置 网卡 (NIC) XDP 驱动层 内核协议栈 TC Ingress TC Egress 套接字 (Socket) 处理层次对比 XDP 在协议栈之前处理包 可以 DROP 包,避免协议栈开销 TC 在协议栈内部处理 可以访问 sk_buff 结构 支持 Ingress 和 Egress 方向 XDP 性能优于 TC TC 功能更丰富

4.6 套接字过滤:socket filter

BPF_PROG_TYPE_SOCKET_FILTER 是最早的 eBPF 程序类型之一,它允许你在套接字层过滤数据包。它通常与 setsockopt 系统调用配合使用,附加到特定的 socket 上。

  • 过滤粒度:可以基于协议、IP 地址、端口、数据包内容等进行过滤。
  • 应用场景:tcpdump 工具底层就使用了 socket filter 来实现抓包过滤。
  • 性能:在内核态执行过滤,避免了将不需要的数据包拷贝到用户空间。
// socket filter 示例:只允许 TCP 包通过
SEC("socket")
int socket_filter(struct __sk_buff *skb)
{
    // 获取数据包长度
    int len = skb->len;

    // 解析以太网头
    // 检查 IP 协议类型
    // 如果是 TCP 包,则放行
    // 否则丢弃

    return 0; // 返回 0 表示丢弃,返回 skb->len 表示放行
}

4.7 程序类型选择决策树

在实际开发中,选择合适的 eBPF 程序类型至关重要。以下决策树可以帮助你快速做出选择:

eBPF 程序类型选择决策树 你的目标是什么? 监控内核事件 处理网络数据包 有 tracepoint? 需要性能数据? 需要最高性能? 需要协议栈交互? tracepoint perf_event XDP TC 决策要点 • 优先选择 tracepoint(稳定) • 网络场景:XDP > TC > socket filter

4.8 本章小结

本章详细介绍了 eBPF 的核心程序类型,包括它们的触发机制、数据上下文和典型应用场景。理解这些程序类型是编写高效 eBPF 程序的关键。在实际开发中,应根据具体需求选择合适的程序类型,并注意不同程序类型之间的性能差异和稳定性特点。