eBPF监听recv()并转发【Linux C】
想象Linux内核是一座戒备森严的城堡│ Linux 系统 ││ │ 用户空间 (城堡外面) │ ││ │ │ 你的程序 │ │ 浏览器 │ │ 终端 │ │ ││ ↕ 系统调用 (城门) ││ │ 内核空间 (城堡内部) │ ││ │ │网络协议栈│ │文件系统 │ │进程调度 │ │ ││ │ │ ││ │ 🤖 eBPF程序 = 你派进城堡的"安全间谍机器人" │ │传统方式:想观察城堡内部
eBPF Ring Buffer
基于 kvstore 项目的 sync_bpf 实现,手把手教你掌握 eBPF 数据拦截与转发
目录
- 什么是eBPF?形象理解
- Ring Buffer核心概念
- 项目文件结构
- 共享数据结构 bpf_common.h
- 内核程序详解 sync_bpf.bpf.c
- 用户空间程序详解 sync_bpf.c
- 编译流程与Makefile
- 最小化Demo示例
- 常见问题与调试
1. 什么是eBPF?形象理解
🏰 城堡比喻
想象Linux内核是一座戒备森严的城堡:
┌─────────────────────────────────────────────────────────────┐
│ Linux 系统 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 用户空间 (城堡外面) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ 你的程序 │ │ 浏览器 │ │ 终端 │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↕ 系统调用 (城门) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 内核空间 (城堡内部) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │网络协议栈│ │文件系统 │ │进程调度 │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ │ │ │
│ │ 🤖 eBPF程序 = 你派进城堡的"安全间谍机器人" │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
传统方式:想观察城堡内部?需要重建城堡(修改内核源码、重新编译)
eBPF方式:派一个经过安检的机器人进去(加载eBPF程序),随时观察,随时撤回
🤖 eBPF程序的特点
| 特点 | 说明 | 比喻 |
|---|---|---|
| 安全 | 验证器检查所有代码 | 机器人进城堡前要安检 |
| 高效 | 运行在内核态 | 机器人在城堡内部行动 |
| 动态 | 无需重启即可加载/卸载 | 随时派遣/召回机器人 |
| 受限 | 不能做危险操作 | 机器人只能观察,不能破坏 |
2. Ring Buffer核心概念
🍣 旋转寿司比喻
Ring Buffer就像一个旋转寿司传送带:
┌────────────────────────────────────┐
│ Ring Buffer │
│ (旋转寿司传送带) │
│ │
│ ┌──┐ ┌──┐ ┌──┐ ┌──┐ │
│ ←─ │🍣│←─│🍣│←─│🍣│←─│ │ ←───┐ │
│ └──┘ └──┘ └──┘ └──┘ │ │
│ │ │
│ 厨师(内核)在这里放寿司 ────┘ │
│ │
│ 顾客(用户空间)在这里取寿司 │
│ ↓ │
│ 🍽️ │
└────────────────────────────────────┘
厨师 = eBPF内核程序 (生产数据)
顾客 = 用户空间程序 (消费数据)
传送带 = Ring Buffer (环形队列)
📊 Ring Buffer vs 其他方案
| 方案 | 内存使用 | 事件顺序 | 性能 |
|---|---|---|---|
| Ring Buffer | 共享一个buffer | ✅ 全局有序 | ⭐⭐⭐ 最优 |
| Perf Buffer | 每CPU独立buffer | ❌ 需排序 | ⭐⭐ 较好 |
| Map轮询 | 看map类型 | ❌ 无序 | ⭐ 一般 |
🔧 Ring Buffer 核心API
内核侧(eBPF程序中使用):
// 1. 预留空间 - 告诉传送带"我要放一盘寿司"
void *bpf_ringbuf_reserve(ringbuf, size, flags);
// 2. 提交数据 - 把寿司放上传送带
void bpf_ringbuf_submit(data, flags);
// 3. 丢弃数据 - 算了不放了
void bpf_ringbuf_discard(data, flags);
用户侧(C程序中使用):
// 1. 创建消费者 - 坐到传送带旁边准备吃
struct ring_buffer *ring_buffer__new(map_fd, callback, ...);
// 2. 轮询数据 - 看看有没有新寿司
int ring_buffer__poll(rb, timeout_ms);
// 3. 释放资源 - 吃完走人
void ring_buffer__free(rb);
3. 项目文件结构
kvstore的eBPF实现包含以下文件:
kvstore/
├── bpf_common.h # 📋 共享数据结构(内核和用户空间都用)
├── sync_bpf.bpf.c # 🔧 eBPF内核程序(运行在内核态)
├── sync_bpf.c # 💻 用户空间程序(加载eBPF、处理数据)
├── sync_bpf.skel.h # 🤖 自动生成的skeleton头文件
├── sync_bpf.bpf.o # 📦 编译后的eBPF字节码
└── Makefile # 🔨 编译规则
🔄 数据流向图
┌──────────────────────────────────────────────────────────────────┐
│ 完整数据流 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ① 客户端发送请求 │
│ ┌─────────┐ ┌─────────────┐ │
│ │ Client │ ──TCP──→ │ kvstore │ │
│ └─────────┘ │ (主节点) │ │
│ └──────┬──────┘ │
│ │ │
│ ② 内核tcp_recvmsg()接收数据 │ │
│ ▼ │
│ ┌───────────────────────────────────────┐ │
│ │ Linux Kernel │ │
│ │ │ │
│ │ tcp_recvmsg() ←── kprobe挂钩点 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────┐ │ │
│ │ │sync_bpf.bpf │ eBPF程序拦截 │ │
│ │ │ (内核态) │ │ │
│ │ └──────┬───────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────┐ │ │
│ │ │ Ring Buffer │ 数据传递通道 │ │
│ │ └──────┬───────┘ │ │
│ └──────────┼────────────────────────────┘ │
│ │ │
│ ③ 用户空间读取Ring Buffer │
│ ▼ │
│ ┌──────────────┐ │
│ │ sync_bpf │ 用户空间程序 │
│ │ (用户态) │ │
│ └──────┬───────┘ │
│ │ │
│ ④ 转发到从节点 │
│ ▼ │
│ ┌─────────────┐ │
│ │ kvstore │ │
│ │ (从节点) │ │
│ └─────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
4. 共享数据结构 bpf_common.h
这个文件定义了内核程序和用户程序之间共享的数据结构:
// bpf_common.h
#ifndef __BPF_COMMON_H
#define __BPF_COMMON_H
// 单次事件最大数据量
#define MAX_EVENT_DATA 4096
// 事件结构体 - 从内核传递到用户空间的数据包
struct event {
__u32 pid; // 进程ID
__u32 len; // 数据长度
__u8 data[MAX_EVENT_DATA]; // 实际数据
};
// 配置结构体 - 从用户空间传递到内核的配置
struct config {
__u32 target_ip; // 目标IP地址(网络字节序)
__u16 target_port; // 目标端口
};
#endif
🎯 为什么需要共享头文件?
┌─────────────────┐ ┌─────────────────┐
│ sync_bpf.c │ │ sync_bpf.bpf.c │
│ (用户空间) │ │ (内核空间) │
└────────┬────────┘ └────────┬────────┘
│ │
│ #include "bpf_common.h" │
│ │
└───────────┬───────────────┘
│
▼
struct event 定义必须一致!
否则数据解析会出错
5. 内核程序详解 sync_bpf.bpf.c
5.1 头文件和许可证
// sync_bpf.bpf.c
#include "vmlinux.h" // 内核类型定义(包含所有内核结构体)
#include <bpf/bpf_helpers.h> // eBPF辅助函数
#include <bpf/bpf_tracing.h> // 跟踪相关宏
#include "bpf_common.h" // 我们的共享结构体
// ⚠️ 必须声明许可证!否则无法使用GPL-only的辅助函数
char LICENSE[] SEC("license") = "Dual BSD/GPL";
5.2 定义Maps
Maps是eBPF的数据存储容器,类似于全局变量,但可以在内核和用户空间之间共享:
// Map 1: 配置信息(用户空间 → 内核)
struct {
__uint(type, BPF_MAP_TYPE_ARRAY); // 数组类型
__uint(max_entries, 1); // 只需要1个条目
__type(key, __u32); // 键类型:整数
__type(value, struct config); // 值类型:配置结构体
} config_map SEC(".maps");
// Map 2: Ring Buffer(内核 → 用户空间)
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 1 << 24); // 16MB = 2^24字节
} rb SEC(".maps");
// Map 3: 上下文保存(kprobe入口 → kretprobe出口)
struct {
__uint(type, BPF_MAP_TYPE_HASH); // 哈希表类型
__type(key, u64); // 键:PID+TID
__type(value, struct { // 值:保存的参数
struct sock *sk;
struct msghdr *msg;
});
__uint(max_entries, 1024); // 最多1024个并发
} entry_ctx SEC(".maps");
🗺️ Map类型对比
| Map类型 | 用途 | 比喻 |
|---|---|---|
ARRAY |
固定大小数组,索引快 | 储物柜(按号取) |
HASH |
键值对存储 | 字典(按名查) |
RINGBUF |
高效事件队列 | 旋转寿司 |
PERCPU_ARRAY |
每CPU独立数组 | 每人一个柜子 |
5.3 Kprobe入口函数
Kprobe可以在任意内核函数的入口或出口插入探测点:
// SEC("kprobe/tcp_recvmsg") 告诉系统:
// 当tcp_recvmsg函数被调用时,先执行这个函数
SEC("kprobe/tcp_recvmsg")
int BPF_KPROBE(tcp_recvmsg_entry, struct sock *sk, struct msghdr *msg)
{
// 获取当前进程的唯一标识(高32位=PID,低32位=TID)
u64 pid_tgid = bpf_get_current_pid_tgid();
// 保存函数参数,因为在kretprobe中拿不到
struct {
struct sock *sk; // socket指针
struct msghdr *msg; // 消息头指针
} val = { .sk = sk, .msg = msg };
// 存入hash map,供kretprobe使用
bpf_map_update_elem(&entry_ctx, &pid_tgid, &val, BPF_ANY);
return 0;
}
❓ 为什么需要在入口保存参数?
tcp_recvmsg(sk, msg, len, flags) 函数执行过程:
┌─────────────────────────────┐
│ 函数入口 (kprobe触发) │ ← 此时能拿到 sk, msg 参数
│ │ │
│ ▼ │
│ 函数体执行中... │
│ │ │
│ ▼ │
│ 函数出口 (kretprobe触发) │ ← 此时只能拿到返回值!
│ │ 参数已经从寄存器消失了
└─────────────────────────────┘
解决方案:入口时把参数存到Map里,出口时再取出来用
5.4 Kretprobe出口函数(核心!)
注意出口函数有限制 栈空间不能超过512字节
// 当tcp_recvmsg函数返回时执行
SEC("kretprobe/tcp_recvmsg")
int BPF_KRETPROBE(tcp_recvmsg_exit, long ret)
{
// ========== 第1步:检查返回值 ==========
// ret 是tcp_recvmsg的返回值,表示接收到的字节数
if (ret <= 0 || ret > MAX_EVENT_DATA)
return 0; // 没收到数据或数据太大,跳过
// ========== 第2步:取出保存的参数 ==========
u64 pid_tgid = bpf_get_current_pid_tgid();
struct { struct sock *sk; struct msghdr *msg; } *saved;
saved = bpf_map_lookup_elem(&entry_ctx, &pid_tgid);
if (!saved || !saved->sk || !saved->msg)
return 0; // 找不到上下文,跳过
// 用完立即删除,防止内存泄漏!
bpf_map_delete_elem(&entry_ctx, &pid_tgid);
// ========== 第3步:检查配置(IP和端口过滤)==========
__u32 key = 0;
struct config *cfg = bpf_map_lookup_elem(&config_map, &key);
if (!cfg) return 0;
// 读取socket的本地端口
__u16 local_port;
bpf_probe_read_kernel(&local_port, sizeof(local_port),
&saved->sk->__sk_common.skc_num);
if (local_port != cfg->target_port)
return 0; // 端口不匹配,不是我们关心的连接
// 读取socket的本地IP
__u32 rcv_saddr;
bpf_probe_read_kernel(&rcv_saddr, sizeof(rcv_saddr),
&saved->sk->__sk_common.skc_rcv_saddr);
if (rcv_saddr != cfg->target_ip)
return 0; // IP不匹配,不是我们关心的连接
// ========== 第4步:获取接收到的数据 ==========
const struct iovec *iov;
bpf_probe_read_kernel(&iov, sizeof(iov), &saved->msg->msg_iter.iov);
if (!iov) return 0;
void *buf;
bpf_probe_read_kernel(&buf, sizeof(buf), &iov->iov_base);
if (!buf) return 0;
// ========== 第5步:Ring Buffer操作(核心!)==========
// 5.1 预留空间 - 在Ring Buffer中预留一个位置
struct event *e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
if (!e) return 0; // 空间不足,事件被丢弃
// 5.2 填充数据
e->pid = pid_tgid >> 32; // 取高32位作为PID
e->len = (unsigned int)ret; // 数据长度
// 从用户空间内存读取实际数据
if (bpf_probe_read_user(e->data, ret, buf)) {
// 读取失败,必须丢弃预留的空间!
bpf_ringbuf_discard(e, 0);
return 0;
}
// 5.3 提交数据 - 数据对用户空间可见
bpf_ringbuf_submit(e, 0);
return 0;
}
🎯 Ring Buffer操作流程图
┌────────────────────────────────────────────────────────────┐
│ Ring Buffer 操作三部曲 │
├────────────────────────────────────────────────────────────┤
│ │
│ ① bpf_ringbuf_reserve(&rb, sizeof(*e), 0) │
│ ┌─────────────────────────────────────┐ │
│ │ Ring Buffer 内存布局 │ │
│ │ ┌────┬────┬────┬────┬────┬────┐ │ │
│ │ │已用│已用│预留│空闲│空闲│空闲│ │ ← 预留成功 │
│ │ └────┴────┴─▲──┴────┴────┴────┘ │ │
│ │ │ │ │
│ │ 返回指针e │ │
│ └─────────────────────────────────────┘ │
│ │
│ ② 填充数据(直接写入预留的内存) │
│ e->pid = xxx; │
│ e->len = xxx; │
│ bpf_probe_read_user(e->data, ...); │
│ │
│ ③ 二选一:提交 或 丢弃 │
│ │
│ bpf_ringbuf_submit(e, 0); bpf_ringbuf_discard(e,0)│
│ ┌────┬────┬────┬────┐ ┌────┬────┬────┬────┐ │
│ │已用│已用│已用│空闲│ │已用│已用│空闲│空闲│ │
│ └────┴────┴────┴────┘ └────┴────┴────┴────┘ │
│ 用户空间可读取 ✓ 空间被释放 ✓ │
│ │
└────────────────────────────────────────────────────────────┘
6. 用户空间程序详解 sync_bpf.c
6.1 必要的头文件
#include <stdio.h>
#include <stdlib.h>
#include <bpf/libbpf.h> // libbpf库
#include <bpf/bpf.h> // BPF系统调用封装
#include "bpf_common.h" // 共享数据结构
#include "sync_bpf.skel.h" // 自动生成的skeleton
6.2 什么是Skeleton?
Skeleton(骨架)是bpftool自动生成的C代码,极大简化了eBPF程序的加载过程:
# 生成skeleton的命令
bpftool gen skeleton sync_bpf.bpf.o > sync_bpf.skel.h
生成的skeleton提供这些便捷接口:
// 自动生成的结构体,包含所有maps和programs的指针
struct sync_bpf_bpf {
struct {
struct bpf_map *config_map; // 直接访问config_map
struct bpf_map *rb; // 直接访问ring buffer
struct bpf_map *entry_ctx; // 直接访问hash map
} maps;
struct {
struct bpf_program *tcp_recvmsg_entry; // kprobe程序
struct bpf_program *tcp_recvmsg_exit; // kretprobe程序
} progs;
};
// 自动生成的函数
struct sync_bpf_bpf *sync_bpf_bpf__open(void); // 打开
int sync_bpf_bpf__load(struct sync_bpf_bpf *obj); // 加载到内核
int sync_bpf_bpf__attach(struct sync_bpf_bpf *obj);// 附加到挂载点
void sync_bpf_bpf__destroy(struct sync_bpf_bpf *obj);// 销毁
6.3 主函数流程
int main(int argc, char **argv) {
struct sync_bpf_bpf *skel; // skeleton对象
struct ring_buffer *rb = NULL; // ring buffer句柄
int err;
// ========== 第1步:打开BPF程序 ==========
skel = sync_bpf_bpf__open();
if (!skel) {
fprintf(stderr, "Failed to open BPF skeleton\n");
return 1;
}
// ========== 第2步:加载BPF程序到内核 ==========
err = sync_bpf_bpf__load(skel);
if (err) {
fprintf(stderr, "Failed to load BPF program\n");
goto cleanup;
}
// ========== 第3步:配置Maps ==========
// 将用户配置写入config_map,供内核程序读取
struct config bpf_cfg;
inet_pton(AF_INET, "192.168.153.128", &bpf_cfg.target_ip);
bpf_cfg.target_port = 2000;
__u32 key = 0;
int config_fd = bpf_map__fd(skel->maps.config_map);
bpf_map_update_elem(config_fd, &key, &bpf_cfg, BPF_ANY);
// ========== 第4步:附加到挂载点 ==========
err = sync_bpf_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach BPF program\n");
goto cleanup;
}
// ========== 第5步:创建Ring Buffer消费者 ==========
rb = ring_buffer__new(
bpf_map__fd(skel->maps.rb), // ring buffer的文件描述符
handle_event, // 事件处理回调函数
NULL, // 回调函数的上下文参数
NULL // 选项
);
if (!rb) {
fprintf(stderr, "Failed to create ring buffer\n");
goto cleanup;
}
// ========== 第6步:事件循环 ==========
printf("开始监听...\n");
while (1) {
// poll会阻塞最多100ms,有事件时立即返回
err = ring_buffer__poll(rb, 100);
if (err == -EINTR) break; // 被信号中断,退出
if (err < 0) {
fprintf(stderr, "Ring buffer poll error: %d\n", err);
break;
}
}
cleanup:
// ========== 第7步:清理资源 ==========
ring_buffer__free(rb);
sync_bpf_bpf__destroy(skel);
return err < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
}
6.4 事件处理回调函数
// 每当Ring Buffer中有新数据时,这个函数会被调用
static int handle_event(void *ctx, void *data, size_t size) {
// data指向的就是内核bpf_ringbuf_submit提交的struct event
const struct event *e = data;
// 打印接收到的信息
printf("PID %u 收到 %u 字节数据\n", e->pid, e->len);
// 转发数据到从节点
if (forward_sockfd >= 0) {
send(forward_sockfd, e->data, e->len, MSG_NOSIGNAL);
}
return 0; // 返回0表示处理成功
}
🔄 完整流程图
┌─────────────────────────────────────────────────────────────┐
│ 用户空间程序生命周期 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ │
│ │ sync_bpf_bpf__open │ ──→ 打开BPF程序 │
│ └────────┬─────────┘ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ sync_bpf_bpf__load │ ──→ 加载到内核(验证器检查) │
│ └────────┬─────────┘ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ bpf_map_update_elem │ ──→ 配置参数写入Map │
│ └────────┬─────────┘ │
│ ▼ │
│ ┌──────────────────┐ │
│ │sync_bpf_bpf__attach│ ──→ 附加到kprobe/kretprobe │
│ └────────┬─────────┘ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ ring_buffer__new │ ──→ 创建Ring Buffer消费者 │
│ └────────┬─────────┘ │
│ ▼ │
│ ┌──────────────────┐ ┌─────────────────┐ │
│ │ ring_buffer__poll │ ←→ │ handle_event │ 循环处理 │
│ └────────┬─────────┘ └─────────────────┘ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ ring_buffer__free │ ──→ 释放Ring Buffer │
│ └────────┬─────────┘ │
│ ▼ │
│ ┌────────────────────┐ │
│ │sync_bpf_bpf__destroy│ ──→ 销毁BPF程序 │
│ └────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
7. 编译流程与Makefile
7.1 编译步骤详解
# 步骤1: 生成vmlinux.h(只需执行一次)
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
# 步骤2: 编译eBPF程序为BPF字节码
clang -g -O2 -target bpf -D__TARGET_ARCH_x86 -c sync_bpf.bpf.c -o sync_bpf.bpf.o
# 步骤3: 生成skeleton头文件
bpftool gen skeleton sync_bpf.bpf.o > sync_bpf.skel.h
# 步骤4: 编译用户空间程序
gcc -o sync_bpf sync_bpf.c -lbpf -lelf -lz
7.2 Makefile解析
# 变量定义
BPF_PROG = sync_bpf.bpf.c # eBPF源文件
BPF_OBJ = sync_bpf.bpf.o # eBPF目标文件
BPF_SKEL = sync_bpf.skel.h # skeleton头文件
BPF_USER = sync_bpf.c # 用户空间源文件
BPF_TARGET = sync_bpf # 最终可执行文件
# 编译eBPF程序
$(BPF_OBJ): $(BPF_PROG)
clang -g -O2 \
-target bpf \ # 目标是BPF字节码
-D__TARGET_ARCH_x86 \ # x86架构
-I ./ \ # 头文件搜索路径
-c $< -o $@
# 生成skeleton
$(BPF_SKEL): $(BPF_OBJ)
bpftool gen skeleton $< > $@
# 编译用户空间程序(依赖skeleton)
$(BPF_TARGET): $(BPF_USER) $(BPF_SKEL)
gcc -Wall -O2 -g \
$$(pkg-config --cflags libbpf) \ # libbpf头文件路径
$< \
$$(pkg-config --libs libbpf) \ # libbpf库路径
-o $@
7.3 运行程序
# 编译
make sync_bpf
# 运行(必须root权限!)
sudo ./sync_bpf 192.168.153.128 2000 192.168.153.128 2001
# 或使用配置文件
sudo ./sync_bpf sync_bpf.conf
8. Demo示例
下面是一个最简单的eBPF Ring Buffer示例,监听所有进程启动事件:
8.1 hello_common.h
#ifndef __HELLO_COMMON_H
#define __HELLO_COMMON_H
struct event {
__u32 pid;
char comm[16];
};
#endif
8.2 hello.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include "hello_common.h"
char LICENSE[] SEC("license") = "GPL";
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} rb SEC(".maps");
SEC("tracepoint/syscalls/sys_enter_execve")
int hello_exec(void *ctx) {
struct event *e;
e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
if (!e) return 0;
e->pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&e->comm, sizeof(e->comm));
bpf_ringbuf_submit(e, 0);
return 0;
}
8.3 hello.c
#include <stdio.h>
#include <signal.h>
#include <bpf/libbpf.h>
#include "hello_common.h"
#include "hello.skel.h"
static volatile int running = 1;
void sig_handler(int sig) { running = 0; }
int handle_event(void *ctx, void *data, size_t sz) {
struct event *e = data;
printf("新进程: PID=%d CMD=%s\n", e->pid, e->comm);
return 0;
}
int main() {
struct hello_bpf *skel;
struct ring_buffer *rb;
signal(SIGINT, sig_handler);
skel = hello_bpf__open_and_load();
hello_bpf__attach(skel);
rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL);
printf("监听中... Ctrl+C退出\n");
while (running)
ring_buffer__poll(rb, 100);
ring_buffer__free(rb);
hello_bpf__destroy(skel);
return 0;
}
8.4 编译运行
clang -g -O2 -target bpf -c hello.bpf.c -o hello.bpf.o
bpftool gen skeleton hello.bpf.o > hello.skel.h
gcc -o hello hello.c -lbpf -lelf -lz
sudo ./hello
9. 常见问题与调试
9.1 错误速查表
| 错误信息 | 原因 | 解决方案 |
|---|---|---|
Permission denied |
没有root权限 | sudo ./程序名 |
Invalid func unknown |
使用了不支持的函数 | 检查内核版本 |
R1 type=ctx expected=fp |
栈溢出(>512字节) | 用Map存储大数据 |
back-edge from insn |
循环边界不明确 | 使用有界循环 |
BPF program is too large |
指令数超限 | 简化程序逻辑 |
9.2 调试命令
# 查看已加载的BPF程序
sudo bpftool prog list
# 查看BPF maps
sudo bpftool map list
# 查看map内容
sudo bpftool map dump id <MAP_ID>
# 查看tracepoint列表
sudo cat /sys/kernel/debug/tracing/available_events | grep tcp
9.3 eBPF限制速查
| 限制项 | 数值 | 说明 |
|---|---|---|
| 栈大小 | 512字节 | 不能声明大数组 |
| 指令数 | 100万条 | 程序不能太复杂 |
| 嵌套深度 | 8层 | 函数调用不能太深 |
| 循环 | 必须有界 | 验证器要能证明终止 |
🎉 总结
核心要点:
┌─────────────────────────────────────────────────────────┐
│ eBPF Ring Buffer 要点 │
├─────────────────────────────────────────────────────────┤
│ │
│ 📁 文件结构 │
│ ├── bpf_common.h # 共享结构体 │
│ ├── xxx.bpf.c # 内核程序 │
│ ├── xxx.c # 用户程序 │
│ └── xxx.skel.h # 自动生成 │
│ │
│ 🔧 内核程序三步曲 │
│ ① bpf_ringbuf_reserve() 预留空间 │
│ ② 填充数据 │
│ ③ bpf_ringbuf_submit() 提交 │
│ │
│ 💻 用户程序流程 │
│ open → load → attach → poll → destroy │
│ │
│ ⚠️ 注意事项 │
│ • 必须root权限运行 │
│ • 栈不能超过512字节 │
│ • 循环必须有界 │
│ • reserve后必须submit或discard │
│ │
└─────────────────────────────────────────────────────────┘
https://github.com/0voice
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)