Linux内核 BPF Iterators 深度工程实践:内核数据遍历的可编程范式

一、为什么需要 BPF Iterators

传统内核观测依赖 procfs、sysfs 或 ftrace 等静态接口。当你需要遍历某个内核数据结构时——比如实时获取所有 TCP 连接的 RTT、列出所有 cgroup 的 PSI 压力值、甚至遍历 BPF map 本身——这些接口要么不支持,要么需要反复调用陷入内核,性能代价高。

BPF Iterators(内核 5.8+ 引入)解决了这个问题:它允许你注册一个 BPF 程序,作为迭代器安全地遍历任意内核数据结构,并通过 BPF ringbuf 或 perf event 将数据流式传输到用户态,全程无 fork、无反复陷入、无锁竞争。

二、核心架构

BPF Iterators 本质是一类特殊的 BPF 程序——BPF_PROG_TYPE_TRACING 下挂载到目标迭代点的回调函数。内核启动时,它会为目标数据结构注册 seq_ops 迭代接口,用户态通过读取 /proc/bpf_iter_ 触发遍历。


User Space (cat, bpftool)
    │ read()
    ▼
/proc/bpf_iter_target
    │ bpf_iter 回调
    ▼
Kernel Data (task, tcp, bpf_map, ...)
    │
    ▼
BPF Ringbuf → 用户态实时消费

关键点:

  • 遍历生命周期管理:内核自动处理 start、next、show、stop 四个 seq_ops 回调
  • 终止安全:内核在每个迭代点检查 need_resched() 和 bpf_iter_need_stop(),防止 BPF 程序霸占 CPU
  • 内存分配:迭代器可以使用 bpf_timer、bpf_ringbuf 等辅助函数

三、内建迭代器类型

Linux 5.8+ 已内置多种迭代器:

目标类型 路径 说明
task bpf_iter__task 遍历所有进程(task_struct)
task_file bpf_iter__task_file 遍历进程的所有打开文件
task_vma bpf_iter__task_vma 遍历进程的所有 VMA 区域
bpf_map bpf_iter__bpf_map 遍历所有已注册 BPF map
bpf_map_elem bpf_iter__bpf_map_elem 遍历 map 中的单个元素
bpf_link bpf_iter__bpf_link 遍历所有 BPF link
cgroup bpf_iter__cgroup 遍历 cgroup 层级
netlink bpf_iter__netlink 遍历 netlink socket
ipv6_route bpf_iter__ipv6_route 遍历 IPv6 路由表
tcp / udp bpf_iter__tcp / bpf_iter__udp 遍历 TCP/UDP 控制块

实战示例 1:查看所有进程的 TCP RTT


// tcp_rtt_iter.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>

struct event {
    u32 pid;
    u32 tid;
    u64 rtt_ns;
    u32 saddr;
    u32 daddr;
    u16 sport;
    u16 dport;
};

struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024);
} events SEC(".maps");

SEC("iter/tcp")
int tcp_rtt_dump(struct bpf_iter_tcp *ctx)
{
    struct sock *sk = ctx->sk;
    struct event *e;
    struct tcp_sock *tp;
    struct inet_sock *inet;

    if (!sk)
        return 0;

    e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
    if (!e)
        return 0;

    tp = (struct tcp_sock *)sk;
    inet = (struct inet_sock *)sk;

    bpf_probe_read_kernel(&e->rtt_ns, sizeof(u64),
                          &tp->srtt_us);
    e->rtt_ns >>= 3;  // srtt_us 是 8 倍放大的
    e->rtt_ns *= 1000; // 转 ns

    e->pid = BPF_CORE_READ(task, pid);
    e->tid = BPF_CORE_READ(task, tgid);
    e->saddr = BPF_CORE_READ(inet, inet_saddr);
    e->daddr = BPF_CORE_READ(inet, sk.__sk_common.skc_daddr);
    e->sport = BPF_CORE_READ(inet, inet_sport);
    e->dport = bpf_ntohs(BPF_CORE_READ(inet,
                          sk.__sk_common.skc_dport));

    bpf_ringbuf_submit(e, 0);
    return 0;
}

char _license[] SEC("license") = "GPL";

通过 bpftool iter pin 将迭代器绑定到 /sys/fs/bpf/tcp_rtt,然后 cat /sys/fs/bpf/tcp_rtt 即可流式输出全系统 TCP RTT。

实战示例 2:遍历 BPF Map 的自省迭代器


// introspect_map.bpf.c
// 遍历一个 LRU hash map,统计各 bucket 填充率

struct bucket_stat {
    u32 bucket_id;
    u32 elem_count;
};

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, u32);
    __type(value, struct bucket_stat);
    __uint(max_entries, 1024);
} bucket_stats SEC(".maps");

SEC("iter/bpf_map_elem")
int lru_inspect(struct bpf_iter_bpf_map_elem *ctx)
{
    struct bpf_map *map = ctx->map;
    void *key = ctx->key;
    void *value = ctx->value;
    u32 bucket_id;

    if (!key || !value)
        return 0;

    bucket_id = (u32)(uintptr_t)key % 1024;

    // 原子递增 bucket 计数
    u32 *count = bpf_map_lookup_elem(&bucket_stats, &bucket_id);
    if (count) {
        __sync_fetch_and_add(count, 1);
    }

    return 0;
}

四、与 eBPF 其他观测路径对比

维度 BPF Iterators Kprobes/Tracepoints Ringbuf Events
触发方式 用户主动拉取 事件驱动推送 事件驱动推送
数据完整性 遍历全量快照 单点事件 单点事件
开销模式 仅 read() 时触发 常驻 hook 常驻 hook
长期占用 可长时间不卸载 需要维护 map 状态 需要维护 buffer
适用场景 定期全量审计、状态导出 实时细粒度追踪 高频事件传输

黄金法则:

  • 如果你需要周期性的全量状态快照,用 Iterators
  • 如果你需要实时事件通知,用 kprobe/tracepoint + ringbuf
  • 如果你需要长时间缺失检测,用 bpf_timer 配合 Iterators 的拉取模式

五、性能陷阱与最佳实践

1. Ringbuf 背压处理

BPF Iterators 在执行期间持有 RCU 锁。如果 bpf_ringbuf_reserve 失败或 bpf_ringbuf_submit 被阻塞,迭代器会持续持有 RCU,阻塞其他 RCU 临界区。

错误做法:在 ringbuf 满时重试


SEC("iter/task")
int bad_dump(struct bpf_iter_task *ctx)
{
    struct event *e;
    e = bpf_ringbuf_reserve(&buf, sizeof(*e), 0);
    while (!e) {  // 危险!死占用 RCU
        bpf_iter_continue(ctx);
    }
    // ...
}

正确做法:直接跳过,等待下一个 resched 点


SEC("iter/task")
int good_dump(struct bpf_iter_task *ctx)
{
    struct event *e;
    e = bpf_ringbuf_reserve(&buf, sizeof(*e), 0);
    if (!e) {
        return 0; // 跳过,下一个迭代点会 resched
    }
    // ...
}

2. 内核栈空间限制

BPF Iterators 的内核栈仅 512 字节。在 BPF 程序中分配大型结构体会触发 verifier 拒绝。


错误:大栈上数组
struct big_data x[64]; // verifier: stack depth exceeds 512

正确:用 map 或 ringbuf 作为暂存区

3. 提前终止条件

BPF Iterators 返回 0 继续下一个元素,返回 1 则提前终止。如果你在遍历大 map 过程中找到目标:


SEC("iter/bpf_map_elem")
int scan_condition(struct bpf_iter_bpf_map_elem *ctx)
{
    struct event *data = ctx->value;
    if (data->threshold > CRITICAL) {
        // 找到目标,终止遍历
        return 1;
    }
    return 0;
}

六、进阶:TCP 连接池健康度监控

分析网络问题时,bpf_iter__tcp 是第一手的信息源。它遍历 tcp_hashinfo 结构,覆盖 LISTEN 和 ESTABLISHED 状态。


// tcp_conn_iter.bpf.c
struct tcp_health {
    u32 saddr, daddr;
    u16 sport, dport;
    u16 state;
    u32 retrans;
    u64 bytes_sent, bytes_recv;
    u32 rto;
};

SEC("iter/tcp")
int tcp_watchdog(struct bpf_iter_tcp *ctx)
{
    struct sock *sk = ctx->sk;
    struct tcp_sock *tp;
    struct tcp_health h = {};
    struct inet_sock *inet;

    if (!sk || sk->sk_state != TCP_ESTABLISHED)
        return 0;

    tp = (struct tcp_sock *)sk;
    inet = (struct inet_sock *)sk;

    h.saddr = inet->inet_saddr;
    h.daddr = sk->sk_daddr;
    h.sport = inet->inet_sport;
    h.dport = ntohs(sk->sk_dport);
    h.state = sk->sk_state;
    h.retrans = tp->total_retrans;
    h.bytes_sent = tp->bytes_sent;
    h.bytes_recv = tp->bytes_received;
    h.rto = inet->icsk_rto;

    if (h.retrans > 10 || h.rto > 200) {
        bpf_ringbuf_submit(&h, 0);
    }

    return 0;
}

用户态通过 bpftool iter pin 绑定后,一步操作即可全量扫描问题连接,比轮询 /proc/net/tcp + ss 的组合方案高效得多,尤其适合 K8s CNI 层或 Service Mesh sidecar 的网关心跳检测。

七、自定义目标迭代器

Linux 5.11+ 开放了注册自定义迭代器的接口。你不需要修改内核源码就能为任意内核数据结构注册迭代器。

注册流程:


// 内核态:注册自定义迭代器
static struct bpf_iter_reg my_iter_reg = {
    .target = "my_struct",
    .seq_ops = &my_seq_ops,
    .seq_priv_size = sizeof(struct my_iter_ctx),
    .ctx_arg_info_size = 1,
    .ctx_arg_info = my_ctx_info,
};

bpf_iter_reg_target(&my_iter_reg);

用户态的 BPF 程序通过 SEC("iter/my_struct") 挂载。实际工程中,通常配合内核模块暴露自定义序列化接口来实现业务特定的遍历需求,例如遍历容器运行时内部的任务调度队列或自定义文件系统的元数据索引。

八、工程落地建议

高频审计场景(每 30s 一次全量遍历):

  • BPF_MAP_TYPE_QUEUE 或 BPF_MAP_TYPE_STACK 保存遍历结果
  • 配合用户态 libbpf 的 bpf_iter_create() 实现按需触发

告警驱动场景:

  • Iterators 不适合做实时告警(拉取延迟不可控)
  • 使用 kprobe + ringbuf 为主,Iterators 做定期校对

生产环境注意事项:

  • 始终检查 bpf_iter_target_supported(),确保目标内核支持你的迭代类型
  • 大 map 遍历设置 BPF_F_RESCHED 标志防止 RCU stall
  • 通过 bpf_jiffies64() 在长循环中检查超时
  • 迭代器的 BPF_JIT 开启时,确认 verifier 不会过于保守地拒绝循环边界

总结

BPF Iterators 填补了 eBPF 可编程观测生态中的"全量快照"空白。理解它的生命周期管理、RCU 约束和栈限制后,你可以用它构建零侵入的系统自省探针——从 TCP 异常连接到 cgroup 压力发散,从安全基线审计到性能异常回溯。在 Kubernetes 场景下,结合 Cilium 的 hubble 和自定义 BPF Iterators,能实现比 sysfs 接口更丰富的观测能力。

核心认知是:BPF 不是万能胶水,而是精确手术刀。Iterators 负责"全面体检快照",kprobes/tracepoints 负责"实时告警监测",两者协同才能构建完整的可观测性体系。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部