4. AMD-Vi(IOMMU)与设备隔离

在 Ryzen Embedded 平台上,CPU 侧的 MMU 只负责把虚拟地址翻译成物理地址,而外设发起的 DMA 请求使用的是设备自己的地址空间。AMD-Vi 就是 AMD 平台上的 IOMMU 实现,它位于设备与内存之间,为每个设备提供独立的地址翻译与访问控制。理解 AMD-Vi 的关键在于:它把“设备可见的地址”与“真实物理内存”解耦,从而让直通、隔离和中断投递都变得可控。

IOMMU 的工作原理

当设备发起 DMA 时,请求中携带的是 IOVA(I/O Virtual Address)。AMD-Vi 根据请求来源的 BDF(Bus/Device/Function)号找到对应的设备表项,再沿多级页表把 IOVA 翻译成系统物理地址 SPA。翻译过程中会检查权限位(读、写、执行)以及页表项的有效性。若地址未映射或权限不符,硬件会阻止该访问并产生 IOMMU 故障事件,而不是让设备直接写坏内存。

AMD-Vi 的翻译结构可以概括为“设备表 → 页表”两级查找。设备表(Device Table)以 BDF 为索引,每个表项指向该设备的页表根;页表则采用与 x86 类似的多级结构,通常为 4 级或 5 级,具体取决于平台配置。设备表项还包含中断重映射表指针、域 ID 等字段,因此设备隔离与中断投递共享同一套设备描述结构。

AMD-Vi 地址翻译与隔离路径 PCIe 设备 GPU / NVMe / 网卡 发起 DMA 请求 携带 BDF + IOVA DMA 请求 AMD-Vi IOMMU 设备表查找(按 BDF) 多级页表翻译 IOVA → SPA 权限检查:R / W / X 翻译后访问 系统物理内存 Guest 内存页 Host 内存页 MMIO 区域 IOMMU 故障事件 未映射 / 权限不符 → 阻止访问并记录 中断重映射表 IRT MSI/MSI-X 地址重写 中断投递到目标 vCPU 与设备表项共享域 ID IOMMU 隔离域 每个域独立页表 设备只能访问本域映射 跨域访问被硬件拦截 设备表以 BDF 为索引,页表完成 IOVA→SPA 翻译,IRT 负责中断重映射,三者共同构成设备隔离基础

设备表与页表

设备表是 AMD-Vi 的入口。系统启动时,BIOS 或内核会为每个可能发起 DMA 的 BDF 分配设备表项。表项中的“页表根指针”决定了该设备使用哪一棵页表树。若把两个设备指向同一棵页表树,它们就处于同一隔离域,可以互相访问对方映射的内存;若指向不同页表树,则天然隔离。因此,IOMMU 组的本质就是“共享同一页表树的设备集合”。

页表翻译的粒度通常是 4KB 页,也支持 2MB 或 1GB 大页以减少 TLB 压力。IOMMU 内部有 IOTLB 缓存翻译结果,设备频繁访问同一区域时命中率较高。当页表被修改后,需要通过命令队列发送 IOTLB 失效命令,否则设备可能继续使用旧映射。这一点在热插拔或动态重映射场景中尤其重要。

中断重映射

传统 MSI/MSI-X 中断由设备直接写目标 CPU 的 APIC 地址。在虚拟化环境中,如果让设备直接写物理 APIC,中断会绕过 VMM,无法正确投递给 Guest。AMD-Vi 的中断重映射功能把设备发出的 MSI 地址和数据进行重写:设备仍然写一个“中断地址”,但该地址被 IOMMU 截获,查中断重映射表后转换为目标 vCPU 的虚拟 APIC 投递。这样,直通设备的中断也能被正确注入 Guest,而不会误投到 Host 或其他 Guest。

DMA 隔离与 IOMMU 组

IOMMU 组是 Linux 内核根据 PCIe 拓扑和 IOMMU 能力划分的最小隔离单位。同一个 IOMMU 组内的设备共享页表,无法单独分配给不同 Guest;只有位于不同 IOMMU 组的设备才能分别直通给不同虚拟机。判断一个设备能否安全直通,第一步就是查看它所在的 IOMMU 组是否只包含目标设备。

在 Ryzen Embedded 上,常见的 IOMMU 组划分受 PCIe 根端口和桥的影响。例如,CPU 直连的 PCIe 插槽通常各自成组,而经过 PCIe Switch 扩展的多个设备可能落在同一组内。若一个组内混有必须留给 Host 的设备(如管理网卡),则该组内所有设备都无法直通,需要调整插槽或使用 ACS 覆盖等方案。

在 Ryzen Embedded 上配置 IOMMU 与直通

以下操作以 Linux 环境为例,示例参数属于教学假设,实际值需根据平台手册和硬件拓扑调整。

首先确认固件已开启 IOMMU。在 BIOS 中启用 “IOMMU” 或 “AMD-Vi” 选项,并确保 “ACS Enable” 按需开启。启动后检查内核是否识别:

dmesg | grep -i amd-vi
# 期望看到:AMD-Vi: Interrupt remapping enabled
# 以及:AMD-Vi: Virtual APIC enabled

ls /sys/kernel/iommu_groups/
# 列出所有 IOMMU 组编号

接着查看目标设备所在的组。假设我们要直通一块 NVMe 控制器,BDF 为 0000:03:00.0:

for g in /sys/kernel/iommu_groups/*; do
  echo "IOMMU Group ${g##*/}:"
  for d in $g/devices/*; do
    echo -e "\t$(lspci -nns ${d##*/})"
  done
done

若输出显示 03:00.0 所在组只有它自己,则具备直通条件。若组内还有其它设备,需要评估是否可一并直通,或调整硬件插槽。

然后绑定 VFIO 驱动。以 PCI 设备为例,先获取 vendor:device ID:

lspci -nns 03:00.0
# 示例输出:03:00.0 0108: 1022:1234 (rev 01)

echo "1022 1234" > /sys/bus/pci/drivers/vfio-pci/new_id
echo "0000:03:00.0" > /sys/bus/pci/devices/0000:03:00.0/driver/unbind
echo "0000:03:00.0" > /sys/bus/pci/drivers/vfio-pci/bind

绑定后,该设备不再由 Host 原生驱动管理,而是交给 VFIO 框架,由用户态虚拟机监控器将其映射进 Guest。此时 Guest 内的驱动可以正常初始化设备,DMA 访问经过 AMD-Vi 翻译,中断经过 IRT 重映射投递。

判断直通是否成功,可在 Guest 内执行 lspci 确认设备可见,并观察 dmesg 中是否有 IOMMU 故障事件。若出现 “IOMMU fault” 或 “DMAR” 类报错,通常说明页表映射缺失或权限配置有误。

常见问题与排查方向

直通失败时,优先检查三件事:一是设备是否真的位于独立 IOMMU 组;二是内核是否启用了 iommu=pt 或 amd_iommu=on 等参数;三是中断重映射是否开启。若设备能识别但 DMA 报错,多半是页表映射问题;若设备无法识别,则可能是 VFIO 绑定或 PCI 复位问题。对于 Ryzen Embedded,还需注意部分集成外设与 CPU 共享 IOMMU 组,这类设备通常不建议直通。

整体而言,AMD-Vi 把设备隔离从“物理插槽决定”提升为“页表与设备表决定”。掌握设备表、页表、中断重映射表三者的关系,再结合 IOMMU 组判断,就能在 Ryzen Embedded 上稳定地完成设备直通与隔离配置。