缘起

自前年全面从 Windows 转到 ArchLinux 之后,我的的大部分时间都在 Linux 下,但为了运行 CAD、打游戏,我不得不选择双系统方案

双系统虽然能解决软件兼容性的问题,但每次切换都需要重的体验很割裂,那么虚拟机呢?

行,也不行。虚拟机虽然能跑上面的应用,但那羸弱的 3D 性能,也就只能跑一些 wine 运行不了的软件

这么一来,显卡直通就成了唯一可行的方案

网上显卡直通的教程有很多,其中 Lan Tian 的教程就很不错,但这些教程都有一个共性:显卡在开机时就已经屏蔽了 NVIDIA 驱动,并将其交给了 vfio-pci

这就意味着,每次开机后,如果不先手动卸载 vfio-pci 并重新加载 NVIDIA 驱动,我就无法在 Linux 下使用独显

然而,我并不是每次开机都要运行虚拟机。毕竟当下 Linux 的软件生态已经相当完善,许多游戏也能借助 Wine 与 Proton 顺畅运行。对我而言,更迫切的需求是:找到一种办法,让 NVIDIA 显卡能够在无需重启的情况下自由切换

下文方案的前部分教程和网上的许多教程大体上一致

先决条件

本文是基于 Optimus MUXed 架构的笔记本(型号 Lenovo Legion y7000p 2024,仅供参考)

安装好 libvirt,需要新建一台正常的 UEFI 启动的 Windows 虚拟机

如果你在后续直通和安装驱动时有问题,可尝试关闭虚拟机的安全启动

配置宿主机

为了让虚拟机可以使用宿主机的 PCIe 设备,需要用到 IOMMUvfio-pci 模块

启用IOMMU

为什么要用IOMMU

当操作系统在虚拟机内运行时(包括使用半虚拟化的系统,例如Xen),其通常不知道它要访问的内存的主机物理地址。这使其难以直接访问计算机硬件,因为如果客户机系统尝试用客户机的物理地址进行直接存储器访问(DMA)来吩咐硬件,其可能损坏内存数据,因为硬件不知道给定虚拟机客户机物理地址与主机物理地址之间的映射关系。而由管理程序或主机操作系统介入I/O操作来应用翻译则可以避免损坏,但会增加此I/O操作的延迟。
IOMMU可以依靠将客户机物理地址映射到主机物理地址的相同或兼容转换表重映射硬件访问地址,从而解决延迟问题。

首先,要确保你的 BIOS 已经开启了 VT-x 或者 AMD-v 虚拟化

对于 Intel CPU,需要向内核参数中添加 intel_iommu=oniommu=pt

对于 AMD CPU,则需要添加 amd_iommu=oniommu=pt

Grub

使用 Grub 的用户,在 /etc/default/grub 中添加:

1
2
3
4
5
# Intel CPU
GRUB_CMDLINE_LINUX_DEFAULT="quiet intel_iommu=on iommu=pt ..."

# AMD CPU
GRUB_CMDLINE_LINUX_DEFAULT="quiet amd_iommu=on iommu=pt ..."

UKI

使用 UKI 的用户,在 /etc/kernel/cmdline 中添加:

1
root=UUID=... rw splash loglevel=3... intel_iommu=on iommu=pt

设置 VFIO

修改 mkinitcpio

为了使用 VFIO,我们需要调整 VFIO 和 NVIDIA 驱动的顺序,修改 /etc/mkinitcpio.conf

1
2
3
MODULES=(vfio_pci vfio vfio_iommu_type1 nvidia nvidia_modeset nvidia_uvm nvidia_drm)
# 如果你的内核版本小于 6.2, 那你可你需要添加 vfio_virqfd 这个模块
# 为了在有需要的时候才直通显卡,需要保留此处的 nvidia 驱动

重新生成 Initramfs

#

至此,宿主机的配置就算完成了

配置虚拟机

这部分我会略去一些特殊情况和问题,如果有没办法解决的问题,可以去看看 Lan Tian 的两篇教程

反虚拟化

为了避免某些应用的虚拟机检测,需要编辑虚拟机的XML,在 <features> 段内添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
<features>
<acpi/>
<apic/>
<hyperv mode="custom">
...
</hyperv>
<!--新增-->
<kvm>
<hidden state="on"/>
</kvm>

<vmport state="off"/>
</features>

添加假电池

在直通后,虚拟机并不能直接使用 NVIDIA 显卡,可能是由于笔记本的限制,显卡只有在有假电池的情况使用

<domain>段添加自定义的 qemu 指令:

1
2
3
4
5
6
7
8
<!--请不要忘了添加 xmlns:qemu="http://libvirt.org/schemas/domain/qemu/1.0" 否则无法保存!-->
<domain xmlns:qemu="http://libvirt.org/schemas/domain/qemu/1.0" type="kvm">
...
<qemu:commandline>
<qemu:arg value="-acpitable"/>
<qemu:arg value="file=/opt/UserItems/ssdt.dat"/>
</qemu:commandline>
</domain>

上面 /opt/UserItems/ssdt.dat 为修改后的ACPI表,来自 Lan Tian 的教程

添加 PCIe

依旧是在 <domain> 段内添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
<domain xmlns:qemu="http://libvirt.org/schemas/domain/qemu/1.0" type="kvm">
...
<hostdev mode='subsystem' type='pci' managed='yes'>
<source>
<address domain='0x0000' bus='0x01' slot='0x00' function='0x0'/>
</source>
<rom bar='off'/>
<address type='pci' domain='0x0000' bus='0x01' slot='0x00' function='0x0' multifunction='on'/>
</hostdev>
<!--禁用内存动态伸缩,影响性能-->
<memballoon model="none"/>
...
</domain>

请确保显卡的总线只能是上面的,如果提示冲突,请删掉占用这个地址的那个设备的 <address .../>段,让其重新分配一个新的地址

启动直通!

目前,配置好的虚拟机还不能启动,现在需要我们先手动卸载 NVIDIA 驱动,将 NVIDIA 交给 VFIO

手动切换显卡

  1. 手动关闭所有占用 NVIDIA 显卡的进程,进程可通过 lsof 查看:

    $
  2. 暂停 nvidia 服务:

    $
  3. 卸载 NVIDIA 驱动:

    $
  4. 绑定 VFIO:

    $

    此处的 ids=xxx 需要所有的 NVIDIA 设备都交给 VFIO,通过 lspci -nn | grep NVIDIA 查看,多个设备使用 , 分隔

    我的设备在 Linux 下只有 VGA,并没有出现 Audio 设备

完成上面的步骤后,通过下面的指令查看显卡是否成果交给 VFIO:

$

如果出现类似下面的信息,即为成功:

1
2
[  170.607580] vfio-pci 0000:01:00.0: vgaarb: VGA decodes changed: olddecodes=none,decodes=io+mem:owns=none
[ 170.607741] vfio_pci: add [10de:28e0[ffffffff:ffffffff]] class 0x000000/00000000

此时,可以开启虚拟机了

画面显示

为了把虚拟机的画面传输到宿主机,大体上有两种方法:

  • 使用 Sunshine、Parsec 串流
  • Looking Glass 直出画面

无论上面那种方法,都需要先安装一个虚拟显示器软件比如 VDD,串流方案比较简单,不需要再配置虚拟机

Looking Glass

我在这里介绍一下 Looking Glass 方案,虚拟机上只需要安装 Looking Glass Host 版,宿主机上的配置有下面两种办法

KVMFR

这是官方推荐的方案,通过 DMA 传输数据,能提供更好的画面和更好的帧率

安装模块
$
  1. 设置开机加载 kvmfr 模块,创建 /etc/modules-load.d/looking-glass.conf

    1
    kvmfr
  2. 创建 /etc/modprobe.d/looking-glass.conf 内容如下:

    1
    options kvmfr static_size_mb=计算出的数值

    数值的计算方法:${\frac{分辨率宽\times 分辨率高\times4\times2}{1024\times1024}}$,将计算出的结果以 ${2^n}$ 向上取整的整数

    如分显示器辨率为 2560x1600,其结果为:
    $${\frac{2560\times1600\times4\times2}{ 1024 \times 1024 } =31.25}$$
    最接近 31.25 的是 ${2^6= 64}$,因此上面的值为 64

  3. /dev/kvmfr0 赋予访问权,创建 udev 规则 /etc/udev/rules.d/99-kvmfr.rules

    1
    SUBSYSTEM=="kvmfr", OWNER="qemu", GROUP="kvm", MODE="0660"

    其中的 OWNER 不要写任何 uid > 1000 的用户,不然该 udev 规则不会生效,请确保当前用户在 kvm 组内,重启后生效

设置虚拟机

编辑虚拟机的 XML,在 <domain> 段内添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
  ...
<qemu:commandline>
<qemu:arg value="-acpitable"/>
<qemu:arg value="file=/opt/UserItems/ssdt1.dat"/>
<!--添加下面的内容-->
<qemu:arg value="-device"/>
<qemu:arg value="{'driver':'ivshmem-plain','id':'shmem0','memdev':'looking-glass'}"/>
<qemu:arg value="-object"/>
<qemu:arg value="{'qom-type':'memory-backend-file','id':'looking-glass','mem-path':'/dev/kvmfr0','size':67108864,'share':true}"/>
<!--这里的 size 和上面的一样-->
</qemu:commandline>
</domain>
修改客户端

修改全局设置 /etc/looking-glass-client.ini

1
2
[app]
shmFile=/dev/kvmfr0

如果使用该方案,就不能使用 virtiofs 共享文件,只能选择 SMB、NFS 或者硬盘直通,如果就是想用 virtiofs,那需要使用下面的方案

shmem

  1. 编译虚拟机的 XML,在 <device> 段内添加:

    1
    2
    3
    4
    5
    6
    7
    8
            ...
    <shmem name='looking-glass'>
    <model type='ivshmem-plain'/>
    <size unit='M'>64</size>
    </shmem>
    </device>
    ...
    </domain>
  2. 修改权限,创建 /etc/tmpfiles.d/looking-glass.conf

    1
    f /dev/shm/looking-glass 0660 vconet kvm -

    请把 vconet 改为自己的用户名,运行 sudo systemd-tmpfiles /etc/tmpfiles.d/looking-glass.conf --create 生效

之后你可以添加 virtiofs 了,大概… (我没有测试)

键鼠

这一部分是为了解决 Looking Glass 糟糕的键鼠性能:丢包,根本没法玩游戏

解决办法比较粗暴,我有一套单独的键鼠,直通后不影响笔记本的键盘和触控板

为了保证随时可以切换连接状态,不应在 virt-manager 里直接添加 USB 主机设备,而是用 virsh 这个 CLI 工具

连接:

$

断开:

$

定义键鼠的 XML 格式如下:

1
2
3
4
5
6
<hostdev mode="subsystem" type="usb" managed="yes">
<source>
<vendor id="0x3554"/>
<product id="0xfa09"/>
</source>
</hostdev>

重新启用 NVIDIA 显卡

至此,虚拟机已经用上 NVIDIA 显卡了,但如何把显卡重连回 Linux?

步骤和上面的差不多

  1. 卸载 vfio-pic 驱动:

    $
  2. 重新加载 NVIDIA 驱动:

    $
  3. 重启 nvidia 服务:

    $

GUI

虽然上面的方法实现了自由切换,但我不想每次都手动完成

于是,我用 PySide6 写了一个简单的 GUI 用来简化流程,毕竟点点点总比一个个输方便

toolbox
toolbox

我把它放在 Github 上了,有需要的可以自行下载使用

结尾

文章到这里就暂时结束了,如果我后期有什么新的需求,会在继续这里更新
后续可能更新 Apparmor 和 Samba 相关的部分…