☰
IOMMUFD脏页跟踪与Dirty Bits读取实现详解
2026/9/29 19:52:58 网站建设 项目流程

一台物理机上跑了三台虚机,其中一台绑定了万兆网卡,迁移这台虚机时,QEMU 能通过 KVM 把普通内存的脏页抓得清清楚楚,但设备 DMA 写过的页面它在 KVM 侧根本看不到。这些年做设备直通迁移踩过最深的一个坑就是“CPU 看到的干净页,设备已经写脏了”。VFIO 引入 IOMMUFD 之后,脏页跟踪能力从 VFIO type1 下沉到了 IOMMU domain,由 IOMMU 硬件直接记录设备 DMA 写脏的页表项,这就涉及 Dirty Bits 的跟踪、读取和清除。这篇是 VFIO 框架源码分析的第二十二篇,专门聊 IOMMUFD 里的脏页跟踪和 Dirty Bits 读取实现,适合要理解设备直通热迁移链路、准备改内核 IOMMU 驱动或正在排查迁移丢页问题的人。

1. 为什么脏页跟踪会成为 IOMMUFD 的硬需求

1.1 设备直通场景下的迁移困境

设备直通(device passthrough)说白了就是把物理设备直接交给虚机,让它的 DMA 直接打进客户机内存。这时宿主机上的页表与设备页表要严格一致,而设备写过的页面,CPU 侧的 PTE 完全不会感知,KVM 的 dirty page tracking 也就无能为力。于是迁移时就会出现一种“看起来干净、实际脏”的页面——设备刚才写过了,但你没有记录下来。设备直通要做热迁移,必须在 IOMMU 侧建立一套独立于 CPU 的脏页记账机制。IOMMUFD 的 dirty tracking 就是干这个的。

传统的 VFIO 用 vfio_iommu_type1 这个后端自己做脏页跟踪,代码逻辑堆在 VFIO 模块里。后来内核引入 IOMMUFD,希望把 IOMMU 地址空间管理从 VFIO 这个“设备管理框架”里剥离出来,变成通用内核服务。脏页跟踪也就跟着权限下放到了 IOMMU domain 这一层。理解这条线,再看代码会顺手很多。

1.2 从 VFIO type1 到 IOMMUFD 的脏页能力转移

传统 VFIO 里,用户态通过 VFIO_IOMMU_DIRTY_PAGES 这个 ioctl 拿脏页。它的实现是 vfio_iommu_type1 自己遍历 dma entry,再通过 iommu 层的接口读取。问题是 type1 把大量与 IOMMU 相关的东西耦合在一起,导致新的 IOMMU 特性很难独立发展。IOMMUFD 出现后,VFIO 可以配置成使用 iommufd 作为底层后端,脏页接口也顺势改由 iommufd 提供,设备侧只需要调用 VFIO_IOMMU_DIRTY_PAGES 的兼容层,内核内部会转换成 IOMMUFD_CMD_IOAS_DIRTY_BITMAP 或 IOMMUFD_CMD_HWPT_DIRTY_BITMAP。

1.3 概念边界:IOAS、HWPT 与脏页位图

在 IOMMUFD 里,IOAS(I/O Address Space)是用户态可见的虚拟地址空间集合,HWPT(Hardware Page Table)是真正提交给 IOMMU 硬件使用的页表。一个 IOAS 可以对应多个 HWPT,同一个地址空间给不同设备使用时,可能就是不同页表实例。脏页跟踪发生的位置默认在 IOAS 这一层,也可以细化到某个 HWPT。之所以要做这个区分,是为了应对嵌套虚拟化、多设备共享 IOAS 等场景。脏页位图保存在内核对象里,平时不占太多内存,只在 dirty tracking 开启期间维护。

2. 核心数据结构和 UAPI 设计解读

2.1 用户态看到的 ioctl 长什么样

当前主线内核里,用户态和 IOMMUFD 交互脏页主要用这样一个命令簇(示意结构体,字段以 include/uapi/linux/iommufd.h 为准):

struct iommu_ioas_dirty_bitmap { __u32 size; __u32 ioas_id; __u64 flags; __u64 iova; __u64 length; __u64 data; };

flags 里定义四种基本操作:enable、disable、get bitmap、clear bitmap。命令本身可以支持同时传入多个标志,但实际使用时几乎都是单操作。整体设计的出发点是让用户态一次调用就能完成“打开追踪、读取位图、清零脏位”的闭环,减少 ioctl 次数。

2.2 flags 的含义与组合逻辑

  • IOMMU_DIRTY_TRACKING_ENABLE:在当前 ioas 或 hwpt 上开启脏页跟踪。开启时内核会检查该地址空间上的映射状态。
  • IOMMU_DIRTY_TRACKING_DISABLE:关闭,释放位图相关资源。
  • IOMMU_DIRTY_BITMAP_GET:把一段 [iova, iova+length) 范围内的脏页位图复制到用户缓冲。
  • IOMMU_DIRTY_BITMAP_CLEAR:把范围内脏位清掉,用于迭代迁移的下一轮。

组合使用时,最常见的是 GET+CLEAR,即内核先收集位图,再立刻把硬件脏位清掉。这个组合之所以设计得如此自然,是因为热迁移预拷贝阶段每轮都要“读完清掉,再等下一轮脏页”。

2.3 内核对象如何持有脏页状态

在 ioas 内部,每个 iopt_area(一段已映射的 DMA 区域)都有对应的内存页信息。开启 dirty tracking 后,IOMMUFD 会在对象上维护一个 dirty bitmap 结构,记录当前映射范围、页大小、已分配的内核位图指针。一个常见误区是以为脏页状态存在 PTE 之外某个独立缓存里。实际上 IOMMU 硬件是在页表项本身维护 dirty/accessed 位,内核只是定时把这些位“收割”到软件位图里,再交给用户空间。IOMMUFD 内部的 dirty bitmap 主要是承上启下的中转站。

2.4 驱动回调:set_dirty_tracking 与 read_and_clear_dirty

IOMMUFD 不直接操作任意一家 IOMMU 的寄存器,而是通过 iommu_domain_ops 回调干活。两个关键回调是:

int (*set_dirty_tracking)(struct iommu_domain *domain, bool enabled); int (*read_and_clear_dirty)(struct iommu_domain *domain, unsigned long iova, size_t size, unsigned long *bits);

set_dirty_tracking 负责打开或关闭硬件层的 dirty bit 记录能力。read_and_clear_dirty 负责扫描指定范围内页表项,收集脏位并清零。具体到厂商硬件,Intel 在二级页表上用 S/D 位,AMD 维护 updated 位,ARM SMMUv3 依赖 HTTU 机制。IOMMUFD 只关心调用层的语义,不关心每家的实现细节,这正是它比 vfio_iommu_type1 设计得干净的地方。

3. 脏页跟踪的开启与关闭流程

3.1 enable 时的内部动作

用户发起 enable 后,内核先检查 ioas/hwpt 状态,再检查底层 IOMMU 驱动有没有提供 set_dirty_tracking 回调。没有这项能力时,直接返回 EOPNOTSUPP。接下来分配 dirty bitmap 管理结构,遍历该地址空间已有的全部映射区域,把对应 domain 的硬件 dirty tracking 打开。这里有个容易被忽略的细节:如果 enable 之前已经有大量映射区域,不同厂商对“已经存在的页表项”处理办法不同,有的驱动会直接重建页表让 dirty 位生效,有的只是标志位翻转。因此线上生产环境建议先开启 dirty tracking、再建立 DMA 映射,避免出现“映射早于跟踪”导致的空窗期。

3.2 映射与 dirty tracking 的先后顺序问题

如果你在 dirty tracking 开启后,再往 ioas 里 map 一页,新映射页表项创建时驱动自然会带上 dirty tracking 属性,没问题。麻烦的是反过来:先 map 后 enable。此时页表项可能不会自动补上 dirty 记录能力,导致那部分区域的脏页信息在上电后到 enable 之间的写入完全丢失。对迁移来说,丢失窗口就是漏页,可能会损坏目标端数据。我的建议是:把 enable 放在 VM 启动阶段、所有 IOMMU 映射建立之前;或者在代码评审时坚持“映射范围一旦 mask 住就不允许中途 enable”。

3.3 disable 的清理与资源释放

disable 时内核会把该对象维护的脏页位图释放,底层驱动关闭硬件 dirty bit 记录。注意 disable 与 GET 的顺序:如果最后一轮迁移忘了 GET,直接 disable,那最后一波的脏页就丢在硬件里了。很多迁移代码在 stop-and-copy 后会做一次 final GET,然后才 disable,这个顺序不能乱。

4. Dirty Bits 读取的核心实现

4.1 GET 位图的完整调用路径

以 ioas 为例,GET 流程大体是:ioctl 进入 iommufd 核心,找到对象,锁住地址空间,校验 iova/length 是否落在已映射区域,根据区域记录的页大小计算出需要多少个 bit,然后调用底层驱动的 read_and_clear_dirty,把硬件脏位收集到内核位图,最后 copy_to_user 回用户缓冲。整个过程在持有锁的状态下进行,避免并发 map/unmap 改变页表导致位图错乱。

GET 的数据拷贝有一点值得注意:用户传入的 buffer 大小要足够。内核通常会先检查 data 指针指向的空间容量,不够就返回需要的字节数。建议用户态预留至少 length / page_size / 8 的大小,并且把页大小按 domain 的最小支持粒度算,否则遇到 2MB 和 4KB 混合映射时容易踩到 buffer 溢出。

struct iommu_ioas_dirty_bitmap arg = { .size = sizeof(arg), .ioas_id = ioas_id, .flags = IOMMU_DIRTY_TRACKING_ENABLE, }; ioctl(iommufd, IOMMUFD_CMD_IOAS_DIRTY_BITMAP, &arg); /* 读取时 */ arg.flags = IOMMU_DIRTY_BITMAP_GET | IOMMU_DIRTY_BITMAP_CLEAR; arg.iova = start; arg.length = len; arg.data = (uintptr_t)user_buf; ioctl(iommufd, IOMMUFD_CMD_IOAS_DIRTY_BITMAP, &arg);

4.2 位图编码、对齐与半页边界

位图编码很简单:每个 bit 对应一个 IOMMU 页。iova 起始地址对应的页是 bit0,地址每增加一页,bit index 加 1。读取范围的首尾地址要求页对齐,否则驱动没法把“半个页”的脏位塞进位图。实际内核会要求用户把 iova 和 length 都对齐到页大小,遇到结尾不是整页的情况一般直接报 EINVAL,或者把末尾超出部分静默丢弃。所以用户态在触发 GET 前,最好用 iommu_page_size 对齐,别以为传任意地址内核会好心兜底。

4.3 read-and-clear 为什么必须是原子操作

为什么 GET 和 CLEAR 要绑在一起?因为如果先 GET 再 CLEAR,两次调用之间设备 DMA 可能又在同一页上写了新数据。第一轮 GET 把脏位取走了,紧接着 CLEAR 把页面清成干净状态,没有复制给用户的“新脏”就白白丢了。反过来,先 CLEAR 再 GET 更糟,会把已有脏页全部清掉但你不读。唯一安全的做法是硬件驱动在扫描页表时一边收集脏位,一边对页表项做原子清理,让“读取并清除”成为一个不可分割的流程。这也是 read_and_clear_dirty 这个名字的含义。

4.4 硬件加速:不同 IOMMU 的 dirty bit 实现差异

各家的实现虽然都叫 dirty bit,差异却不小。Intel VT-d 在二级页表中维护 Accessed/Dirty 位,开启 dirty tracking 后,设备 DMA 写入会让对应 PTE 的 D 位自动置 1。AMD IOMMU 类似但历史兼容性更复杂,老平台甚至不支持完整 dirty tracking。ARM SMMUv3 靠 HTTU(Hardware Translation Table Update)把访问和脏状态写到页表,效率很高,但对 PTE 的缓存一致性要求也高。在支持度不一的现实下,IOMMUFD 能做的就是统一回调接口,把硬件差异全部挡在驱动层,用户态拿到的都是同样的内核位图。不要试图在用户态感知底层到底是哪家硬件。

IOMMU 方案脏位维护机制注意事项
Intel VT-d二级页表 S/D 位取决于处理器和 IOMMU 版本
AMD IOMMUupdated 位老平台兼容性需要额外验证
ARM SMMUv3HTTU 更新页表项对 PTE 缓存一致性要求高

5. 常见问题与排查技巧实录

5.1 错误码与典型场景对照

错误码可能原因建议排查方向
EOPNOTSUPPIOMMU 驱动不支持 dirty tracking检查内核配置和 IOMMU 硬件能力
EINVALiova/length 未对齐、超出映射范围、buffer 太小用页大小对齐后重试;检查 map 范围
ENOENT传入的 ioas_id/hwpt_id 无效检查对象是否已创建、是否已被释放
EBUSYdirty tracking 已经开启/关闭,重复操作先查询状态再执行对应动作

遇到 EOPNOTSUPP 先别怀疑 IOMMU,先看是不是拿了一台老机器或者开了嵌套虚拟化。嵌套虚拟化场景下,二级 IOMMU 往往不能直接提供 dirty tracking,常见做法是上层虚拟 IOMMU 用软件模拟跟踪,这时候 IOMMUFD 直接操作硬件的路径就不可用。排查时第一步是查看硬件页表能力,第二步是确认当前 domain 是否用了正确的 ops。

5.2 迁移集成时的脏页集合合并

设备直通的 VM 在做热迁移时,KVM 侧有一份脏页位图,IOMMUFD 侧又有一份。两个位图描述的是不同视角的脏页:一个是 CPU/EPT 写过的,一个是设备 DMA 写过的。合并时千万别用“OR”就完事,还要注意两个位图的页粒度可能不一致。KVM 常用 4KB 粒度或 dirty ring 的 64 页粒度,IOMMU 侧可能混合 4KB 和 2MB。稳妥做法是统一换算到最小粒度再做 OR,否则大页区域的脏页信息会被丢在粒度缝隙里。这个坑我实际见过不止一次,最后丢页的位置恰好是设备 DMA 频繁写入的匿名页区域。

5.3 我的调试习惯和测试小工具

调试脏页跟踪最有效的办法永远是“小范围单页测试”。我会先把 dirty tracking 开启,map 一页,用一个可编程 DMA 设备往这页写数据,然后 GET 位图,检查第一个 bit 是否置位。反复跑几轮,再验证 CLEAR 后第二轮不会再出现旧脏位。为了快速定位,我还会在驱动回调里临时加上 trace_printk,把 read_and_clear_dirty 的 iova、size、返回的脏页数打出来,对比用户态看到的位图数量是否一致。一套组合拳下来,90% 的问题都能定位到是页粒度不匹配还是缓冲区容量不足。

在 IOMMUFD 里读 Dirty Bits,我实际使用中最大的体会是:不要指望一次 GET 就拿到“全量准确”的位图。设备 DMA 总是在动的,预拷贝阶段每轮读取之间总有新的写入,stop-and-copy 阶段的 final GET 才是最终依据。所以设计工具和迁移流程时,要把“最后一轮读取”作为可靠边界,而不是把每一轮都当最终版本。另一个小技巧是尽量把 GET 请求按连续的 DMA 映射区域拆分,一次查一个大范围,减少 ioctl 往返次数,在密集迁移时能省下不少时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询