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的图形管理界面。
起来后,大家直奔创建虚拟机:
整个创建过程和普通虚拟机并没有什么区别,就是分配磁盘,设置资源。(除了界面真的不好用)
在创建虚拟机的最后一步,建议勾选Customize configuration before install
在CPU型号的选项框中填入:host-passthrough
,(这一步在我这里好像不是必须的,但是考虑到可能涉及到cpu差异,我还是写上了)
然后点Begin Installation
OK,接下来就是安装Windows的环节了,大家该怎么装怎么装,该怎么调怎么调。安装完Windows后,关机虚拟机,给虚拟机增加PCI-E。
点击虚拟机上工具栏的第二个按钮进入配置界面,点击左下角的Add Hardware
,选择PCI Host Device
,选择我们要添加的PCI设备。
重复上面的动作多次,直到所有的设备都被添加完成。
注意,必须一次性把整个iommu group的设备全部加入到虚拟机,否则虚拟机会无法启动。
同样,被加入的设备必须是已经被操作系统预留的,否则会直接死机,还可能损坏硬件。
最后点保存,找个备用显示器,插入到虚拟机的显卡,开机虚拟机。 开机完后,检查Windows驱动管理器,看看有没有识别到新的设备。 如果你用的是AMD显卡的话,可能到这步就结束了。 如果你用的是Nvidia显卡的话,到这步看到的是不可识别的PCI设备,或者是43错误。我们需要在下面的步骤修正这个问题。
阻止nvidia驱动发现虚拟机
nvidia在他自己带的驱动里,会默认阻止驱动在虚拟机上工作。 我不知道nvidia为什么要这么做,可能是为了避免问题?也有可能是nvidia有什么开发模式之类的。 修正这个问题很容易。
- 关机虚拟机
- 打开命令行,运行
EDITOR=gedit virsh edit your-windows-vm-name
- 在打开的编辑器中,添加截图中的几行字
- 保存退出
- 重启虚拟机
设置鼠标键盘显示器
完成了上面的动作之后,整个gpu直通就搞定了。 但是接下来鼠标键盘显示器的问题非常的蛋疼。你有两个选择:
- 买两套鼠标键盘显示器
把第二个显示器插到虚拟机挂载的显卡上 把第二套鼠标键盘接到usb,然后usb直通到虚拟机上
如果用这套方案的话,相当与有了两台电脑,emmmm….
- 搞个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模式来绕开这个问题:
- 关闭虚拟机
- 打开命令行,运行
EDITOR=gedit virsh edit your-windows-vm-name
- 在打开的编辑器中,添加截图中的内容
- 保存退出
- 重启虚拟机