一、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, &reg);
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 设备,供虚拟机内的操作系统使用。它的出现解决了两个问题:

  1. 嵌套直通:L1 Guest 使用 VFIO 直通物理设备;L2 Guest(嵌套在 L1 中)需要 L1 提供自定义的 IOMMU 视图
  2. 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)系列漏洞:

  1. Thunderclap (2015):Thunderbolt 热插拔 PCIe 设备,操作系统未就绪时,设备即可发起 DMA 攻击( kernel 未配置 IOMMU 保护)。攻击者在BitLocker 锁屏状态窃取明文密钥
  2. Thunderclap 2 (2017):Thunderbolt IOMMU 在硬件可配置但默认禁用。现代 UEFI 设置 VT-d=Enabled 但仍然没有对 Thunderbolt 端口进行隔离
  3. 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 吞吐的影响取决于设备类型和配置:

场景无 IOMMUIOMMU passthroughIOMMU strict (DMA map)
NVMe SSD 读 (4K IOPS)~950K~945K~880K
NVMe SSD 写 (4K IOPS)~550K~545K~505K
10GbE 网卡单流 TCP9.44 Gbps9.40 Gbps8.50 Gbps
DPDK 单核小包转发14.88 Mpps14.80 Mpps12.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 虚拟化栈的每一层细节。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部