eBPF Ring Buffer

基于 kvstore 项目的 sync_bpf 实现,手把手教你掌握 eBPF 数据拦截与转发


目录

  1. 什么是eBPF?形象理解
  2. Ring Buffer核心概念
  3. 项目文件结构
  4. 共享数据结构 bpf_common.h
  5. 内核程序详解 sync_bpf.bpf.c
  6. 用户空间程序详解 sync_bpf.c
  7. 编译流程与Makefile
  8. 最小化Demo示例
  9. 常见问题与调试

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

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐