Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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/tcpTCP socket连接状态导出
iter/udpUDP socketUDP 端点枚举
iter/bpf_map_elemBPF Map 条目Map 批量操作
iter/netlinkNetlink socket网络诊断

2.4 与 kprobe/kretprobe/uprobe 的区别

维度kprobe/kretprobe/uprobeBPF Iterator
触发机制被动:等待事件发生主动:用户态驱动遍历
执行时机函数入口/出口被调用时用户态 read() 时
数据来源当前执行上下文(寄存器、栈)内核全局数据结构
观测范围单次函数调用整个数据结构集合
状态感知只能捕获“变化“可获取“当前全貌“
实现方式断点指令 + 异常处理seq_file + 内核遍历框架

简单来说:

  • kprobe 在函数入口插入断点(如 BRK #4),函数被调用时触发,只能观测到“正在发生的调用“。
  • kretprobe 在函数返回时触发,捕获返回值,同样是事件驱动。
  • uprobe 和 kprobe 类似,但作用于用户态程序。
  • Iterator 不依赖任何事件,直接遍历内核维护的数据结构。比如可以获取“此刻系统中所有 TCP 连接“这样的全局快照。

2.5 内核数据结构依赖

Iterator 程序直接访问内核内部数据结构。以 iter/tcp 为例,程序需要访问 struct sock_commonstruct 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 listVM ID、名称、状态、vCPU 数、内存大小
vm show <id>同上(--full/--config/--stats flag )
trace list已注册的 tracepoint 和 kprobe
trace stateBPF 程序收集的事件统计

缺失的信息:

维度缺失内容诊断价值
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_regionGuest 物理地址区域审计内存映射
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 状态),需要做这几件事:

  1. 实现遍历框架:类似 seq_file 的迭代器基础设施
  2. 注册遍历目标:为 VM、vCPU、设备等对象提供遍历回调
  3. 定义上下文结构:如 bpf_iter__vmbpf_iter__vcpu
  4. 暴露触发接口:通过 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 解析NetlinkBPF Iterator
系统调用次数O(n)O(n)O(1)
数据拷贝内核→用户→内核内核→用户→内核内核内部
一致性强(RCU 保护)
可编程性完全可编程

5.4 结论

对于“热接入已有 TCP 连接“这个需求:

  • /proc 解析:可行但低效,适合一次性脚本
  • Netlink:数据结构化但仍需用户态处理
  • BPF Iterator:最合适的方案,内核态完成全部工作

Iterator 是唯一能在内核态直接将连接信息写入 SOCKHASH Map 的方案。