通过 OVMF 的 PCI 直通
开放虚拟机固件 (OVMF) 是一个为虚拟机启用 UEFI 支持的项目。从 Linux 3.9 和最新版本的 QEMU 开始,现在可以直通显卡,为虚拟机提供原生图形性能,这对于图形密集型任务非常有用。
如果您有一台台式电脑,并且有一张可以专用于主机的备用 GPU(无论是集成 GPU 还是旧的 OEM 卡,品牌甚至不需要匹配),并且您的硬件支持它(请参阅#先决条件),则可以使用任何操作系统的虚拟机,该虚拟机具有自己的专用 GPU 和接近原生的性能。有关技术的更多信息,请参阅背景演示文稿 (pdf)。
先决条件
VGA 直通依赖于许多当今并不普及的技术,并且可能在您的硬件上不可用。 除非满足以下要求,否则您将无法在您的机器上执行此操作
- 您的 CPU 必须支持硬件虚拟化 (用于 kvm) 和 IOMMU (用于直通本身)。
- 兼容 Intel CPU 列表 (Intel VT-x 和 Intel VT-d).
- 所有 Bulldozer 世代及更高版本(包括 Zen)的 AMD CPU 应该都兼容。
- 您的主板也必须支持 IOMMU。
- 芯片组和 BIOS 都必须支持它。 始终一目了然地判断是否是这种情况并不容易,但是在 Xen wiki 以及 维基百科:支持 IOMMU 的硬件列表 上有一个相当全面的列表。
- 您的客户机 GPU ROM 必须支持 UEFI。
- 如果您可以找到此列表中的任何 ROM,它适用于您的特定 GPU 并据说支持 UEFI,那么您通常可以放心了。 2012 年及之后的所有 GPU 都应该支持这一点,因为微软将 UEFI 作为设备以 Windows 8 兼容名义销售的必要条件。
您可能需要备用显示器或具有连接到不同 GPU 的多个输入端口的显示器(如果没有插入屏幕,直通 GPU 将不会显示任何内容,并且使用 VNC 或 Spice 连接将无助于您的性能),以及可以传递给虚拟机的鼠标和键盘。 如果出现任何问题,您至少可以通过这种方式控制主机。
设置 IOMMU
- IOMMU 是 Intel VT-d 和 AMD-Vi 的通用名称。
- VT-d 代表 Intel 定向 I/O 虚拟化技术,不应与 VT-x Intel 虚拟化技术 混淆。 VT-x 允许一个硬件平台充当多个“虚拟”平台,而 VT-d 提高了系统的安全性和可靠性,还提高了虚拟化环境中 I/O 设备的性能。
使用 IOMMU 可以实现 PCI 直通和内存保护等功能,以防止故障或恶意设备,请参阅 维基百科:输入输出内存管理单元#优点 和 内存管理 (计算机编程): 你能用通俗易懂的英语解释一下 IOMMU 吗?。
启用 IOMMU
确保您的 CPU 支持 AMD-Vi 或 Intel VT-d,并在 BIOS 设置中启用。 这些选项通常与其他 CPU 功能一起出现,这些功能可能位于与超频相关的菜单中。 它们可能以实际名称(“VT-d”或“AMD-Vi”)或更模糊的术语(例如“虚拟化技术”)列出,主板手册中可能会或可能不会对其进行解释。
使用 dmesg 检查是否已启用 IOMMU
# dmesg | grep -i IOMMU
[ 0.000000] ACPI: DMAR 0x00000000BDCB1CB0 0000B8 (v01 INTEL BDW 00000001 INTL 00000001) [ 0.000000] Intel-IOMMU: enabled [ 0.028879] dmar: IOMMU 0: reg_base_addr fed90000 ver 1:0 cap c0000020660462 ecap f0101a [ 0.028883] dmar: IOMMU 1: reg_base_addr fed91000 ver 1:0 cap d2008c20660462 ecap f010da [ 0.028950] IOAPIC id 8 under DRHD base 0xfed91000 IOMMU 1 [ 0.536212] DMAR: No ATSR found [ 0.536229] IOMMU 0 0xfed90000: using Queued invalidation [ 0.536230] IOMMU 1 0xfed91000: using Queued invalidation [ 0.536231] IOMMU: Setting RMRR: [ 0.536241] IOMMU: Setting identity map for device 0000:00:02.0 [0xbf000000 - 0xcf1fffff] [ 0.537490] IOMMU: Setting identity map for device 0000:00:14.0 [0xbdea8000 - 0xbdeb6fff] [ 0.537512] IOMMU: Setting identity map for device 0000:00:1a.0 [0xbdea8000 - 0xbdeb6fff] [ 0.537530] IOMMU: Setting identity map for device 0000:00:1d.0 [0xbdea8000 - 0xbdeb6fff] [ 0.537543] IOMMU: Prepare 0-16MiB unity mapping for LPC [ 0.537549] IOMMU: Setting identity map for device 0000:00:1f.0 [0x0 - 0xffffff] [ 2.182790] [drm] DMAR active, disabling use of stolen memory
要手动启用 IOMMU 支持,请根据使用的 CPU 类型设置正确的内核参数
- 对于 Intel CPU (VT-d),设置
intel_iommu=on
,除非您的内核设置了CONFIG_INTEL_IOMMU_DEFAULT_ON
配置选项。 - 对于 AMD CPU (AMD-Vi),如果内核检测到 BIOS 中的 IOMMU 硬件支持,则会自动启用 IOMMU 支持。
确保组有效
以下脚本应允许您查看各种 PCI 设备如何映射到 IOMMU 组。 如果它没有返回任何内容,则您要么没有正确启用 IOMMU 支持,要么您的硬件不支持它。
#!/bin/bash shopt -s nullglob for g in $(find /sys/kernel/iommu_groups/* -maxdepth 0 -type d | sort -V); do echo "IOMMU Group ${g##*/}:" for d in $g/devices/*; do echo -e "\t$(lspci -nns ${d##*/})" done; done;
示例输出
IOMMU Group 1: 00:01.0 PCI bridge: Intel Corporation Xeon E3-1200 v2/3rd Gen Core processor PCI Express Root Port [8086:0151] (rev 09) IOMMU Group 2: 00:14.0 USB controller: Intel Corporation 7 Series/C210 Series Chipset Family USB xHCI Host Controller [8086:0e31] (rev 04) IOMMU Group 4: 00:1a.0 USB controller: Intel Corporation 7 Series/C210 Series Chipset Family USB Enhanced Host Controller #2 [8086:0e2d] (rev 04) IOMMU Group 10: 00:1d.0 USB controller: Intel Corporation 7 Series/C210 Series Chipset Family USB Enhanced Host Controller #1 [8086:0e26] (rev 04) IOMMU Group 13: 06:00.0 VGA compatible controller: NVIDIA Corporation GM204 [GeForce GTX 970] [10de:13c2] (rev a1) 06:00.1 Audio device: NVIDIA Corporation GM204 High Definition Audio Controller [10de:0fbb] (rev a1)
IOMMU 组是可以传递给虚拟机的最小物理设备集。 例如,在上面的示例中,06:00.0 中的 GPU 和 6:00.1 中的音频控制器都属于 IOMMU 组 13,并且只能一起传递。 但是,前置 USB 控制器有自己的组(组 2),它与 USB 扩展控制器(组 10)和后置 USB 控制器(组 4)都分开,这意味着 它们中的任何一个都可以传递到虚拟机而不会影响其他设备。
注意事项
将客户机 GPU 插入未隔离的基于 CPU 的 PCIe 插槽
并非所有 PCI-E 插槽都相同。 大多数主板都具有由 CPU 和 PCH 提供的 PCIe 插槽。 根据您的 CPU,您的基于处理器的 PCIe 插槽可能无法正确支持隔离,在这种情况下,PCI 插槽本身将显示为与连接到它的设备分组在一起。
IOMMU Group 1: 00:01.0 PCI bridge: Intel Corporation Xeon E3-1200 v2/3rd Gen Core processor PCI Express Root Port (rev 09) 01:00.0 VGA compatible controller: NVIDIA Corporation GM107 [GeForce GTX 750] (rev a2) 01:00.1 Audio device: NVIDIA Corporation Device 0fbc (rev a1)
只要此处仅包含您的客户机 GPU 即可,如上所述。 根据插入其他 PCIe 插槽的设备以及它们分配给 CPU 还是 PCH,您可能会发现同一组中有其他设备,这将迫使您也传递这些设备。 如果您乐于将其中包含的所有内容传递给虚拟机,则可以继续。 否则,您要么需要尝试将 GPU 插入其他 PCIe 插槽(如果有),看看这些插槽是否提供与其余部分的隔离,要么安装 ACS 覆盖补丁,该补丁有其自身的缺点。 有关更多信息,请参阅#绕过 IOMMU 组 (ACS 覆盖补丁)。
隔离 GPU
为了将设备分配给虚拟机,此设备和所有共享同一 IOMMU 组的设备都必须将其驱动程序替换为存根驱动程序或 VFIO 驱动程序,以防止主机与它们交互。 对于大多数设备,这可以在虚拟机启动之前动态完成。
但是,由于 GPU 驱动程序的大小和复杂性,它们往往不支持动态重新绑定,因此您不能只是将主机上使用的某些 GPU 透明地传递给虚拟机,而不会让两个驱动程序相互冲突。 因此,通常建议在启动虚拟机之前手动绑定这些占位符驱动程序,以阻止其他驱动程序尝试声明它。
以下部分详细介绍了如何配置 GPU,以便在启动过程中尽早绑定这些占位符驱动程序,这会使所述设备处于非活动状态,直到虚拟机声明它或驱动程序被切换回。 这是首选方法,因为它比系统完全联机后切换驱动程序具有更少的注意事项。
从 Linux 4.1 开始,内核包含 vfio-pci。 这是一个 VFIO 驱动程序,意味着它履行与 pci-stub 相同的作用,但它也可以在一定程度上控制设备,例如通过在设备不使用时将其切换到 D3 状态。
通过设备 ID 绑定 vfio-pci
Vfio-pci 通常通过 ID 定位 PCI 设备,这意味着您只需要指定您打算直通的设备的 ID。 对于以下 IOMMU 组,您将需要使用 10de:13c2
和 10de:0fbb
绑定 vfio-pci,这将用作本节其余部分的示例值。
IOMMU Group 13: 06:00.0 VGA compatible controller: NVIDIA Corporation GM204 [GeForce GTX 970] [10de:13c2] (rev a1) 06:00.1 Audio device: NVIDIA Corporation GM204 High Definition Audio Controller [10de:0fbb] (rev a1)
- 如果主机 GPU 和客户机 GPU 共享相同的配对(即:如果两者是相同的型号),则无法使用供应商设备 ID 对指定要隔离的设备。 如果是这种情况,请改为阅读 #使用相同的客户机和主机 GPU。
- 如果在 #将客户机 GPU 插入未隔离的基于 CPU 的 PCIe 插槽 中指出,您的 pci 根端口是您的 IOMMU 组的一部分,则您不得将其 ID 传递给
vfio-pci
,因为它需要保持连接到主机才能正常运行。 但是,该组中的任何其他设备都应留给vfio-pci
绑定。 - 绑定音频设备(上述示例中的
10de:0fbb
)是可选的。 Libvirt 能够自行将其从snd_hda_intel
驱动程序中解除绑定。
通过 内核模块参数 ids=10de:13c2,10de:0fbb
为 vfio-pci
提供设备 ID。
如果想要将 HDAudio 保留在主机中,则可以使用内核模块参数 gpu_bind=0
用于 snd-hda-core
和 enable_acomp=n
用于 snd-hda-codec-hdmi
来分离它。
您可以使用此 bash 脚本,而无需内核 vfio-pci
ID。 在调用 unbind_vfio
后,GPU 可以在主机中运行。
#!/bin/bash gpu="0000:06:00.0" aud="0000:06:00.1" gpu_vd="$(cat /sys/bus/pci/devices/$gpu/vendor) $(cat /sys/bus/pci/devices/$gpu/device)" aud_vd="$(cat /sys/bus/pci/devices/$aud/vendor) $(cat /sys/bus/pci/devices/$aud/device)" function bind_vfio { echo "$gpu" > "/sys/bus/pci/devices/$gpu/driver/unbind" echo "$aud" > "/sys/bus/pci/devices/$aud/driver/unbind" echo "$gpu_vd" > /sys/bus/pci/drivers/vfio-pci/new_id echo "$aud_vd" > /sys/bus/pci/drivers/vfio-pci/new_id } function unbind_vfio { echo "$gpu_vd" > "/sys/bus/pci/drivers/vfio-pci/remove_id" echo "$aud_vd" > "/sys/bus/pci/drivers/vfio-pci/remove_id" echo 1 > "/sys/bus/pci/devices/$gpu/remove" echo 1 > "/sys/bus/pci/devices/$aud/remove" echo 1 > "/sys/bus/pci/rescan" }
提前加载 vfio-pci
由于 Arch 的 linux 将 vfio-pci 构建为模块,我们需要强制它在图形驱动程序有机会绑定到显卡之前加载。 有两种方法:modprobe 配置,或将模块添加到 initramfs。
modprobe.d
此方法在 udev 加载 GPU 驱动程序时加载 vfio
。 这避免了 initramfs 膨胀和不必要地减慢启动时间。
/etc/modprobe.d/vfio.conf
softdep drm pre: vfio-pci
如果您要直通 Nvidia GPU 并安装了专有的 nvidia 驱动程序,请改用以下方法
/etc/modprobe.d/vfio.conf
softdep nvidia pre: vfio-pci
initramfs
mkinitcpio
将 vfio_pci
、vfio
和 vfio_iommu_type1
添加到 mkinitcpio
/etc/mkinitcpio.conf
MODULES=(... vfio_pci vfio vfio_iommu_type1 ...)
- 从内核 6.2 开始,
vfio_virqfd
功能已折叠到基本vfio
模块中。 - 如果您还通过这种方式加载了另一个驱动程序用于早期模式设置(例如
nouveau
、radeon
、amdgpu
、i915
等),则所有上述 VFIO 模块必须位于其前面。 - 如果您正在为
nvidia
驱动程序进行模式设置,则vfio-pci.ids
必须嵌入到 initramfs 映像中。 如果通过内核参数给出,则读取它们的时间太晚而无法生效。 按照 #通过设备 ID 绑定 vfio-pci 中的说明将 id 添加到 modprobe conf 文件。
此外,请确保 modconf 钩子包含在 mkinitcpio.conf
的 HOOKS 列表中
/etc/mkinitcpio.conf
HOOKS=(... modconf ...)
由于已将新模块添加到 initramfs 配置,因此您必须重新生成 initramfs。
booster
与 mkinitcpio 类似,您需要指定要提前加载的模块
/etc/booster.yaml
modules_force_load: vfio_pci,vfio,vfio_iommu_type1
dracut
按照相同的思路,我们需要确保所有 vfio 驱动程序都在 initramfs 中。 将以下文件添加到 /etc/dracut.conf.d
10-vfio.conf
force_drivers+=" vfio_pci vfio vfio_iommu_type1 "
请注意,我们使用了 force_drivers
而不是常用的 add_drivers
选项,这将确保驱动程序尝试通过 modprobe 尽早加载 (Dracut#早期内核模块加载)。
与 mkinitcpio 一样,您必须在之后重新生成 initramfs。 有关更多详细信息,请参阅 dracut。
验证配置是否生效
重新启动并验证 vfio-pci 是否已正确加载,以及它现在是否已绑定到正确的设备。
# dmesg | grep -i vfio
[ 0.329224] VFIO - User Level meta-driver version: 0.3 [ 0.341372] vfio_pci: add [10de:13c2[ffff:ffff]] class 0x000000/00000000 [ 0.354704] vfio_pci: add [10de:0fbb[ffff:ffff]] class 0x000000/00000000 [ 2.061326] vfio-pci 0000:06:00.0: enabling device (0100 -> 0103)
并非所有来自 vfio.conf
的设备(甚至预期的设备)都必须出现在 dmesg 输出中。 即使设备未出现,它仍然可能在客户机虚拟机中可见和可用。
$ lspci -nnk -d 10de:13c2
06:00.0 VGA compatible controller: NVIDIA Corporation GM204 [GeForce GTX 970] [10de:13c2] (rev a1) Kernel driver in use: vfio-pci Kernel modules: nouveau nvidia
$ lspci -nnk -d 10de:0fbb
06:00.1 Audio device: NVIDIA Corporation GM204 High Definition Audio Controller [10de:0fbb] (rev a1) Kernel driver in use: vfio-pci Kernel modules: snd_hda_intel
设置基于 OVMF 的客户机虚拟机
OVMF 是用于 QEMU 虚拟机的开源 UEFI 固件。 虽然可以使用 SeaBIOS 获得与实际 PCI 直通类似的结果,但设置过程不同,并且如果您的硬件支持 EFI 方法,则通常最好使用 EFI 方法。
配置 libvirt
Libvirt 是许多虚拟化实用程序的包装器,它极大地简化了虚拟机的配置和部署过程。 在 KVM 和 QEMU 的情况下,它提供的前端使我们能够避免处理 QEMU 的权限,并使在活动的虚拟机上添加和删除各种设备变得更加容易。 然而,它作为包装器的地位意味着它可能并非始终支持所有最新的 qemu 功能,这最终可能需要使用包装器脚本来为 QEMU 提供一些额外的参数。
安装 qemu-desktop, libvirt, edk2-ovmf, 和 virt-manager。 对于默认网络连接,需要 dnsmasq。
按照 Libvirt#配置 配置 libvirt 以供使用。
您可能还需要激活默认的 libvirt 网络
# virsh net-autostart default # virsh net-start default
设置客户机操作系统
使用 virt-manager
设置虚拟机的过程在很大程度上是不言自明的,因为大多数过程都带有相当全面的屏幕说明。
但是,您应该特别注意以下步骤
- 当虚拟机创建向导要求您命名虚拟机时(单击“完成”之前的最后一步),选中“安装前自定义”复选框。
- 在“概述”部分中,将固件设置为“UEFI”。 如果该选项灰显,请确保
- 您的虚拟机监控程序作为系统会话而不是用户会话运行。 可以通过 单击,然后悬停 在 virt-manager 中的会话上来验证这一点。 如果您不小心以用户会话身份运行它,则必须通过单击“文件”>“添加连接..”来打开新连接,然后从下拉菜单站“QEMU/KVM”中选择该选项,而不是“QEMU/KVM 用户会话”。
- 在“CPU”部分中,将您的 CPU 型号更改为“host-passthrough”。 如果它不在列表中,您将不得不手动键入它或使用
virt-xml vmname --edit --cpu host-passthrough
。 这将确保正确检测到您的 CPU,因为它使 libvirt 能够完全按照原样公开您的 CPU 功能,而不是仅公开它识别的功能(这是使 CPU 行为更易于重现的首选默认行为)。 如果没有它,某些应用程序可能会抱怨您的 CPU 是未知型号。 - 如果您想最大限度地减少 IO 开销,最好在安装之前设置 #Virtio 磁盘
其余安装过程将使用在窗口中运行的标准 QXL 视频适配器正常进行。 此时,无需为其余虚拟设备安装其他驱动程序,因为稍后将删除其中的大多数驱动程序。 客户机操作系统完成安装后,只需关闭虚拟机即可。 首次启动虚拟机时,您可能会进入 UEFI 菜单而不是开始安装。 有时未自动检测到正确的 ISO 文件,您需要手动指定要启动的驱动器。 通过键入 exit 并导航到“启动管理器”,您将进入一个允许您在设备之间进行选择的菜单。
连接 PCI 设备
安装完成后,现在可以在 libvirt 中编辑硬件详细信息并删除虚拟集成设备,例如 spice 通道和虚拟显示器、QXL 视频适配器、模拟鼠标和键盘以及 USB 平板电脑设备。 例如,从您的 XML 文件中删除以下部分
<channel type="spicevmc"> ... </channel> <input type="tablet" bus="usb"> ... </input> <input type="mouse" bus="ps2"/> <input type="keyboard" bus="ps2"/> <graphics type="spice" autoport="yes"> ... </graphics> <video> <model type="qxl" .../> ... </video>
由于这使您没有输入设备,您可能还想将一些 USB 主机设备绑定到虚拟机,但请记住至少保留一个分配给您的主机的鼠标和/或键盘,以防客户机出现问题。 这可以通过使用 添加硬件 > USB 主机设备
来完成。
此时,也可以连接之前隔离的 PCI 设备; 只需单击“添加硬件”并选择您要直通的 PCI 主机设备。 如果一切顺利,插入 GPU 的屏幕应显示 OVMF 启动画面,并且您的虚拟机应正常启动。 从那里,您可以为虚拟机的其余部分设置驱动程序。
显卡驱动程序虚拟化检测
AMD 的显卡驱动程序包含非常基本的虚拟机检测,目标是 Hyper-V 扩展。 如果此检测机制触发,驱动程序将拒绝运行,从而导致黑屏。
如果是这种情况,则需要修改报告的 Hyper-V 供应商 ID
$ virsh edit vmname
... <features> ... <hyperv> ... <vendor_id state='on' value='randomid'/> ... </hyperv> ... </features> ...
版本 465 之前的 Nvidia 客户机驱动程序表现出类似的行为,这导致卡设备管理器状态中出现通用错误 43。 因此,使用这些较旧驱动程序的系统也需要进行上述修改。 此外,它们还需要隐藏 KVM CPU 叶
$ virsh edit vmname
... <features> ... <kvm> <hidden state='on'/> </kvm> ... </features> ...
请注意,上述步骤并不等同于“隐藏”虚拟机,使其对 Windows 或虚拟机中运行的任何驱动程序/程序不可见。 此外,此处提到的任何检测机制无关的各种其他问题也可能触发错误 43。
通过 Evdev 传递键盘/鼠标
如果您没有备用鼠标或键盘来专用于您的客户机,并且您不想遭受 Spice 的视频开销,则可以设置 evdev 以在 Linux 主机和虚拟机之间共享它们。
Ctrl
键可在主机和客户机之间交换控制。您可以更改这些热键。您需要将 grabToggle
变量设置为以下可用组合之一:ctrl-ctrl
、alt-alt
、shift-shift
、meta-meta
、scrolllock
或 ctrl-scrolllock
,具体取决于您的键盘。更多信息:https://github.com/libvirt/libvirt/blob/master/docs/formatdomain.rst#input-devices
首先,在 /dev/input/by-id/
中找到您的键盘和鼠标设备。只有名称中包含 event
的设备才是有效的。您可能会找到与鼠标或键盘关联的多个设备,因此请尝试 cat /dev/input/by-id/device_id
,然后敲击键盘上的某些键或晃动鼠标,看看是否有输入信号,如果有,则表示您找到了正确的设备。现在将这些设备添加到您的配置中。
$ virsh edit vmname
... <devices> ... <input type='evdev'> <source dev='/dev/input/by-id/MOUSE_NAME'/> </input> <input type='evdev'> <source dev='/dev/input/by-id/KEYBOARD_NAME' grab='all' repeat='on' grabToggle='ctrl-ctrl'/> </input> ... </devices>
将 MOUSE_NAME
和 KEYBOARD_NAME
替换为您的设备路径。现在您可以启动 guest 操作系统,并通过同时按下左右 Ctrl 键来测试在主机和 guest 系统之间交换鼠标和键盘的控制权。
您还可以考虑在配置中将 PS/2 输入切换为 Virtio 输入。添加以下两个设备:
$ virsh edit vmname
... <input type='mouse' bus='virtio'/> <input type='keyboard' bus='virtio'/> ...
在安装 guest 驱动程序之前,virtio 输入设备实际上不会被使用。QEMU 将继续向 PS2 设备发送按键事件,直到检测到 virtio 输入驱动程序初始化。请注意,PS2 设备无法移除,因为它们是模拟 Q35/440FX 芯片组的内部功能。
注意事项
在基于 OVMF 的虚拟机上使用非 EFI 镜像
OVMF 固件不支持从非 EFI 介质启动。如果在启动后立即进入 UEFI shell,则可能是 EFI 启动介质无效。尝试使用备用的 Linux/Windows 镜像来确定您是否拥有无效的介质。
性能调优
PCI 直通的大多数用例都与性能密集型领域有关,例如视频游戏和 GPU 加速任务。虽然 PCI 直通本身是迈向达到原生性能的一步,但在主机和 guest 系统上仍然需要进行一些调整,以充分利用您的虚拟机。
CPU 绑定
KVM guest 系统的默认行为是将来自 guest 系统的操作作为多个线程运行,这些线程代表虚拟处理器。这些线程由 Linux 调度器像任何其他线程一样管理,并根据优先级和优先级队列分派到任何可用的 CPU 核心。因此,每次主机调度器在不同的物理 CPU 上重新调度虚拟 CPU 线程时,本地 CPU 缓存优势(L1/L2/L3)都会丢失。这会显着损害 guest 系统的性能。CPU 绑定的目的是通过限制虚拟 CPU 允许在哪些物理 CPU 上运行来解决这个问题。理想的设置是一对一映射,使虚拟 CPU 核心与物理 CPU 核心匹配,同时考虑超线程/SMT。
此外,在一些现代 CPU 中,核心组通常共享一个公共 L3 缓存。在这种情况下,应注意精确绑定共享特定 L3 的物理核心。否则可能会导致缓存驱逐,从而导致微卡顿。
CPU 拓扑
大多数现代 CPU 都支持硬件多任务处理,也称为 Intel CPU 上的超线程或 AMD CPU 上的 SMT。超线程/SMT 只是在任何给定时间在一个 CPU 核心上高效运行两个线程的一种方式。您需要考虑到,您选择的 CPU 绑定将在很大程度上取决于虚拟机运行时您在主机上执行的操作。
要查找 CPU 的拓扑,请运行 lscpu -e
在 6c/12t Ryzen 5 1600 上运行 lscpu -e
CPU NODE SOCKET CORE L1d:L1i:L2:L3 ONLINE MAXMHZ MINMHZ 0 0 0 0 0:0:0:0 yes 3800.0000 1550.0000 1 0 0 0 0:0:0:0 yes 3800.0000 1550.0000 2 0 0 1 1:1:1:0 yes 3800.0000 1550.0000 3 0 0 1 1:1:1:0 yes 3800.0000 1550.0000 4 0 0 2 2:2:2:0 yes 3800.0000 1550.0000 5 0 0 2 2:2:2:0 yes 3800.0000 1550.0000 6 0 0 3 3:3:3:1 yes 3800.0000 1550.0000 7 0 0 3 3:3:3:1 yes 3800.0000 1550.0000 8 0 0 4 4:4:4:1 yes 3800.0000 1550.0000 9 0 0 4 4:4:4:1 yes 3800.0000 1550.0000 10 0 0 5 5:5:5:1 yes 3800.0000 1550.0000 11 0 0 5 5:5:5:1 yes 3800.0000 1550.0000
考虑到 L3 映射,建议绑定和隔离 CPU 6–11。绑定和隔离少于这些(例如 8–11)将导致主机系统使用核心 6 和 7 中的 L3 缓存,这最终会导致缓存驱逐,从而导致性能不佳。
在 6c/12t Intel 8700k 上运行 lscpu -e
CPU NODE SOCKET CORE L1d:L1i:L2:L3 ONLINE MAXMHZ MINMHZ 0 0 0 0 0:0:0:0 yes 4600.0000 800.0000 1 0 0 1 1:1:1:0 yes 4600.0000 800.0000 2 0 0 2 2:2:2:0 yes 4600.0000 800.0000 3 0 0 3 3:3:3:0 yes 4600.0000 800.0000 4 0 0 4 4:4:4:0 yes 4600.0000 800.0000 5 0 0 5 5:5:5:0 yes 4600.0000 800.0000 6 0 0 0 0:0:0:0 yes 4600.0000 800.0000 7 0 0 1 1:1:1:0 yes 4600.0000 800.0000 8 0 0 2 2:2:2:0 yes 4600.0000 800.0000 9 0 0 3 3:3:3:0 yes 4600.0000 800.0000 10 0 0 4 4:4:4:0 yes 4600.0000 800.0000 11 0 0 5 5:5:5:0 yes 4600.0000 800.0000
由于在此示例中所有核心都连接到相同的 L3,因此绑定和隔离多少 CPU 并不重要,只要您以正确的线程对进行即可。例如,(0, 6)、(1, 7) 等。
正如我们在上面看到的,对于 AMD,Core 0 与 CPU 0 & 1 是连续的,而 Intel 将 Core 0 放在 CPU 0 & 6 上。
lstopo
以生成 CPU/线程分组的帮助性图像。如果您不需要 guest 系统的所有核心,那么最好至少留出一个核心供主机使用。为主机或 guest 系统选择使用哪些核心应基于 CPU 的特定硬件特性,但是 Core 0 在大多数情况下都是主机的不错选择。如果为主机保留了任何核心,则建议将模拟器和 iothread(如果使用)绑定到主机核心,而不是 VCPU。这可能会提高 guest 系统的性能并减少延迟,因为这些线程不会污染缓存或与 guest VCPU 线程争夺调度。如果所有核心都传递给 guest 系统,则无需或没有好处来绑定模拟器或 iothread。
XML 示例
4c/1t CPU 无超线程示例
$ virsh edit vmname
... <vcpu placement='static'>4</vcpu> <cputune> <vcpupin vcpu='0' cpuset='0'/> <vcpupin vcpu='1' cpuset='1'/> <vcpupin vcpu='2' cpuset='2'/> <vcpupin vcpu='3' cpuset='3'/> </cputune> ...
4c/2t Intel/AMD CPU 示例(ComboPI AGESA bios 更新后)
$ virsh edit vmname
... <vcpu placement='static'>8</vcpu> <iothreads>1</iothreads> <cputune> <vcpupin vcpu='0' cpuset='2'/> <vcpupin vcpu='1' cpuset='8'/> <vcpupin vcpu='2' cpuset='3'/> <vcpupin vcpu='3' cpuset='9'/> <vcpupin vcpu='4' cpuset='4'/> <vcpupin vcpu='5' cpuset='10'/> <vcpupin vcpu='6' cpuset='5'/> <vcpupin vcpu='7' cpuset='11'/> <emulatorpin cpuset='0,6'/> <iothreadpin iothread='1' cpuset='0,6'/> </cputune> ... <cpu mode='host-passthrough'> <topology sockets='1' cores='4' threads='2'/> </cpu> ...
4c/2t AMD CPU 示例(ComboPi AGESA bios 更新前)
$ virsh edit vmname
... <vcpu placement='static'>8</vcpu> <iothreads>1</iothreads> <cputune> <vcpupin vcpu='0' cpuset='2'/> <vcpupin vcpu='1' cpuset='3'/> <vcpupin vcpu='2' cpuset='4'/> <vcpupin vcpu='3' cpuset='5'/> <vcpupin vcpu='4' cpuset='6'/> <vcpupin vcpu='5' cpuset='7'/> <vcpupin vcpu='6' cpuset='8'/> <vcpupin vcpu='7' cpuset='9'/> <emulatorpin cpuset='0-1'/> <iothreadpin iothread='1' cpuset='0-1'/> </cputune> ... <cpu mode='host-passthrough'> <topology sockets='1' cores='4' threads='2'/> </cpu> ...
如果您不打算在主机上进行任何计算密集型工作(甚至根本不做任何事情),同时在虚拟机上进行操作,您可能希望将虚拟机线程绑定到所有核心,以便虚拟机可以充分利用主机可用的空闲 CPU 时间。请注意,绑定 CPU 的所有物理和逻辑核心可能会导致 guest 虚拟机中出现延迟。
巨页内存
当处理需要大量内存的应用程序时,内存延迟可能会成为一个问题,因为使用的内存页越多,应用程序就越有可能尝试跨多个内存“页”访问信息,这是内存分配的基本单位。解析内存页的实际地址需要多个步骤,因此 CPU 通常会缓存最近使用的内存页上的信息,以加快后续对同一页的使用速度。使用大量内存的应用程序会遇到一个问题,例如,一个虚拟机使用 4 GiB 的内存,分为 4 KiB 页(这是普通页的默认大小),总共 104 万页,这意味着这种缓存未命中可能会变得非常频繁,并大大增加内存延迟。巨页的存在是为了通过为这些应用程序提供更大的单个页面来缓解这个问题,从而增加多个操作连续定位到同一页面的几率。
透明巨页
QEMU 将自动使用 2MiB 大小的透明巨页,而无需在 QEMU 或 Libvirt 中进行任何显式配置,但这有一些重要的注意事项。当使用 VFIO 时,页面在启动时被锁定,透明巨页在虚拟机首次启动时预先分配。如果内核内存高度碎片化,或者虚拟机使用了大部分剩余可用内存,则内核可能没有足够的 2MiB 页面来完全满足分配。在这种情况下,它会静默失败,方法是混合使用 2MiB 和 4KiB 页面。由于页面在 VFIO 模式下被锁定,内核也无法在虚拟机启动后将这些 4KiB 页面转换为巨页。THP 可用的 2MiB 巨页数量与以下部分中描述的 #动态巨页 机制相同。
要检查 THP 全局使用的内存量
$ grep AnonHugePages /proc/meminfo
AnonHugePages: 8091648 kB
要检查特定的 QEMU 实例。必须在 grep 命令中替换 QEMU 的 PID
$ grep -P 'AnonHugePages:\s+(?!0)\d+' /proc/[PID]/smaps
AnonHugePages: 8087552 kB
在此示例中,虚拟机分配了 8388608KiB 的内存,但只有 8087552KiB 可通过 THP 获得。剩余的 301056KiB 以 4KiB 页面的形式分配。除了手动检查外,没有迹象表明何时发生部分分配。因此,THP 的有效性在很大程度上取决于虚拟机启动时主机系统的内存碎片情况。如果这种权衡是不可接受的,或者需要严格的保证,则建议使用 #静态巨页。
Arch 内核已编译并默认启用 THP,/sys/kernel/mm/transparent_hugepage/enabled
设置为 madvise
模式。
静态巨页
虽然透明巨页在绝大多数情况下都应该有效,但也可以在启动期间静态分配它们。这应该只是为了在支持 1 GiB 巨页的机器上使用 1 GiB 巨页,因为透明巨页通常只达到 2 MiB。
要在启动时分配巨页,必须简单地在内核命令行中使用 hugepages=x
指定所需的数量。例如,使用 hugepages=1024
保留 1024 页,默认大小为每巨页 2048 KiB,将创建 2 GiB 的内存供虚拟机使用。
如果 CPU 支持,可以手动设置页面大小。可以通过 grep pdpe1gb /proc/cpuinfo
验证 1 GiB 巨页支持。通过内核参数设置 1 GiB 巨页大小:default_hugepagesz=1G hugepagesz=1G hugepages=X
。
此外,由于静态巨页只能由专门请求它的应用程序使用,因此您必须在 libvirt 域配置中添加此部分,以允许 kvm 从中受益:
$ virsh edit vmname
... <memoryBacking> <hugepages/> </memoryBacking> ...
动态巨页
可以通过 vm.nr_overcommit_hugepages
sysctl 参数手动分配巨页。
/etc/sysctl.d/10-kvm.conf
vm.nr_hugepages = 0 vm.nr_overcommit_hugepages = num
其中 num
- 是巨页的数量,默认大小为 2 MiB。页面将自动分配,并在虚拟机停止后释放。
更手动的方式
# echo num > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages # echo num > /sys/kernel/mm/hugepages/hugepages-1048576kB/nr_hugepages
分别用于 2 MiB 和 1 GiB 页面大小。它们应该以相同的方式手动释放。
强烈建议在启动虚拟机之前删除缓存、压缩内存并等待几秒钟,因为可能没有足够的连续空闲内存用于所需的巨页块。尤其是在主机系统运行一段时间后。
# echo 3 > /proc/sys/vm/drop_caches # echo 1 > /proc/sys/vm/compact_memory
理论上,1 GiB 页面与 2 MiB 页面一样有效。但实际上 - 没有找到保证获得连续 1 GiB 内存块的方法。每个后续的 1 GiB 块请求都会导致动态分配的计数越来越少。
CPU 频率调节器
根据您的 CPU 频率调节器 的配置方式,虚拟机线程可能无法达到频率提升的 CPU 负载阈值。实际上,KVM 本身无法更改 CPU 频率,如果它不随着 vCPU 使用率的增加而扩展,这可能会成为一个问题,因为它会导致性能不佳。查看它是否正常运行的一种简单方法是,在 guest 系统上运行 CPU 密集型任务时,检查 watch lscpu
报告的频率是否升高。如果您确实遇到卡顿并且频率没有升高到其报告的最大值,则可能是由于 CPU 缩放由主机操作系统控制。在这种情况下,尝试将所有核心设置为最大频率,看看是否可以提高性能。请注意,如果您使用的是具有默认 pstate 驱动程序的现代 intel 芯片,则 cpupower 命令将 无效,因此请监控 /proc/cpuinfo
以确保您的 CPU 实际上处于最大频率。
隔离绑定的 CPU
CPU 绑定本身并不能阻止其他主机进程在绑定的 CPU 上运行。正确隔离绑定的 CPU 可以减少 guest 虚拟机中的延迟。
使用 isolcpus 内核参数
在此示例中,我们假设您正在使用 CPU 4-7。使用 内核参数 isolcpus nohz_full
将 CPU 完全与内核隔离。例如
isolcpus=4-7 nohz_full=4-7
然后,使用 taskset 和 chrt 运行 qemu-system-x86_64
# chrt -r 1 taskset -c 4-7 qemu-system-x86_64 ...
chrt
命令将确保任务调度器将循环分配工作(否则所有工作都将停留在第一个 CPU 上)。对于 taskset
,CPU 编号可以用逗号和/或短划线分隔,例如“0,1,2,3”或“0-4”或“1,7-8,10”等。
有关更多信息,请参阅 Reddit 线程的 Removeddit 镜像的 Internet Archive 副本。
动态隔离 CPU
isolcpus 内核参数将永久保留 CPU 核心,即使 guest 系统未运行时也是如此。一种更灵活的替代方案是在启动 guest 系统时动态隔离 CPU。这可以通过以下替代方案实现:
- cpuset-gitAUR (vfio-users 帖子, 博客文章, 示例脚本)
- vfio-isolateAUR
- systemd
systemd 示例
在此示例中,我们假设主机有 12 个 CPU,其中 CPU 2-5 和 8-11 绑定 到 guest 系统。然后运行以下命令将主机隔离到 CPU 0、1、6 和 7:
# systemctl set-property --runtime -- user.slice AllowedCPUs=0,1,6,7 # systemctl set-property --runtime -- system.slice AllowedCPUs=0,1,6,7 # systemctl set-property --runtime -- init.scope AllowedCPUs=0,1,6,7
关闭 guest 系统后,运行以下命令将所有 12 个 CPU 重新分配回主机:
# systemctl set-property --runtime -- user.slice AllowedCPUs=0-11 # systemctl set-property --runtime -- system.slice AllowedCPUs=0-11 # systemctl set-property --runtime -- init.scope AllowedCPUs=0-11
您可以使用 libvirt hook 在 guest 系统启动/关闭时自动运行上述操作,如下所示:
使用以下内容创建或编辑 /etc/libvirt/hooks/qemu
。
/etc/libvirt/hooks/qemu
#!/bin/sh command=$2 if [ "$command" = "started" ]; then systemctl set-property --runtime -- system.slice AllowedCPUs=0,1,6,7 systemctl set-property --runtime -- user.slice AllowedCPUs=0,1,6,7 systemctl set-property --runtime -- init.scope AllowedCPUs=0,1,6,7 elif [ "$command" = "release" ]; then systemctl set-property --runtime -- system.slice AllowedCPUs=0-11 systemctl set-property --runtime -- user.slice AllowedCPUs=0-11 systemctl set-property --runtime -- init.scope AllowedCPUs=0-11 fi
之后使其成为 可执行文件。
重启 libvirtd.service
,然后启动您的虚拟机。如果您现在在主机上创建一些重度多线程负载,您应该会看到它使您选择的 CPU 免受负载影响,而虚拟机仍然可以利用它。您还应该看到,一旦您终止虚拟机,这些 CPU 就会自动被主机完全使用。
以下 reddit 线程中包含更多示例:[1] [2] [3]
请注意,这需要 systemd 244 或更高版本,以及 cgroups v2,现在默认启用。
提高 AMD CPU 的性能
从 QEMU 3.1 开始,TOPOEXT cpuid 标志默认禁用。为了在 AMD CPU 上使用超线程 (SMT),您需要手动启用它:
<cpu mode='host-passthrough' check='none'> <topology sockets='1' cores='4' threads='2'/> <feature policy='require' name='topoext'/> </cpu>
commit: https://gitlab.com/qemu-project/qemu/-/commit/7210a02c58572b2686a3a8d610c6628f87864aed
Virtio 磁盘
默认磁盘类型是开箱即用的 SATA 或 IDE 模拟。这些控制器提供最大的兼容性,但不适合高效虚拟化。存在两种加速模型:用于 SCSI 模拟和直通的 virtio-scsi
,或用于更基本的块设备模拟的 virtio-blk
。
驱动程序
- Linux guest 系统应在任何现代内核上开箱即用地支持这些驱动程序。
- macOS 从 Mojave 开始通过
AppleVirtIO.kext
支持virtio-blk
。 - Windows 需要 Windows virtio 驱动程序。
virtio-scsi
使用vioscsi
驱动程序。virtio-blk
使用viostor
驱动程序。 - 可以通过在安装程序磁盘选择菜单上选择“加载驱动程序”将 Windows 直接安装到这些磁盘上。在安装过程中,Windows iso 和 virtio 驱动程序 iso 都应作为常规 SATA/IDE cdrom 连接。
- 要在现有 Windows 安装上将启动磁盘切换到 virtio:
virtio-blk
:添加一个总线为virtio
的临时磁盘,启动 Windows 并加载磁盘的驱动程序,然后关闭并切换启动磁盘的总线为virtio
。virtio-scsi
:添加一个模型为virtio
的 scsi 控制器,启动 Windows 并加载控制器的驱动程序,然后关闭并切换启动磁盘的总线为scsi
(不是 virtio)。
注意事项
virtio-scsi
TRIM 支持已成熟,所有版本都应支持它。传统上,virtio-scsi
一直是出于这个原因的首选方法。virtio-blk
TRIM 支持是新的,这需要 qemu 4.0+、guest linux 内核 5.0+、guest windows 驱动程序 0.1.173+。- 精简配置通过在稀疏映像文件上启用 TRIM 来工作:
discard='unmap'
。未使用的块将被释放,磁盘使用量将下降(适用于 raw 和 qcow2)。可以使用du /path/to/disk.img
检查稀疏映像文件的实际磁盘大小。 - 精简配置也可以与块存储一起使用,例如 zfs zvols 或 thin lvm。
- Virt 队列计数将影响 guest 内核中用于 IO 处理的线程数,建议使用
queues='4'
或更多。 - 本机模式 (
io='native'
) 使用基于 linux AIO 的单线程模型,CPU 效率更高,但峰值性能可能较低,并且不允许使用主机端缓存。 - 线程模式 (
io='threads'
) 将在磁盘使用时按需生成数十个线程。效率较低,但如果主机有足够的可用核心来运行它们,并且允许使用主机端缓存,则性能可能会更好。 - 现代版本的 libvirt 会将使用线程模式时创建的动态工作线程与 iothread=1 cgroup 分组在一起以用于绑定目的。非常旧版本的 libvirt 将这些线程留在模拟器 cgroup 中。
IO 线程
IO 线程是用于处理磁盘事件的专用线程,而不是使用主 qemu 模拟器循环。这不应与使用 io='threads'
按需生成的工作线程混淆。
- 每个磁盘控制器只能使用一个 iothread。必须使用
<driver>
标记中的iothread='X'
将线程分配给特定的控制器。此外,额外的和未分配的 iothread 将不会被使用,并且不会执行任何操作。 - 在
virtio-scsi
的情况下,一个控制器用于多个 scsi 磁盘。iothread 在控制器上分配:<controller><driver iothread='X'>
。 - 在
virtio-blk
的情况下,每个磁盘都有自己的控制器。iothread 在磁盘本身的 driver 标记下分配:<disk><driver iothread='X'>
。 - 由于模拟磁盘会产生大量的 CPU 开销,这可能会导致在高磁盘负载(尤其是高随机 IOPS)下出现 vcpu 卡顿。在这种情况下,将 IO 绑定到与 vcpu 不同的核心会有所帮助,使用
<iothreadpin>
。
libvirt 示例
virtio-scsi + iothread + 工作线程 + 主机端写回缓存 + 全磁盘块设备后端
<domain> <devices> <disk type='block' device='disk'> <driver name='qemu' type='raw' cache='writeback' io='threads' discard='unmap'/> <source dev='/dev/disk/by-id/ata-Samsung_SSD_840_EVO_1TB_S1D9NSAF206396F'/> <target dev='sda' bus='scsi'/> </disk> <controller type='scsi' index='0' model='virtio-scsi'> <driver iothread='1' queues='8'/> </controller>
virtio-blk + iothread + 本机 aio + 无主机缓存 + raw 稀疏映像后端
<domain> <devices> <disk type='file' device='disk'> <driver name='qemu' type='raw' cache='none' io='native' discard='unmap' iothread='1' queues='8'/> <source file='/var/lib/libvirt/images/pool/win10.img'/> <target dev='vda' bus='virtio'/> </disk>
创建 iothread
<domain> <iothreads>1</iothreads>
绑定 iothread
<domain> <cputune> <iothreadpin iothread='1' cpuset='0-1,6-7'/>
virt-manager 示例
这将创建一个 virtio-blk
设备。
- 打开虚拟机首选项。
- 转到
添加硬件 > 存储
。 - 创建或选择一个存储文件。
- 选择
设备类型:磁盘设备
和总线类型:VirtIO
。 - 单击完成。
Virtio 网络
默认 NIC 型号 rtl8139 或 e1000 可能是千兆位以上速度的瓶颈,并且与 virtio-net
相比,具有大量的 CPU 开销。
- 使用 libvirt 选择
virtio
作为 NIC 的型号,或在裸机 qemu 中使用virtio-net-pci
设备。 - Windows 需要 Windows virtio 驱动程序 中的
NetKVM
驱动程序。 - Virtio 默认使用 vhost-net 进行内核内数据包处理,而无需退出到用户空间。
- 可以启用多队列以进一步加速多连接,但通常不会提高单流速度。对于 libvirt,在接口标记下添加
<driver queues='8'/>
。 - 也可以通过设置模块参数
vhost_net.experimental_zcopytx=1
在 macvtap 上启用零拷贝传输,但这实际上可能会导致更差的性能,请参阅 commit。
带有桥接的 Libvirt 示例
<interface type='bridge'> <mac address="52:54:00:6d:6e:2e"/> <source bridge='br0'/> <model type='virtio'/> <driver queues='8'/> </interface>
带有桥接的 MACVTAP 示例
<interface type="direct"> <source dev="eno1" mode="vepa"/> <target dev="macvtap0"/> <model type="virtio"/> <alias name="net0"/> </interface>
模式的可能选项是 'vepa'、'bridge'、'private' 和 'passthrough'。有关差异描述的指南可从 redhat [4] 获得。
将源 /dev
设备替换为您自己的设备地址。您可以使用以下命令获取本地地址:
$ ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: eno1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000 link/ether 30:9c:23:ac:51:d0 brd ff:ff:ff:ff:ff:ff altname enp0s31f6
进一步调优
更多专业的虚拟机调优技巧可在 Red Hat 的虚拟化调优和优化指南 中找到。
特殊步骤
某些设置需要特定的配置调整才能正常工作。如果您在使主机或虚拟机正常工作时遇到问题,请查看您的系统是否符合以下情况之一,并尝试相应地调整您的配置。
使用相同的 guest 和主机 GPU
由于 vfio-pci 如何使用您的供应商和设备 ID 对来识别它们需要在启动时绑定到的设备,如果您有两个 GPU 共享这样的 ID 对,您将无法仅使直通驱动程序与其中一个绑定。这种设置使得必须使用脚本,以便您正在使用的任何驱动程序都使用 driver_override
机制按 pci 总线地址分配。
脚本变体
直通除启动 GPU 之外的所有 GPU
在这里,我们将创建一个脚本,将 vfio-pci 绑定到除启动 GPU 之外的所有 GPU。创建脚本 /usr/local/bin/vfio-pci-override.sh
#!/bin/sh for i in /sys/bus/pci/devices/*/boot_vga; do if [ $(cat "$i") -eq 0 ]; then GPU="${i%/boot_vga}" AUDIO="$(echo "$GPU" | sed -e "s/0$/1/")" USB="$(echo "$GPU" | sed -e "s/0$/2/")" echo "vfio-pci" > "$GPU/driver_override" if [ -d "$AUDIO" ]; then echo "vfio-pci" > "$AUDIO/driver_override" fi if [ -d "$USB" ]; then echo "vfio-pci" > "$USB/driver_override" fi fi done modprobe -i vfio-pci
直通选定的 GPU
在这种情况下,我们手动指定要绑定的 GPU。
#!/bin/sh DEVS="0000:03:00.0 0000:03:00.1" if [ ! -z "$(ls -A /sys/class/iommu)" ]; then for DEV in $DEVS; do echo "vfio-pci" > /sys/bus/pci/devices/$DEV/driver_override done fi modprobe -i vfio-pci
基于 GPU 的直通 IOMMU 组
简化从选定 GPU 直通其他必要设备的过程。例如显卡的板载音频、USB 和 RGB 控制器。
#!/bin/sh DEVS="0000:03:00.0" if [ ! -z "$(ls -A /sys/class/iommu)" ]; then for DEV in $DEVS; do for IOMMUDEV in $(ls /sys/bus/pci/devices/$DEV/iommu_group/devices) ; do echo "vfio-pci" > /sys/bus/pci/devices/$IOMMUDEV/driver_override done done fi modprobe -i vfio-pci
脚本安装
编辑 /etc/mkinitcpio.conf
编辑 /etc/modprobe.d/vfio.conf
- 添加以下行:
install vfio-pci /usr/local/bin/vfio-pci-override.sh
- 重新生成 initramfs 并重启。
将启动 GPU 传递给 guest 系统
标记为 boot_vga
的 GPU 在进行 PCI 直通时是一种特殊情况,因为 BIOS 需要使用它来显示启动消息或 BIOS 配置菜单等内容。为此,它会 复制 VGA 启动 ROM,然后可以自由修改。这个修改后的副本是系统看到的版本,直通驱动程序可能会拒绝它,因为它无效。因此,通常建议在 BIOS 配置中更改启动 GPU,以便使用主机 GPU,或者,如果不可能,则在机器本身中交换主机和 guest 卡。
使用 Looking Glass 将 guest 屏幕流式传输到主机
可以使用 Looking Glass 使虚拟机共享显示器,以及可选的键盘和鼠标。
向虚拟机添加 IVSHMEM 设备
Looking Glass 的工作原理是在主机和 guest 系统之间创建共享内存缓冲区。这比通过 localhost 流式传输帧快得多,但需要额外的设置。
在虚拟机关闭的情况下,打开机器配置。
$ virsh edit vmname
... <devices> ... <shmem name='looking-glass'> <model type='ivshmem-plain'/> <size unit='M'>32</size> </shmem> </devices> ...
您应该将 32 替换为您自己根据要直通的分辨率计算出的值。它可以这样计算:
width x height x 4 x 2 = total bytes total bytes / 1024 / 1024 = total mebibytes + 10
例如,对于 1920x1080 的情况:
1920 x 1080 x 4 x 2 = 16,588,800 bytes 16,588,800 / 1024 / 1024 = 15.82 MiB + 10 = 25.82
结果必须向上取整到最接近的 2 的幂,由于 25.82 大于 16,我们应该选择 32。
接下来,创建一个配置文件以在启动时创建共享内存文件:
/etc/tmpfiles.d/10-looking-glass.conf
f /dev/shm/looking-glass 0660 user kvm -
将 user 替换为您的用户名。
要求 systemd-tmpfiles 立即创建共享内存文件,而无需等待下次启动:
# systemd-tmpfiles --create /etc/tmpfiles.d/10-looking-glass.conf
将 IVSHMEM Host 安装到 Windows guest 系统
当前,Windows 不会通知用户有关新 IVSHMEM 设备的信息,它会静默安装一个虚拟驱动程序。要实际启用该设备,您必须转到设备管理器,并更新“系统设备”节点下 “PCI 标准 RAM 控制器” 设备的驱动程序。从 Red Hat 下载已签名的驱动程序。
安装驱动程序后,您必须下载与您将从 AUR 安装的客户端匹配的 looking-glass-host 软件包,并将其安装在您的 guest 系统上。为了运行它,您还需要从 Microsoft 安装 Microsoft Visual C++ Redistributable。最新版本将自动安装一个服务,该服务在启动时启动守护程序。主机守护程序的日志位于 guest 系统上的 %ProgramData%\Looking Glass (host)\looking-glass-host.txt
。
设置 null 视频设备
(检索自:https://looking-glass.io/docs/stable/install/#spice-server)
如果您想使用 Spice 为您提供键盘和鼠标输入以及剪贴板同步支持,请确保您有一个 <graphics type='spice'>
设备,然后:
- 找到您的
<video>
设备,并设置<model type='none'/>
。 - 如果您找不到它,请确保您有一个
<graphics>
设备,保存并再次编辑。
获取客户端
可以使用 looking-glassAUR 或 looking-glass-gitAUR 软件包从 AUR 安装 Looking glass 客户端。
您可以在虚拟机设置并运行后启动它:
$ looking-glass-client
如果您不想使用 Spice 来控制 guest 鼠标和键盘,您可以禁用 Spice 服务器。
$ looking-glass-client -s
此外,您可能希望以全屏模式启动 Looking Glass Client,否则图像可能会缩小,从而导致图像保真度不佳。
$ looking-glass-client -F
要禁止主机屏幕保护程序,请使用 -S
标志启动 Looking Glass。
$ looking-glass-client -S
这在某些 DE 上不起作用,包括 KDE。KDE 的问题可能与 此行为 有关。要在使用 Looking Glass 和 KDE 时临时禁用主机的屏幕保护程序,您可以将 Looking Glass 包装在以下脚本中:
#!/bin/sh kwriteconfig6 --file kscreenlockerrc --group Daemon --key Autolock false qdbus org.freedesktop.ScreenSaver /ScreenSaver configure looking-glass-client kwriteconfig6 --file kscreenlockerrc --group Daemon --key Autolock true qdbus org.freedesktop.ScreenSaver /ScreenSaver configure
使用 --help
选项启动以获取更多信息。
附加信息
有关更多详细信息,请参阅 上游文档。
在主机和虚拟机之间切换外围设备
Looking Glass 包含一个 Spice 客户端,以便控制 Windows 虚拟机上的鼠标移动。然而,对于某些应用程序(如游戏),这可能会有太多的延迟。另一种方法是直通特定的 USB 设备,以实现最小的延迟。这允许在主机和虚拟机之间切换设备。
首先,为您希望直通的设备创建一个 .xml 文件,libvirt 将使用该文件来识别设备。
~/.VFIOinput/input_1.xml
<hostdev mode='subsystem' type='usb'> <source> <vendor id='0x[Before Colon]'/> <product id='0x[After Colon]'/> </source> </hostdev>
将 [Before/After Colon] 替换为 'lsusb' 命令的内容,该内容特定于您要直通的设备。
例如,我的鼠标是 Bus 005 Device 002: ID 1532:0037 Razer USA, Ltd
,因此我将 vendor id
替换为 1532,并将 product id
替换为 0037。
对于您要直通的任何其他 USB 设备,重复此过程。如果您的鼠标/键盘在 lsusb
中有多个条目(例如,如果是无线设备),则为每个条目创建额外的 xml 文件。
接下来,需要一个 bash 脚本文件来告诉 libvirt 将哪些 USB 设备附加/分离到虚拟机。
~/.VFIOinput/input_attach.sh
#!/bin/sh virsh attach-device [VirtualMachine-Name] [USBdevice]
将 [VirtualMachine-Name] 替换为您的虚拟机的名称,可以在 virt-manager 下看到。此外,将 [USBdevice] 替换为您希望直通的设备的 .xml 文件的完整路径。为多个设备添加额外的行。例如,这是我的脚本
~/.VFIOinput/input_attach.sh
#!/bin/sh virsh attach-device win10 /home/$USER/.VFIOinput/input_mouse.xml virsh attach-device win10 /home/$USER/.VFIOinput/input_keyboard.xml
接下来,复制该脚本文件,并将 attach-device
替换为 detach-device
。确保两个脚本都是可执行的。
现在可以执行这两个脚本文件,以将您的 USB 设备从主机附加或分离到客户虚拟机。重要的是要注意,它们可能需要以 root 身份执行。要从 Windows 虚拟机运行脚本,一种可能性是使用 PuTTY 通过 SSH 连接到主机,并执行脚本。在 Windows 上,PuTTY 附带 plink.exe,它可以先通过 SSH 执行单个命令,然后在退出登录之前,而不是打开 SSH 终端,这一切都在后台进行。
detach_devices.bat
"C:\Program Files\PuTTY\plink.exe" root@$HOST_IP -pw $ROOTPASSWORD /home/$USER/.VFIOinput/input_detach.sh
将 $HOST_IP
替换为主机 IP 地址,并将 $ROOTPASSWORD 替换为 root 密码。
您可能还希望使用快捷键执行脚本文件。在 Windows 上,一个选项是 Autohotkey,在主机上是 Xbindkeys。由于需要以 root 身份运行脚本,您可能还需要使用 Polkit 或 Sudo,它们都可以用于验证特定的可执行文件能够以 root 身份运行,而无需密码。
绕过 IOMMU 组(ACS 覆盖补丁)
如果您发现您的 PCI 设备与您不希望直通的其他设备分组在一起,您或许可以使用 Alex Williamson 的 ACS 覆盖补丁来分离它们。请确保您理解这样做 潜在的风险。
您将需要一个应用了补丁的内核。获取此内核最简单的方法是通过 linux-zen 或 linux-vfioAUR 软件包。
此外,ACS 覆盖补丁需要通过内核命令行选项启用。补丁文件添加了以下文档
pcie_acs_override = [PCIE] Override missing PCIe ACS support for: downstream All downstream ports - full ACS capabilties multifunction All multifunction devices - multifunction ACS subset id:nnnn:nnnn Specfic device - full ACS capabilities Specified as vid:did (vendor/device ID) in hex
选项 pcie_acs_override=downstream,multifunction
应该尽可能多地分解设备。
安装和配置后,重新配置您的 内核参数,以加载启用 pcie_acs_override=
选项的新内核。
不使用 libvirt 的纯 QEMU
除了使用 libvirt 设置虚拟机之外,还可以使用带有自定义参数的纯 QEMU 命令来运行旨在与 PCI 直通一起使用的虚拟机。这对于某些用例(如脚本化设置)是可取的,在这些用例中,需要与其他脚本一起使用的灵活性。
要在 #设置 IOMMU 和 #隔离 GPU 后实现此目的,请按照 QEMU 文章设置虚拟化环境,启用 KVM,并使用标志 -device vfio-pci,host=07:00.0
,将标识符 (07:00.0) 替换为您之前用于 GPU 隔离的实际设备 ID。
为了利用 OVMF 固件,请确保安装了 edk2-ovmf 软件包,将 UEFI 变量从 /usr/share/edk2/x64/OVMF_VARS.4m.fd
复制到临时位置,如 /tmp/my_OVMF_VARS.4m.fd
,最后通过将以下参数附加到 QEMU 命令来指定 OVMF 路径(顺序很重要)
-drive if=pflash,format=raw,readonly=on,file=/usr/share/edk2/x64/OVMF_CODE.4m.fd
用于实际的 OVMF 固件二进制文件,请注意 readonly 选项-drive if=pflash,format=raw,file=/tmp/my_OVMF_VARS.4m.fd
用于变量
- 确保在
my_OVMF_VARS.4m.fd
之前将OVMF_CODE.4m.fd
作为命令行参数给出。否则,启动顺序将失败。 - 可以使用 QEMU 的默认 SeaBIOS 代替 OVMF,但不建议这样做,因为它可能会导致直通设置出现问题。
建议研究 QEMU 文章,了解如何通过使用 virtio 驱动程序 和其他进一步的配置来提高性能。
您可能还需要使用 -cpu host,kvm=off
参数将主机的 CPU 型号信息转发到虚拟机,并欺骗 Nvidia 以及可能其他制造商的设备驱动程序使用的虚拟化检测,这些驱动程序试图阻止在虚拟化系统内部充分利用硬件。
直通其他设备
USB 控制器
如果您的主板有多个 USB 控制器映射到多个组,则可以直通这些控制器,而不是 USB 设备。直通实际的控制器而不是单个 USB 设备具有以下优点:
- 如果设备在给定操作过程中断开连接或更改 ID(例如,手机正在进行更新),虚拟机将不会突然停止看到它。
- 由该控制器管理的任何 USB 端口都由虚拟机直接处理,并且可以拔出、重新插入和更改其设备,而无需通知 hypervisor。
- 如果您通常传递给虚拟机的 USB 设备之一在启动虚拟机时丢失,Libvirt 不会报错。
与 GPU 不同,大多数 USB 控制器的驱动程序不需要任何特定的配置即可在虚拟机上工作,并且控制权通常可以在主机和虚拟机系统之间来回传递,而不会产生副作用。
您可以使用以下命令查明哪些 USB 设备对应于哪个控制器,以及各种端口和设备如何分配给每个控制器
$ for usb_ctrl in /sys/bus/pci/devices/*/usb*; do pci_path=${usb_ctrl%/*}; iommu_group=$(readlink $pci_path/iommu_group); echo "Bus $(cat $usb_ctrl/busnum) --> ${pci_path##*/} (IOMMU group ${iommu_group##*/})"; lsusb -s ${usb_ctrl#*/usb}:; echo; done
Bus 1 --> 0000:00:1a.0 (IOMMU group 4) Bus 001 Device 004: ID 04f2:b217 Chicony Electronics Co., Ltd Lenovo Integrated Camera (0.3MP) Bus 001 Device 007: ID 0a5c:21e6 Broadcom Corp. BCM20702 Bluetooth 4.0 [ThinkPad] Bus 001 Device 008: ID 0781:5530 SanDisk Corp. Cruzer Bus 001 Device 002: ID 8087:0024 Intel Corp. Integrated Rate Matching Hub Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub Bus 2 --> 0000:00:1d.0 (IOMMU group 9) Bus 002 Device 006: ID 0451:e012 Texas Instruments, Inc. TI-Nspire Calculator Bus 002 Device 002: ID 8087:0024 Intel Corp. Integrated Rate Matching Hub Bus 002 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
这台笔记本电脑有 3 个 USB 端口,由 2 个 USB 控制器管理,每个控制器都有自己的 IOMMU 组。在此示例中,总线 001 管理单个 USB 端口(其中插入了 SanDisk USB 闪存盘,因此它出现在列表中),还有许多内部设备,例如内置网络摄像头和蓝牙卡。另一方面,总线 002 似乎除了插入其中的计算器之外,没有管理任何东西。第三个端口是空的,这就是为什么它没有出现在列表中,但实际上是由总线 002 管理的。
一旦您通过将各种设备插入端口中,确定了哪个控制器管理哪些端口,并决定了要直通哪个控制器,只需将其添加到虚拟机控制的 PCI 主机设备列表中即可。不需要其他配置。
通过 PipeWire 将音频从虚拟机直接传递到主机
这比将音频传递到 pipewire 的 pulseaudio 仿真层更可取。
$ virsh edit vmname
<devices> ... <audio id="1" type="pipewire" runtimeDir="/run/user/1000"> <input name="qemuinput"/> <output name="qemuoutput"/> </audio> </devices>
(请根据您的桌面用户 uid 将 runtimeDir 值从 1000 更改为相应的值)
要解决“Failed to initialize PW context”错误,您可以修改 qemu 配置以使用您的用户。
/etc/libvirt/qemu.conf
user = "example"
通过 PulseAudio 将音频从虚拟机传递到主机
可以使用 libvirt 将虚拟机的音频作为应用程序路由到主机。这样做的好处是可以将多个音频流路由到一个主机输出,并且可以与不支持直通的音频输出设备一起使用。这需要在主机系统上运行 PulseAudio。
首先,安装 qemu-audio-pa。
然后,删除 #user = ""
行的注释。然后在引号中添加您的用户名。这告诉 QEMU 要通过哪个用户的 pulseaudio 流进行路由。
/etc/libvirt/qemu.conf
user = "example"
模拟音频设置由两个组件组成:暴露给虚拟机的模拟声音设备,以及将声音设备连接到主机 PulseAudio 的音频后端。
在可用的模拟声音设备中,主要关注两种:ICH9 和 usb-audio。ICH9 同时具有输出和输入,但仅限于立体声。usb-audio 仅具有音频输出,但支持高达 6 声道的 5.1 配置。对于 ICH9,请删除 <devices>
部分中任何预先存在的音频后端,并添加
$ virsh edit vmname
<sound model='ich9'> <codec type='micro'/> <audio id='1'/> </sound> <audio id='1' type='pulseaudio' serverName='/run/user/1000/pulse/native'/>
请注意匹配的 id
元素。上面的示例假定为单用户系统,用户 ID 为 1000。使用 id
命令查找正确的 ID。如果您有多个用户访问 PulseAudio,也可以使用 /tmp
目录
$ virsh edit vmname
<audio id='1' type='pulseaudio' serverName='unix:/tmp/pulse-socket'/>
如果声音出现爆裂声或失真,请尝试调整一些延迟设置。以下示例使用 20000 微秒
$ virsh edit vmname
<audio id="1" type="pulseaudio" serverName="/run/user/1000/pulse/native"> <input latency="20000"/> <output latency="20000"/> </audio>
您还可以尝试禁用 QEMU 中包含的软件混音器。从理论上讲,这应该更有效率,并允许更低的延迟,因为混音将仅在您的主机上进行
$ virsh edit vmname
<audio id="1" type="pulseaudio" serverName="/run/user/1000/pulse/native"> <input mixingEngine="no"/> <output mixingEngine="no"/> </audio>
对于 usb-audio,相应的元素读取为
$ virsh edit vmname
<sound model='usb'> <audio id='1'/> </sound> <audio id='1' type='pulseaudio' serverName='/run/user/1000/pulse/native'/>
但是,如果需要 5.1 配置,则需要通过 QEMU 命令行参数配置声音设备
$ virsh edit vmname
</devices> <qemu:commandline> <qemu:arg value='-device'/> <qemu:arg value='usb-audio,id=sound0,audiodev=audio1,multi=on'/> </qemu:commandline> </domain>
audiodev
标签必须设置为匹配音频后端的 id
元素。id='1'
对应于 audio1
,依此类推。
- 您可以有多个音频后端,只需在 XML 中多次指定
<audio>
/-audiodev
并为它们分配不同的 id 即可。这对于拥有两个相同后端的用例很有用。使用 PulseAudio,每个后端都是一个单独的流,可以路由到主机上的不同输出设备(使用脉冲混音器,如 pavucontrol 或 pulsemixer)。 - Libvirt/QEMU 中需要 USB 3 仿真才能启用 usb-audio。
- 建议在 ICH9 音频设备上使用诸如 [6] 之类的工具启用 MSI 中断,以减轻虚拟机重启后出现的任何爆裂声、卡顿、加速或无音频的情况。
- 如果音频仍然存在爆裂声/卡顿/加速等问题,您可能需要调整诸如
buffer-length
和timer-period
之类的参数,有关这些参数和更多信息,请参阅 qemu(1) 手册。 - 某些音频芯片组(如 Realtek alc1220)也可能存在开箱即用的问题,因此在使用任何 QEMU 音频仿真时,请考虑这一点。
- 不正确的绑定或主机重度使用而未使用 isolcpus 也可能影响声音错误,尤其是在虚拟机中玩游戏时。
通过 JACK 和 PipeWire 将音频从虚拟机传递到主机
也可以通过 JACK 和 PipeWire 将虚拟机的音频传递到主机。
首先,确保您有一个使用 JACK 支持 的工作 PipeWire 设置。
接下来,您需要告诉 libvirt 以您的用户身份运行 QEMU
/etc/libvirt/qemu.conf
user = "example"
不要忘记 重启 libvirtd.service
。
作为最后的准备工作,必须扩展 XML 模式以允许传递环境变量。为此,请修改虚拟机域配置
$ virsh edit vmname
<domain type='kvm'>
为
$ virsh edit vmname
<domain type='kvm' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>
然后,您可以将实际的音频配置添加到您的虚拟机
$ virsh edit vmname
<devices> ... <audio id="1" type="jack"> <input clientName="vm-win10" connectPorts="your-input"/> <output clientName="vm-win10" connectPorts="your-output"/> </audio> </devices> <qemu:commandline> <qemu:env name="PIPEWIRE_RUNTIME_DIR" value="/run/user/1000"/> <qemu:env name="PIPEWIRE_LATENCY" value="512/48000"/> </qemu:commandline> </domain>
请注意匹配的 id
元素。上面的示例假定为单用户系统,用户 ID 为 1000。使用 id
命令查找正确的 ID。
您可能需要调整 PIPEWIRE_LATENCY
值,以获得所需的延迟,而不会出现爆裂声。
通过 Scream 将音频从虚拟机传递到主机
可以使用名为 Scream 的应用程序,通过桥接网络(例如 Libvirt 提供的网络)或通过向主机添加 IVSHMEM 设备来传递虚拟机的音频。本节仅介绍在主机上使用 PulseAudio 作为接收器。有关其他方法的更多详细信息和说明,请参阅项目页面。
将 Scream 与桥接网络一起使用
- 这是使用此方法的首选方式,尽管结果可能因用户而异
- 建议在使用 Scream 时使用 #Virtio 网络 适配器,QEMU 提供的其他虚拟适配器(如 e1000e)可能会导致性能不佳
要通过网络使用 scream,您需要通过 ip a
找到您的桥接名称,在大多数情况下,它将被称为 br0 或 virbr0。以下是启动 Scream 应用程序所需的命令示例
$ scream -o pulse -i virbr0 &
添加 IVSHMEM 设备以将 Scream 与 IVSHMEM 一起使用
关闭虚拟机后,编辑机器配置
$ virsh edit vmname
... <devices> ... <shmem name='scream-ivshmem'> <model type='ivshmem-plain'/> <size unit='M'>2</size> </shmem> </devices> ...
在上面的配置中,IVSHMEM 设备的大小为 2MB(建议的大小)。根据需要更改此值。
现在请参阅 #将 IVSHMEM 设备添加到虚拟机 以配置主机在启动时创建共享内存文件,将 looking-glass
替换为 scream-ivshmem
。
为 IVSHMEM 配置 Windows 虚拟机
必须在虚拟机上为 IVSHMEM 设备安装正确的驱动程序。请参阅 #将 IVSHMEM 主机安装到 Windows 虚拟机。忽略有关 looking-glass-host
的部分。
在虚拟机上安装 Scream 虚拟音频驱动程序。如果您的虚拟机启用了安全启动,您可能需要禁用它。
使用注册表编辑器,将 DWORD HKLM\SYSTEM\CurrentControlSet\Services\Scream\Options\UseIVSHMEM
设置为 IVSHMEM 设备的大小(以 MB 为单位)。请注意,scream 使用其大小来识别其 IVSHMEM 设备,因此请确保只有一个该大小的设备(建议的默认值为 2
,表示 2MB)。
在管理员 CMD shell 中使用以下命令创建键和 DWORD:REG ADD HKLM\SYSTEM\CurrentControlSet\Services\Scream\Options /v UseIVSHMEM /t REG_DWORD /d 2
(源自 Github 上的 scream)
配置主机
安装 screamAUR。
创建一个 systemd 用户服务 来控制接收器
~/.config/systemd/user/scream-ivshmem-pulse.service
[Unit] Description=Scream IVSHMEM pulse receiver After=pulseaudio.service Wants=pulseaudio.service [Service] Type=simple ExecStartPre=/usr/bin/truncate -s 0 /dev/shm/scream-ivshmem ExecStartPre=/usr/bin/dd if=/dev/zero of=/dev/shm/scream-ivshmem bs=1M count=2 ExecStart=/usr/bin/scream -m /dev/shm/scream-ivshmem [Install] WantedBy=default.target
使用 IVSHMEM 设备的大小(以 MiB 为单位)编辑 count=2
。
pulseaudio.service
替换为 pipewire-pulse.service
,并添加 After=wireplumber.service
。现在启动 scream-ivshmem-pulse.service
用户单元。
物理磁盘/分区
Raw 和 qcow2 尤其在重度 IO 方面可能具有明显的开销。可以直接使用整个磁盘或分区来绕过文件系统并提高 I/O 性能。如果您希望原生双启动客户操作系统,则需要传递整个磁盘,而无需任何分区。建议使用 /dev/disk/by-paths 来引用磁盘,因为 /dev/sdX 条目可能会在启动之间更改。要找出哪个磁盘/分区与您要传递的磁盘/分区关联
$ ls -l /dev/disk/by-id/*
/dev/disk/by-id/ata-ST1000LM002-9VQ14L_Z0501SZ9 -> ../../sdd
请参阅 #Virtio 磁盘,了解如何在 libvirt XML 中添加这些磁盘。您也可以使用 Virt-Manager 的 添加硬件 菜单添加磁盘,然后在 选择或创建自定义存储 框中键入您想要的磁盘,例如 /dev/disk/by-id/ata-ST1000LM002-9VQ14L_Z0501SZ9
注意事项
直通不支持重置的设备
当虚拟机关闭时,客户机操作系统会取消初始化所有使用的设备,以准备关闭。在这种状态下,这些设备不再起作用,必须先断电重启,然后才能恢复正常运行。Linux 可以自行处理此断电重启,但是当设备没有已知的重置方法时,它仍处于禁用状态并且变得不可用。由于 Libvirt 和 Qemu 都希望所有主机 PCI 设备在完全停止虚拟机之前都准备好重新连接到主机,因此当遇到无法重置的设备时,它们将挂起在“正在关闭”状态,在这种状态下,它们将无法重新启动,直到主机系统重新启动。因此,建议仅直通内核能够重置的 PCI 设备,这可以通过 PCI 设备 sysfs 节点中是否存在 reset
文件来证明,例如 /sys/bus/pci/devices/0000:00:1a.0/reset
。
以下 bash 命令显示了哪些设备可以重置,哪些设备不能重置。
for iommu_group in $(find /sys/kernel/iommu_groups/ -maxdepth 1 -mindepth 1 -type d);do echo "IOMMU group $(basename "$iommu_group")"; for device in $(\ls -1 "$iommu_group"/devices/); do if [[ -e "$iommu_group"/devices/"$device"/reset ]]; then echo -n "[RESET]"; fi; echo -n $'\t';lspci -nns "$device"; done; done
IOMMU group 0 00:00.0 Host bridge [0600]: Intel Corporation Xeon E3-1200 v2/Ivy Bridge DRAM Controller [8086:0158] (rev 09) IOMMU group 1 00:01.0 PCI bridge [0604]: Intel Corporation Xeon E3-1200 v2/3rd Gen Core processor PCI Express Root Port [8086:0151] (rev 09) 01:00.0 VGA compatible controller [0300]: NVIDIA Corporation GK208 [GeForce GT 720] [10de:1288] (rev a1) 01:00.1 Audio device [0403]: NVIDIA Corporation GK208 HDMI/DP Audio Controller [10de:0e0f] (rev a1) IOMMU group 2 00:14.0 USB controller [0c03]: Intel Corporation 7 Series/C210 Series Chipset Family USB xHCI Host Controller [8086:1e31] (rev 04) IOMMU group 4 [RESET] 00:1a.0 USB controller [0c03]: Intel Corporation 7 Series/C210 Series Chipset Family USB Enhanced Host Controller #2 [8086:1e2d] (rev 04) IOMMU group 5 [RESET] 00:1b.0 Audio device [0403]: Intel Corporation 7 Series/C210 Series Chipset Family High Definition Audio Controller [8086:1e20] (rev 04) IOMMU group 10 [RESET] 00:1d.0 USB controller [0c03]: Intel Corporation 7 Series/C210 Series Chipset Family USB Enhanced Host Controller #1 [8086:1e26] (rev 04) IOMMU group 13 06:00.0 VGA compatible controller [0300]: NVIDIA Corporation GM204 [GeForce GTX 970] [10de:13c2] (rev a1) 06:00.1 Audio device [0403]: NVIDIA Corporation GM204 High Definition Audio Controller [10de:0fbb] (rev a1)
这表明 00:14.0 中的 xHCI USB 控制器无法重置,因此将阻止虚拟机正常关闭,而 00:1b.0 中的集成声卡以及 00:1a.0 和 00:1d.0 中的其他两个控制器没有此问题,可以无问题地传递。
完整设置和示例
由于许多原因,用户可能希望查看 完整的直通设置示例。
这些示例是对现有硬件兼容性列表的补充。此外,如果您在配置设置中的某个机制时遇到问题,您可能会发现这些示例非常有价值。用户在那里详细描述了他们的设置,有些人还提供了他们的配置文件示例。
我们鼓励那些成功地从该资源构建系统的人通过贡献他们的构建来帮助改进它。由于涉及许多不同的硬件制造商,看似严重缺乏足够的文档,以及由于此过程的性质造成的其他问题,社区贡献是必要的。
故障排除
如果您的issue未在下面提及,您可能需要浏览 QEMU#故障排除。
QEMU 4.0:使用 Q35 安装驱动程序后,无法加载图形驱动程序/BSOD/图形卡顿
从 QEMU 4.0 开始,Q35 机器类型将默认的 kernel_irqchip
从 off
更改为 split
,这会破坏某些客户机设备,例如 nVidia 图形卡(驱动程序加载失败/黑屏/代码 43/图形卡顿,通常在鼠标移动时)。通过在 libvirt 的 <features>
标签下添加 <ioapic driver='kvm'/>
或在 -machine
QEMU 参数中添加 kernel_irqchip=on
,切换到完整的 KVM 模式。
QEMU 5.0:使用 Zen 2 处理器时,内核版本 5.5 到 5.8.1 的 host-passthrough:Windows 10 BSOD 循环“KERNEL SECURITY CHECK FAILURE”
从 QEMU 5.0 开始,在 Zen 2 和更新内核(高于 5.4)上运行的虚拟机将导致 BSOD 循环:“KERNEL SECURITY CHECK FAILURE”。这可以通过更新到内核版本 5.8.2 或更高版本,或禁用 STIBP 来解决
<cpu mode='host-passthrough' ...> ... <feature policy='disable' name='amd-stibp'/> ... </cpu>
这需要 libvirt 6.5 或更高版本。在旧版本上,存在几种解决方法
- 将 CPU 模式从
host-passthrough
切换到host-model
。这仅适用于 libvirt 6.4 或更低版本。 - 手动修补 qemu-desktop 以还原 此 commit。
- 在 qemu 命令行中,将
amd-stibp=off
添加到 cpu 标志字符串。也可以通过 libvirt 通过<qemu:commandline>
条目调用此操作。
移动版 (Optimus/max-q) nvidia GPU 出现“Error 43: Driver failed to load”
出现此错误是因为 Nvidia 驱动程序想要检查电源状态。如果不存在电池,则驱动程序无法工作。无论是 Libvirt 还是 QEMU,默认情况下它们都没有提供模拟电池的可能性。这也可能导致屏幕分辨率降低,并且 Nvidia 桌面管理器拒绝在右键单击桌面时加载,并提示它需要 Windows 10、兼容的 GPU 和 Nvidia 图形驱动程序。
但是,您可以创建自定义 acpi 表文件并将其添加到虚拟机,这将完成这项工作。
首先,您必须通过将以下文件另存为 SSDT1.dat(此处为 base64 编码)来创建自定义 acpi 表文件
echo 'U1NEVKEAAAAB9EJPQ0hTAEJYUENTU0RUAQAAAElOVEwYEBkgoA8AFVwuX1NCX1BDSTAGABBMBi5f U0JfUENJMFuCTwVCQVQwCF9ISUQMQdAMCghfVUlEABQJX1NUQQCkCh8UK19CSUYApBIjDQELcBcL cBcBC9A5C1gCCywBCjwKPA0ADQANTElPTgANABQSX0JTVACkEgoEAAALcBcL0Dk=' | base64 -d > SSDT1.dat
接下来,您必须将处理后的文件添加到虚拟机的主域
<domain xmlns:qemu="http://libvirt.org/schemas/domain/qemu/1.0" type="kvm"> ... <qemu:commandline> <qemu:arg value="-acpitable"/> <qemu:arg value="file=/path/to/your/SSDT1.dat"/> </qemu:commandline> </domain>
确保您的 XML 文件在 <domain>
标签中具有正确的命名空间,如上所示,否则 XML 验证将失败。
启动虚拟机后,dmesg 中出现“BAR 3: cannot reserve [mem]”错误
关于 这篇文章
如果您仍然有代码 43,请在启动虚拟机后检查 dmesg 中是否存在内存保留错误,如果您有类似的错误,则可能是这种情况
vfio-pci 0000:09:00.0: BAR 3: cannot reserve [mem 0xf0000000-0xf1ffffff 64bit pref]
找出您的图形卡连接到的 PCI 桥接器。这将给出设备的实际层次结构
$ lspci -t
在启动虚拟机之前,运行以下行,将 ID 替换为先前输出中的实际值。
# echo 1 > /sys/bus/pci/devices/0000\:00\:03.1/remove # echo 1 > /sys/bus/pci/rescan
此外,尝试添加内核参数 pci=realloc
,它也 有助于解决热插拔问题。
VBIOS 中的 UEFI (OVMF) 兼容性
关于 这篇文章
错误 43 可能是由没有 UEFI 支持的 GPU 的 VBIOS 引起的。要检查您的 VBIOS 是否支持它,您必须使用 rom-parser
$ git clone https://github.com/awilliam/rom-parser $ cd rom-parser && make
转储 GPU VBIOS
# echo 1 > /sys/bus/pci/devices/0000:01:00.0/rom # cat /sys/bus/pci/devices/0000:01:00.0/rom > /tmp/image.rom # echo 0 > /sys/bus/pci/devices/0000:01:00.0/rom
并测试其兼容性
$ ./rom-parser /tmp/image.rom
Valid ROM signature found @600h, PCIR offset 190h PCIR: type 0 (x86 PC-AT), vendor: 10de, device: 1184, class: 030000 PCIR: revision 0, vendor revision: 1 Valid ROM signature found @fa00h, PCIR offset 1ch PCIR: type 3 (EFI), vendor: 10de, device: 1184, class: 030000 PCIR: revision 3, vendor revision: 0 EFI: Signature Valid, Subsystem: Boot, Machine: X64 Last image
要实现 UEFI 兼容性,您需要在结果中看到“type 3 (EFI)”。如果那里没有,请尝试更新您的 GPU VBIOS。GPU 制造商通常在其支持页面上共享 VBIOS 升级。在 TechPowerUp 上提供了已知兼容和可用的 VBIOS 的大型数据库(以及它们的 UEFI 兼容性状态!)。
更新后的 VBIOS 可以在虚拟机中使用,而无需刷写。要在 QEMU 中加载它
-device vfio-pci,host=07:00.0,......,romfile=/path/to/your/gpu/bios.bin \
在 libvirt 中
<hostdev> ... <rom file='/path/to/your/gpu/bios.bin'/> ... </hostdev>
应该使用 nvflash(Linux 版本在显示更多版本下)或 GPU-Z(在 Windows 虚拟机中)比较主机和虚拟机系统之间的 VBIOS 版本。要检查当前加载的 VBIOS
$ ./nvflash --version
... Version : 80.04.XX.00.97 ... UEFI Support : No UEFI Version : N/A UEFI Variant Id : N/A ( Unknown ) UEFI Signer(s) : Unsigned ...
并检查给定的 VBIOS 文件
$ ./nvflash --version NV299MH.rom
... Version : 80.04.XX.00.95 ... UEFI Support : Yes UEFI Version : 0x10022 (Jul 2 2013 @ 16377903 ) UEFI Variant Id : 0x0000000000000004 ( GK1xx ) UEFI Signer(s) : Microsoft Corporation UEFI CA 2011 ...
如果外部 ROM 在虚拟机中无法正常工作,您将必须将较新的 VBIOS 映像刷写到 GPU。在某些情况下,可以使用 GOPUpd 工具创建您自己的具有 UEFI 支持的 VBIOS 映像,但这很冒险,可能会导致 GPU 变砖。
为了避免对您的图形适配器造成不可修复的损坏,必须首先卸载 NVIDIA 内核驱动程序
# modprobe -r nvidia_modeset nvidia
可以使用以下命令刷写 VBIOS
# ./nvflash romfile.bin
通过显卡上的 HDMI 泵送的音频变慢
对于某些用户,通过显卡上的 HDMI 泵送的虚拟机的音频会在一段时间后变慢/开始卡顿/变得像魔鬼的声音。这通常也会减慢图形速度。一种可能的解决方案是启用 MSI(消息信号中断),而不是默认的(线路中断)。
为了检查是否支持或启用了 MSI,以 root 身份运行以下命令
# lspci -vs $device | grep 'MSI:'
其中 `$device` 是卡的地址(例如 `01:00.0`)。
输出应类似于
Capabilities: [60] MSI: Enable- Count=1/1 Maskable- 64bit+
Enable
后的 -
表示支持 MSI,但虚拟机未使用,而 +
表示虚拟机正在使用它。
启用它的过程非常复杂,有关设置的说明和概述可以在 此处 找到。
在 linux 虚拟机上,您可以使用 modinfo 来查看是否有启用 MSI 的选项(例如:“modinfo snd_hda_intel |grep msi”)。如果有,可以通过将相关选项添加到自定义 omdprobe 文件来启用它 - 在“/etc/modprobe.d/snd-hda-intel.conf”中插入“options snd-hda-intel enable_msi=1”
其他提示可以在 lime-technology 的 wiki[死链接 2023-05-06 ⓘ],或在 VFIO tips and tricks 这篇文章中找到。
一个名为 MSI Utility (FOSS Version 2) 的 UI 工具适用于 Windows 10 64 位,并简化了该过程。
为了解决这些问题,仅在 nVidia 卡的 0 功能上启用 MSI(01:00.0 VGA compatible controller: NVIDIA Corporation GM206 [GeForce GTX 960] (rev a1) (prog-if 00 [VGA controller])
)是不够的;还需要在另一个功能上启用它(01:00.1 Audio device: NVIDIA Corporation Device 0fba (rev a1)
)才能解决该问题。
启用 intel_iommu 后主机上没有 HDMI 音频输出
如果在启用 intel_iommu
后,Intel GPU 的 HDMI 输出设备在主机上变得不可用,则设置选项 igfx_off
(即 intel_iommu=on,igfx_off
)可能会使音频恢复,请阅读 iommu.html 以获取有关设置 igfx_off
的详细信息。
启用 vfio_pci 后 X 无法启动
这与宿主机 GPU 被检测为辅助 GPU 有关,当 X 尝试为虚拟机 GPU 加载驱动程序时,会导致 X 失败/崩溃。为了避开这个问题,需要一个 Xorg 配置文件来指定宿主机 GPU 的 BusID。正确的 BusID 可以从 lspci -n
或 Xorg 日志 [7] 中获取。请注意,lspci 输出的值是十六进制的,应在 .conf 文件中转换为十进制。
/etc/X11/xorg.conf.d/10-intel.conf
Section "Device" Identifier "Intel GPU" Driver "modesetting" BusID "PCI:0:2:0" EndSection
Chromium 忽略集成显卡以进行加速
Chromium 及其同类软件会尝试检测系统中尽可能多的 GPU,并选择首选的 GPU(通常是独立的 NVIDIA/AMD 显卡)。它尝试通过查看 PCI 设备而不是系统中可用的 OpenGL 渲染器来选择 GPU - 结果是 Chromium 可能会忽略可用于渲染的集成 GPU,并尝试使用绑定到 vfio-pci
驱动程序的专用 GPU,即使在宿主机系统上不可用,无论访客虚拟机是否正在运行。这会导致使用软件渲染(导致更高的 CPU 负载,这也可能导致视频播放卡顿、滚动不流畅和整体不顺畅)。
这个问题可以通过显式告诉 Chromium 你想使用哪个 GPU 来解决。
虚拟机只使用一个核心
对于某些用户,即使启用了 IOMMU 并且核心数设置为大于 1,虚拟机仍然只使用一个 CPU 核心和一个线程。要解决这个问题,请在 virt-manager
中启用“手动设置 CPU 拓扑”,并将其设置为所需的 CPU 插槽、核心和线程数量。请记住,“线程”是指每个 CPU 的线程数,而不是总数。
直通似乎工作正常,但没有输出显示
如果您正在使用 virt-manager,请确保为您的虚拟机选择了 UEFI 固件。此外,请确保您已将正确的设备传递给虚拟机。
虚拟机关闭后宿主机死锁
这个问题似乎主要影响运行 Windows 10 访客系统的用户,并且通常在虚拟机运行一段时间后出现:宿主机将遇到多个 CPU 核心死锁(参见 [8])。要解决这个问题,请尝试在传递给访客系统的 GPU 上启用消息信号中断。有关如何执行此操作的良好指南可以在 [9] 中找到。您也可以在此处下载此 Windows 应用程序 [10],它应该使该过程更容易。
访客系统运行时宿主机睡眠导致死锁
如果 VFIO 启用的虚拟机在睡眠/唤醒周期中保持运行,则往往会变得不稳定,并且已知会在尝试关闭它们时导致宿主机死锁。为了避免这种情况,可以简单地使用以下 libvirt 钩子脚本和 systemd 单元来阻止宿主机在访客系统运行时进入睡眠状态。钩子文件需要可执行权限才能工作。
/etc/libvirt/hooks/qemu
#!/bin/sh OBJECT="$1" OPERATION="$2" SUBOPERATION="$3" EXTRA_ARG="$4" case "$OPERATION" in "prepare") systemctl start libvirt-nosleep@"$OBJECT" ;; "release") systemctl stop libvirt-nosleep@"$OBJECT" ;; esac
/etc/systemd/system/libvirt-nosleep@.service
[Unit] Description=Preventing sleep while libvirt domain "%i" is running [Service] Type=simple ExecStart=/usr/bin/systemd-inhibit --what=sleep --why="Libvirt domain \"%i\" is running" --who=%U --mode=block sleep infinity
自 Windows 10 1803 以来的启动蓝屏
自 Windows 10 1803 以来,当您使用“host-passthrough”作为 CPU 型号时,会出现问题。机器无法启动,要么是启动循环,要么是出现蓝屏。您可以通过以下方法解决此问题
# echo 1 > /sys/module/kvm/parameters/ignore_msrs
为了使其永久生效,您可以创建一个 modprobe 文件 kvm.conf
options kvm ignore_msrs=1
为了防止 dmesg 被“ignored rdmsr”消息阻塞,您可以额外添加
options kvm report_ignored_msrs=0
AMD Ryzen / BIOS 更新 (AGESA) 导致 “Error: internal error: Unknown PCI header type ‘127’” 错误
AMD 用户在更新主板 BIOS 后,一直遇到 KVM 设置中断的情况。现在有一个内核补丁[死链 2024-07-30 ⓘ],(有关使用自定义补丁编译内核的说明,请参阅 Kernel/Arch 构建系统)可以解决这个问题(截至 2019 年 7 月 28 日),但这并不是 AMD 第一次犯这种性质的错误,因此如果您作为 VFIO 用户考虑在未来更新 BIOS,请考虑到这一点。
AMD GPU 未正确重置导致 “Error: internal error: Unknown PCI header type ‘127’” 错误(与上述问题分开的问题)
直通 AMD GPU 可能会导致一个称为“AMD 重置错误”的问题。在访客系统电源循环后,GPU 未能正确重置其状态,这会导致设备发生故障,直到宿主机也重新启动。这通常与 Windows 访客系统中的“代码 43”驱动程序错误以及宿主机上 libvirt 日志中的消息“Error: internal error: Unknown PCI header type '127'” 配对出现。
过去,这意味着必须使用解决方法来手动重置 GPU,或者求助于不太可能进入上游的内核补丁。目前,不需要修补内核的推荐解决方案是安装 vendor-reset-gitAUR 或 vendor-reset-dkms-gitAUR,并确保在启动访客系统之前加载 'vendor-reset' 内核模块。为了方便起见,您可以自动加载模块。
热插拔带有 USB 的 Nvidia 显卡时宿主机崩溃
如果尝试热插拔带有 USB 端口的 Nvidia 显卡,您可能需要将 i2c_nvidia_gpu
驱动程序列入黑名单。通过将 blacklist i2c_nvidia_gpu
行添加到 /etc/modprobe.d/blacklist.conf
来执行此操作。
启用 vfio 后宿主机无法启动并卡在黑屏
如果在启动期间通过添加 debug ignore_loglevel
内核参数 启用了调试内核消息,您可能会看到启动卡住,最后一条消息类似于
vfio-pci 0000:01:00.0: vgaarb: changed VGA decodes: olddecodes=io+mem,decodes=io+mem:owns=none
可以通过断开直通 GPU 与显示器的连接来缓解此问题。在宿主机启动后,您可以将直通 GPU 重新连接到显示器。
如果您不想在每次启动宿主机时都插入电缆。您可以禁用引导加载程序中的帧缓冲以绕过此消息。对于 UEFI 系统,您可以添加 video=efifb:off
作为内核参数。对于旧版支持,请改用 video=vesafb:off
或结合使用。请注意,这样做可能会导致 Xorg 出现问题。
如果您遇到 Xorg 问题,以下解决方案可能会有所帮助(请记住根据需要替换为您自己的值)。
/etc/X11/xorg.conf.d/10-amd.conf
Section "Device" Identifier "AMD GPU" Driver "amdgpu" BusID "PCI:0:2:0" EndSection
直通 PCIe USB 集线器时出现 AER 错误
在某些情况下,直通 PCIe USB 集线器(例如连接到访客系统 GPU 的集线器)可能会失败,并出现类似于以下的 AER 错误
kernel: pcieport 0000:00:01.1: AER: Uncorrected (Non-Fatal) error received: 0000:00:01.1 kernel: pcieport 0000:00:01.1: AER: PCIe Bus Error: severity=Uncorrected (Non-Fatal), type=Transaction Layer, (Requester ID) kernel: pcieport 0000:00:01.1: AER: device [8086:1905] error status/mask=00100000/00000000 kernel: pcieport 0000:00:01.1: AER: [20] UnsupReq (First) kernel: pcieport 0000:00:01.1: AER: TLP Header: 00000000 00000000 00000000 00000000 kernel: pcieport 0000:00:01.1: AER: device recovery successful
保留内存区域报告 (RMRR) 冲突
如果您在直通设备时遇到由于 BIOS 使用 RMRR 而导致的问题,例如以下错误。
vfio-pci 0000:01:00.1: Device is ineligible for IOMMU domain attach due to platform RMRR requirement. Contact your platform vendor.
您可以尝试此处的补丁:https://github.com/kiler129/relax-intel-rmrr
直通到虚拟机的 AMD GPU 频率限制过低
在某些装有 AMD GPU 的机器上,将设备绑定到 vfio-pci 可能不足以防止来自宿主机的干扰,因为宿主机上的 amdgpu 驱动程序可能会查询全局 ATIF 方法,这可能会改变 GPU 的行为。例如,一位使用 Dell Precision 7540 笔记本电脑(包含 Radon Pro WX 3200 AMD GPU)的用户报告说,在将 AMD GPU 绑定到 vfio-pci 的情况下,直通的 AMD GPU 被限制为 501 MHz 而不是正确的 1295 MHz 限制。使用内核命令行黑名单 amdgpu 内核模块是一种解决方法。
有关更多详细信息,请参阅 此内核邮件列表讨论。
宿主机时钟源是 HPET 而不是 TSC
如果您的宿主机时钟源是 HPET,您可能会在游戏中看到严重的性能下降。虽然这个问题在虚拟机外部运行游戏时也会发生,但不知情的用户可能会开始对他们的虚拟机进行故障排除,而问题实际上出在他们的宿主机系统上。
要查看您当前的时钟源,请在您的宿主机上运行此命令
$ cat /sys/devices/system/clocksource/clocksource*/current_clocksource
如果您的时钟源是 HPET,您可以尝试通过设置 内核参数 clocksource=tsc
和 tsc=reliable
来强制将其设置为 TSC。有关更多信息,请参阅 此 Reddit 帖子。
在 BIOS 中启用 Resizable Bar 时出现代码 43
如果您拥有支持 Resizable BAR / SAM 的 GPU 并且启用了相应的 BIOS 选项,您可能会收到代码 43,因为此功能在 QEMU 中被禁用 [11]。Linux Kernel 6.1 版本添加了通过 sysfs 操作 PCIe Resizable BAR 的选项,因此您可以尝试使用 udev 规则永久调整其大小
/etc/udev/rules.d/01-amd.rules
ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x1002", ATTR{device}=="0x73bf", ATTR{resource0_resize}="14" ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x1002", ATTR{device}=="0x73bf", ATTR{resource2_resize}="8"
或者,要临时调整 PCIe BAR 的大小,您可以将新大小写入 /sys/bus/pci/devices/设备地址/resource编号_resize
,其中设备地址是您的 GPU 的地址,例如 0000:03:00.0
(请注意,为了更改此参数,不能为设备加载任何驱动程序)。
您可以使用 lspci -vvvxxxx
确认值。
操作系统挂起
如果您的虚拟化操作系统(尤其是 Windows)在附加 GPU 时挂起,您也可以尝试打开主板设置并
- 禁用
Re-Size BAR Support
, - 禁用
Above 4G memory / Crypto Currency mining
, - 启用
SR-IOV
, - 将
Max TOLUD
从dynamic
更改为特定值, - 将
Initiate Graphic Adapter
从PEG
切换到IGD
。
至少,VEGA 显卡似乎对这些设置很挑剔,除非您更改它们,否则实际上无法工作。