Linux 6.6内核 IOMMU 深度解析(六):VFIO 设备直通 — 从用户态 ioctl 到 IOMMU 页表 〇、这篇文章讲什么前五篇覆盖了 DMA 重映射原理、IOMMU 初始化、设备挂载、版本演进。这篇进入应用层VFIO 怎么借 IOMMU 把 PCI 设备直通给虚拟机。v6.6 中 VFIO 有两套模型共存Legacycontainer group type1 iommu通过/dev/vfio/vfio和/dev/vfio/$GROUP操作Moderncdev IOMMUFD通过/dev/vfio/devices/vfioX和/dev/iommu操作两套模型底层用的 IOMMU 原语相同只是管理层不同。本篇主线走 modern 路径。一、核心概念DMA 所有权转移VFIO 直通的关键动作是从内核手中接管设备的 DMA 控制权。正常情况下设备挂在内核的 default domain 上dma_map_single()走iommu_dma_ops。VFIO 需要把设备切换到自己的 unmanaged domain由用户态QEMU通过iommu_map()建 GPA→HPA 映射。不能直接切——如果设备正在 DMA直接换页表会导致翻译失败。所以需要一个**阻断域blocking domain**作为中间态。claim_dma_owner()attach_group()release_dma_owner()内核 default domain(DMA API 使用)blocking domain(阻断所有 DMA)VFIO unmanaged domain(GPA→HPA, 用户态控制)// drivers/iommu/iommu.c (v6.6, line 3215-3227)staticint__iommu_take_dma_ownership(structiommu_group*group){// ① 分配 blocking domain如果还没分配__iommu_group_alloc_blocking_domain(group);// ② 全组切换到 blocking domain → DMA 停止__iommu_group_set_domain(group,group-blocking_domain);}blocking domain 的页表所有条目 Present0——任何 DMA 请求触发 IOMMU fault。二、Modern 路径IOMMUFD 接管全景open /dev/vfio/devices/vfioXvfio_device_fops_cdev_open()VFIO_DEVICE_BIND_IOMMUFD→ iommufd_device_bind()→ iommu_device_claim_dma_owner()设备切换到 blocking domainIOMMU_IOAS_ALLOC创建 IOASIO Address SpaceIOMMU_IOAS_MAP→ iopt_map_user_pages()→ pin 用户态内存→ iommu_map() 写页表VFIO_DEVICE_ATTACH_IOMMUFD_PT→ iommufd_hw_pagetable_alloc()→ iommu_domain_alloc(UNMANAGED)→ iommu_attach_group()→ iopt_table_add_domain() 回填已有映射VM 运行设备 DMA 走 GPA→HPA 翻译close device fd→ iommu_detach_group()→ 回到 blocking domain→ device reset→ iommu_device_release_dma_owner()→ 回到 default domain2.1 BIND接管 DMA 所有权// drivers/vfio/device_cdev.c (v6.6, line 56-78)// VFIO_DEVICE_BIND_IOMMUFD 处理:// vfio_df_ioctl_bind_iommufd()// → vfio_df_open()// → vfio_iommufd_physical_bind()// → iommufd_device_bind() // drivers/iommu/iommufd/device.c:161// → iommu_device_claim_dma_owner() // ★ 切换到 blocking domain// → vfio_pci_open_device() // PCI 设备初始化BAR 映射、IRQBIND 是设备首次打开的入口。iommu_device_claim_dma_owner()将设备所属的整个 group 切换到 blocking domain——此后所有设备的 DMA 被阻断直到 VFIO 挂上自己的 domain。IOMMUFD 还在此阶段做 MSI 隔离检查iommu_group_has_isolated_msi防止未隔离的 MSI 中断被用于攻击其他 VM。2.2 IOAS_MAP建 GPA→HPA 映射用户态QEMU通过IOMMU_IOAS_MAPioctl 将 guest 物理页映射到 host 物理页// drivers/iommu/iommufd/ioas.c (v6.6, line 200)// IOMMU_IOAS_MAP → iommufd_ioas_map()// → iopt_map_user_pages() // io_pagetable.c:385// → iopt_alloc_pages() // pin 用户态页面// → iopt_map_pages() // 分配 IOVA 空间// → iopt_fill_domains_pages() // 对所有已附着的 domain 做// → iopt_area_fill_domain()// → batch_to_domain() // pages.c:475// → iommu_map(domain, iova, paddr, size, prot, gfp) // ★关键MAP 可能在 ATTACH之前调用QEMU 先准备好 guest 内存布局。此时 IOAS 上还没有 domainiopt_map_pages()只记录在struct io_pagetable的 interval tree 中。后续 ATTACH 时通过iopt_table_add_domain()回填所有已有映射。// drivers/iommu/iommufd/hw_pagetable.c (v6.6, line 122)// iommufd_hw_pagetable_alloc() 最后一步:iopt_table_add_domain(hwpt-ioas,hwpt-domain);// → 遍历 ioas 上已有的 IOVA 区域 → 逐个 iommu_map() 回填2.3 ATTACH挂载页表到硬件// drivers/vfio/device_cdev.c (v6.6, line 155)// VFIO_DEVICE_ATTACH_IOMMUFD_PT:// → iommufd_device_attach() // device.c:629// → iommufd_hw_pagetable_alloc() // hw_pagetable.c:73// → iommu_domain_alloc(UNMANAGED) // ★ 分配无管理 domain// → iommufd_hw_pagetable_attach() // device.c:328// → iopt_table_enforce_dev_resv_regions() // 强制保留区域// → iommufd_group_setup_msi() // MSI 窗口// → iommu_attach_group() // ★ 写硬件iommu_attach_group()内部走__iommu_attach_group()将整个 group 从 blocking domain 切换到新分配的 unmanaged domain并回调ops-attach_dev()写 ContextEntry/DTE。2.4 释放回到内核控制// 设备 fd close:// vfio_df_unbind_iommufd() // iommu detach// → iommufd_device_detach()// → iommu_detach_group() // 切回 blocking domain// → iommufd_device_unbind()// → iommu_device_release_dma_owner() // ★ 切回 default domain// → vfio_pci_core_disable() // 设备 reset// → pci_reset_function() // FLR / bus reset// → 保存/恢复 PCI 配置空间释放分三步① detach回到 blocking② unbind释放所有权回到 default domain③ 设备 resetFLR 或 bus reset 清状态。三、Legacy 路径container type1当IOMMUFDn或用户态用旧接口时走这条路径// 1. open /dev/vfio/vfio → vfio_fops_open() // container.c:359// 2. open /dev/vfio/$GROUP → vfio_group_fops_open() // group.c:432// 3. VFIO_GROUP_SET_CONTAINER → vfio_container_attach_group()// → iommu_group_claim_dma_owner() // blocking domain// 4. VFIO_SET_IOMMU → vfio_iommu_type1_open()// → vfio_iommu_type1_attach_group()// → iommu_domain_alloc(UNMANAGED) iommu_attach_group()// 5. VFIO_IOMMU_MAP_DMA → vfio_dma_do_map()// → vfio_pin_map_dma() → iommu_map() // drivers/vfio/vfio_iommu_type1.c:1437// 6. 释放: group fd close → detach → release_dma_ownerLegacy 和 modern 的区别只在管理层Legacy/dev/vfio/vfio container → type1 驱动管理 domain →iommu_map()Modern/dev/iommu → IOMMUFD core 管理 domain →iommu_map()底层iommu_domain_alloc(UNMANAGED)iommu_attach_group()iommu_map()完全一致。v6.6 的兼容性VFIO_GROUP_SET_CONTAINER现在也可以传 iommufd fd不是 container fdIOMMUFD 会内部创建一个隐藏的 “compat IOAS” 把老 ioctl 翻译成新调用。四、IOMMUFD 核心数据结构/dev/iommu (iommufd_ctx) ├── IOAS (iommufd_ioas, IOMMUFD_OBJ_IOAS) │ ├── io_pagetable │ │ ├── area_itree: IOVA→area 的 interval tree │ │ ├── pages_itree: pin 的 struct iopt_pages (去重共享) │ │ └── domains[]: 挂在 ioas 上的 struct iommu_domain 数组 │ └── hwpt_list: 关联的 hw_pagetable 链表 │ ├── HW_PAGETABLE (iommufd_hwpt, IOMMUFD_OBJ_HW_PAGETABLE) │ ├── domain: struct iommu_domain * ← UNMANAGED domain │ ├── ioas: struct iommufd_ioas * ← 挂在哪 │ └── msi_cookie │ └── DEVICE (iommufd_device, IOMMUFD_OBJ_DEVICE) ├── idev: struct iommufd_device * ├── igroup: struct iommufd_group * └── hwpt: struct iommufd_hw_pagetable *关键设计一个 IOAS 可以挂多个 HWPT多个 domainiommu_map()时iopt_fill_domains_pages()遍历所有 domain 写入相同映射——支持一个 IOAS 对应多个 IOMMU 实例如 NUMA 多 socket。五、完整调用链modern 路径open(/dev/vfio/devices/vfioX) → vfio_device_fops_cdev_open() VFIO_DEVICE_BIND_IOMMUFD └─ vfio_df_ioctl_bind_iommufd() // device_cdev.c:56 └─ vfio_df_open() └─ vfio_iommufd_physical_bind() // drivers/vfio/iommufd.c:113 └─ iommufd_device_bind() // iommu/iommufd/device.c:161 ├─ iommu_device_claim_dma_owner() // iommu.c:3274 │ └─ __iommu_take_dma_ownership() // → blocking domain └─ iommufd_object_alloc(DEVICE) └─ vfio_pci_open_device() // PCI enable IRQ 配置空间 // ── QEMU 准备 guest 内存 ── IOMMU_IOAS_ALLOC → iommufd_ioas_alloc_ioctl() // ioas.c:37 IOMMU_IOAS_MAP → iommufd_ioas_map() // ioas.c:200 └─ iopt_map_user_pages() // io_pagetable.c:385 ├─ iopt_alloc_pages() // pin guest 内存页 └─ iopt_map_pages() // 记录 IOVA→物理页映射 └─ iopt_fill_domains_pages() // 遍历 ioas 上所有 domain └─ iopt_area_fill_domain() └─ batch_to_domain() // pages.c:475 └─ iommu_map(domain, iova, paddr, ...) // ★ // ── 挂载设备 ── VFIO_DEVICE_ATTACH_IOMMUFD_PT └─ vfio_df_ioctl_attach_pt() // device_cdev.c:155 └─ iommufd_device_attach() // device.c:629 ├─ iommufd_hw_pagetable_alloc() // hw_pagetable.c:73 │ └─ iommu_domain_alloc(UNMANAGED) // iommu.c:2004 ├─ iommufd_hw_pagetable_attach() // device.c:328 │ ├─ iopt_table_enforce_dev_resv_regions() │ ├─ iommufd_group_setup_msi() │ └─ iommu_attach_group() // ★ 从 blocking → unmanaged │ └─ __iommu_attach_group() │ └─ ops-attach_dev() // 写 ContextEntry / DTE └─ iopt_table_add_domain() // 回填所有已有映射 └─ 遍历 area_itree → iommu_map() // ── VM 运行设备 DMA 直接翻译 ── // ── 释放 ── close(device_fd) → vfio_device_fops_release() ├─ vfio_df_unbind_iommufd() │ ├─ iommufd_device_detach() // device.c:675 │ │ └─ iommu_detach_group() // → blocking domain │ └─ iommufd_device_unbind() // device.c:277 │ └─ iommu_device_release_dma_owner() // → default domain ★ └─ vfio_pci_core_disable() // vfio_pci_core.c:561 ├─ pci_clear_master() ├─ vfio_pci_dev_set_try_reset() // FLR / bus reset └─ pci_restore_state()六、记住三点① 阻断域是门卫设备从内核 default domain 切换到 VFIO unmanaged domain 前必须先经过 blocking domain。这是 DMA 所有权转移的安全机制。② 映射可以提前IOMMUFD 的 IOAS_MAP 可以在 ATTACH 之前调用。映射暂存在 io_pagetable 的 interval tree 中ATTACH 时通过iopt_table_add_domain()一次性回填到硬件页表。③ 释放先 block 后 reset设备 fd close 时先切回 blocking domain阻断 DMA再做设备 resetFLR/bus reset最后释放 DMA ownership回到 default domain。顺序不能乱——如果先 reset 后 blockreset 期间的 DMA 可能通过旧页表泄露。七、关键文件索引文件v6.6关键函数drivers/iommu/iommu.ciommu_domain_alloc,iommu_attach_group,iommu_device_claim_dma_owner,__iommu_take_dma_ownershipdrivers/iommu/iommufd/device.ciommufd_device_bind,iommufd_device_attach,iommufd_hw_pagetable_attachdrivers/iommu/iommufd/hw_pagetable.ciommufd_hw_pagetable_alloc,iopt_table_add_domaindrivers/iommu/iommufd/io_pagetable.ciopt_map_user_pages,iopt_map_pages,iopt_fill_domains_pagesdrivers/iommu/iommufd/pages.cbatch_to_domain,iommu_map调用点drivers/iommu/iommufd/ioas.ciommufd_ioas_alloc_ioctl,iommufd_ioas_mapdrivers/vfio/device_cdev.cvfio_df_ioctl_bind_iommufd,vfio_df_ioctl_attach_ptdrivers/vfio/iommufd.cvfio_iommufd_physical_bind,vfio_iommufd_physical_unbinddrivers/vfio/vfio_iommu_type1.cvfio_iommu_type1_attach_group,vfio_dma_do_map(legacy)drivers/vfio/pci/vfio_pci_core.cvfio_pci_core_enable,vfio_pci_core_disableinclude/uapi/linux/vfio.hVFIO_DEVICE_BIND_IOMMUFD,VFIO_DEVICE_ATTACH_IOMMUFD_PT