尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

IOMMUFD脏页跟踪机制解析:从Dirty Bits读取到热迁移实践

发布时间:2026/9/29 19:52:57

资讯中心
01
ARTICLE

IOMMUFD脏页跟踪机制解析:从Dirty Bits读取到热迁移实践

IOMMUFD脏页跟踪机制解析:从Dirty Bits读取到热迁移实践
这个系列写到现在前面把 iommufd 的 ioas 管理、hwpt 创建还有 iova 映射都拆得差不多了这一篇终于要碰热迁移里最要命的一环脏页跟踪与 Dirty Bits 读取。VFIO 设备直通场景下虚机要热迁移宿主机不可能把几十个 G 的客户机内存全量拷贝过去只能靠 IOMMU 硬件记录设备 DMA 写脏的页再把这些脏页位图交给迁移工具做增量同步。IOMMUFD 作为新一代用户态 IOMMU 接口把脏页跟踪从 VFIO type1 的老框架里抽出来下沉到了 iommu_domain 层面这套设计的思路很值得单独开一篇来聊。不管你是做虚拟化平台底层开发还是在调 QEMU 的 VFIO migration 流程或者单纯想搞明白 IOMMUFD 的 uapi 里那几个 dirty bitmap 相关结构体怎么用这篇都适合你。我会从“为什么需要脏页跟踪”开始把 iommu_dirty_ops 的软件骨架、双缓冲位图的设计意图、ioctl 读取的完整调用链以及 Intel/AMD/ARM 这几类硬件实现的差异逐个讲清楚。最后放几个我在实际调试中踩过的坑给后面动手的人省点时间。1. 背景热迁移为什么离不开脏页跟踪1.1 数据面需求从全量拷贝到增量同步设备直通的虚机做热迁移和普通虚机最大的区别在于除了 CPU 寄存器和内存你还要考虑设备 DMA 写过的内存。普通虚机迁移时QEMU 自己就能追踪 vCPU 写脏的页但直通设备走的是 IOMMU设备 DMA 直接写客户机物理内存QEMU 根本不知道哪一页被设备写过了。如果迁移过程中不处理这部分脏页目标端恢复出来的内存状态就会缺数据设备一接管就要出问题。最简单的方案是全量拷贝先把所有内存页传给目标端再停设备、停 vCPU把剩余状态同步过去。这对小内存虚机还能忍但对动辄几十 G 的数据库实例来说全量拷贝的耗时和带宽占用是完全不可接受的。于是就有了脏页跟踪迁移过程中只同步“从上一次同步点到现在又被设备写脏的那些页”。IOMMU 硬件在页表项里维护 dirty 位软件定期读取并清除这些位就能得到一个增量位图。这里要强调一个很容易被忽视的细节新映射的 iova 在开启脏页跟踪后会被默认视为脏页。原因是目标端还没同步过这片内存如果新 map 的区域不标记为脏迁移就会漏数据。这个逻辑是在 iommu core 的 map/unmap 路径里实现的后面第 2 章我会具体说。1.2 IOMMUFD 与 VFIO 老框架的路线差异老的 VFIO 框架里脏页跟踪是通过VFIO_IOMMU_DIRTY_PAGES这个 ioctl 做的由vfio_iommu_type1这个模块自己维护一份位图。它的问题是脏页记录逻辑和 vfio 的 iommu 映射管理强耦合type1 代码里到处是 bitmap 操作的边角逻辑而且不同的 IOMMU 驱动能力差异被掩盖在这一层抽象后面很难做精细控制。IOMMUFD 的方案是把脏页跟踪下沉到 iommu_domain。IOMMU 核心层定义了统一的iommu_dirty_ops每个 IOMMU 驱动自己实现“开跟踪、读脏页、清脏页”这三个动作。IOMMUFD 的用户态接口IOMMU_HWPT_GET_DIRTY_BITMAP只是薄薄一层封装把用户的请求翻译成对iommu_domain_dirty_bitmap()的调用。这样做的好处是VFIO、vfio-pci、甚至其他非 VFIO 的上层模块都能复用同一套脏页跟踪机制不再各写各的。从源码看IOMMUFD 的 dirty tracking 还引入了引用计数和 ioas 生命周期管理确保在 ioas 被 unmap 或者 hwpt 释放的瞬间不会出现正在读位图、底层的页表却被拆了的竞态。这一层在 VFIO 老框架里是靠大锁硬扛的IOMMUFD 里的处理要干净得多。2. 核心数据结构与双缓冲位图设计2.1 iommu_domain 里的脏页跟踪基础设施IOMMUFD 的脏页跟踪核心落在 iommu_domain 这个结构体上。在include/linux/iommu.h里和 dirty tracking 相关的字段大致是这样struct iommu_domain { ... unsigned long pgsize_bitmap; ... struct iommu_dirty_ops *dirty_ops; unsigned long *dirty_bitmap; spinlock_t dirty_lock; ... };pgsize_bitmap表示这个 domain 支持的页粒度集合比如4K | 2M | 1G脏页位图的每一位对应的就是最小的那段粒度。dirty_bitmap是 IOMMU core 自己维护的一份“软件侧”脏页位图dirty_lock是保护它和脏页状态切换的自旋锁。dirty_ops则是 IOMMU 驱动注册的回调集合定义如下struct iommu_dirty_ops { int (*set_dirty_tracking)(struct iommu_domain *domain, bool enabled); int (*read_and_clear_dirty)(struct iommu_domain *domain, unsigned long *bitmap, unsigned long iova, size_t size, unsigned long flags); int (*dirty_bitmap_clear)(struct iommu_domain *domain, unsigned long *bitmap, unsigned long iova, size_t size, unsigned long flags); };这三个回调分别对应三件事打开或关闭脏页跟踪、读取并清除硬件记录的脏页、只清除不读取。大部分 IOMMU 驱动只需要实现前两个dirty_bitmap_clear在部分场景下可以直接复用 read_and_clear 然后丢弃结果但单独拆出来是为了给某些硬件提供更高效的清脏路径。2.2 map/unmap 时脏页标记的联动逻辑脏页跟踪开启后内存映射的变化也必须同步反映到位图里否则会出现漏记或误记。在 iommu core 的 map 路径里大致是这样的逻辑如果当前 domain 开启了脏页跟踪那么新映射的 iova 范围会被整体标记为脏反过来unmap 的时候这个范围对应的位会被清除。为什么新 map 的页要标记为脏因为热迁移场景下目标端的内存镜像只包含“已经同步过的页”。新映射出来的内存目标端一定还没有如果不在脏页位图里标出来下次增量同步就不会带它过去迁移完成后目标端访问这片内存就是空的。这个设计思路和 CPU 内存迁移工具比如用户态的 migration bitmap是一致的新 map 即脏。unmap 清除位图则比较直观这段 iova 对应的物理页都不在这个 domain 里了自然也就不需要再跟踪它的写脏状态。但这里有一个需要注意的时序问题如果正在读脏页位图的过程中有人 unmap可能读到半新半旧的位图。IOMMUFD 层通过 ioas 的引用计数和 hwpt 的状态机来串行化这些操作保证脏页读取和映射修改不会交错。2.3 双缓冲位图为什么这么设计很多第一次看代码的人会疑惑IOMMU 驱动自己不是有硬件 dirty 位吗为什么 iommu core 还要再维护一份domain-dirty_bitmap这其实是双缓冲的精髓硬件侧记录的是“自上次清除以来的脏页”软件侧记录的是“已经上报过、但还没返回给用户态的脏页状态”的补充。举个实际场景用户态第一次读取脏页位图把硬件侧读出来并清零了但返回给用户态的过程中设备又 DMA 写脏了同一页。用户态拿到位图后执行第二轮增量同步如果只有硬件侧这一份记录这一页的“新脏”状态会丢失吗不会因为硬件侧会再次置位。真正的问题是另一类场景某些 IOMMU 驱动在 read_and_clear 的时候只能按整段 iova 范围清除或者清除动作本身有延迟如果用户态拿到的位图还没拷贝完硬件侧已经被清了而软件侧又没有备份这中间产生的脏页就丢了。双缓冲的语义是read_and_clear_dirty 把硬件侧脏页读出来先和domain-dirty_bitmap合并再把合并结果返回给用户态最后把domain-dirty_bitmap里这一段清掉。这样即使硬件侧因为清除时序丢了几个位软件侧也能在下次读取时兜底。代价就是多一份内存和一次位图合并的 CPU 开销但在热迁移场景里这笔开销换来的数据完整性是值得的。3. IOMMUFD 脏页读取的完整调用链3.1 uapi 层的数据容器结构IOMMUFD 的脏页读取接口是IOMMU_HWPT_GET_DIRTY_BITMAP对应的用户态结构体在include/uapi/linux/iommufd.h里struct iommu_hwpt_get_dirty_bitmap { __u32 size; __u32 hwpt_id; __u32 flags; __u32 hugepages; __aligned_u64 data; __aligned_u64 data_len; }; struct iommu_dirty_bitmap_data { __aligned_u64 iova; __aligned_u64 length; __aligned_u64 bitmap; __aligned_u64 bitmap_size; };外层结构指定 hwpt 的 ID 和 flagsdata指针指向一个iommu_dirty_bitmap_data里面才是真正要查询的 iova 范围、位图用户态缓冲区地址和缓冲区大小。flags里有个IOMMU_HWPT_GET_DIRTY_BITMAP_NO_CLEAR置上之后这次读取不会清除硬件侧的脏页标志适合做“只观察不清除”的调试或最后确认。hugepages字段值得单独说一下。置 1 时位图每一位对应的是 huge page 粒度而不是 IOMMU 页表的最小页粒度。对大内存虚机来说按 4K 粒度维护位图几十 G 内存对应几百万个 bit用户态处理和网络传输都很重按 2M 甚至 1G 粒度位图体积直接少几个数量级。代价是精度变粗同步时可能会多传一些“其实没脏”的页但在大内存场景这是非常划算的取舍。3.2 ioctl 入口到 iommu_domain_dirty_bitmap 的调用链从 ioctl 入口到底层驱动整个链路不算长但每一层要做的事情很明确ioctl(IOMMU_HWPT_GET_DIRTY_BITMAP) └─ iommufd_hwpt_get_dirty_bitmap() ├─ 校验 uapi 结构体和 data 指针 ├─ 根据 hwpt_id 查找到 iommufd_hwpt ├─ 访问 iommu_dirty_bitmap_data取出 iova/length/bitmap/bitmap_size ├─ iommu_domain_dirty_bitmap(domain, kbitmap, iova, length, flags) │ ├─ 调用 domain-dirty_ops-read_and_clear_dirty() │ ├─ 把结果与 domain-dirty_bitmap 合并到用户传入的位图 │ └─ 清除 domain-dirty_bitmap 中对应区域 └─ copy_to_user 把最终位图拷回用户态 data.bitmapiommufd_hwpt_get_dirty_bitmap做的事主要是对象查找和权限校验。它根据 hwpt_id 在 iommufd 的对象表里找到对应的 hwpt然后取到它背后的 iommu_domain。接下来真正的脏页读取逻辑在 iommu core 的iommu_domain_dirty_bitmap()里完成。iommu_domain_dirty_bitmap()先检查这个 domain 是否支持脏页跟踪dirty_ops是否为 NULL再检查 user 传入的 iova 和 size 是否按页对齐。然后它会临时分配一块内核位图调用驱动的read_and_clear_dirty把硬件侧脏页读进来再通过位图合并操作把domain-dirty_bitmap里尚未消费的脏页并进去最终结果才拷贝给用户态。注意这个函数返回之后驱动侧已经被清过了如果用户态在拿到位图后又发生 DMA 写那要等下一次读取才能看到。3.3 位图数据如何组织粒度、对齐与长度计算位图的长度计算是新手最容易出错的地方。假设 domain 的最小页粒度是 4K用户要查询的 iova 范围是 [0x100000, 0x500000)长度是 4M那么对应 1024 页位图需要 1024 bit也就是 128 字节。bitmap_size必须按 8 字节对齐并且内核一般要求不小于实际需要的字节数。如果用户传小了调用会直接失败不会给你静默截断。更严谨一点位图不同位的索引偏移是page_index (iova - range_start) granule_shift;granule_shift在 hugepages0 时取ilog2(min_io_pagesize)在 hugepages1 时取用户指定或驱动支持的最大 huge page 粒度。看到这里你应该理解为什么我不能直接建议“固定用 4K 粒度”了如果你的虚机内存大、迁移窗口又紧2M 粒度能省掉大量位图传输时间但如果你的工作负载 DMA 写非常分散2M 粒度会导致每个 2M 块里只要有一页被写就整块重传可能比 4K 粒度还慢。这个 trade-off 要结合业务选不能一概而论。4. 不同 IOMMU 硬件背后的 dirty 实现差异4.1 Intel/AMD 的页表 Dirty 位机制Intel IOMMU 和 AMD IOMMU 的脏页跟踪思路很接近都是靠页表项里的 dirty 位来记录设备写操作。开启脏页跟踪时驱动会确保新分配的页表项支持 dirty 维护DMA 写发生的时候IOMMU 硬件在地址翻译过程中自动把对应 PTE 的 dirty 位置上。软件要做的事就是遍历用户查询范围内的页表项把 dirty 位搬到软件位图里然后清掉这些位。注意这里有个页表遍历的开销问题。如果 iova 范围很大、页表层级深一次全量扫描可能涉及几千个页表项而且清 dirty 位之后还要考虑 IOTLB 的失效。很多驱动在 read_and_clear_dirty 里会合并连续页面的处理尽量用批量操作而不是一次页表项一个操作。还有如果开了 super page比如 2M/1G 映射一个 PTE 就对应一大片内存遍历起来会快很多这也是为什么前面说的 hugepages 参数在真实迁移场景里有价值。4.2 ARM SMMU 的 HTTU 与其它实现ARM SMMUv3 的情况要复杂一点。它有一套硬件机制叫 HTTUHardware Translation Table Update可以自动维护页表项里的访问位和脏位。但不是所有 SMMUv3 实现都开了 HTTU需要固件和内核驱动配合使能。在驱动层面arm_smmu_read_and_clear_dirty的实现逻辑和 Intel/AMD 类似都是扫页表但具体判断哪一位是 dirty、要不要先做 TLBI就得看 SMMU 的架构版本和实现细节了。另外也提醒一下不是所有 IOMMU 驱动都有完整的 dirty_ops。比如一些老平台或者嵌入式场景下使用的 IOMMU 驱动可能只支持 set_dirty_tracking 但 read_and_clear_dirty 返回 -EOPNOTSUPP或者干脆三个回调都是 NULL。这时候 iommu core 的能力探测就会返回不支持用户态用IOMMU_GET_HW_INFO查询IOMMU_CAP_DIRTY也会拿不到。遇到这种情况先别急着怀疑 IOMMUFD 的代码先确认底层 iommu_driver 有没有注册 dirty_ops。4.3 用户态如何探测能力并选择脏页跟踪策略IOMMUFD 的能力探测是通过IOMMU_GET_HW_INFO这个 ioctl 做的。内核填回的信息里带一组 capabilities其中就包括IOMMU_CAP_DIRTY。只有这个位被置上你才能期待后续创建 hwpt 时传入IOMMU_HWPT_ALLOC_DIRTY_TRACKING会成功。实际项目中我一般建议用户态在初始化阶段就把能力和粒度信息查出来而不是每次都靠错误码去猜。查询结果包括pgsize_bitmap和 dirty 能力然后你根据这个决定是否开启脏页跟踪位图粒度用 4K 还是 huge page最后的“停设备前确认”要不要带 NO_CLEAR 读一次。如果 hwpt 创建时没带 dirty tracking 标志后面又想临时开按照目前 IOMMUFD 的接口设计只能销毁 hwpt 重建。这在运行期是个比较大的动作所以虚机启动时如果知道自己有迁移需求最好一开始就带上这个标志省得迁移前临时重建。下面用表格总结一下几类常见 IOMMU 驱动的 dirty tracking 能力特点驱动能力探测dirty 位来源read_and_clear 典型实现Intel IOMMUIOMMU_CAP_DIRTYPTE dirty 位遍历页表搬位并清位AMD IOMMUIOMMU_CAP_DIRTYPTE dirty 位遍历页表搬位并清位ARM SMMUv3视 HTTU 而定PTE dirty/access 位扫页表 TLBI其它老旧驱动通常不支持无dirty_ops 为空5. 实操心得与问题排查5.1 开启脏页跟踪后性能下降明显第一次在真实设备上打开 DIRTY_TRACKING 之后我注意到持续 DMA 写入的场景下吞吐有可感知的下降。原因是每次 DMA 写都可能涉及页表 dirty 位的维护而且热迁移过程中还要周期性地扫描页表、清 dirty 位、做 TLB 失效。这个开销是硬性的不可能完全消除。所以实操上我的建议是只有在确实要做迁移时才开 dirty tracking虚机正常运行时不要一直开着。如果业务上确实需要长期开启比如做持续容灾备份那就接受性能代价并且把脏页读取间隔调到业务能接受的频率。不要每 10ms 读一次位图那等于把 CPU 全花在页表扫描上了。另外如果你发现某次读取脏页位图时耗时特别长多半和 huge page 映射状况有关。检查一下 iova 范围内是否有大页映射如果大量 4K 映射位图遍历的页表项数量就会暴涨。5.2 bitmap 长度计算错了导致读取截断这是我见过最多的使用错误。用户态把bitmap_size传小了或者iova/length没有按页对齐内核返回 -EINVAL。更隐蔽的是用户态按 4K 粒度分配了位图但创建 hwpt 时由于底层 IOMMU 的pgsize_bitmap最小粒度是 8K 或者 64K有些平台默认页就是 64K导致实际的位图每一位对应的内存大小比你预期的大算出来的位图大小直接就不对。碰到这类问题先打印pgsize_bitmap和iommu_domain的granule_shift确认你算位图长度用的粒度是不是和内核一致。不要假设一定是 4K这对 ARM 平台特别重要很多 ARM 平台的默认 IOMMU page size 就是 64K。我在调试时一般会写一个小工具先查询能力再手动构造一小段 iova 映射DMA 写几个页然后读位图验证每一位的对应关系。先把最小的 case 跑通再上大内存迁移能省很多排查时间。5.3 设备未完全停止就取脏页最后一批数据不一致热迁移的脏页读取是有阶段的。迁移初期可以边跑边同步脏页但最后收尾时必须先让设备进入 quiesce静止状态再取最后一轮脏页。原因很简单设备还在 DMA 写入的时候你永远不知道当前这次的位图是不是“最终状态”。我在一次自研迁移工具里犯过这个错最后一轮取脏页时设备还在跑位图拿回来了同步也做完了但迁移完成后目标端设备一恢复发现有一小块内存数据不对。排查到最后发现就是设备在“取完位图之后、设备真正停下之前”又写了一个页。这个页既不在最后一轮位图里也没有被后续流程兜住。正确流程应该是迁移工具发出设备暂停请求确认设备已经进入静止状态然后做最后一轮脏页读取和同步。如果设备驱动支持最好再配合 NO_CLEAR 读一次做校验确保硬件侧已经没有新 dirty 位了。这个步骤不能省。5.4 常见问题速查表问题现象可能原因排查方向IOMMU_HWPT_ALLOC 返回 EOPNOTSUPP底层 IOMMU 驱动没注册 dirty_ops用 IOMMU_GET_HW_INFO 查 IOMMU_CAP_DIRTYGET_DIRTY_BITMAP 返回 EINVALiova/length 未对齐或 bitmap_size 不足打印 pgsize_bitmap按最小页粒度重新计算读到的位图全是 0脏页跟踪没真正开启或设备从未 DMA 写目标范围确认 hwpt 创建时带 DIRTY_TRACKING 标志迁移完成后目标端数据不一致最后一轮取脏页前设备未静止先进 quiesce再读位图开启跟踪后性能骤降脏页维护开销 位图读取频率过高降低读取频率考虑 hugepages1最后分享一个我实际调过的 caseQEMU 侧做 VFIO migration 时有一版内核iommu_domain_dirty_bitmap()对 iova 范围合法性检查比较严格用户态传来的 length 跨了多个已经不存在的映射区域结果返回了错误。那个问题查了很久最后发现是用户态在调用前没有做 ioas 映射的连通性检查传了一个“洞”进去。如果你也遇到类似问题先确认你查询的 iova 范围在 ioas 里是完整映射的而不是中间有空洞这个前提一旦不满足内核的行为就可能和你预期不一致。脏页跟踪这块的水很深但把 iommu_dirty_ops 的职责、双缓冲的语义、以及 uapi 的长度计算规则吃透之后后面做迁移工具就顺手多了。下一篇我准备写 IOMMUFD 的 page table mapp/unmap 的并发处理那个方向也是坑不少。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。