
说个有意思的事。我最早注意到 iommufd不是在看内核邮件列表而是某天升级实验台内核时无意中扫到 kconfig 里多了一个 CONFIG_IOMMUFD。当时第一反应是V FIO 是不是要被替换掉了后来花了几周时间把虚拟化直通、用户态驱动和内核 IOMMU 框架重新捋了一遍才发现这事不是“换了个名字”这么简单。今天这篇就当是给大家的一份迁移观察报告也是一份实操记录聊聊 VFIO 到底在做什么iommufd 为什么会出现以及我们这些做虚拟化和高性能计算的人未来该怎么选、怎么迁、怎么避坑。如果你平时用 QEMU 直通网卡或 GPU或者你在弄 DPDK、SPDK 这类用户态驱动这篇文章基本就是给你准备的。就算你只是偶尔编译内核、关注虚拟化新特性搞清楚这两套接口的来龙去脉也会让你在以后查问题、做性能调优的时候少走不少弯路。1. VFIO 到底在干什么为什么大家突然开始提 iommufd1.1 设备直通的基本逻辑先把 VFIO 的定位说清楚。VFIO 是 Linux 内核提供的一个用户态驱动框架核心作用是让用户态进程比如 QEMU 虚拟机直接操作物理设备同时由内核帮你把设备的 DMA 访问隔离到 IOMMU 里。所谓直通就是把一块物理网卡、一张 GPU 或者一块 NVMe 控制器直接分配给某个虚拟机而不是用内核网络栈和存储栈去中转。没有 IOMMU 的时候设备 DMA 会直接读写物理内存用户态拿去一个设备 FD 之后理论上设备可以访问整个内存空间这是灾难性的安全漏洞。有了 IOMMU设备看到的是一个由内核管理的 IO 地址空间用户态需要先把物理内存映射到这个空间设备 DMA 才能访问。VFIO 做的就是把“需要哪些内存、映射到哪个 IO 地址、设备是哪个”这些事情管起来。从这个角度看VFIO 是 KVM/QEMU 直通场景里的标准答案。过去十几年几乎所有虚拟化直通方案都长这样内核里装 vfio-pci 驱动设备被绑定到这个驱动QEMU 通过 /dev/vfio/ 下的接口把设备交给虚拟机内核用 vfio_iommu_type1 模块管理 DMA 映射。这套东西非常成熟稳定性也久经考验。1.2 VFIO 的容器、组、设备三层模型我在处理直通问题的时候经常被同事问VFIO 里面的 container、group、device 到底什么关系这里用大白话解释一下。VFIO 把设备按安全边界分成了 group也就是 IOMMU group。IOMMU group 是内核根据硬件拓扑和安全约束自动划分的同一个 group 里的设备共享同一个 IOMMU 隔离域。用户态需要用到一个设备时必须先把这个设备所在的 group 放进去一个 container。container 是用户态面对的主要对象内核在 container 上关联一个 IOMMU domain所有 DMA 映射都是针对 container 来做的。最后你才从 group 里 open 出具体的 device FD。所以旧接口的顺序是open 一个 /dev/vfio/vfio 得到全局容器 FDopen 设备对应的 /dev/vfio/N 组 FD把组倒进容器给容器设置 IOMMU 类型再从组 FD 里拿到设备 FD最后用容器的 DMA map/unmap 接口做映射。伪代码大概是这样int vfio_fd open(/dev/vfio/vfio, O_RDWR); int container_fd ioctl(vfio_fd, VFIO_GET_API_VERSION); int group_fd open(/dev/vfio/8, O_RDWR); ioctl(group_fd, VFIO_GROUP_SET_CONTAINER, container_fd); ioctl(container_fd, VFIO_SET_IOMMU, VFIO_TYPE1_IOMMU); int device_fd open(/dev/vfio/8/0000:01:00.0, O_RDWR); ioctl(container_fd, VFIO_IOMMU_MAP_DMA, dma_map);这套模型最大的问题是用户态被迫去理解内核里的 group 概念。group 固然是安全边界但用户态真正关心的是“我要用这台设备”而不是“我和哪些邻居设备被内核绑在了同一个 IOMMU 域里”。很多初学者第一次直通失败就是因为同一 group 里的其他设备没有被全部绑定到 vfio-pci导致整个 group 不可用。1.3 VFIO 的“历史包袱”在哪里VFIO 用得久了各种问题开始显现。第一接口语义混在一起。container 既管设备组又管 DMA 映射还管 IOMMU domain 类型里外里把所有事情都揉在一个 FD 上新特性想加进来就得不停给 ioctl 加分支。第二对嵌套页表支持很勉强。虚拟化里经常要给虚拟机里的客户机 IOMMU 做二次地址翻译也就是 host 页表和 guest 页表嵌套VFIO 的接口设计里没有独立的“IO 地址空间”和“硬件页表”两层概念导致这类需求只能打补丁。第三扩展用户态生态的时候很难受。DPDK、SPDK、vDPA、用户态 virtio 设备这些场景需要更小的接口面、更直接的内存管理VFIO 那套 group/container 模型太笨重。内核社区其实早就注意到了。最初有人尝试在 VFIO 内部修修补补后来发现框架性问题改动成本太高于是决定另起炉灶搞一套面向未来了新接口这就是 iommufd。2. iommufd 的设计内核把 IOMMU 编程模型重新梳理一遍2.1 新接口的命名和背景“iommufd”这个名字乍一看像是 IOMMU 文件描述符其实它是一个完整的内核模块位于 drivers/iommu/iommufd。它最早是作为框架提案出现在内核社区经过多轮 review 后在合入主线目前已经是标准配置的一部分。值得注意的是iommufd 不是单纯把 VFIO 改名而是把设备直通场景里“设备管理”和“DMA 地址空间管理”两级概念彻底拆开。可以这么理解VFIO 是旧式租房中介你租房得先搞清楚整栋楼哪些房客必须一起住group然后中介用一个总合同container把这些房客和房间都绑在一起。iommufd 则是直接把一把钥匙给你告诉你这个地址空间ioasid是你的你可以把任意设备挂到这个地址空间上不需要关心楼上楼下住了谁。当然这是一个非常粗略的类比下面细说。2.2 核心对象ioasid、hwpt、设备 attachiommufd 的模型没有 group没有 container核心对象就三个ioasid、hwpt、设备。ioasid 就是 IO 地址空间 ID你可以把它理解成用户态拥有的一个虚拟地址空间。这个空间可以 MAP 进物理内存页也可以 UNMAP 出去。多个设备可以被 attach 到同一个 ioasid 上这样它们看到的就是同一张 DMA 映射表彼此之间共享地址空间。hwpt 是 hardware page table也就是 IOMMU 硬件页表。为什么要把 ioasid 和 hwpt 分开因为同一份地址空间内容最终落到具体 IOMMU 硬件上时可能因为设备挂在不同 IOMMU 下而需要不同的页表实例。虽然软件上它们是同一个 DMA 地址空间但硬件上每个 IOMMU 需要有一份自己的页表。把这两层拆开之后内核可以按需为 ioasid 复制或共享硬件页表这是 VFIO 时代很难优雅做到的。设备 attach 则是指把某个设备绑定到某个 ioasid 或 hwpt。iommufd 的设备粒度比 VFIO 清晰得多你不再需要先处理 group 再拿设备只要你的设备能被内核里的 vfio-pci 或其他支持 iommufd 的驱动接管就可以直接把它的 FD 交给 iommufd附加到指定的地址空间上。2.3 和 VFIO 的直观对照我整理了一张对照表方便切换思路功能维度VFIO 旧接口iommufd 新接口顶层对象container容器ioasidIO 地址空间安全边界IOMMU group 必须整体操作设备为粒度 attachDMA 映射VFIO_IOMMU_MAP_DMAIOMMUFD_CMD_IOASID_MAP硬件页表由内核隐式管理独立对象 hwpt显式分配嵌套页表/vIOMMU支持困难补丁沉重设计之初就支持用户态生态QEMU、libvirt 传统方案新 QEMU、rust-vmm、vDPA 等这张表不是想说 VFIO 一无是处。VFIO 十几年的稳定性摆在那里尤其在传统 QEMU 直通场景下它仍然是很多生产环境的默认选择。iommufd 的价值在于把原先混在一起的东西解耦了给新场景留下了扩展空间。2.4 一次典型的 iommufd 调用流程直接看调用流程更直观。使用 iommufd 时用户态打开 /dev/iommufd然后依次分配 ioasid、映射内存、分配硬件页表、attach 设备。典型的调用序列大概是这样int iommu_fd open(/dev/iommufd, O_RDWR); struct iommu_ioasid_alloc alloc { .size sizeof(alloc), .ioasid_id 0 }; ioctl(iommu_fd, IOMMUFD_CMD_IOASID_ALLOC, alloc); // 得到 ioasid 之后把一段内存映射进去 struct iommu_ioasid_map map { ... }; ioctl(iommu_fd, IOMMUFD_CMD_IOASID_MAP, map); // 分配硬件页表并绑定到 ioasid struct iommu_hwpt_alloc hwpt { ... }; ioctl(iommu_fd, IOMMUFD_CMD_HWPT_ALLOC, hwpt); // 把设备 FD attach 到 ioasid / hwpt struct iommu_device_attach attach { ... }; ioctl(iommu_fd, IOMMUFD_CMD_DEVICE_ATTACH, attach);你会发现用户态和内核的交互非常扁平每个操作都对应一个明确的语义对象。实际使用中这些细节通常由 QEMU 和内核驱动封装好了但你理解了这一层才能明白 QEMU 后面到底在干什么。3. 从 VFIO 到 iommufd 的迁移路线内核、QEMU、libvirt 三层联动3.1 内核侧要做什么准备iommufd 从内核 6.7 版本开始合入主线但不同发行版的开启状态不一样。如果你用的是较新的内核一般只需要保证 CONFIG_IOMMUFD 是开启的。有些发行版默认开启有些则需要你在内核配置里勾选。比较稳妥的做法是先用下面的命令确认当前内核的支持情况zgrep IOMMUFD /proc/config.gz # 或者直接查模块 ls /lib/modules/$(uname -r)/kernel/drivers/iommu/iommufd/ ls /dev/iommufd如果 /dev/iommufd 存在说明内核已经编译并加载了 iommufd 支持。如果看不到则可能需要在 BIOS 里打开 IOMMU 相关开关比如 VT-d / IOMMU 选项或者在内核命令行加上对应平台的 IOMMU 开启参数。不同平台写法不同我这里不展开各自的特定参数大家搜自己平台的 IOMMU 开启方式即可。另外要注意IOMMUFD 和使用哪个 IOMMU 驱动无关它对 x86 的 IOMMU、ARM 的 SMMU 等都是一视同仁的。iommufd 是用户态接口层下面接不同的 IOMMU 硬件实现。3.2 QEMU 侧的三种配置方式我实际迁移的时候QEMU 侧是变化最明显的。老式 QEMU 直通用法大家都熟比如直通一块 PCIe 网卡qemu-system-x86_64 \ -device vfio-pci,host0000:01:00.0开启 iommufd 后端之后同样的直通要显式创建一个 iommufd 对象然后在设备参数上引用它qemu-system-x86_64 \ -object iommufd,idiommufd0 \ -device vfio-pci,host0000:01:00.0,iommufdiommufd0如果你的 QEMU 版本稍老可能需要在编译时启用对应配置或者在命令行看到 experimental 字样。我建议先在一个测试虚拟机上确认你的 QEMU 版本对 iommufd 的支持程度再看生产环境迁移。libvirt 管理 QEMU 的用户一般有两种改法。一种是在 domain XML 里通过 qemu:commandline 给 QEMU 传参另一种是等你的发行版 libvirt 版本原生支持 iommufd 属性。我自己的习惯是先用 qemu:commandline 验证功能确认没问题后再整理成正式的 libvirt 配置模板避免中途排查问题时又多一层故障点。3.3 迁移时优先拿什么设备开刀如果你也准备从 VFIO 迁到 iommufd我个人的建议是先把测试环境里最不重要的直通设备切过去比如一张额外的网卡或者一块测试用 NVMe 盘而不是上来就动生产 GPU 直通。为什么因为 GPU 直通往往还牵扯显示输出、vBIOS、复位逻辑和厂商私有驱动这些环节和 iommufd 本身的关联度没那么高。先把简单设备跑通确认 IOMMU 映射、中断、DMA 都没有问题再逐步扩大范围。我见过有人一上来就把生产环境的 GPU 切过去结果虚拟机上显示卡死排查了一整天最后发现只是某些旧工具链没有正确处理设备 FD 的传递问题。3.4 如何确认当前直通到底走了哪条路径迁移之后最怕的就是你以为新接口生效了其实背后还在用旧接口。判断方法其实很简单。第一看 /dev/iommufd 是否被 QEMU 进程打开ls -l /proc/qemu_pid/fd | grep iommufd第二看内核日志里有没有 iommufd 相关的分配记录一般会有 ioasid 和 hwpt 创建/释放的痕迹。第三在 QEMU 的调试日志里开启 verbose 模式后如果走的是 iommufd 后端日志里会出现 iommufd 相关字符串而不再是 vfio group/container 的打开记录。这一步很重要因为有些发行版虽然内核支持 iommufd但 QEMU 默认后端仍然是 VFIO你自己不显式指定的话测了半天可能测的还是老代码路径。4. 实操过程中的经验教训dirty tracking、多设备、虚拟化场景4.1 在线迁移最看重的脏页追踪能力虚拟化环境里做在线迁移核心需求是脏页追踪。所谓脏页追踪就是虚拟机在源宿主机上跑的时候不断有内存页被写入迁移软件要记录这些被写过的页才能在最终切换前把增量数据同步过去。直通设备场景下设备 DMA 写到内存里的页也要能被追踪到否则迁移过程中会丢数据。VFIO 旧接口也有脏页追踪能力通过 VFIO_IOMMU_DIRTY_PAGES 这种 ioctl 实现但它和 container 强绑定在嵌套页表场景下用起来很别扭。iommufd 把脏页追踪能力放在了 hwpt 这一层用户态可以让某个硬件页表开启 dirty tracking然后按需查询 bitmap这就灵活了很多。我实际测试时最明显的感觉是在带虚拟 IOMMU 的虚拟机里iommufd 的脏页追踪接口能用比较少的 ioctl 调用完成一轮同步而旧 VFIO 里同样的操作牵涉的接口和状态更多。如果你的环境经常做在线迁移这个点值得优先验证。4.2 多设备共享地址空间的好处我工作中常遇到的一个需求是同一台虚拟机上既要直通网卡又要直通加速卡而且希望两个设备访问同一份 DMA 缓冲区。用 VFIO 旧接口时这类需求非常考验你对 container 和 group 的理解不同组的设备要进同一个 container 会有不少限制。换到 iommufd 后只需要把两个设备 attach 到同一个 ioasidDMA 地址空间自然就共享了。这个我在测试环境里实际跑过两块设备 attach 到同一个 ioasid用户态程序在两块设备之间搬运数据省掉了跨映射的数据拷贝延迟明显降低。对搞 DPDK 或 SPDK 的人来说这个特性非常实用。4.3 中介设备支持迁移前必须确认的隐藏条件提到 VFIO就不能不提它下面的 mdevmediated device机制也就是像虚拟 GPU 这类由厂商驱动拆分出来的虚拟设备。VFIO 在这块有相当长的历史很多虚拟 GPU 方案依赖它。iommufd 这边对传统 mdev 的支持并不是一开始就完善的虽然整个框架在设计上不排斥这类设备但实际厂商驱动和用户态工具链的跟进速度有差异。如果你当前的生产环境重度依赖某个虚拟 GPU 方案我强烈建议你在迁移前先查清楚该方案的内核驱动、用户态库和管理面工具链是否完全支持 iommufd 模式。有些厂商可能只验证过 VFIO 旧路径你贸然切过去功能开不起来还算好的最怕是出现隐秘的稳定性问题。4.4 一个容易忽略的问题中断和直通平台设备的差异PC 上的 PCIe 设备走 MSI/MSI-X 中断直通时由内核负责把中断重定向给虚拟机的虚拟中断控制器这一块在 iommufd 和 VFIO 里都有对应机制一般问题不大。但如果你在嵌入式环境直通 platform 设备或者直通一些平台总线设备情况会复杂得多。旧 VFIO 对 platform 设备有专门支持iommufd 在这方面的覆盖程度未必和 VFIO 完全一致。这个问题冷门但对特定人群致命。我调试某 ARM 平台时直通过一个平台设备走了 iommufd 后中断始终进不来最后翻内核代码才发现当前内核版本里对应平台的设备支持还不完整切回 VFIO 就正常。所以如果你的直通设备不是标准 PCIe 设备迁移前最好先用最小配置验证。5. 常见问题速查和排查技巧5.1 报错“Operation not permitted”或设备无法 attach最常见的原因仍然是 IOMMU group 的约束。虽然 iommufd 的接口不再让用户态直接操作 group但底层 IOMMU 隔离原理没变同一个 group 里的设备必须整体完成安全绑定否则内核不会允许你把其中某个设备 attach 到 ioasid。排查方法先去 /sys/kernel/iommu_groups/ 看看目标设备所在的组里有哪些设备。比如目标设备在 group 11 里就列出ls /sys/kernel/iommu_groups/11/devices/如果这个组里还有其他设备就把它们也绑定到 vfio-pci让内核认为整组安全然后再在 QEMU 里重试。当然实际操作中你可以只继续 attach 你想用的那个设备但必须确保同组所有设备都被 bind 到 vfio-pci 或者其他支持 direct assignment 的驱动组才能被正常打开。如果你确认是 PCIe ACS 问题导致的组拆分过细一般只能靠平台 BIOS/PROM 设置打开 ACS 或者升级硬件软件层面没有银弹。5.2 DMA 映射失败或虚拟机启动后设备直接报 BUS ERRORDMA 映射失败通常和物理内存锁页pinned pages有关。iommufd 在映射用户态内存时同样需要把这些页 pin 住防止内核回收它们。如果你的虚拟机内存过大或者不限制锁页内存的 ulimit映射请求就会被内核拒绝。排查方法看 dmesg 里有没有 iommufd 或 IOMMU 相关的“cannot pin”错误同时检查一下进程的 RLIMIT_MEMLOCK 是不是被设置得很低。临时放开可以用ulimit -l unlimited生产环境里则建议通过 systemd 或 libvirt 的配置来提升限额不要盲目放开所有限制。另外主机本身不能过度超卖内存否则 pin 内存时容易陷入长时间等待甚至 OOM。5.3 迁移时卡在 final switch数据同步不收敛如果在线迁移打开脏页追踪后脏页同步一直不收敛常见原因是设备 DMA 活动太频繁每次同步窗口内又有大量新脏页产生。不一定是 iommufd 本身的问题但 iommufd 新接口让你更容易看到 hwpt 层 dirty bitmap 的变化速度。排查技巧先关闭设备的高频中断和流量做一轮静默测试确认 dirty synchronization 能收敛。然后把流量逐步加上去观察每轮增量的大小是不是线性增长。如果是跳变式增长优先检查是不是有驱动开启了不必要的 I/O 缓冲或者是不是设备在持续扫描大块地址空间。5.4 迁移后退回 VFIO发现原有配置反而报错这种情况我在实验环境里遇到过。退回 VFIO 时提示 group 已经被占用或者设备状态异常原因通常是你当前内核对两种路径的模块加载顺序有状态残留。解决办法比较简单先卸载模块重新绑定设备驱动再加载模块最后重启相关服务。echo 0000:01:00.0 /sys/bus/pci/drivers/vfio-pci/unbind echo 0000:01:00.0 /sys/bus/pci/drivers/ixgbe/bind如果你不确定设备原来的驱动是什么可以通过 lspci -k 查看。这类问题多数是操作顺序导致的不是 iommufd 本身有 bug。6. 我的实践体会接口换了思维也得换从 VFIO 迁到 iommufd最大的收获不是新功能而是重新理解了 IOMMU 的抽象模型。以前用 VFIO 的时候container 对我来说就是一个“往里面塞映射的黑盒”虽然也能用但你很难说清楚里面到底有几个 IOMMU domain、硬件页表是什么状态。用 iommufd 之后ioasid 和 hwpt 的分层让整个 DMA 映射路径变得非常透明调试问题的时候打开 trace 可以直接看到地址空间和页表的对应关系。另外一点经验是不要把 iommufd 当成 VFIO 的“一键替换”。它在接口设计上更先进但生态覆盖度还在追赶特别是非 PCIe 设备、传统 mdev、以及一些老版本 QEMU 的组合场景旧路径仍然有自己的存在价值。我目前的建议是新项目直接基于 iommufd 设计老项目保持 VFIO 稳定运行等到厂商工具链和新内核 api 都成熟了再择机切换。这样既不耽误新技术落地也不会因为盲目追新把自己埋进坑里。最后分享一个小技巧调试 iommufd 时可以多用内核的 trace 机制观察 hwpt 和 ioasid 的创建销毁配合 ftrace 的过滤条件能很快定位到是哪个设备 attach 失败、哪个地址段映射冲突。这个技巧比反复看 dmesg 高效得多尤其是当你面对多个直通设备、多套虚拟机同时运行的时候。希望这些踩坑经验能帮到你祝大家直通顺利。