参考文献:

  1. Ubuntu配置GPU直传kvm虚拟机 – CodeAntenna
  2. KVM虚拟机GPU直通,step by step – 机械意志 (mechanical-consciousness.com)
  3. lspci的输出简单分析 – 成蹊0xc000 – 博客园 (cnblogs.com)
  4. PCI passthrough via OVMF – Arch Linux 中文维基 (archlinuxcn.org)
  5. Win10/11 如何开启 第二屏幕/副屏/虚拟显示器,让平板成为副屏 – 知乎 (zhihu.com)

首先声明,本文的背景是在两张同型号的3090ti中选择一张进行直通,在整个直通过程中,上面的参考文献给与了我很大的帮助,本篇内容是我基于他们描述完整操作后的记录,其中不乏对原文的直接摘抄,但也结合了我个人的机器情况进行了重新梳理,并进行了多次可行性验证,因为每个人的机器情况可能都大不相同,希望我的记录能给和我相同配置的同学一些帮助。如果有不懂的命令,建议大家查一下GPT,可能理解起来会快很多。

直通步骤:

1. 确定是否支持虚拟化:

确定本机是否支持VT-P虚拟化,一般需要主板BIOS开启VT-P。如果不能进BIOS确认,也可以在终端里确认。

检查CPU是否支持虚拟化,运行如下命令:

$ egrep -c '(svm|vm)' /proc/cpuinfo

如果显示为0,则不支持虚拟化。

系统配置:Ubuntu22.04.1 LTS,GPU 3090 Ti*2,CPU i7 13700K,主板 华硕Z790-P

2. 在宿主机系统中启用iommu组:

打开/etc/default/grub,找到GRUB_CMDLINE_LINUX_DE FAULT,修改为下面内容:

GRUB_CMDLINE_LINUX_DEFAULT="quiet splash intel_iommu=on"

接着我们需要更新grub:

$ sudo update-grub -u

然后重启电脑,重启完后我们需要检查iommu有没有被正确启用:

$ dmesg | grep -i iommu

如果你在输出中看到很多pci xxxx:xx:xx.x: Adding to iommu group xx,那么恭喜你,第一步完成了。
系统中的所有设备只能按照iommu group为单位分配给宿主机或者虚拟机。因此如果你看到两个不相关的设备在一个group里,那也没招,你只能给这俩设备一起丢虚拟机里去。

很可惜的是,Linux系统并不支持预留iommu group。

iommu group是硬件实现上的分组。在Linux的实现中,只认得各个在总线上的硬件,并挨个挨个的启动起来。

我们要做的就是阻止Linux内核在启动时初始化某些组里的硬件。

3. 寻找我们需要预留的iommu组:

首先确认我们显卡所在的PCIE总线和产品型号:

$ lspci -nnv | grep -i nvidia

我的电脑上输出如下:

01:00.0 VGA compatible controller [0300]: NVIDIA Corporation Device [10de:2203] (rev a1) (prog-if 00 [VGA controller])Kernel driver in use: nvidiaKernel modules: nvidiafb, nouveau, nvidia_drm, nvidia01:00.1 Audio device [0403]: NVIDIA Corporation GA102 High Definition Audio Controller [10de:1aef] (rev a1)05:00.0 VGA compatible controller [0300]: NVIDIA Corporation Device [10de:2203] (rev a1) (prog-if 00 [VGA controller])Kernel modules: nvidiafb, nouveau, nvidia_drm, nvidia05:00.1 Audio device [0403]: NVIDIA Corporation GA102 High Definition Audio Controller [10de:1aef] (rev a1)

可以看到输出分成两部分,编号加设备描述。01:00.001:00.1是编号通过冒号 “:” 又分成了三部分,第一个部分是PCIe的 domain ID,第二个部分是 bus ID,第三个部分是 device id.function id。PCI设备的组织形式是一个树形,这表示一个domain可以包含多个bus,一个bus又包含了多个device,一个device又包含多个function。

0300 是一个用来区分不同设备的编号,网络设备、存储设备、多媒体设备、显卡都有自己的编号。

10de:2203 这是一个厂商特有的编号,10de是 Nvidia的编号(Vendor ID),Nvidia所有的设备都是使用这一编号,2203 表示这是一个3090Ti显卡。

以上参考:lspci的输出简单分析 – 成蹊0xc000 – 博客园 (cnblogs.com),可以在这个博客里继续查找自己的显卡信息。

接下来我们需要找出我们的设备所在的iommu group:

$ sudo dmesg | grep iommu | grep 01:00.0

输出如下:

[0.536462] pci 0000:01:00.0: Adding to iommu group 17$ sudo dmesg | grep iommu | grep 05:00.0[0.536491] pci 0000:05:00.0: Adding to iommu group 20

确认iommu group中的所有设备:

$ sudo dmesg | grep "iommu group 17"

输出如下:

[0.536462] pci 0000:01:00.0: Adding to iommu group 17[0.536467] pci 0000:01:00.1: Adding to iommu group 17

4. 屏蔽显卡

4.1 型号屏蔽法:

如果只有一张显卡,或者两张显卡的ID不同,可以直接用pci-stub或者vfio-pci的方法来屏蔽即可,修改/etc/default/grub

GRUB_CMDLINE_LINUX_DEFAULT="quiet splash intel_iommu=on pci-stub.ids=10de:2203,10de:1aef"或者GRUB_CMDLINE_LINUX_DEFAULT="quiet splash intel_iommu=on vfio-pci.ids=10de:2203,10de:1aef"

更新内核,并重启:

$ sudo update-grub -u

如果屏蔽成功,内核驱动就会显示如下:

$ lspci -nnv | grep -E "(^\S|Kernel driver in use)" | grep 01:00 -A 1

01:00.0 VGA compatible controller [0300]: NVIDIA Corporation Device [10de:2203] (rev a1) (prog-if 00 [VGA controller])Kernel driver in use: vfio-pci01:00.1 Audio device [0403]: NVIDIA Corporation GA102 High Definition Audio Controller [10de:1aef] (rev a1)Kernel driver in use: vfio-pci

否则下面还是驱动被nvidia接管的状态(其实nvidia-smi就能看了):

$ lspci -nnv | grep -E "(^\S|Kernel driver in use)" | grep 01:00 -A 1

01:00.0 VGA compatible controller [0300]: NVIDIA Corporation Device [10de:2203] (rev a1) (prog-if 00 [VGA controller])Kernel driver in use: nvidia01:00.1 Audio device [0403]: NVIDIA Corporation GA102 High Definition Audio Controller [10de:1aef] (rev a1)Kernel driver in use: snd_hda_intel
4.2 脚本屏蔽法

当然如果两张显卡都是同一个型号,是不能用上面的方法屏蔽的。

这个时候我们要为显卡指定驱动:

$ sudo vim /etc/initramfs-tools/scripts/init-top/vfio.sh

脚本内容为:

#!/bin/shecho "vfio-pci" > /sys/bus/pci/devices/0000:01:00.0/driver_overrideecho "vfio-pci" > /sys/bus/pci/devices/0000:01:00.1/driver_override#echo "0000:01:00.0" > /sys/bus/pci/drivers/vfio-pci/bind # 这里我没有加这个绑定,有人加了这两句,但我注释了,因为本地找不到这个文件夹#echo "0000:01:00.1" > /sys/bus/pci/drivers/vfio-pci/bind # 需要modprobe -i vfio-pci之后才有这个文件夹,但不加仍然可用exit 0

给脚本增加权限,否则无法执行:

$ sudo chmod 744 /etc/initramfs-tools/scripts/init-top/vfio.sh

完成后刷新initramfs,这个命令会执行上面的脚本:

$ sudo update-initramfs -u -k all

输出下面的结果,重启就可以正常屏蔽了。

update-initramfs: Generating /boot/initrd.img-6.2.0-31-genericupdate-initramfs: Generating /boot/initrd.img-6.2.0-26-generic

重启配置成功后按照上一节的命令就能看到显卡的驱动被vfio-pci接管了,nvidia-smi也能发现显卡已经无法识别了。

如果想恢复显卡的挂载,就把脚本中的两行echo...注释掉,并刷新initramfs,重启机器即可。

4.3 开机脚本屏蔽法

有些地方用下面的方法成功了,但我试了没有效果,我把方法留在这里供大家尝试:

$ sudo vim /usr/bin/vfio-pci-override.sh

脚本的内容:

#!/bin/shif [ ! -z "$(ls -A /sys/class/iommu)" ]; then echo "vfio-pci" > /sys/bus/pci/devices/0000:01:00.0/driver_override echo "vfio-pci" > /sys/bus/pci/devices/0000:01:00.1/driver_overridefimodprobe -i vfio-pci

再给脚本增加运行权限,否则启动时是不能运行的:

$ sudo chmod 744 /usr/bin/vfio-pci-override.sh

最后在/etc/modprobe.d/vfio.conf内指定我们的脚本路径。

$ sudo vim /etc/modprobe.d/vfio.conf

install vfio-pci /usr/bin/vfio-pci-override.sh

Arch Linux的官方中文文档也有详细的解释,但实际操作和Ubuntu有差异,放在这里作为一个参考资料。PCI passthrough via OVMF – Arch Linux 中文维基 (archlinuxcn.org)。

因为用了 modprobe,所以 vfio 也能正常工作了,上面说的bind文件也能找到了。

$ lsmod | grep vfio

输出如下:

vfio_pci163840vfio_pci_core942081 vfio_pcivfio_iommu_type1491520vfio614403 vfio_pci_core,vfio_iommu_type1,vfio_pciiommufd737281 vfioirqbypass163842 vfio_pci_core,kvm

5. 安装KVM虚拟机并配置显卡:

至此显卡已经被成功虚拟化,接下来就是加载到KVM之中,KVM的安装很简单,GUI界面的安装网上有很多资料,这里留下安装命令。

$ sudo apt-get install qemu-kvm libvirt-clients libvirt-daemon-system bridge-utils virt-manager ovmf

启用libvirt服务和开机自启

sudo systemctl start libvirtd.service
sudo systemctl enable libvirtd.service

如果你装完上面的命令后不想再重启更新用户组之类的,可以直接sudo virt-manager

创建Win10镜像,这里就不赘述了,操作很简单,找好ISO镜像,安装即可,这里备注几个重要的点。

首先,如果这里没有自动识别ISO中的系统可以输入后自己手动选择一下,比如这里选择win10就可以了。

其次,创建成功后再把显卡的PCIE添加进来,注意Nividia的声卡也要加进来。


进入系统后安装Nvidia官方的驱动就可以在设备管理器里找到显卡了,但还需要继续配置才能使用显卡接管显示输出。

6. 高清显示与Parsec远程

parsec和其他软件都是需要启动显卡才能高清输出的,所以这里还要创建一个虚拟高分辨率显示器,创建步骤很简单,参考这个博客即可,Win10/11 如何开启 第二屏幕/副屏/虚拟显示器,让平板成为副屏 – 知乎 (zhihu.com)。

安装里面的脚本,在CMD里执行下面的命令:

deviceinstaller64 install usbmmidd.inf usbmmidd
deviceinstaller64 enableidd 1 # 创建屏幕
deviceinstaller64 enableidd 0 # 用来关闭屏幕

完成后就能在windows的设立里看到第二个屏幕可用了,再把第二块屏的分辨率调到3840×2160(16:9)的4k,并设置为主屏。

最后打开parsec之类的软件就能够正常高清输出了,当然parsec需要网络能够满足p2p的环境,如果连接不了报错,可以在B站搜索相关错误代码,确认自己的情况,我这里尝试了同一个路由内虚拟机默认的网络配置是可以连接的,但设备在不同路由下面的时候会跨网段,虚拟机就不能建立p2p连接了(例如宿主机在172.19.1.10,虚拟机在宿主机中但没有相关172.19的网段,测试用的笔记本在172.20.1.20),可能的思路是需要修改虚拟机的网卡让其转到172.19网段下面再尝试。

ToDesk里也会显示当前是否为p2p的连接,可以利用ToDesk来辅助检测网络环境。