什么是VGA Passthrough

经常使用Linux的用户一般都会保留至少一个手段能继续使用Windows操作系统。原因多种多样,可能是要使用专业软件(并且Wine或者CrossOver的支持不够好),也可能是因为要使用QQ等较为特殊情景的软件,又或者是需要网银支付缺要求安装浏览器控件等等。不过一般来说这些都能通过虚拟机等手段解决。

然而对于需要使用GPU加速的情景,比如玩游戏(大多数游戏还是不支持Linux,或者干脆是老游戏)或者使用专业的软件需要GPU加速时,似乎就比较困难了。专有方案有VMWare的Workstation,其独有的设备也能提供一定性能的GPU加速,但和原生比起来依旧差强人意。

既然在其他虚拟机方案下有USB主控端口等设备直接将控制权交给虚拟机来控制的先例。那么GPU是否也可以做到呢?答案是肯定的,只不过GPU的控制权转交更加复杂一点。原理上同样是将PCI-Express的主控端口从宿主机中隔离出来,并通过某种方式转给虚拟机,这种在Linux下就叫做PCI Passthrough,进一步将显卡转交就叫做VGA Passthrough,这需要操作系统内核、PCI驱动以及虚拟化软件技术的支持。索性在该功能在Linux内核下是支持的。

本质上Linux操作系统在访问设备时都是访问的主内存地址,CPU通过MMU即虚拟内存地址访问,而外部设备(除了CPU、内存以外的都可以看成是“外部设备”)通过IOMMU,映射到内存。所以第一步基本要求,即正在使用的CPU、主板(芯片组)要支持IOMMU。这方面需要用户自己翻阅设备说明书或者厂商的规格说明。

比较遗憾的是,Intel中低端的CPU以及芯片组都不支持IOMMU,甚至高端的CPU中也有个别型号是不支持的 ,所以Intel的用户请查询ark.intel.com翻阅使用的CPU以及芯片组是否支持,Intel的这项技术叫做 VT-d 。而AMD方面一如既往和支持虚拟化硬件指令的AMDV(Intel的叫做VT-x)类似,甚至入门级的CPU与芯片组都完整支持IOMMU,具体的还需要翻阅相应的资料加以确认。

VGA Passthrough在Gentoo下的应用

准备工作

笔者在这里使用的恰巧支持。配置是

  • CPU - Intel Core i7 4770

  • 芯片组 - Z87 (Intel官方资料宣称Z87不支持VT-d,但这块主板上实测支持)

  • 主板 - Gigabyte GA-Z87X-HD3

  • 显卡 - 蓝宝石 R9 290X Tri-X

  • 32GiB 内存

首先第一步,欲使用VGA Passthrough, 机子上必须至少有2个GPU ,因为用于直通虚拟机的GPU需要从宿主机隔离掉,那么宿主机显示只能使用另外一颗GPU了。笔者在这里宿主机使用CPU内置自带的Intel HD 4600,将AMD显卡直通留给虚拟机。

内核方面,若使用vfio驱动的话内核要求4.1以上。4.1以前的内核只能使用pci-stub来隔离PCI端口了。太老的内核比如比3.0以前的笔者怀疑会有问题。当然现在主流发行版2014年以后一般至少都已经用到3.2以上。笔者的配置为

  • 内核 gentoo-sources 4.12.12

  • 内核驱动 intel i915

  • 内核驱动 radeon admgpu (带上这两个是为了能不直通的时候,主机也能获得良好的GPU加速)

  • qemu 2.10

内核模块部分参考下面

## Intel CPU
Device Drivers --->
    [*] IOMMU Hardware Support  --->
        [*]   Support for Intel IOMMU using DMA Remapping Devices
Bus options (PCI etc.)  --->
    <M>   PCI Stub driver

## AMD CPU
Device Drivers --->
    [*] IOMMU Hardware Support  --->
        [*]   AMD IOMMU support
Bus options (PCI etc.)  --->
    <M>   PCI Stub driver

此外,为了使用显卡直通还需要额外配置几个选项

  • src_shell{CONFIG_IRQ_REMAP}

  • src_shell{CONFIG_VFIO}

  • src_shell{CONFIG_VFIO_PCI}

  • src_shell{CONFIG_VFIO_PCI_VGA}

软件配置完毕后,我们还需要将机箱上两个显卡的输出到显示器准备。有两台显示器最佳,这样处理操作起来很直观。如果只有一台显示器,有多个信号输入也可以。比如笔者是将主板上HDMI接出,显卡上的DisplayPort也接到同一台显示器,通过选择输入源来查看不同的画面输出。

配置

内核启动参数添加IOMMU的开关配置

## Intel
iommu=on intel_iommu=on

##
iommu=on amd_iommu=on

比较重要的一步是在使用显卡直通时,需要防止宿主机直接对想要直通GPU的直接启用。需要禁用内核的显卡模块(前提是不要builtin,而是作为模块)。编辑 src_shell{/etc/modprobe.d/radeon.conf}

blacklist radeon amdgpu

或者在bootloader的配置中加入内核启动参数

radeon.blacklist=1 amdgpu.blacklist=1

如果读者与笔者一样,使用的是tty终端,再自行startx的话,需要指定终端设备,添加内核启动参数

## 使用Intel内置GPU
fbcon=map:0

## 不用直通的时候,使用独立GPU
fbcon=map:1

最终得到的内核配置类似

## Intel
vmlinuz iommu=on intel_iommu=on radeon.blacklist=1 amdgpu.blacklist=1 fbcon=map:0

## AMD
vmlinuz iommu=on intel_iommu=on radeon.blacklist=1 amdgpu.blacklist=1 fbcon=map:0

VFIO模块的配置在Gentoo下可以不需要配置,在后面使用章节时会自动匹配加载 。如果实际使用中遇到问题,可以考虑把以下模块放到自动加载列表。

  • src_shell{vfio}

  • src_shell{vfio_iommu_intel}

  • src_shell{vfio_pci}

配置完成后重启系统,并检查内核日志

dmesg | grep -e "Directed I/O"

## 得到输出类似
DMAR: Intel(R) Virtualization Technology for Directed I/O

检查GPU驱动模块是否被加载,应该不能加载,否则后面一部绑定PCI设备时会报错哦。

lsmod | grep -i -e 'radeon' -e 'amdgpu'

修改系统限制配置,允许分配更多内存给某一用户,修改 src_shell{/etc/security/limits.conf}

<username> soft memlock 80000000
<username> hard memlock 80000000

最后,准备一个VFIO绑定PCI设备用的工具脚本,内容如下。放在 src_shell{/usr/local/bin/vfio-bind} 。

#!/bin/bash
modprobe vfio-pci
for dev in "$@"; do
        vendor=$(cat /sys/bus/pci/devices/$dev/vendor)
        device=$(cat /sys/bus/pci/devices/$dev/device)
        if [ -e /sys/bus/pci/devices/$dev/driver ]; then
                echo $dev > /sys/bus/pci/devices/$dev/driver/unbind
        fi
        echo $vendor $device > /sys/bus/pci/drivers/vfio-pci/new_id
done

启用

如果遇到问题,请第一时间检查内核消息是否有异常(命令 src_shell{dmesg} )。

第一步,找出IOMMU的分组情况,由于原理上必须把一整个组的PCI设备绑定给VFIO驱动,借以直通分配给虚拟机。所以和显卡设备同一组的设备,宿主机也无法使用了。具体哪些设备是根据具体主板上硬件来的,所以并没有固定的用法。

使用以下命令检查

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 echo -n $'\t'; lspci -nns "$device"; done; done

在笔者的设备上得到如下输出

IOMMU group 7
	00:1b.0 Audio device [0403]: Intel Corporation 8 Series/C220 Series Chipset High Definition Audio Controller [8086:8c20] (rev 05)
IOMMU group 15
	04:00.0 PCI bridge [0604]: Intel Corporation 82801 PCI Bridge [8086:244e] (rev 41)
IOMMU group 5
	00:16.0 Communication controller [0780]: Intel Corporation 8 Series/C220 Series Chipset Family MEI Controller #1 [8086:8c3a] (rev 04)
IOMMU group 13
	00:1f.0 ISA bridge [0601]: Intel Corporation Z87 Express LPC Controller [8086:8c44] (rev 05)
	00:1f.2 SATA controller [0106]: Intel Corporation 8 Series/C220 Series Chipset Family 6-port SATA Controller 1 [AHCI mode] [8086:8c02] (rev 05)
	00:1f.3 SMBus [0c05]: Intel Corporation 8 Series/C220 Series Chipset Family SMBus Controller [8086:8c22] (rev 05)
IOMMU group 3
	00:03.0 Audio device [0403]: Intel Corporation Xeon E3-1200 v3/4th Gen Core Processor HD Audio Controller [8086:0c0c] (rev 06)
IOMMU group 11
	00:1c.4 PCI bridge [0604]: Intel Corporation 8 Series/C220 Series Chipset Family PCI Express Root Port #5 [8086:8c18] (rev d5)
IOMMU group 1
	00:01.0 PCI bridge [0604]: Intel Corporation Xeon E3-1200 v3/4th Gen Core Processor PCI Express x16 Controller [8086:0c01] (rev 06)
	01:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Hawaii XT / Grenada XT [Radeon R9 290X/390X] [1002:67b0]
	01:00.1 Audio device [0403]: Advanced Micro Devices, Inc. [AMD/ATI] Hawaii HDMI Audio [Radeon R9 290/290X / 390/390X] [1002:aac8]
IOMMU group 8
	00:1c.0 PCI bridge [0604]: Intel Corporation 8 Series/C220 Series Chipset Family PCI Express Root Port #1 [8086:8c10] (rev d5)
IOMMU group 16
	06:00.0 Audio device [0403]: C-Media Electronics Inc CM8888 [Oxygen Express] [13f6:5011]
IOMMU group 6
	00:1a.0 USB controller [0c03]: Intel Corporation 8 Series/C220 Series Chipset Family USB EHCI #2 [8086:8c2d] (rev 05)
IOMMU group 14
	03:00.0 Ethernet controller [0200]: Realtek Semiconductor Co., Ltd. RTL8111/8168/8411 PCI Express Gigabit Ethernet Controller [10ec:8168] (rev 06)
IOMMU group 4
	00:14.0 USB controller [0c03]: Intel Corporation 8 Series/C220 Series Chipset Family USB xHCI [8086:8c31] (rev 05)
IOMMU group 12
	00:1d.0 USB controller [0c03]: Intel Corporation 8 Series/C220 Series Chipset Family USB EHCI #1 [8086:8c26] (rev 05)
IOMMU group 2
	00:02.0 VGA compatible controller [0300]: Intel Corporation Xeon E3-1200 v3/4th Gen Core Processor Integrated Graphics Controller [8086:0412] (rev 06)
IOMMU group 10
	00:1c.3 PCI bridge [0604]: Intel Corporation 8 Series/C220 Series Chipset Family PCI Express Root Port #4 [8086:8c16] (rev d5)
IOMMU group 0
	00:00.0 Host bridge [0600]: Intel Corporation 4th Gen Core Processor DRAM Controller [8086:0c00] (rev 06)
IOMMU group 9
	00:1c.2 PCI bridge [0604]: Intel Corporation 8 Series/C220 Series Chipset Family PCI Express Root Port #3 [8086:8c14] (rev d5)

我们可以看到显卡GPU在IOMMU组号1下面,同样的还有PCI-E控制器设备与显卡的音频设备。这里请记下PCI设备号 src_shell{00:01.0 01:00.0 01:00.1}。

接下来将这些设备通过VFIO驱动绑定。

## 请注意这里给设备号前面加上高位地址号 0000:
$ vfio-bind 0000:00:01.0 0000:01:00.0 0000:01:00.1

如果这一步做完后设备出现死机,则说明宿主机对该设备仍有访问,需要检查是否正确禁用了驱动。

接下来,如果使用普通用户对VFIO设备进行访问,还需要对设备结点文件权限进行修改。

$ sudo chown /dev/vfio/1

也可以通过udev的规则实现自动修改,笔者暂时还没有研究出正确的配置

安装虚拟机与使用

以上步骤正确设置完毕后,我们开始启用qemu的KVM虚拟机。

第一次启动安装

#!/bin/bash

qemu-system-x86_64 -enable-kvm -m 8192 \
  -cpu host,kvm=off \
  -smp 8,sockets=1,cores=8 \
  -drive file=/dev/vg-win7/win7-root,format=raw,if=virtio,cache=none \
  -soundhw hda \
  -netdev tap,id=t0,ifname=tap0,script=no,downscript=no \
  -device virtio-net,netdev=t0,mac=72:d4:35:18:07:01 \
  -usbdevice tablet \
  -vga std \
  -drive file=/mnt/home/tools/system/windows/win7sp1_ultimate_x64_en_amd88usb3.iso,media=cdrom \
  -drive file=virtio-win-0.1.141.iso,media=cdrom

此时虚拟机和平时使用的并无二异,使用的是GTK界面的图形,完成操作系统的安装。

参数说明

  • src_shell{-enable-kvm} 启用KVM硬件虚拟化

  • src_shell{-cpu host,kvm=off} 在虚拟机的CPU参数中消除KVM的标志,对于某些显卡驱动安装程序隐藏掉这点,比如Nvidia的显卡驱动就会检查是否是虚拟机从而导致安装失败

  • src_shell{-drive file=/dev/vg-win7/win7-root,format=raw,if=virtio} 使用物理设备作为硬盘,如果是SSD的话,会得到更好的性能。同时这里使用的是LVM卷,对块设备使用更加灵活。 特别需要提醒的是:这里qemu的硬盘设备是不支持BTRFS的subvolume的,因为后者实际上是文件系统,而不是块设备 。 对于LVM的使用可以参考笔者以前的文章。

  • src_shell{-usbdevice tablet} 不带这个选项,虚拟机启动后外面鼠标的移动精度和内部鼠标将不一致,无法使用。

  • src_shell{-netdev tap … -device virtio-net} 这里笔者使用tap设备桥接到外部,需要额外配置一些网络基础设施,如果懒得弄也可以使用 src_shell{-netdev user} 直接用虚拟机自带的NAT网络。

  • src_shell{-drive file=virtio-win-0.1.141.iso,media=cdrom} virtio设备的驱动,可以从fedora的页面进行下载。

系统安装配置完成后,修改参数, 启动GPU直通

qemu-system-x86_64 -enable-kvm -m 8192 \
  -cpu host,kvm=off \
  -smp 8,sockets=1,cores=8 \
  -drive file=/dev/vg-win7/win7-root,format=raw,if=virtio,cache=none \
  -soundhw hda \
  -netdev tap,id=t0,ifname=tap0,script=no,downscript=no \
  -device virtio-net,netdev=t0,mac=72:d4:35:18:07:01 \
  -usbdevice tablet \
  -vga none \
  -device vfio-pci,host=01:00.0,x-vga=on \
  -device vfio-pci,host=01:00.1

这里传递的两个设备号,一个是显卡的,所以带上标志标明是显示设备,还有一个是显卡的音频设备,这样也可以输出声音。启动正常的话,应该可以在显卡连接的那台显示器上看到画面输出。使用Windows的设备管理器也可以看到一个未安装的新设备。

重点是接下来虚拟机的输入设备问题 ,此时虚拟机是没有输入设备的。因为显示到另外一台显示器上去(准确说是另外一颗GPU),临时方案可以使用QEMU自带的VNC,默认也开启了。

$ vncviewer localhost

但是这样显然无法满足玩游戏的需求。网上的方案有三个

  • 使用另外一套键盘鼠标,并使用 src_shell{evedv passthrough} 将X11的输入设备直通给虚拟机。可行,但桌子上还得另外搞一套设备,比较占地方来回切换也麻烦。

  • KVM切换,即专用的键盘鼠标切换器来实现一套输入设备使用一个开关来回拨动在多台物理主机上切换。可行,但需要另外买一台设备,同样占用桌面空间。

  • 使用远程输入,这里有一个免费可行的方案是 synergy ,一个开源的小软件,相信在使用多设备的用户可能都听过使用过。

笔者选择使用 synergy ,首先在宿主机与虚拟机里都装上软件,并设置宿主机为服务端,虚拟机里为客户端,连接起来。需要注意的是 synergy 默认会将多个屏幕连起来计算鼠标的“绝对坐标”, 所以在玩FPS这种始终会将鼠标箭头归位的游戏时会有问题 。此时需要做两个设置,一个是将鼠标位置的汇报改为“相对坐标”,第二个是当鼠标在某个屏幕上时按下 ScreenLock 键,会将输入锁定在当前屏幕。这样玩 FPS 时输入就没问题了。

笔者实测R9 290X直通玩求生之路2时,特效配置与Windows原生开的一样,除了抗狗牙2x,VSync关闭(因为笔者使用了支持FreeSync的显示器,所以不需要;而且默认开了的话FPS玩起来总觉得画面延迟严重),其他均为最高。绝大多数场景下FPS都能超过100,和Windows下感觉区别并不大。

此外由于是直通连接,GPU也支持显示器设备的FreeSync,144hz完全没有任何问题。

VGA Passthrough相信是对Linux玩家的一大福音吧。下次购买新硬件的时候,也许会把支持IOMMU也列入必备选项呢。

参考资料