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 监控 |
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;
}
4.3 内核静态插桩:tracepoint
tracepoint 是内核开发者预定义的静态探测点,它们位于内核代码的关键路径上,如系统调用入口/出口、调度器事件、文件系统操作等。与 kprobe 相比,tracepoint 具有更稳定的 API,因为其参数结构是固定的。
- 稳定性: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;
}
4.4 性能监控:perf_event
BPF_PROG_TYPE_PERF_EVENT 程序类型与 Linux 性能监控子系统(perf)深度集成。它可以附加到硬件 PMU(Performance Monitoring Unit)事件、软件事件或 tracepoint 上,用于收集细粒度的性能数据。
- 硬件事件:CPU 周期、指令数、缓存未命中、分支预测错误等。
- 软件事件:上下文切换、页面错误、CPU 迁移等。
- 采样模式:可以配置为周期性采样(如每 10000 个周期触发一次),用于生成火焰图。
// 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;
}
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 程序类型至关重要。以下决策树可以帮助你快速做出选择:
4.8 本章小结
本章详细介绍了 eBPF 的核心程序类型,包括它们的触发机制、数据上下文和典型应用场景。理解这些程序类型是编写高效 eBPF 程序的关键。在实际开发中,应根据具体需求选择合适的程序类型,并注意不同程序类型之间的性能差异和稳定性特点。