BPF Iterator 支持方案
1. 概述
本文档说明为什么 AxVisor 需要支持 BPF Iterator 程序类型,以及如何实现。
1.1 灵感来源
这个想法来自 FI (Fault Injection) 项目,一个基于 eBPF 的故障注入系统,用于 Go 微服务的全链路追踪和故障注入。
FI 的流量拦截流程是这样的:sock_ops 程序捕获 TCP 连接事件,将连接添加到 INTERCEPT_MAP,然后 sk_msg 程序对这些连接注入追踪信息。但 sock_ops 只能捕获程序附加后新建的连接,Agent 启动前已存在的长连接(Keep-Alive、gRPC 连接池、数据库连接等)抓不到。
我们写了 iter_tcp.bpf.c 来解决这个问题:用 BPF Iterator 在 Agent 启动时扫描系统中所有已建立的 TCP 连接,做到“启动即全量覆盖“。不过 aya-rs 目前不支持 Iterator 程序类型,只能用 C 写。
这就引出了本文的主题:如何在 ArceOS eBPF 生态中支持 Iterator。
2. BPF Iterator 工作原理
2.1 什么是 BPF Iterator
BPF Iterator(iter/*)是 Linux 5.8 引入的一种 eBPF 程序类型。和事件驱动型程序不同,Iterator 是主动遍历机制,允许 eBPF 程序在内核态安全地遍历内核数据结构。
2.2 执行流程
用户态 内核态
│ │
│ 1. bpf_link_create() │
├──────────────────────────────────>│ 创建 iterator link
│ │
│ 2. open("/sys/fs/bpf/iter_xxx") │
├──────────────────────────────────>│ 获取 iterator fd
│ │
│ 3. read(fd, buf, size) │
├──────────────────────────────────>│ 触发遍历
│ │
│ │ ┌─────────────────────────┐
│ │ │ 内核 Iterator 框架 │
│ │ │ │
│ │ │ for each object: │
│ │ │ call bpf_prog(ctx) │
│ │ │ seq_write(output) │
│ │ └─────────────────────────┘
│ │
│<──────────────────────────────────│ 返回遍历结果
│ │
2.3 支持的遍历目标
| Iterator 类型 | 遍历对象 | 典型用途 |
|---|---|---|
iter/task | 进程/线程 | 进程列表快照 |
iter/task_file | 进程打开的文件 | fd 审计 |
iter/tcp | TCP socket | 连接状态导出 |
iter/udp | UDP socket | UDP 端点枚举 |
iter/bpf_map_elem | BPF Map 条目 | Map 批量操作 |
iter/netlink | Netlink socket | 网络诊断 |
2.4 与 kprobe/kretprobe/uprobe 的区别
| 维度 | kprobe/kretprobe/uprobe | BPF Iterator |
|---|---|---|
| 触发机制 | 被动:等待事件发生 | 主动:用户态驱动遍历 |
| 执行时机 | 函数入口/出口被调用时 | 用户态 read() 时 |
| 数据来源 | 当前执行上下文(寄存器、栈) | 内核全局数据结构 |
| 观测范围 | 单次函数调用 | 整个数据结构集合 |
| 状态感知 | 只能捕获“变化“ | 可获取“当前全貌“ |
| 实现方式 | 断点指令 + 异常处理 | seq_file + 内核遍历框架 |
简单来说:
- kprobe 在函数入口插入断点(如
BRK #4),函数被调用时触发,只能观测到“正在发生的调用“。 - kretprobe 在函数返回时触发,捕获返回值,同样是事件驱动。
- uprobe 和 kprobe 类似,但作用于用户态程序。
- Iterator 不依赖任何事件,直接遍历内核维护的数据结构。比如可以获取“此刻系统中所有 TCP 连接“这样的全局快照。
2.5 内核数据结构依赖
Iterator 程序直接访问内核内部数据结构。以 iter/tcp 为例,程序需要访问 struct sock_common、struct sock 等结构体,而这些结构体的布局在不同内核版本间可能不同,字段顺序、大小甚至字段本身都可能变。
Linux 通过 BTF(BPF Type Format)和 CO-RE(Compile Once, Run Everywhere)来处理这个问题:
- BTF:内核编译时生成的类型信息,描述所有数据结构的布局
- vmlinux.h:从 BTF 导出的头文件,包含当前内核的完整类型定义
- CO-RE:编译时记录字段访问意图,运行时根据目标内核的 BTF 重定位
所以 Iterator 程序和内核版本紧密耦合,需要专门的构建和加载流程来处理跨版本兼容性。
3. 应用场景
3.1 宿主侧:热接入已有连接
这是最直接的应用场景,来源于前面提到的 FI 项目。
AxVisor 作为 Type-1 Hypervisor 运行在宿主 Linux 上。eBPF agent 拦截网络流量时,sock_ops 只能捕获新建连接。agent 启动前已存在的长连接(管理面的 gRPC 连接、监控系统的 Keep-Alive 连接等)需要用 iter/tcp 遍历并加入拦截 Map。
3.2 AxVisor 内部:现有诊断能力的不足
AxVisor 的 shell 命令系统提供了基本的 VM 管理能力,但在诊断信息获取方面有不少缺口。
已有的命令:
| 命令 | 提供的信息 |
|---|---|
vm list | VM ID、名称、状态、vCPU 数、内存大小 |
vm show <id> | 同上(--full/--config/--stats flag ) |
trace list | 已注册的 tracepoint 和 kprobe |
trace stat | eBPF 程序收集的事件统计 |
缺失的信息:
| 维度 | 缺失内容 | 诊断价值 |
|---|---|---|
| vCPU 状态 | 寄存器值、运行模式、异常状态 | 调试 Guest 卡死、分析 VM Exit |
| 内存映射 | GPA→HPA 映射、Stage-2 页表条目 | 诊断内存访问异常、审计内存分配 |
| 设备列表 | virtio 设备配置、passthrough 设备 | 排查设备模拟问题 |
| 设备统计 | I/O 请求数、中断次数 | 性能分析、瓶颈定位 |
| VM Exit 分布 | HVC/SMC/MMIO/中断等原因统计 | 优化 VMM 性能 |
| 中断状态 | GIC 配置、pending 中断队列 | 调试中断注入问题 |
3.3 Iterator 能做什么
现有的 tracepoint/kprobe 是事件驱动的,只能在事件发生时采集数据。Iterator 提供了主动遍历的方式:
| 场景 | 事件驱动的局限 | Iterator 怎么解决 |
|---|---|---|
| 诊断 vCPU 卡死 | 没有事件触发,无法采集 | 主动遍历所有 vCPU,导出当前状态 |
| 审计内存分配 | 需要 hook 每次分配,开销大 | 按需遍历页表,获取当前全貌 |
| 排查设备问题 | 需要预先埋点 | 遍历设备注册表,不用改代码 |
| 生成诊断快照 | 需要多个 tracepoint 配合 | 单次遍历,原子性获取一致状态 |
3.4 潜在的 AxVisor Iterator 类型
如果在 AxVisor 内部实现 Iterator 框架,可以定义这些遍历目标:
| Iterator 类型 | 遍历对象 | 用途 |
|---|---|---|
iter/vm | 所有 VM | 导出 VM 列表和详细配置 |
iter/vcpu | 所有 vCPU | 导出寄存器状态、运行模式 |
iter/gpa_region | Guest 物理地址区域 | 审计内存映射 |
iter/device | 模拟和透传设备 | 设备配置和统计 |
iter/irq | 中断描述符 | 中断路由和状态 |
这需要在 AxVisor 内部实现类似 Linux seq_file 的遍历框架。
4. AxVisor 环境下的实现挑战
4.1 运行环境分析
Iterator 程序依赖 Linux 内核的 seq_file 遍历框架。在 AxVisor 场景下,需要搞清楚程序在哪里运行:
┌─────────────────────────────────────────────────────────┐
│ Host Linux │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
│ │ iter_tcp │ │ iter_task │ │ Other Iterators │ │
│ │ (Traverses │ │ (Traverses │ │ │ │
│ │ Host TCP) │ │ Host Task) │ │ │ │
│ └──────┬──────┘ └──────┬──────┘ └────────┬────────┘ │
│ │ │ │ │
│ v v v │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Linux Kernel Iterator Framework │ │
│ │ (seq_file + bpf_iter_reg) │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ v │
│ ┌─────────────────────────────────────────────────┐ │
│ │ AxVisor │ │
│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │
│ │ │ VM 1 │ │ VM 2 │ │ VM N │ │ │
│ │ └───────────┘ └───────────┘ └───────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
4.2 两种实现路径
| 路径 | 说明 | 复杂度 | 适用场景 |
|---|---|---|---|
| 宿主侧 Iterator | 在宿主 Linux 上运行,遍历宿主内核数据结构 | 低 | TCP/UDP 连接、进程列表等标准对象 |
| AxVisor 侧 Iterator | 在 AxVisor 内部实现遍历框架,遍历 VMM 对象 | 高 | VM、vCPU、虚拟设备等 Hypervisor 对象 |
4.3 宿主侧 Iterator
直接复用 Linux 内核的 Iterator 框架:
- 优点:不用改 AxVisor,标准 eBPF 工具链直接可用
- 缺点:只能遍历宿主内核对象,访问不了 VMM 内部状态
- 适用于网络流量拦截、进程审计等宿主侧需求
4.4 AxVisor 侧 Iterator
如果需要遍历 VMM 内部对象(比如所有 VM 的 vCPU 状态),需要做这几件事:
- 实现遍历框架:类似
seq_file的迭代器基础设施 - 注册遍历目标:为 VM、vCPU、设备等对象提供遍历回调
- 定义上下文结构:如
bpf_iter__vm、bpf_iter__vcpu - 暴露触发接口:通过 shell 命令或特殊文件触发遍历
5. 替代方案对比
为什么选 Iterator 而不是别的方案?
5.1 方案对比
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| BPF Iterator | 内核态遍历,BPF 程序处理每个对象 | 高效、安全、可编程 | 需要特定程序类型支持 |
| 读取 /proc | 解析 /proc/net/tcp 等文件 | 简单、通用 | 用户态解析开销大,格式不稳定 |
| Netlink 查询 | 通过 NETLINK_SOCK_DIAG 获取 socket 信息 | 结构化数据 | 需要多次系统调用,无法直接操作 Map |
| 定时 kprobe 快照 | 周期性触发 kprobe 记录状态 | 复用现有机制 | 不完整,依赖事件触发 |
| 遍历 BPF Map | 用户态 bpf_map_get_next_key | 不需要额外程序类型 | 只能遍历 Map,不能遍历内核对象 |
5.2 详细分析
读取 /proc/net/tcp:
User Space Kernel Space
│ │
│ open("/proc/net/tcp") │
├────────────────────────────────────>│
│ │
│ read() × N times │
├────────────────────────────────────>│ Every read triggers formatting
│ │
│ Parse text in user space │
│ Construct data structures │
│ Update BPF Map │
├────────────────────────────────────>│ bpf() syscall
│ │
问题在于:文本解析有开销,需要多次系统调用,而且遍历期间连接状态可能变化,数据一致性没有保证。
BPF Iterator:
User Space Kernel Space
│ │
│ read(iter_fd) │
├────────────────────────────────────>│
│ │ Kernel-side traversal
│ │ Direct BPF Map manipulation
│ │ Single atomic operation
│<────────────────────────────────────│
│ │
只需要一次系统调用,内核态直接操作 Map,遍历期间持有 RCU 锁保证数据一致。
5.3 性能对比(理论)
| 指标 | /proc 解析 | Netlink | BPF Iterator |
|---|---|---|---|
| 系统调用次数 | O(n) | O(n) | O(1) |
| 数据拷贝 | 内核→用户→内核 | 内核→用户→内核 | 内核内部 |
| 一致性 | 弱 | 弱 | 强(RCU 保护) |
| 可编程性 | 无 | 无 | 完全可编程 |
5.4 结论
对于“热接入已有 TCP 连接“这个需求:
- /proc 解析:可行但低效,适合一次性脚本
- Netlink:数据结构化但仍需用户态处理
- BPF Iterator:最合适的方案,内核态完成全部工作
Iterator 是唯一能在内核态直接将连接信息写入 SOCKHASH Map 的方案。