KVM虚拟机GPU直通,step by step

最近工作的事情有点烦躁,就开始打酱油了。折腾了好久,可算把家里的nas和游戏机合为一体了。

这其中最蛋疼的步骤可算是要在Linux虚拟机上跑游戏了。在无数次死机重启之后,可算把这个问题搞定的。

网上GPU虚拟机直通的教程不少,但是都有一个问题,就是只告诉你怎么一步一步做,然后启用了一个又一个Linux模块,但是往往搞到最后一启动虚拟机就整个电脑死机了(囧,对的,整个电脑死机,不是虚拟机死机)。

踩了n天的坑后,总算把GPU直通的所有坑都踩了一遍。也都看了一遍。这里记录下来,有兴趣的朋友一起研究研究吧。

整个虚拟机GPU直通大约有那么几个步骤(缺1不可),我的平台是Intel + Nvidia,因此用AMD的朋友可能会有所不同。但是整体思路应该是一致的。

  • 确认硬件支持

    主要是CPU和主板需要支持VT-d技术,如果是AMD平台的话,应该是AMD-Vi支持。 具体行不行,直接Google之。

  • 买买买

    如果你只有一张显卡的话,GPU直通显然是没法玩的,显卡分配给了虚拟机,宿主机怎么办?

    当然你可以搞两张一样的卡,我个人的话直接搞了张二手GTX 650,便宜实惠。反正宿主机对显卡也没啥要求。

    理论上来说A卡N卡混合应该无所谓,但我没试过。

    理论上来说,如果你有集成显卡,有些主板有选项,可以默认从集成显卡启动,这样的话你可以省掉一张显卡。(很遗憾i9连核显都没有) 省掉一张显卡主要不是为了省钱啊,我现在的机箱已经重到搬不动了,两张显卡,3块大硬盘,2个SSD,还有水冷啥的,塞的满满当当的,囧。

    另外推荐入手一个kvm切换器,等显卡直通搞定了,你就相当与有了两台电脑了,没个kvm不得烦死?

    另外,如果主板的所有USB口都在一个iommu group的话,推荐再入手个PCI-E的usb扩展卡。后面会仔细解释这个事情。

  • 在BIOS中调整相关参数

    首先肯定是要启用VT-d的啦,(AMD的话就是AMD-Vi)

    然后还有一个重要选项是默认从哪张显卡输出。可惜不是所有BIOS都支持这个选项,我的老板子就不支持,前端时间坏了(真假),被我顺利淘汰了。

    如果有这个选项的话,就可以把好显卡插在主PCI-E槽,然后默认从差的显卡启动。如果没有的话,想开点,交换一下吧。

  • 在宿主系统中启用iommu

    iommu是GPU虚拟机直通的核心。

    虽然我们总是在讲GPU直通,其实本质上来说,通过VT-d技术,各种乱七八糟的东西都可以丢到虚拟机里面去,让虚拟机直接访问。

    iommu负责把主板上的设备分组,分成一个一个group。每个group都可以单独的启用或者停用。

    在没启用iommu之前,宿主系统会占用全部的硬件。在启用iommu之后,宿主系统可以有选择的预留一些硬件资源,把这些硬件资源分配给虚拟机。

  • 在宿主系统中预留iommu group

    无论有没有启用iommu,Linux默认会扫描系统中的所有硬件,并尝试给每个硬件分配驱动,并初始化。

    这个事情的直接后果就是,系统启动完了后,iommu倒是启用了,但是没有闲着的分组???

    所以我们需要在Linux内核启动足够早的阶段把硬件预留下来。

    这个步骤是整个事情中最麻烦的,也最有可能遇到问题的。

  • 创建kvm虚拟机,并将iommu group分配给虚拟机

    这个简单

  • 给虚拟机安装系统

    emmm….

  • 阻止nvidia驱动发现虚拟机

    这个步骤我是真的真的想吐槽,所有一切都就绪了之后,系统起来被锁定在800x600的分辨率,驱动提示错误35。

    刚开始我始终以为是自己哪里做错了。结果上网一搜发现,竟然是nvidia的驱动直接去侦测自己是不是在虚拟机里跑的。如果是的话就直接报错。

    修正这个问题不难,kvm自身有特性可以隐藏虚拟机,直接打开就能解决这个问题。但是这破问题坑了我好几个小时。

  • 安装steam, 安装游戏, 加我steam好友

    [狗头]

总体来说就这么多步骤。其中大部分的步骤都不难,最有挑战的部分应该是预留iommu group。因为每个人的硬件不一样,遇到的情况也不一样。

本文不覆盖买买买,因为不打广告。 本文也不覆盖BIOS设置,因为我这非主流主板,覆盖了大家也没兴趣啊。

我们直接从如何启用iommu开始,一步一步的讲解,解完一步确认一步。

本文使用的操作系统为Ubuntu 18.04,其实系统影响不大,主要还是内核和硬件。但是不少步骤是系统相关的,例如如何修改grub启动参数等,如果在其他系统上实施,需要做相应的动作。

在宿主系统中启用iommu

在宿主系统中启用iommu很简单,就是给Linux内核一个额外的启动参数。

对Ubuntu来说,我们只需要修改/etc/default/grub配置文件即可。

打开/etc/default/grub,找到GRUB_CMDLINE_LINUX_DEFAULT,默认情况下这行大概是这样的:

GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"

我们把这行改为:

GRUB_CMDLINE_LINUX_DEFAULT="quiet splash intel_iommu=on"

如果是AMD的话,据说是加amd_iommu=on iommu=pt kvm_amd.npt=1 kvm_amd.avic=1,没实验过,请自行尝试。

接着我们需要更新grub:

$ sudo update-grub -u

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

$ dmesg | grep -i iommu

如果你在输出中看到很多pci xxxx:xx:xx.x: Adding to iommu group xx,那么恭喜你,第一步完成了。

系统中的所有设备只能按照iommu group为单位分配给宿主机或者虚拟机。因此如果你看到两个不相关的设备在一个group里,那也没招,你只能给这俩设备一起丢虚拟机里去。

在宿主系统中预留iommu group

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

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

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

这里有一点需要非常注意的是,如果我们需要预留某个iommu组,我们必须完整的预留iommu组里的所有硬件。 如果我们的预留是不完整的,当我们把iommu组分配给虚拟机后,宿主系统会直接IO总线冲突,连重启的机会都没有。

寻找我们需要预留的iommu group

首先我们需要找到我们需要预留的iommu group的id:

$ lspci | grep -i nvidia

(用amd卡的同学自行修改啊,后面不累述了,太累了)

在我的电脑上,上面命令的输出如下:

17:00.0 VGA compatible controller: NVIDIA Corporation GK106 [GeForce GTX 650 Ti] (rev a1)
17:00.1 Audio device: NVIDIA Corporation GK106 HDMI Audio Controller (rev a1)
65:00.0 VGA compatible controller: NVIDIA Corporation Device 2184 (rev a1)
65:00.1 Audio device: NVIDIA Corporation Device 1aeb (rev a1)
65:00.2 USB controller: NVIDIA Corporation Device 1aec (rev a1)
65:00.3 Serial bus controller [0c80]: NVIDIA Corporation Device 1aed (rev a1)

可以看出我的电脑上有两张显卡,一张是GTX 650 Ti,另一张是NVIDIA Corporation Device 2184。 Google搜索NVIDIA Corporation Device 2184可知这是一张1660。

很有意思的是,我的1660显卡竟然带了一个USB控制器????就是这个控制器给我挖了个好大坑。 相当好奇的研究了一下Nvidia为什么要在显卡上带USB控制器,发现DP 1.2开始竟然是可以传输USB信号的。 坑爹的是,只有标准,没有产品,囧。

我们需要把GTX 1660预留给虚拟机,因此我们记下id:65:00.0

如果你需要预留更多的东西,也都记下来,例如你觉得显卡带的声卡也是必要的。 (实际上,无论这里记不记声卡,最后还是得给声卡带上,因为65:00.1和65:00.0在同一个iommu group)

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

$ dmesg | grep iommu | grep 65:00.0
[    1.210839] pci 0000:65:00.0: Adding to iommu group 34

这说明我们需要预留的iommu group是34。

找出iommu group中的所有设备

这里再次强调,一个iommu group可能带有很多设备,我们只能按照iommu group为单位分配设备给虚拟机。

在上面我们知道,我们需要预留的iommu group是34,那么我们继续:

$ dmesg | grep "iommu group 34"
[    1.210839] pci 0000:65:00.0: Adding to iommu group 34
[    1.210874] pci 0000:65:00.1: Adding to iommu group 34
[    1.210907] pci 0000:65:00.2: Adding to iommu group 34
[    1.210939] pci 0000:65:00.3: Adding to iommu group 34

因此我们知道,我们需要把65:00.0 ~ 65:00.3全部给预留掉。

然后我们需要找出65:00.0 ~ 65:00.3 PCI总线上的设备的id:

$ lspci -nn | grep 65:00
65:00.0 VGA compatible controller [0300]: NVIDIA Corporation Device [10de:2184] (rev a1)
65:00.1 Audio device [0403]: NVIDIA Corporation Device [10de:1aeb] (rev a1)
65:00.2 USB controller [0c03]: NVIDIA Corporation Device [10de:1aec] (rev a1)
65:00.3 Serial bus controller [0c80]: NVIDIA Corporation Device [10de:1aed] (rev a1)

注意右侧中括号中的id,因此我们需要预留的设备id列表为:

10de:2184,10de:1aeb,10de:1aec,10de:1aed

在Linux系统中预留设备

2020-04-07更新:最新Ubuntu 20.04已经将vfio-pci模块编译进默认内核,推荐直接装Ubuntu 20.04,能避免一票子麻烦。

这一步主要有两个方案,一个是老一点的pci-stub,另一个是新一点的vfio-pci,至于这两个的区别可以看这里: https://unix.stackexchange.com/questions/328422/pci-stub-vs-vfio-pci

如果不想折腾的话,推荐直接上pci-stub,动手能力强,喜欢折腾的,可以考虑vfio.

  • pci-stub方案

    我个人倾向于推荐pci-stub方案,因为大部分发行版都直接带了,不想要自己动手。 用pci-stub预留设备非常简单,只需要在kernel启动参数中再加一个pci-stub.ids=即可。 同样,我们打开/etc/default/grub,并且修改GRUB_CMDLINE_LINUX_DEFAULT为:

    GRUB_CMDLINE_LINUX_DEFAULT="quiet splash intel_iommu=on pci-stub.ids=10de:2184,10de:1aeb,10de:1aec,10de:1aed"
    

    注意ids=后面就是我们前面记录下来的设备id列表 修改完了后,记得要sudo update-grub和重启电脑

  • vfio-pci方案

    我这里并不想仔细的描述vfio-pci方案,因为实在是太tmd的折腾了。我在这里只描述vfio-pci方案可能遇到的问题,以及怎么解决,并不列具体怎么处理了。

    Ubuntu在默认情况下,把vfio-pci编译成了一个内核模块,而不是编译进内核。

    Linux现有的驱动机制是每个驱动在初始化时,自行去寻找没有初始化的,自己关注的硬件。因此驱动的初始化顺序在我们的需求里非常的关键。 例如,我们的vfio-pci就必须在nvidia之前初始化,否则话nvidia就会把两张显卡都启用了,然后我们的预留就失败了。

    Linux内核总是先初始化所有内建的驱动,然后才逐个初始化外部模块。 在外部模块中,我们可以通过softdep来增加模块间的依赖关键: 例如我们可以把vfio-pci列为nvidia的依赖,那么vfio-pci总会被在nvidia之前初始化,而确保能抢占到硬件。

    在实施vfio-pci方案之前,我们必须先检查我们需要预留的所有硬件目前是被什么启动使用的。

    $ lspci -nnv
    

    上面的命令会输出大量的内容,节选一段如下:

    17:00.0 VGA compatible controller: NVIDIA Corporation GK106 [GeForce GTX 650 Ti] (rev a1) (prog-if 00 [VGA controller])
            Subsystem: NVIDIA Corporation GK106 [GeForce GTX 650 Ti]
            Flags: bus master, fast devsel, latency 0, IRQ 44, NUMA node 0
            Memory at b4000000 (32-bit, non-prefetchable) [size=16M]
            Memory at a8000000 (64-bit, prefetchable) [size=128M]
            Memory at b0000000 (64-bit, prefetchable) [size=32M]
            I/O ports at 7000 [size=128]
            [virtual] Expansion ROM at 000c0000 [disabled] [size=128K]
            Capabilities: <access denied>
            Kernel driver in use: nvidia
            Kernel modules: nvidiafb, nouveau, nvidia_drm, nvidia
    
    17:00.1 Audio device: NVIDIA Corporation GK106 HDMI Audio Controller (rev a1)
            Subsystem: NVIDIA Corporation GK106 HDMI Audio Controller
            Flags: bus master, fast devsel, latency 0, IRQ 61, NUMA node 0
            Memory at b5080000 (32-bit, non-prefetchable) [size=16K]
            Capabilities: <access denied>
            Kernel driver in use: snd_hda_intel
            Kernel modules: snd_hda_intel
    

    我们可以在上面的输出中找到Kernel driver in use,也就是说:17:00.0目前被nvidia驱动使用,17:00.1目前被snd_hda_intel使用

    如果我们需要预留的所有设备的驱动都是外部模块,那么我们可以通过softdep强制修正驱动的加载顺序。以确保vfio-pci在这些驱动加载之前先加载。 如果我们需要预留的设备中,有某个设备是内核内建驱动驱动的,那我们也有两个选择,要么把vfio-pci编译成内建驱动,要么把相应的驱动从内核剥离出来成为模块。

    很不幸,我遇到的就是第二种情况。由于我的显卡上带了一usb控制器,而usb控制器的驱动xhci_hcd是内建在内核的。

    我最后选择的方式就是把vfio-pci编译进内核。

    如果选择把vfio-pci编译进内核,那么剩下的事情就非常的简单了,和pci-stub方案类似,只需要在内核启动参数中加入:vfio-pci.ids=参数即可。

    如果还是选择从模块加载vfio-pci,那么

    • 首先我们需要修改/etc/initramfs-tools/modules,vfio相关的模块全列进去。以及vfio的参数也在这里
    • 然后在/etc/modprobe.d目录增加一些文件,声明模块间的依赖关系
    • 再然后用sudo update-initramfs -u更新系统的initramfs

完成预留后,我们需要确保预留的设备被正确的预留了

$ lspci -nnv | grep -E "(^\S|Kernel driver in use)" | grep 65:00 -A 1
65:00.0 VGA compatible controller [0300]: NVIDIA Corporation Device [10de:2184] (rev a1) (prog-if 00 [VGA controller])
        Kernel driver in use: vfio-pci
65:00.1 Audio device [0403]: NVIDIA Corporation Device [10de:1aeb] (rev a1)
        Kernel driver in use: vfio-pci
65:00.2 USB controller [0c03]: NVIDIA Corporation Device [10de:1aec] (rev a1) (prog-if 30 [XHCI])
        Kernel driver in use: vfio-pci
65:00.3 Serial bus controller [0c80]: NVIDIA Corporation Device [10de:1aed] (rev a1)
        Kernel driver in use: vfio-pci

注意在上面的输出中,有一个Kernel driver in use, 确保所有需要预留的设备都是被vfio-pci或者pci-stub使用。 如果不是的话,请反复检查到底哪里出错了。如果这一步不正确,请不要继续尝试下面的动作,避免损坏硬件。

创建kvm虚拟机,并将iommu group分配给虚拟机

如果你到了这里了,恭喜恭喜,你已经顺利通关90%了。

下一步我们需要安装kvm虚拟机,kvm虚拟机和平时用的虚拟机其实没有什么大区别,尤其是用图形节目的话。

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

Ubuntu 18.04的qemu默认配置有个bug,貌似selinux起不来。我们需要先给这个处理一下:(其他系统不需要) 打开/etc/libvirt/qemu.conf,找到security_driver所在行,改为:

security_driver = "none"

然后保存,重启libvirt服务:

$ sudo systemctl restart libvirtd.service

在这节里面我不打算去解释例如如何在kvm设置网桥之类的,或者如何改进kvm虚拟机IO性能之类的话题。 因为这和我们这次的主题并没有什么关系。GPU直通搞定了后,大家可以单独慢慢研究琢磨。 我在上面的apt-get中安装网桥和ovmf,如果大家不需要,可以直接删掉。

安装完后,可以直接启用virt-manager,这是一个libvirt的图形管理界面。 起来后,大家直奔创建虚拟机: 1

整个创建过程和普通虚拟机并没有什么区别,就是分配磁盘,设置资源。(除了界面真的不好用) 在创建虚拟机的最后一步,建议勾选Customize configuration before install 2

在CPU型号的选项框中填入:host-passthrough,(这一步在我这里好像不是必须的,但是考虑到可能涉及到cpu差异,我还是写上了) 然后点Begin Installation 3

OK,接下来就是安装Windows的环节了,大家该怎么装怎么装,该怎么调怎么调。安装完Windows后,关机虚拟机,给虚拟机增加PCI-E。

点击虚拟机上工具栏的第二个按钮进入配置界面,点击左下角的Add Hardware,选择PCI Host Device,选择我们要添加的PCI设备。 重复上面的动作多次,直到所有的设备都被添加完成。 注意,必须一次性把整个iommu group的设备全部加入到虚拟机,否则虚拟机会无法启动。 同样,被加入的设备必须是已经被操作系统预留的,否则会直接死机,还可能损坏硬件。 4

最后点保存,找个备用显示器,插入到虚拟机的显卡,开机虚拟机。 开机完后,检查Windows驱动管理器,看看有没有识别到新的设备。 如果你用的是AMD显卡的话,可能到这步就结束了。 如果你用的是Nvidia显卡的话,到这步看到的是不可识别的PCI设备,或者是43错误。我们需要在下面的步骤修正这个问题。

阻止nvidia驱动发现虚拟机

nvidia在他自己带的驱动里,会默认阻止驱动在虚拟机上工作。 我不知道nvidia为什么要这么做,可能是为了避免问题?也有可能是nvidia有什么开发模式之类的。 修正这个问题很容易。

  1. 关机虚拟机
  2. 打开命令行,运行EDITOR=gedit virsh edit your-windows-vm-name
  3. 在打开的编辑器中,添加截图中的几行字 5
  4. 保存退出
  5. 重启虚拟机

设置鼠标键盘显示器

完成了上面的动作之后,整个gpu直通就搞定了。 但是接下来鼠标键盘显示器的问题非常的蛋疼。你有两个选择:

  1. 买两套鼠标键盘显示器

把第二个显示器插到虚拟机挂载的显卡上 把第二套鼠标键盘接到usb,然后usb直通到虚拟机上

如果用这套方案的话,相当与有了两台电脑,emmmm….

  1. 搞个kvm,俺一下切换鼠标键盘显示器

emmmm… 这不也相当于是两台电脑么。

但是用kvm这个方案的话其实还是有点特殊的。如果你接的两个键盘,那么kvm把其中一个键盘分配给虚拟机。 但是如果你用一个键盘+kvm。kvm切换的时候,其实只是相当与键盘鼠标换了个usb插口啊。键盘还是那个键盘,鼠标还是那个鼠标。 要么全分配给了宿主机,要么全分配给了虚拟机,这不就囧了么。

幸运的是,我主板上有2个usb控制器,分别在iommu group 4和13 于是重复一下上面的动作,我把第二个usb控制器也划给了虚拟机。 但是我觉得像我这种情况的可能应该不在多数。一个简单的解决方案是,再买个PCI-E的usb扩展卡,不到100大洋,这样也是一个独立的iommu group。

修正spice模式下的鼠标问题

有时候我们只是想开个小窗口,看看虚拟机里到底发生了啥。而并不想把kvm切换过去看。 这个时候,一个非常好的主意就是留一个kvm的spice显示器,用来观察虚拟机。 如果用的kvm还是比较低端的硬件kvm(不支持虚拟显示器)的话,还可以把显卡直通的输出显示器设置为扩展显示器,在Windows中设置仅显示扩展显示。

但是kvm的spice有点小问题,一旦你把显示器切换到外接显示器,再切换回来后,鼠标就不能正常工作了。 我们可以在kvm的配置里强制spice的鼠标为server模式来绕开这个问题:

  1. 关闭虚拟机
  2. 打开命令行,运行EDITOR=gedit virsh edit your-windows-vm-name
  3. 在打开的编辑器中,添加截图中的内容
    6
  4. 保存退出
  5. 重启虚拟机

本文遵守 CC-BY-NC-4.0 许可协议。

Creative Commons License

欢迎转载,转载需注明出处,且禁止用于商业目的。

下篇如何设计一个平衡树结构