3. eBPF 挂载点

249 阅读7分钟

在谈论 eBPF 的挂载点时,**Probe(探针)**是绕不开的重要主题之一。

Probe 是 eBPF 程序实现内核行为监控、分析和调试的核心工具。它们通过挂载到内核的函数或事件上,在特定条件下触发 eBPF 程序。

下面我们详细介绍 Probe 的概念、分类、工作机制以及使用场景。


  1. Probe 的概念

Probe 是一种轻量级探针机制,用于插入到内核或用户空间的特定位置,从而捕获行为或监控性能。

  • eBPF 中的 Probe 主要有以下两种类型:

    • Kprobe 和 Kretprobe:挂载到内核函数的入口和返回点。
    • Uprobe 和 Uretprobe:挂载到用户态函数的入口和返回点。

  1. Probe 的分类与特点

      (1) Kprobe 和 Kretprobe

      Kprobe 和 Kretprobe 是 eBPF 中用于监控内核函数执行的重要工具。

      Kprobe

    • 挂载点:内核函数的入口。

    • 作用

      • 捕获内核函数的调用参数。
      • 分析函数调用的上下文信息。
    • 上下文

      • struct pt_regs,包含寄存器状态和参数信息。

      示例

    SEC("kprobe/do_sys_open")
    int handle_kprobe(struct pt_regs *ctx) {
        const char *filename = (const char *)PT_REGS_PARM1(ctx);
        bpf_trace_printk("Opening file: %s\n", filename);
        return 0;
    }
    

      Kretprobe

    • 挂载点:内核函数的返回点。

    • 作用

      • 捕获函数的返回值。
      • 分析函数执行的结果或后续行为。
    • 上下文

      • struct pt_regs,返回寄存器状态。

      示例

    SEC("kretprobe/do_sys_open")
    int handle_kretprobe(struct pt_regs *ctx) {
        int retval = PT_REGS_RC(ctx); // 获取返回值
        bpf_trace_printk("File opened, retval: %d\n", retval);
        return 0;
    }
    

      Kprobe 和 Kretprobe 的区别

    特性KprobeKretprobe
    挂载点函数入口函数返回点
    捕获内容函数参数、调用上下文函数返回值及上下文
    使用场景入参分析、逻辑捕获返回值监控、结果验证

      (2) Uprobe 和 Uretprobe

      Uprobe 和 Uretprobe 用于用户态的函数跟踪,与 Kprobe 类似,但适用于用户空间程序。

      Uprobe

    • 挂载点:用户态函数入口。

    • 作用

      • 监控用户态函数的调用。
      • 分析函数的参数和行为。
    • 上下文

      • 参数信息可通过特定方式获取,依赖用户空间函数签名。

      示例

    SEC("uprobe/libc.so.6:malloc")
    int handle_uprobe(struct pt_regs *ctx) {
        size_t size = (size_t)PT_REGS_PARM1(ctx);
        bpf_trace_printk("malloc called with size: %lu\n", size);
        return 0;
    }
    

      Uretprobe

    • 挂载点:用户态函数返回点。

    • 作用

      • 捕获用户态函数返回值。
      • 分析函数结果或行为。
    • 上下文

      • 返回值可通过寄存器获取。

      示例

    SEC("uretprobe/libc.so.6:malloc")
    int handle_uretprobe(struct pt_regs *ctx) {
        void *retval = (void *)PT_REGS_RC(ctx);
        bpf_trace_printk("malloc returned pointer: %p\n", retval);
        return 0;
    }
    

      Uprobe 和 Uretprobe 的特点

    • 与特定的用户态函数绑定,适用于分析用户程序。
    • 无需修改用户程序代码,实时捕获运行时信息。

  1. Probe 的工作机制

    1. 挂载过程

      1. Kprobe 和 Kretprobe:通过 eBPF 程序挂载到内核函数入口或返回点。
      2. Uprobe 和 Uretprobe:通过 eBPF 程序挂载到用户态函数的符号地址。
    2. 触发机制

      1. 当目标函数被调用或返回时,Probe 被触发。
      2. 内核会将当前上下文信息传递给 eBPF 程序。
    3. 执行与返回

      1. eBPF 程序执行预定义的逻辑(如参数记录、上下文分析)。
      2. 执行结束后,内核恢复函数的正常执行。

  1. 使用场景

      (1) 调试与内核跟踪

    • 目标:实时监控内核函数调用和返回。

    • 场景

      • 捕获文件操作(如 sys_open)。
      • 分析系统调用性能(如 sched_switch)。

      (2) 性能分析

    • 目标:定位性能瓶颈。

    • 场景

      • 统计函数调用次数。
      • 测量函数执行时间。

      (3) 用户态程序监控

    • 目标:分析用户空间程序行为。

    • 场景

      • 捕获动态库调用(如 malloc)。
      • 分析请求处理逻辑。

  1. Kprobe 和 Tracepoint 的对比

    特性KprobeTracepoint
    挂载点内核函数入口或返回点内核预定义的事件触发点
    性能较高开销开销更低,预先优化
    功能复杂性灵活,但需知道函数签名适合监控特定行为
    适用场景精细化调试与跟踪性能分析与事件监控

总结

  Probe 是 eBPF 强大能力的核心之一,尤其是 Kprobe /KretprobeUprobe/Uretprobe,提供了对内核和用户态函数的全面监控能力。它们在调试、性能分析、问题定位等领域有广泛应用,同时结合 eBPF 的强大数据处理能力,可以实现灵活而高效的监控和优化方案。

eBPF 程序通过挂载到内核的不同触发点运行,并在特定事件或条件下执行。这些挂载点定义了程序的触发位置和作用范围,并为 eBPF 提供了对应的上下文。


  1. 网络路径挂载点

      网络路径挂载点主要用于处理网络流量,提供高性能、低延迟的解决方案。

      (1) XDP (eXpress Data Path)

    • 功能:直接在网卡驱动(NIC)层处理网络包,绕过 Linux 网络栈。

    • 适用场景

      • 高性能包过滤。
      • DDoS 防护。
      • 网络负载均衡。
    • 上下文

      • 提供网络包相关信息,如 struct xdp_md,包含包数据指针和长度。
    • 特点

      • 性能极高,但逻辑复杂性受限。

      示例

    SEC("xdp")
    int xdp_prog(struct xdp_md *ctx) {
        void *data_end = (void *)(long)ctx->data_end;
        void *data = (void *)(long)ctx->data;
        // 简单过滤器,丢弃所有包
        return XDP_DROP;
    }
    

      (2) TC ( Traffic Control )

    • 功能:挂载在传输层或网络层,用于流量控制。

    • 适用场景

      • 带宽限制。
      • 网络流量统计。
      • 流量重定向。
    • 上下文

      • 提供 struct __sk_buff,包含数据包的元数据(如协议、源地址等)。
    • 特点

      • 相比 XDP,性能略低,但支持更复杂的逻辑。

      示例

    SEC("tc")
    int tc_prog(struct __sk_buff *skb) {
        // 获取源 IP 地址(IPv4 示例)
        if (bpf_ntohs(skb->protocol) == ETH_P_IP) {
            struct iphdr *iph = bpf_hdr_pointer(skb, sizeof(*iph));
            if (iph) {
                bpf_printk("Source IP: %d\n", iph->saddr);
            }
        }
        return TC_ACT_OK;
    }
    

  1. 内核跟踪挂载点

用于监控和分析内核行为,支持挂载到内核函数或事件上。

(1) Kprobe 和 Kretprobe

  • 功能

    • Kprobe:挂载到内核函数入口,记录函数调用时的上下文。
    • Kretprobe:挂载到函数返回点,获取返回值。
  • 适用场景

    • 调试内核函数。
    • 性能分析。
  • 上下文

    • 提供 void *ctx,包含函数的寄存器状态和调用信息。

示例

SEC("kprobe/sys_open")
int kprobe_sys_open(struct pt_regs *ctx) {
    const char *filename = (const char *)PT_REGS_PARM1(ctx);
    bpf_trace_printk("Opening file: %s\n", filename);
    return 0;
}

(2) Tracepoints

  • 功能:挂载到内核预定义的事件触发点(如文件打开、调度器事件等)。

  • 适用场景

    • 监控内核行为。
    • 捕获关键性能指标。
  • 上下文

    • 提供一个特定的 struct,包含事件的上下文信息(如文件描述符、进程 ID 等)。

示例

SEC("tracepoint/syscalls/sys_enter_execve")
int tracepoint_execve(struct trace_event_raw_sys_enter *ctx) {
    const char *filename = (const char *)ctx->args[0];
    bpf_trace_printk("Executing program: %s\n", filename);
    return 0;
}

  1. cgroup 挂载点
  • 功能:挂载到 cgroup,用于限制资源或监控行为。

  • 适用场景

    • 限制网络带宽或内存使用。
    • 监控特定容器的行为。
  • 上下文

    • 提供 struct cgroupstruct __sk_buff,根据功能不同上下文变化。

示例

SEC("cgroup/skb")
int cgroup_skb_filter(struct __sk_buff *skb) {
    // 简单限制 TCP 包
    if (skb->protocol == htons(ETH_P_IP)) {
        struct iphdr *iph = bpf_hdr_pointer(skb, sizeof(*iph));
        if (iph->protocol == IPPROTO_TCP) {
            return 0; // 拒绝包
        }
    }
    return 1; // 允许包
}

  1. 性能事件挂载点
  • 功能:结合性能监控工具(如 perf),采集系统运行时的性能数据。

  • 适用场景

    • 分析热点函数。
    • 优化性能瓶颈。
  • 上下文

    • 提供性能事件的上下文信息(如 CPU 使用率等)。

示例

SEC("perf_event")
int perf_event_sample(struct bpf_perf_event_data *ctx) {
    u64 counter = ctx->sample_period;
    bpf_trace_printk("Sample period: %llu\n", counter);
    return 0;
}

总结

eBPF 程序的挂载点决定了程序的触发机制和适用场景。选择适合的挂载点,需要综合考虑:

  1. 性能需求(如 XDP 优先于 TC)。
  2. 功能复杂性(如 Tracepoint 可捕获更多上下文)。
  3. 开发和调试成本(如 Kprobe 和 Tracepoint 的事件定义差异)。

这些挂载点与内核提供的上下文紧密结合,既扩展了 eBPF 的功能,也确保了安全性和高性能。