Cloud Hypervisor 虚拟 IOMMU(virtio-iommu)使用指南:从嵌套虚拟化保护到 VFIO 直通与巨页优化
【免费下载链接】cloud-hypervisorA Virtual Machine Monitor for modern Cloud workloads. Features include CPU, memory and device hotplug, support for running Windows and Linux guests, device offload with vhost-user and a minimal compact footprint. Written in Rust with a strong focus on security.项目地址: https://gitcode.com/GitHub_Trending/cl/cloud-hypervisor
导读
本文基于 Cloud Hypervisor 官方文档 docs/iommu.md 及仓库内 virtio-iommu 设备实现(virtio-devices/src/iommu.rs)与设备管理代码(vmm/src/device_manager.rs),系统讲解如何向 Guest 暴露虚拟 IOMMU:包括其设计动机(嵌套虚拟机内存保护、嵌套 VFIO 设备直通)、virtio-iommu 的选型理由、命令行启用方式、AArch64 下 FDT 的行为差异,以及如何通过 2MiB 巨页与专用 IOMMU PCI Segment 优化映射性能并支持设备热插拔。读完本文,你将掌握在 Cloud Hypervisor 中为设备打开iommu=on、验证 IOMMU 分组、配置巨页后端以及使用--platform iommu_segments规划热插拔的完整实战方案。
为什么需要虚拟 IOMMU
Cloud Hypervisor 的虚拟 IOMMU 面向两类典型场景,核心思路都是在 Guest 驱动与其设备实现之间插入一层"地址翻译与校验"的中间层(interposition)。
保护嵌套虚拟机(L2)的内存安全
虚拟设备(VIRTIO 设备)在代表 Guest 驱动访问 Guest 内存时,默认情况下地址不受约束。在单层虚拟化(一个 VMM 跑一个 VM)场景下,由于 Guest 被视为不可信方、可能被攻破并获得 VM 内特权,限制设备访问整个 Guest 内存没有实际意义,因此虚拟 IOMMU 的防护在这种简单场景下并不必要。
但嵌套虚拟化场景完全不同:假设 Host VMM 运行了一个第一层 VM(L1),L1 被认为是可信的(用户打算在 L1 中运行多个 VM),于是得到多个 L2 VM 运行在同一个 L1 之上。此时如果不向 L1 暴露虚拟 IOMMU,任何 L2 Guest 都能借助 Host VMM 的设备实现来访问整个 L1 Guest 内存——这是一个跨层的内存越权漏洞。虚拟 IOMMU 会校验设备被授权访问的地址,从而切断这种攻击路径。
实现多级 VFIO 嵌套直通
第二个动机是让物理设备能够穿透多层虚拟化。其机制是:Host 侧存在物理 IOMMU,L1 内运行带虚拟 IOMMU 的 VM;虚拟 IOMMU 的实现负责在每次 DMA 映射变化时更新物理 DMA Remapping 表(DMAR)。由于 Host 上只有 VFIO 这一用户态接口可以操作物理 IOMMU,因此这种更新必须经由 Host 的 VFIO 框架完成。依赖这套更新机制,物理设备可以被挂到虚拟 IOMMU 上,从而允许设备从 L1 直通到更下一层虚拟化(L2)。
为什么选择 virtio-iommu
Cloud Hypervisor 选择实现全新的 virtio-iommu 设备来提供虚拟 IOMMU,原因是半虚拟化(paravirtualization)方案的简洁性:一侧由 Guest 自身配合处理,免去了模拟物理 IOMMU 时对内存页访问进行陷入(trap)与影子(shadow)的复杂度。因此项目不会去完整模拟一个物理 IOMMU,而是基于 virtio 规范实现。
从源码看,设备实现位于 virtio-devices/src/iommu.rs,其队列配置为两个队列(requestq与eventq),队列大小 256(见 iommu.rs),并支持VIRTIO_IOMMU_F_INPUT_RANGE、VIRTIO_IOMMU_F_MAP_UNMAP、VIRTIO_IOMMU_F_PROBE、VIRTIO_IOMMU_F_BYPASS_CONFIG等特性,页大小掩码同时支持 2MiB 与 4KiB(VIRTIO_IOMMU_PAGE_SIZE_MASK = (2 << 20) | (4 << 10),见 iommu.rs),这正是后文"更快映射"一节的基础。设备还会通过 PROBE 属性向 Guest 声明一块 MSI 保留内存区域(RESV_MEM),避免 x86 平台上出现 ARM 风格的默认保留区间冲突(见 iommu.rs)。
启用前提
- 内核:从 Linux 内核 5.14 起,virtio-iommu 在 X86-64 与 AArch64 两个架构上均可用。请确认 Guest 内核版本不低于 5.14 并已启用
CONFIG_VIRTIO_IOMMU相关配置。 - Cloud Hypervisor:需使用支持 virtio-iommu 的构建版本。
基本用法:把设备挂到虚拟 IOMMU 上
向 Guest 暴露虚拟 IOMMU,需要创建一个 virtio-iommu 设备并通过 ACPI IORT 表暴露给 Guest——这可以通过"至少把一个设备挂到虚拟 IOMMU 上"来简单达成。
具体做法是在命令行中给设备显式打上iommu=on标签,设备即被视为位于该 IOMMU 之后:
- 并非所有设备都支持这个额外选项;
- 默认值始终为
off,因为大多数用户不需要它,开启会带来性能影响。
可通过./cloud-hypervisor --help查看哪些设备支持挂载到虚拟 IOMMU。从 vmm/src/vm_config.rs 与 vmm/src/vm_config.rs 的iommu: bool字段可见,该选项贯通了设备级公共配置与全局 VmConfig;在设备管理器中,handle.pci_common.iommu为 true 的设备会获得共享的iommu_mapping并被记录进 IOMMU 附加设备列表(见 device_manager.rs)。
下面是一个把virtio-blk设备挂到虚拟 IOMMU 的简单示例:
./cloud-hypervisor \ --cpus boot=1 \ --memory size=512M \ --disk path=focal-server-cloudimg-amd64.raw,iommu=on,image_type=raw \ --kernel custom-vmlinux \ --cmdline "console=ttyS0 console=hvc0 root=/dev/vda1 rw"在 Guest 中验证 IOMMU 分组
从 Guest 视角,检查/sys/kernel/iommu_groups下的目录即可验证设备是否受虚拟 IOMMU 保护:
ls /sys/kernel/iommu_groups 0本例中只应创建一个 IOMMU 分组。在该分组下可以看到属于该组的设备的 b/d/f:
ls /sys/kernel/iommu_groups/0/devices/ 0000:00:03.0再用lspci确认该设备正是预期的那一个:
lspci 00:00.0 Host bridge: Intel Corporation Device 0d57 00:01.0 Unassigned class [ffff]: Red Hat, Inc. Device 1057 00:02.0 Unassigned class [ffff]: Red Hat, Inc. Virtio console 00:03.0 Mass storage controller: Red Hat, Inc. Virtio block device 00:04.0 Unassigned class [ffff]: Red Hat, Inc. Virtio RNG可以看到00:03.0正是 Virtio block device,说明它已被放置在 IOMMU 分组内。
AArch64 下通过 FDT 使用虚拟 IOMMU
在 AArch64 架构上,即使不启用 ACPI,虚拟 IOMMU 依然可用,但其效果与上面 x86 的测试不同。
当 ACPI 禁用时,虚拟 IOMMU 通过 Flattened Device Tree(FDT)支持。此时 Guest 内核无法分辨哪些设备应当挂到 IOMMU 之后:无论你通过iommu=on挂了多少设备,PCI 总线上的所有设备(IOMMU 自身除外)都会被挂到虚拟 IOMMU 上,且每个设备会被各自加入一个 IOMMU 分组。
因此/sys/kernel/iommu_groups的目录内容将是:
ls /sys/kernel/iommu_groups/0/devices/ 0000:00:02.0 ls /sys/kernel/iommu_groups/1/devices/ 0000:00:03.0 ls /sys/kernel/iommu_groups/2/devices/ 0000:00:04.0即 console、blk、RNG 等每个 PCI 设备各占一个独立分组。
更快的大块映射:用 2MiB 巨页
问题背景:4KiB 映射的性能瓶颈
默认情况下,Guest 内存以 4KiB 页映射且不启用巨页,导致虚拟 IOMMU 设备只能收到 4KiB 粒度的映射请求。要建立大块映射需要发出大量请求,这会拖慢物理 IOMMU 的建立过程。
嵌套 VFIO 场景受影响更明显:把设备直通到 L2 Guest 时,运行在 L1 中的 VFIO 驱动会为该设备更新 DMAR 条目。由于 VFIO 会钉住(pin)整个 Guest 内存,L2 Guest 的全部映射都需要以多个 4KiB 映射形式存储——L2 Guest 内存越大,映射更新耗时越长。还有一个额外问题:如果 L2 Guest 内存相当大,大量映射可能会超过 Host 上设置的 VFIO 映射数量上限。该默认值为 65536,256MiB 的 RAM 就能轻易触达这个上限。
解决这两个问题(耗时与超限)的办法是减少描述同一大块映射所需的请求数量,即使用 2MiB 巨页。把 Guest 内存看作更大的页,由于虚拟 IOMMU 设备本身支持 2MiB 页,Guest 需要的映射数量就会减少,既避免超过上限,也缩短 Host 端处理时间。这正是"尽可能多地使用巨页可以加快 VM 启动时间"的原理——从源码看,设备的页大小掩码同时声明支持 4KiB 与 2MiB(VIRTIO_IOMMU_PAGE_SIZE_MASK,iommu.rs),使 2MiB 映射请求得以被接受。
基本用法:为普通 Guest 配置巨页
首先确保系统有足够的巨页覆盖整个 Guest 内存:
# 本示例创建 4096 个巨页 echo 4096 > /proc/sys/vm/nr_hugepages接下来创建 VM。有两个要点:一是希望 VM 内存以巨页承载,需用/dev/hugepages作为后端;二是需要在 Guest 自身内也创建一些巨页供其消耗。
./cloud-hypervisor \ --cpus boot=1 \ --memory size=8G,hugepages=on \ --disk path=focal-server-cloudimg-amd64.raw,image_type=raw \ --kernel custom-vmlinux \ --cmdline "console=ttyS0 console=hvc0 root=/dev/vda1 rw hugepagesz=2M hugepages=2048" \ --net tap=,mac=,iommu=on关键参数说明:
| 参数 | 作用 |
|---|---|
--memory size=8G,hugepages=on | 让 VM 内存映射在 Host 巨页上(配合/dev/hugepages后端) |
hugepagesz=2M hugepages=2048(Guest cmdline) | 在 Guest 内预留 2MiB 巨页供 Guest 内核/驱动消耗 |
--net ... iommu=on | 将 virtio-net 设备挂到虚拟 IOMMU 之后 |
嵌套用法:L2 也使用巨页
为达到优化性能,L2 Guest 同样需要基于巨页映射。假设要直通的物理设备是0000:00:01.0,步骤如下。
第一步,启动 L1 VM(注意 cmdline 中包含嵌套虚拟化与 VFIO 相关内核参数):
./cloud-hypervisor \ --cpus boot=1 \ --memory size=8G,hugepages=on \ --disk path=focal-server-cloudimg-amd64.raw,image_type=raw \ --kernel custom-vmlinux \ --cmdline "console=ttyS0 console=hvc0 root=/dev/vda1 rw kvm-intel.nested=1 vfio_iommu_type1.allow_unsafe_interrupts rw hugepagesz=2M hugepages=2048" \ --device path=/sys/bus/pci/devices/0000:00:01.0,iommu=on第二步,L1 VM 运行起来后,在 Guest 内把设备从默认驱动解绑,并绑定到 VFIO(设备此时应显示为0000:00:04.0):
echo 0000:00:04.0 > /sys/bus/pci/devices/0000\:00\:04.0/driver/unbind echo 8086 1502 > /sys/bus/pci/drivers/vfio-pci/new_id echo 0000:00:04.0 > /sys/bus/pci/drivers/vfio-pci/bind第三步,以巨页内存后端启动 L2 Guest:
./cloud-hypervisor \ --cpus boot=1 \ --memory size=4G,hugepages=on \ --disk path=focal-server-cloudimg-amd64.raw,image_type=raw \ --kernel custom-vmlinux \ --cmdline "console=ttyS0 console=hvc0 root=/dev/vda1 rw" \ --device path=/sys/bus/pci/devices/0000:00:04.0这样 L1 与 L2 两层都基于 2MiB 巨页映射,DMAR 更新请求数大幅下降,既规避了 VFIO 的 65536 映射上限,也缩短了映射建立时间。
专用 IOMMU PCI 段:为热插拔铺路
为了便于热插拔那些必须位于 IOMMU 之后的设备,可以把整个 PCI Segment 标记为位于 IOMMU 之后。这通过--platform num_pci_segments=<number_of_segments>,iommu_segments=<range of segments>完成,API 侧则对应PlatformConfig中的等价字段(vmm/src/vm_config.rs)。
示例(启用 API socket 并创建第二个位于 IOMMU 之后的 PCI Segment):
./cloud-hypervisor \ --api-socket=/tmp/api \ --cpus boot=1 \ --memory size=4G,hugepages=on \ --disk path=focal-server-cloudimg-amd64.raw,image_type=raw \ --kernel custom-vmlinux \ --cmdline "console=ttyS0 console=hvc0 root=/dev/vda1 rw" \ --platform num_pci_segments=2,iommu_segments=1随后即可把一个需要 IOMMU 的 VFIO 设备热插到该 Segment 上:
./ch-remote --api-socket=/tmp/api add-device path=/sys/bus/pci/devices/0000:00:04.0,iommu=on,pci_segment=1注意事项:
- 无法被放到 IOMMU 之后的设备(例如没有
iommu=选项的设备)不能被放到 IOMMU Segment 上; - 从源码看,所有落在
iommu_segments内的 bdf(每个 Segment 下设备号 0~31)都会被并入 IOMMU 附加设备列表(见 device_manager.rs),因此这些 Segment 天然成为"IOMMU 专属段"; - 除了
num_pci_segments与iommu_segments,PlatformConfig还支持iommu_address_width=<bits>(默认 64 位,见 vmm/src/vm_config.rs)等参数,用于约束 IOMMU 输入地址宽度。
小结
Cloud Hypervisor 通过 virtio-iommu 为多级虚拟化提供了两条关键能力:一是用虚拟 IOMMU 隔离 L2 Guest 对 L1 内存的越权访问,二是借助 DMAR 更新链路实现物理设备的嵌套 VFIO 直通。启用路径清晰直接——给目标设备加上iommu=on即可自动创建 virtio-iommu 设备并经 ACPI IORT 暴露;AArch64 无 ACPI 时则退化为 FDT 全量挂载行为。性能层面,2MiB 巨页是规避 VFIO 65536 映射上限、加速大块映射建立的关键手段;而--platform iommu_segments则把"必须挂 IOMMU"的设备约束提升到 PCI Segment 粒度,为热插拔提供了干净的落点。
【免费下载链接】cloud-hypervisorA Virtual Machine Monitor for modern Cloud workloads. Features include CPU, memory and device hotplug, support for running Windows and Linux guests, device offload with vhost-user and a minimal compact footprint. Written in Rust with a strong focus on security.项目地址: https://gitcode.com/GitHub_Trending/cl/cloud-hypervisor
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考