一、IOMMU 的起源:为什么需要 DMA 重映射
在计算机体系结构中,DMA(Direct Memory Access)允许外设直接在内存和设备之间搬运数据,无需 CPU 介入。然而传统的 DMA 机制存在一个根本性安全隐患:外设使用的是物理地址(bus address),它可以访问系统内存中的任意物理页面,包括内核代码段、进程页表、甚至其他虚拟机的内存。
这意味着:一个恶意的或存在缺陷的 PCIe 设备可以通过 DMA 读写任意物理内存,彻底颠覆系统安全边界。2015 年披露的 Thunderclap 系列攻击就利用了 Thunderbolt 接口的热插拔 PCIe 设备,在数秒内获取了目标机器的 root 权限。
IOMMU(Input-Output Memory Management Unit) 正是为此而生。它位于 CPU 和南桥/PCIe 根联合体之间,为每个设备维护独立的页表(I/O Page Table),在 DMA 事务到达内存控制器之前,将设备视角的 IO Virtual Address(IOVA)翻译为实际物理地址。这样就实现了:
- 设备隔离:每个设备只能访问分配给它的物理页面
- 地址转换:设备看到的连续 IOVA 可以映射到物理上离散的页面,消除 scatter-gather 约束
- 中断重映射:强制 MSI/MSI-X 中断由合法源头发送,防止设备伪造中断注入虚拟机
二、硬件 IOMMU 架构:Intel VT-d vs AMD-Vi vs ARM SMMU
Intel VT-d(Virtualization Technology for Directed I/O) 是最广泛部署的 IOMMU 实现:
- DMAR Table:ACPI 表描述 IOMMU 单元与 PCIe 总线段的归属关系,每个 DRHD(DMA Remapping Hardware Unit)管理一组设备
- 多级页表:支持 3-level(39-bit IOVA)到 5-level(57-bit IOVA)页表结构,根表指向 Context Entry,再指向 PML4 等各级页表
- QI(Invalidation Queue):软件提交 TLB 无效化命令的队列接口,避免阻塞等待同步完成
- OTLB:IOMMU 内部的中转翻译缓存(IOTLB),缓存频繁地址转换以降低延迟
- ATS/PRI: Address Translation Services/Page Request Interface,允许 PCIe 设备侧缓存转换结果(类似 CPU TLB),由 ATS Translation Completes 通知 IOMMU
- PTT/PASID: Process Address Space ID,支持 SR-IOV VF 在不同进程间共享单个物理设备,每个进程拥有独立的 PASID 地址空间
- AER(Advanced Error Reporting):记录 DMAR 错误事件,通过 MSI 中断通知 OS 处理
AMD-Vi(常称 AMD IOMMU):
- 基于 ACPI IVRS 表,描述 IOMMU 设备与 PCIe 功能的映射
- 使用 Device Table、Domain Table、Command Ring Buffer 机制
- 通过 Event/PPI(Peripheral Page Request Interface)日志事件
- 与 Intel 类似的核心概念:domain、interrupt remapping table、IANA(I/O NPT ATC NPT Address Translation Cache)
- Linux 内核中 Intel/AMD 的后端分别位于
drivers/iommu/intel/和drivers/iommu/amd/,统一在drivers/iommu/iommu.c等通用层之下
ARM SMMU(System Memory Management Unit):
- 面向 ARM 服务器 SoC(Ampere Altra、NVIDIA Grace、海光等)
- SMMUv3 支持 stage-1(VA→IPA)、stage-2(IPA→PA)两级地址转换,嵌套虚拟化时两阶段可同时启用
- 与 PASID 异步绑定,通过命令队列(CMDQ)、事件队列(EVTQ)异步提交
- Linux 内核
drivers/iommu/arm/arm-smmu-v3/
三、Linux IOMMU 子系统架构
Linux 的 drivers/iommu/ 目录提供总线无关的统一 IOMMU 框架:
struct iommu_device —— 硬件 IOMMU 单元抽象
struct iommu_group —— 物理上直通隔离的最小单元(同一 group 内设备不能分开给不同 domain)
struct iommu_domain —— 地址空间隔离上下文,含 iommu_ops 回调
struct iommu_resv_region —— 设备保留区域(如 MSI 映射窗口)
struct iommu_fwspec —— 固件/总线特定数据(ACPI DT 等)
核心数据结构的层次关系:
iommu_group
├── device (PCIe 设备成员,同一 group 不可拆分)
├── default_domain (type: IOMMU_DOMAIN_DMA / IOMMU_DOMAIN_UNMANAGED)
└── domain (当前绑定的 domain)
iommu_domain
├── iommu_ops (硬件后端操作集: map/unmap/flush_iotlb/...]
├── pgsize_bitmap (支持的页面大小)
└── geometry (地址空间范围)
驱动注册的两种方式:
IOMMU_DOMAIN_DMA:由内核 DMA API 驱动(dma_map_sg/dma_map_page等),I/O 页表自动维护,用户无感知IOMMU_DOMAIN_UNMANAGED:由 VFIO 等组件手动管理,用于设备直通场景
关键操作流程:
iommu_probe_device() // PCI/ACPI 总线枚举时调用
→ iommu_group_get() // 或分配或加入已有 group(同物理限制设备不可分割)
→ iommu_group_alloc() // 新 group
→ iommu_group_set_iommand() // 设置iommu_ops
iommu_domain_alloc(type) // 分配地址空间
iommu_attach_group(domain, group)
→ ...ops->attach_group(...) // 硬件后端:设置 Context Entry、加载页表根
// DMA API 场景
dma_map_page(dev, page, ...)
→ iommu_dma_map_page() // 分配 IOVA,建立映射,flush IOTLB
四、VFIO 框架:用户态设备直通的抽象模型
VFIO(Virtual Function I/O) 是 Linux 3.6+ 引入的用户态设备驱动框架,它解决了传统 KVM 设备直通(/dev/kvm 私有接口)的不足:通过 /dev/vfio/vfio 容器 + /dev/vfio/$group 组设备,提供标准化的安全的用户态 I/O API。
VFIO 的三层核心抽象:
Container (/dev/vfio/vfio)
│ 包含:iommu_group 集合、IOMMU 地址空间
│ ioctl: VFIO_GET_API_VERSION, VFIO_CHECK_EXTENSION, VFIO_SET_IOMMU
│
├── Group (/dev/vfio/$group)
│ 包含:一个或多个物理设备
│ ioctl: VFIO_GROUP_GET_STATUS, VFIO_GROUP_SET_CONTAINER, VFIO_GROUP_GET_DEVICE_FD
│
└── Device ($dev_fd)
包含:BAR 空间、IRQ 通知、reset 等
ioctl: VFIO_DEVICE_GET_INFO, VFIO_DEVICE_GET_REGION_INFO, VFIO_DEVICE_GET_IRQ_INFO,
VFIO_DEVICE_RESET, VFIO_REGION_MMAP, VFIO_SET_IRQS
QEMU 使用 VFIO 的标准流程:
// 1. 打开 VFIO 容器
int container = open("/dev/vfio/vfio", O_RDWR);
// 2. 检查 API 版本与 IOMMU 类型(Type1 = 软件页表同步,即 Linux IOMMU 标准模式)
ioctl(container, VFIO_CHECK_EXTENSION, VFIO_TYPE1_IOMMU);
// 3. 将 group 加入 container
ioctl(group, VFIO_GROUP_SET_CONTAINER, &container);
// 4. 设置 IOMMU 后端为 Type IOMMU(Type1 会同步用户态映射与 IOMMU 页表)
ioctl(container, VFIO_SET_IOMMU, VFIO_TYPE1_IOMMU);
// 5. device fd,获取 BAR region 信息,mmap 到 QEMU 进程地址空间
int dev_fd = ioctl(group, VFIO_GROUP_GET_DEVICE_FD, "0000:03:00.0");
ioctl(dev_fd, VFIO_DEVICE_GET_REGION_INFO, ®);
void *bar0 = mmap(NULL, reg.size, PROT_READ|PROT_WRITE, MAP_SHARED, dev_fd, reg.offset);
// 6. 配置中断(MSI/MSI-X):eventfd → 内核 IRQFD → KVM IRQFD → VM 中断注入
struct vfio_irq_set irq_set = {...};
ioctl(dev_fd, VFIO_DEVICE_SET_IRQS, &irq_set);
IOMMU Type1 语义:这是 QEMU 使用的最常见模式。其规则是:用户在容器地址空间中通过 mmap() 映射虚机物理地址(GPA=HVA 的前 API),内核 IOMMU 硬件自动建立 IOVA→PA 的同步映射——也即 "when you map memory to userspace, the IOMMU automatically sees it"。
VFIO-mdev(Mediated Device):NVIDIA vGPU、Intel GVT-g 等基于 VFIO 框架的子设备机制。mdev core(drivers/vfio/mdev/)将物理设备分割为可单独分配的虚拟实例,每个实例对应一个 mdev_device。
五、中断重映射与 IRQ 注入
IOMMU 的另一大职责是 中断重映射(Interrupt Remapping):
MSI/MSI-X 中断本质是内存写入事务(地址 0xFEEx_xxxx,数据=向量号)。IOMMU 对这类写入做截获和重查表:
- 验证源设备(Source ID)是否被允许发送指定中断向量
- 将 MSI 地址/数据字段按重映射表翻译为实际中断控制器目标
- 若设备伪造重映射条目,IOMMU 触发中断重映射 fault,记录事件并 MSI 通知 Host
在 VFIO 中,中断的典型路径:
用户空间 (QEMU/KVM Guest) 内核 VFIO/KVM
┌─────────────────┐ ┌─────────────────┐
VM exit │ VM-EXIT: MSI-X │ │ │ IRQFD
─────────▶ write detected │──eventfd─────▶ │ VFIO IRQ │───────▶ VMCS
└─────────────────┘ (KVM_IRQFD) │ set_irq() │ IRQ inject
└─────────────────┘
// guest_driver.c 伪代码路径
// 1. QEMU 打开 device msi-x table BAR
// 2. KVM 注册对应 HVA 区域的 write-trace
// 3. guest 写入 MSI-X table → 触发 KVM_EXIT io
// 4. QEMU 通过 eventfd_signal 通知 irqfd
// 5. KVM irqfd 向 vCPU 注入中断 → vCPU 进入中断 handler
IRQ 亲和性配置:
struct vfmsirq_set_irqs {
__u32 argsz;
__u32 flags; // VFMAIRQ_SET_DATA_EVENTFD | VFMAIRQ_SET_ACTION_TRIGGER
__u32 index; // VFIO_PCI_MSI_IRQ_INDEX / MSIX
__u32 start;
__u32 count;
__u8 data[]; // 指向 eventfd 的指针数组
};
六、vIOMMU 嵌套虚拟化与 DMA 隔离
vIOMMU(Virtual IOMMU) 是 QEMU 模拟的 IOMMU 设备,供虚拟机内的操作系统使用。它的出现解决了两个问题:
- 嵌套直通:L1 Guest 使用 VFIO 直通物理设备;L2 Guest(嵌套在 L1 中)需要 L1 提供自定义的 IOMMU 视图
- Guest 端隔离:在 Guest OS 内部,多个进程/容器使用独立的 DMA 地址空间(VFIO cgroup 模型)
QEMU vIOMMU 设备类型:
# Intremap (vtd) —— Intel VT-d 虚拟化
qemu-system-x86_64 -device intel-iommu,intremap=on,device-iotlb=on ...
# virtio-iommu —— 半虚拟化 IOMMU,比模拟 VT-d 低延迟
qemu-system-x86_64 -device virtio-iommu-pci ...
vIOMMU 的实现细节:
- 地址转换模拟:vIOMMU 维护两级页表(GVA→GPA),拦截 guest DMA 请求并进行转换
- 与物理 IOMMU 协同:通过 VFIO Type1 的嵌套(nested)映射,v 表 → IOMMU 表形成 3 级转换(GVA→GPA→PA)
- 性能开销:virtio-iommu 使用 virtqueue 替代 PIO 模拟,吞吐开销从模拟 VT-d 的 ~40% 降至 ~15%
- VFIO vIOMMU 容器模型:VFIO_TYPE1v2_IOMMU 支持 vIOMMU 嵌套和 DMA 可用性查询,通过
VFIO_IOMMU_TYPE1_INFO_AVAIL告知QEMU可映射区域
七、ATS、PRI 与 PASID:性能优化机制
ATS(Address Translation Services):
ATS 是 PCIe 设备端的地址转换缓存(ATC)。设备先向 IOMMU 请求翻译并获得 "Translation Complete" 数据包,其中 Address Type(AT 字段)指示:
Untranslated:后续事务直接携带物理地址("已翻译直达"),跳过 IOMMU 查询Translated:事务仍携带 IOVA,但设备侧 ATC 缓存了翻译结果作为性能参考Resume:翻译请求已发出但结果未返回,等待 IOMMU 回复
内核中 ATS 的处理(drivers/iommu/io-pgtable-arm.c):设备发起 ATS 转换请求(Address Translation Request 报文),IOMMU 查找页表回复 Translation Complete + 翻译结果。
PRI(Page Request Interface):
ATS 的扩展机制,当 IOMMU 发生错误页(Page Fault)时,向其发送 Page Request 报文:设备收到 PRG Response(Page Request Group Response)后重试事务。Linux 通过 drivers/pci/pci.c 中缺失的 PRI 支持与 VFIO 配合响应:
// 设备端:ATC miss → 发起 ATS Translation Request
// IOMMU 端:I/O Page Fault → ATS 回复 Redirect 到 PRI(如果页不存在或权限不匹配)
// VFIO 端:用户态通过 VFIO_IOMMU_MAP_DMA/HANDLE_IOEVENTFD 分配页面
// 内核重新映射 ATS Translation Request 为 Untranslated/Translated
PASID(Process Address Space ID):
SRIOV VF 或 Scalable IOV 需要将同一个物理设备分配给不同进程。传统方式是:VF→进程 一对一映射(缺乏灵活性)。PASID 扩展了 PCIe 事务:每个事务带有一 20-bit PASID TLP Prefix,标识发送者所属的进程地址空间上下文。
Linux VFIO 对 PASID 的支持(drivers/vfio/vfio_iommu_type1.c):
struct vfio_iommu_type1_bind_pasid {
__u32 argsz;
__u32 flags;
__u32 pasid;
};
VFIO vIOMMU 通过 VFIO_IOMMU_BIND_PROCESS 将 PID 与 PASID 解耦关联,配合 io-pgtable 支持多 PASID 上下文。
八、DMA 攻击与防护实战
Thunderclap(2015-2019)系列漏洞:
- Thunderclap (2015):Thunderbolt 热插拔 PCIe 设备,操作系统未就绪时,设备即可发起 DMA 攻击( kernel 未配置 IOMMU 保护)。攻击者在BitLocker 锁屏状态窃取明文密钥
- Thunderclap 2 (2017):Thunderbolt IOMMU 在硬件可配置但默认禁用。现代 UEFI 设置
VT-d=Enabled但仍然没有对 Thunderbolt 端口进行隔离 - CVE-2019-19495:AMD-Vi interrupt remapping table 配置错误,可绕过 AMD IOMMU 保护
防护最佳实践:
# BIOS/UEFI 层面
1. 启用 Intel VT-d / AMD-Vi(IOMMU ON by default)
2. 确保 Thunderbolt Security Level = Security Level 2+(user authorized + secure connect)
# Linux 内核命令行
intel_iommu=on iommu=pt # passthrough:未映射设备直接物理地址
# 或
intel_iommu=on iommu=strict # strict:强制映射(默认模式推荐安全场景)
# 或
intel_iommu=on iommu=1g # 允许 1GB 大页
# 黑名单问题设备(避免 DMA 攻击)
vfio-pci.ids=1234:5678 # 提前绑定到 vfio-pci
rd.driver.pre=vfio-pci # initramfs 中预先绑定
# vfio-noiommu(DANGEROUS:关闭 IOMMU 仅做用户态驱动 API 框架)
# 仅用于研发调试,生产千万禁用
DMA Remapping Fault 日志解读:
# dmesg 输出示例(IOMMU fault 事件)
DMAR: [DMA Read] Request device [03:00.0] fault addr 0x0
DMAR: fault reason 05 (PTE Read bit not set) # 0x5 表示 PTE 读权限不足
DMAR: fault reason 06 (Access bit not set) # 0x6 表示 A/D 位未设置
DMAR: fault reason 07 (PTE Write bit not set) # 0x7 表示 PTE 写权限不足
DMAR: fault reason 20 (non-zero reserved field in PTE) # 0x20 PTE 保留位非 0
字段含义:
- fault addr:触发 violation 的 IOVA
- source id:发起者的 BDF(Bus:Device.Function)
- fault reason:编码的 violation 类型
- fault type:Read/Write 操作方向
九、QEMU/KVM VFIO 实战配置
完整的 NFV 平台 server 配置:
# 1. 查看可用 IOMMU group
for d in /sys/kernel/iommu_groups/*/devices/*; do
n="${d#*/iommu_groups/}"; n="${n%%/*}"
printf 'IOMMU Group %s: ' "$n"
lspci -nns "${d##*/}"
done
# 2. 确定目标设备及其 group(e.g., 03:00.0 + 03:00.1)
lspci -v -s 03:00
# 3. 加载 vfio-pci 模块
modprobe vfio-pci
# 4. 按需求情况配置
# 方案 A:QEMU hot-plug via virsh
virsh nodedev-detach pci_0000_03_00_0
# 方案 B:直接命令
echo "8086 10fb" > /sys/bus/pci/drivers/vfio-pci/new_id
echo 0000:03:00.0 > /sys/bus/pci/devices/0000:03:00.0/driver/unbind
echo "8086 10fb" > /sys/bus/pci/drivers/vfio-pci/new_id
# 5. QEMU 启动命令
qemu-system-x86_64 \
-enable-kvm -cpu host -smp 8 \
-m 16G,slots=2,maxmem=32G \
-machine q35,accel=kvm \
-device ioh3420,bus=pcie.0,addr=1c.0,multifunction=on,port=1,chassis=1 \
-device vfio-pci,host=03:00.0,bus=pcie.0,addr=00.0,multifunction=on \
-device vfio-pci,host=03:00.1,bus=pcie.0,addr=00.1 \
-object memory-backend-file,id=mem1,size=16G,mem-path=/dev/shm/ram1=on,prealloc=on \
-device ivshmem-plain,id=shmem0,memdev=mem1,bus=pcie.0
SYSFS 自动绑定脚本:
#!/bin/bash
# vfio-bind.sh
DEVS=("0000:03:00.0" "0000:03:00.1")
for dev in "${DEVS[@]}"; do
vendor=$(cat /sys/bus/pci/devices/$dev/vendor)
device=$(cat /sys/bus/pci/devices/$dev/device)
driver=$(readlink -f /sys/bus/pci/devices/$dev/driver)
if [[ "$driver" == *"vfio-pci"* ]]; then
echo "$dev 已绑定 VFIO"
continue
fi
# 解除原有驱动
echo $dev > /sys/bus/pci/devices/$dev/driver/unbind
echo "$vendor $device" > /sys/bus/pci/drivers/vfio-pci/new_id
echo $dev > /sys/bus/pci/drivers/vfio-pci/bind
echo "$dev → vfio-pci OK"
done
十、性能基准测试
IOMMU 保护对 IO 吞吐的影响取决于设备类型和配置:
| 场景 | 无 IOMMU | IOMMU passthrough | IOMMU strict (DMA map) |
|---|---|---|---|
| NVMe SSD 读 (4K IOPS) | ~950K | ~945K | ~880K |
| NVMe SSD 写 (4K IOPS) | ~550K | ~545K | ~505K |
| 10GbE 网卡单流 TCP | 9.44 Gbps | 9.40 Gbps | 8.50 Gbps |
| DPDK 单核小包转发 | 14.88 Mpps | 14.80 Mpps | 12.50 Mpps |
| GPU 计算 (HBM copy) | ~900 GB/s | ~895 GB/s | ~860 GB/s |
关键发现:
- iommu=pt(passthrough)模式性能代价极小(<1%),是现代虚拟化平台推荐的安全配置
- strict 模式 在频繁小 IO 场景下有 5-15% 开销(页表 IOTLB miss 引发上下文切换)
- ATS 可进一步减少延迟:设备缓存翻译后,每次 IO 事务免除 PCIe 双向 Round Trip
- IOMMU 大页(2M/1G):通过
iommu.pgsize_bitmap启用,减少 TLB miss 率 - VFIO 直通 vs virtio-blk:直通 NVMe 比 virtio 快 15-30%(尤其在队列深度 32+ 场景)
十一、IOMMU 前沿方向
- PASID + SIOV(Scalable IOV):PCIe 6.0 规范引入,替代 SR-IOV 的粗粒度虚拟化。每个设备可生成数万可分配接口(AInterface),无需独立配置空间,大幅降低 PF 晶体管开销。Linux 内核
drivers/iommu/iommu-sva.c预留了 SVA(Shared Virtual Addressing)接口 - IOMMU 硬件虚拟化:Intel vIOMMU 支持 stage-1/stage-2 同时启用(类似 ARM Two-Stage),实现 Guest DMA 硬件隔离 without QEMU shadow page table 切换
- SMMUv3 PASID + PRI:NVIDIA Grace SMMU 支持 ATC + PASID + PRI,可让 GPU 直接寻址 CPU 进程页表(SVA),零拷贝 CPU-GPU 协作
- 共态 IOMMU:如 DARPA CHI 项目提出 IOMMU 的 inline 加密扩展,DMA 数据自动加解密(非对称 IOMMU —— 硬件加密标签)
- eBPF IOMMU 拦截:利用 eBPF trace IOMMU fault 事件、监控异常 IoVA 访问模式,可构建 DMA 攻击实时检测器
十二、总结
IOMMU 已从早期的"VT-d 虚拟化特性"演变为 Linux 内核安全基石。VFIO 通过用户态标准化的 Container/Group/Device 抽象,支撑了 GPU 直通、NVMe 直通、网卡直通等高性能虚拟化场景。在云原生时代,IOMMU + VFIO + KVM 构成了私有云/NFV 平台的硬件虚拟化三件套——任何对延迟敏感、吞吐敏感的 IO 负载,几乎都逃不开这层隔离与翻译。
理解 IOMMU 的页表模型、VFIO 的状态机、中断路径的 eventfd 链式通知,是迈向高级 Linux 虚拟化/云原生/网络存储开发的必经之路。建议读者从一块闲置的 PCIe 网卡开始,手动完成 QEMU VFIO 直通的全流程:从驱动 unbind 到中断注入基准测试,亲手感受这条 I/O 虚拟化栈的每一层细节。

发表评论 取消回复