Optimus MUXed 笔记本上的 NVIDIA 虚拟机显卡直通(2023-05 更新)

一年前,为了能够一边用 Arch Linux 浏览网页、写代码,一边用 Windows 运行游戏等没法在 Linux 上方便地完成的任务,我试着在我的联想 R720 游戏本上进行了显卡直通。但是由于那台电脑是 Optimus MUXless 架构(前文有各种架构的介绍),也就是独显没有输出端口、全靠核显显示画面,那套配置的应用受到了很大的阻碍,最后被我放弃。

但是现在,我换了台新电脑。这台电脑的 HDMI 输出接口是直连 NVIDIA 独立显卡的,也就是 Optimus MUXed 架构。在这种架构下,有办法让虚拟机识别到一个「独显上的显示器」,从而正常启用大部分功能。于是,我终于可以配置出一套可以长期使用的显卡直通配置。

更新日志

  • 2023-05-08:针对新版 Looking Glass B6 更新部分内容。
  • 2022-01-26:PCIe 省电补丁实测无效。

准备工作

在按照本文进行操作前,你需要准备好:

  1. 一台 Optimus MUXed 架构的笔记本电脑。我的电脑型号是 HP OMEN 17t-ck000(i7-11800H,RTX 3070)。

    • (2022-01)我用的操作系统是 Arch Linux,更新到最新版本。
    • (2023-05)本次更新时我用的操作系统是 NixOS,但大部分步骤同样适用于其它 Linux 发行版。
    • 建议关闭安全启动功能,但既然你已经装上了 Linux,你大概率已经关掉了。安全启动理论上可能会对 PCIe 直通功能造成一定的限制。
  2. 用 Libvirt(Virt-Manager)配置好一台 Windows 10 或 Windows 11 的虚拟机,我用的是 Windows 11。

    • 我的虚拟机用的是 UEFI(OVMF)模式启动,但理论上用 BIOS 方式(SeaBIOS)也可以。这次的步骤没有必须用 UEFI 启动方式的地方。
    • 一定要关闭虚拟机的安全启动!不然有些驱动装不上!
      • Windows 11 安装程序会检测安全启动是否开启,关闭安全启动后可能会提示计算机不兼容,拒绝安装。此时可以参照这篇文章解决问题:https://sysin.org/blog/windows-11-no-tpm/
    • 先配置好 QXL 虚拟显卡,保证自己可以看得到虚拟机的视频输出。
  3. (可选)根据电脑视频输出接口的不同,一个 HDMI,DP,或 USB Type-C 接口的假显示器(诱骗接头),淘宝上一般几块到十几块钱一个。

    • (2023-05)或者你也可以选择安装虚拟显示器驱动。
    • HDMI 假显示器
  4. (可选)外接一套 USB 键鼠套装。

开始操作之前,预先提醒:

  • 整个步骤中会多次重启宿主系统,同时一些操作存在导致宿主系统崩溃的风险,请备份好你的数据。
  • 整个步骤中你不需要手动下载任何 NVIDIA 显卡驱动,交给 Windows 自动下载就好。
    • 如果 Windows 自动下载失败,手动安装驱动的底线是下载驱动 EXE 然后双击安装。
    • 千万不要在设备管理器中手动指定设备安装。
    • 手动安装显卡驱动有时反而会干扰判断。

购买 Optimus MUXed 架构的新电脑

如果你有兴趣尝试显卡直通,并正准备购买一台新电脑,你可以参考以下我的选择方法。

显卡直通的前提条件是:

  1. NVIDIA 独立显卡本身要具有视频输出功能
  2. 机身上至少有一个连接到独立显卡的视频接口

但是,游戏本厂商很少会在宣传页上写明视频接口连接的是独显还是核显。因此我们只能根据常见的参数进行推测:

  1. 优先选择支持「独显直连内屏」的电脑,因为这种情况下独显一定具有视频输出功能,并且厂家大概率会将机身视频接口连接到独显上。

    • 典型的例子包括:2020 和 2021 款的联想拯救者系列、惠普暗影精灵系列,以及戴尔游侠 G15。
    • ** 但我不保证这些例子是准确的!** 请自行查阅资料或询问客服,确保电脑支持「独显直连」功能。
  2. 或者选择带有中高端独立显卡的电脑,一般 NVIDIA 显卡型号要以 60 或以上结尾。

    • 中高端 NVIDIA 显卡一般都有视频输出功能,此时厂家大概率会将机身视频接口连接到独显上。
    • 请勿购买显卡型号以 50 或以下结尾的电脑,例如 RTX 3050,GTX 1650 Ti 等等。它们大概率不支持视频输出。
  3. 用好七天无理由退货服务。

    • 因为厂家不会宣传、甚至不会特别在意视频输出接口的连接方式,我们只能看配置参数和宣传页盲猜。因此,你完全可能按照以上规则挑选到一台无法进行显卡直通的电脑,此时可以考虑退货或者转卖。
    • 在一些国家(包括中国),笔记本电脑厂家对无理由退货的要求都是「电脑自带的 Windows 和 Office 都没有联网激活」,而最新的 Windows 11 在首次启动的配置向导中会强制联网激活,因此可以考虑用 U 盘或移动硬盘上的 Linux 启动电脑测试,通过后再激活 Windows。

关于 Intel GVT-g 虚拟核显

Intel 第五代到第九代的 CPU 核显都支持对显卡本身进行虚拟化,也就是划分出几个虚拟的显卡,将虚拟显卡直通进虚拟机、让虚拟机享受显卡加速的同时,允许宿主机同时使用显卡进行显示。

但是 Linux 下的 GVT-g 驱动不支持第十代及更新的 CPU,而且 Intel 也没有支持的计划。再加上 GVT-g 虚拟显卡无法和 NVIDIA 独显组成 Optimus 结构,它也没有什么用。

所以,我们不用管 GVT-g 了,只直通 NVIDIA 独显就好。

(2023-05)关于 Intel 核显 SR-IOV 虚拟化

Intel 十一代及之后的 CPU 核显使用另一种虚拟化方式:SR-IOV。Intel 官方已经发布了 SR-IOV 的内核模块代码,但尚未合入 Linux 主线。有第三方项目将这部分内核代码移植成 DKMS 模块,但根据 Issues 反馈成功率不高,我在 i7-11800H 上测试也没成功。所以,本文将不涉及 Intel 核显的 SR-IOV 功能。

操作步骤

禁止宿主系统管理 NVIDIA 独显

这一段的大部分内容和 2021 年的这篇文章是一样的。

宿主系统上的 NVIDIA 的驱动会占用独显,阻止虚拟机调用它,因此需要先用 PCIe 直通用的 vfio-pci 驱动替换掉它。

禁用 NVIDIA 驱动,把独显交给处理虚拟机 PCIe 直通的内核模块管理的步骤如下:

  1. 运行 lspci -nn | grep NVIDIA,获得类似如下输出:

    1
    2
    0000:01:00.0 VGA compatible controller [0300]: NVIDIA Corporation GA104M [GeForce RTX 3070 Mobile / Max-Q] [10de:249d] (rev a1)
    0000:01:00.1 Audio device [0403]: NVIDIA Corporation GA104 High Definition Audio Controller [10de:228b] (rev a1)

    这里的 [10de:249d] 就是独显的制造商 ID 和设备 ID,其中 10de 代表这个 PCIe 设备由 NVIDIA 生产,而 249d 代表这是张 3070。228b 是 HDMI 接口的音频输出,也需要用 vfio-pci 驱动接管。

  2. 创建 /etc/modprobe.d/lantian.conf,添加如下内容:

    1
    options vfio-pci ids=10de:249d,10de:228b

    vfio-pci 这个负责 PCIe 直通的内核驱动一个配置,让它去管理独显。ids 参数就是要直通的独显的制造商 ID 和设备 ID。

  3. 修改 /etc/mkinitcpio.conf,在 MODULES 中添加以下内容:

    1
    MODULES=(vfio_pci vfio vfio_iommu_type1 vfio_virqfd)

    删除 nvidia 等与 NVIDIA 驱动相关的内核模块,或者确保它们排在 VFIO 驱动后面。这样 PCIe 直通模块就会在系统启动的早期抢占独显,阻止 NVIDIA 驱动后续占用独显。

  4. 运行 mkinitcpio -P 更新 initramfs。

  5. 重启电脑。

(2023-05)如果你用的是 NixOS 系统,可以直接使用下面的配置:

1
2
3
4
5
6
7
8
9
{
boot.kernelModules = ["vfio-pci"];
boot.extraModprobeConfig = ''
# 这里改成你的显卡的制造商 ID 和设备 ID
options vfio-pci ids=10de:249d
'';

boot.blacklistedKernelModules = ["nouveau" "nvidiafb" "nvidia" "nvidia-uvm" "nvidia-drm" "nvidia-modeset"];
}

配置 NVIDIA 独显直通

2021 年的这篇文章中,我在这里介绍了一大堆绕过 NVIDIA 驱动限制的内容。但是从 465 版本开始,NVIDIA 解除了大部分的限制,理论上来说现在直接把显卡直通进虚拟机就能用。

但也只是理论上而已。

我依然建议大家做完所有的隐藏虚拟机的步骤,因为:

  1. (2022-01)对于笔记本电脑来说,NVIDIA 并没有解除所有的限制。

    • 至少在我测试时,显卡的 PCIe 总线位置和系统是否存在电池依然会导致直通失败、驱动报错代码 43。
    • (2023-05)这次测试时,PCIe 总线位置和是否存在电池不再影响直通结果。
  2. 即使 NVIDIA 驱动不检测虚拟机,你运行的程序也会检测虚拟机,隐藏虚拟机特征可以提高成功运行这些程序的概率。

    • 典型例子包括带有反作弊系统的网游,或者部分需要联网激活的商业软件。

那么,开始操作:

  1. 与 Optimus MUXless 架构不同,我这次没有手动提取显卡 BIOS、修改 UEFI 固件就成功进行了显卡直通。

    • 如果你的显卡直通进虚拟机后无法安装驱动,包括 Windows 不会自动下载安装、手动下载 NVIDIA 官网驱动安装器也提示找不到兼容的显卡,那么你大概率仍然需要提取显卡 BIOS。
    • 为了二次确认,你可以在虚拟机里进入设备管理器,找到你的显卡,查看它的硬件 ID,类似 PCI\VEN_10DE&DEV_1C8D&SUBSYS_39D117AA&REV_A1。如果 SUBSYS 后面跟着的是一串 0,这就意味着显卡 BIOS 加载失败,你需要手动提取显卡 BIOS。
    • 具体步骤请看去年的文章的「配置 NVIDIA 独显直通」一段。
  2. 编辑你的虚拟机配置,virsh edit Windows,做如下修改:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    <!-- 把 features 一段改成这样,就是让 QEMU 隐藏虚拟机的特征 -->
    <features>
    <acpi/>
    <apic/>
    <hyperv mode="custom">
    <relaxed state="on"/>
    <vapic state="on"/>
    <spinlocks state="on" retries="8191"/>
    <vpindex state="on"/>
    <runtime state="on"/>
    <synic state="on"/>
    <stimer state="on"/>
    <reset state="on"/>
    <vendor_id state="on" value="GenuineIntel"/>
    <frequencies state="on"/>
    <tlbflush state="on"/>
    </hyperv>
    <kvm>
    <hidden state="on"/>
    </kvm>
    <vmport state="off"/>
    </features>
    <!-- 添加显卡直通的 PCIe 设备 -->
    <hostdev mode='subsystem' type='pci' managed='yes'>
    <source>
    <address domain='0x0000' bus='0x01' slot='0x00' function='0x0'/>
    </source>
    <rom bar='off'/>
    <!-- 注意这里的 PCIe 总线地址必须是 01:00.0,一点都不能差 -->
    <!-- 如果保存时提示 PCIe 总线地址冲突,就把其它设备的 <address> 全部删掉 -->
    <!-- 这样 Libvirt 会重新分配一遍 PCIe 地址 -->
    <address type='pci' domain='0x0000' bus='0x01' slot='0x00' function='0x0' multifunction='on'/>
    </hostdev>
    <!-- 添加一块在虚拟机和宿主机之间共享的内存,以便将虚拟机显示内容传回宿主机 -->
    <shmem name='looking-glass'>
    <model type='ivshmem-plain'/>
    <!-- 这里内存大小的公式是:分辨率宽 x 分辨率高 / 131072,然后向上取到 2 的 n 次方 -->
    <!-- 因为大部分 HDMI 假显示器的分辨率都是 3840 x 2160,计算结果是 63.28MB,向上取到 64MB -->
    <size unit='M'>64</size>
    </shmem>
    <!-- 禁用内存 Balloon,也就是内存动态伸缩,严重影响性能 -->
    <memballoon model="none"/>
    <!-- 在 </qemu:commandline> 之前添加这些参数 -->
    <qemu:arg value='-acpitable'/>
    <qemu:arg value='file=/ssdt1.dat'/>

    此处的 ssdt1.dat 是一个修改后的 ACPI 表,用来模拟一块满电的电池。它对应如下 Base64,可以用 Base64 解码网站转换成二进制文件,放在根目录,或者从本站下载

    1
    2
    3
    U1NEVKEAAAAB9EJPQ0hTAEJYUENTU0RUAQAAAElOVEwYEBkgoA8AFVwuX1NCX1BDSTAGABBMBi5f
    U0JfUENJMFuCTwVCQVQwCF9ISUQMQdAMCghfVUlEABQJX1NUQQCkCh8UK19CSUYApBIjDQELcBcL
    cBcBC9A5C1gCCywBCjwKPA0ADQANTElPTgANABQSX0JTVACkEgoEAAALcBcL0Dk=
  3. 修改共享内存文件的权限。

    1. 修改 /etc/apparmor.d/local/abstractions/libvirt-qemu 文件增加一行:

      1
      /dev/shm/looking-glass rw,

      然后运行 sudo systemctl restart apparmor 重启 AppArmor。

    2. 创建 /etc/tmpfiles.d/looking-glass.conf,写入以下内容,把 lantian 换成你的用户名:

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

      然后运行 sudo systemd-tmpfiles /etc/tmpfiles.d/looking-glass.conf --create 生效。

  4. 启动虚拟机,等一会,Windows 会自动装好 NVIDIA 驱动。

    • 如果设备管理器里显卡打感叹号,显示代码 43,即驱动程序加载失败,你需要检查上面的步骤有没有遗漏,所有配置是否正确。
      • (2022-01)将设备管理器切换到 Device by Connection(按照连接方式显示设备),确认显卡的地址是总线 Bus 1,接口 Slot 0,功能 Function 0,并且确认显卡上级的 PCIe 接口是总线 Bus 0,接口 Slot 1,功能 Function 0。
      • 如果对不上,你需要按上面的方法重新分配一遍设备的 PCIe 地址。
      • (2023-05)我这次尝试时不再需要进行这一步骤。
    • 如果系统没有自动安装 NVIDIA 驱动,并且你手动下载的也显示系统不兼容 / 找不到显卡,那么你需要查看显卡的属性,其硬件 ID 中,SUBSYS 后是否跟着一串 0。
      • 如果是一串 0,请参照第一步。
  5. 关闭虚拟机并再次启动,注意不是直接重启,再次在设备管理器里确认显卡工作正常。

    • 如果此时出现代码 43 了,检查你有没有添加好第二步最后的模拟电池。
    • 我第一次尝试用的是 Windows 10 LTSC 2019,也是重启后出现了代码 43。但因为当时我没有添加模拟电池,我无法确认是 NVIDIA 驱动不兼容系统版本,还是模拟电池的原因。建议使用最新版本的 Windows 10 或 Windows 11。
  6. 以下步骤二选一:

    1. (2022-01)把你的 HDMI 假显示器插入电脑,虚拟机应该识别到一个新的显示器。
    2. (2023-05)安装虚拟显示器驱动:
      1. 下载 ge9/IddSampleDriver 这份虚拟显示器驱动,解压到 C:\IddSampleDriver。注意这个文件夹不能移动到其它位置!
      2. 打开 C:\IddSampleDriver\option.txt,你会看到第一行是一个数字 1(不要修改),然后是分辨率 / 刷新率列表。只保留你想要的一项分辨率 / 刷新率,把其它的分辨率 / 刷新率都删掉。
      3. 打开设备管理器,在菜单中选择「操作 - 添加过时硬件」,点击「从列表中选择 - 全部 - 我有驱动磁盘」,然后选择 C:\IddSampleDriver\IddSampleDriver.inf 并一路下一步完成安装。
      4. 此时 Windows 系统应该检测到了一个新的显示器。
      5. 在我的测试中,使用虚拟显示器时,Looking Glass 显示的内容会有部分像素出错。有条件的话,还是建议使用 HDMI 假显示器。
  7. (2023-05)现在新版 Looking Glass 会自动安装 IVSHMEM 驱动(虚拟机和宿主机共享内存的驱动),你无需再手动安装驱动。这里保留手动安装步骤以供参考:

    1. (2022-01)下载这份 Virtio 驱动复制到虚拟机内解压,注意一定是这份,其它的版本大都没有 IVSHMEM 驱动

      https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/upstream-virtio/virtio-win10-prewhql-0.1-161.zip

    2. 在虚拟机里进入设备管理器,找到系统设备 - PCI 标准内存控制器(PCI standard RAM controller):

      1. 右键选择「更新驱动」
      2. 点击「浏览我的电脑查找驱动程序」
      3. 点击「从列表中选择」
      4. 点击「我有驱动磁盘」按钮
      5. 选择 Virtio 驱动/Win10/amd64/ivshmem.inf 文件
      6. 一路下一步安装驱动,此时它的名字应该已经变成了 IVSHMEM
  8. 安装 Looking Glass,这是一个将虚拟机的显示画面传输到宿主机的工具。

    • 我们插入的假显示器将成为虚拟机唯一能识别到的显示器。如果不安装 Looking Glass,就看不到虚拟机的画面了。
    • 在上面的链接点击「Windows Host Binary」下载,在虚拟机内双击安装。
  9. (2023-05)如果按照 2022-01 的步骤操作,虚拟机开机过程中、Looking Glass 启动前你将无法看到开机画面。因此我推荐在设备管理器中直接禁用 QXL 虚拟显卡。以下旧版步骤保留以供参考。

    • (2022-01)关闭虚拟机,virsh edit Windows 编辑虚拟机配置。

      找到 <video><model type="qxl" ...></video>,将 type 改为 none,以禁用 QXL 虚拟显卡:

      1
      2
      3
      <video>
      <model type="none"/>
      </video>
  10. 在宿主机上安装 Looking Glass 的客户端,Arch Linux 用户可以直接从 AUR 安装 looking-glass 包。运行 looking-glass-client 命令启动客户端。

  11. 回到 Virt-Manager,关掉虚拟机的窗口(就是查看虚拟机桌面、编辑配置的窗口),在 Virt-Manager 主界面右键选择你的虚拟机,点击启动。

  12. 稍等片刻,Looking Glass 的客户端就会显示出虚拟机的画面,此时显卡直通就配置完成了。

性能和体验优化

虽然显卡直通已经完成,但是虚拟机的体验还需要优化。具体来说:

  1. (2022-01)Looking Glass 可以传输鼠标键盘操作,但无法传输声音,意味着虚拟机无法发声;
    • (2023-05)最新的 Looking Glass 已经可以传输声音。
  2. (2022-01)Looking Glass 传输鼠标键盘操作有时会丢键;
    • (2023-05)最新的 Looking Glass 已经可以稳定传输鼠标键盘操作。
  3. IVSHMEM 共享内存功能其实有一个宿主机的内核模块,可以让宿主机的 Looking Glass 使用 DMA 模式提高性能;
  4. 关闭虚拟机后,独立显卡会被设置成 PCIe D3hot 模式,在该模式下显卡仍会消耗 10W 左右的电力,影响电池续航。

我们将一个个解决以上问题。

传输虚拟机声音

(2023-05)新版 Looking Glass 已经可以传输声音。以下步骤保留以供参考。

虽然 Virt-Manager 本身可以通过 SPICE 协议连接虚拟机,从而传输虚拟机的声音,但是 Looking Glass 也会通过 SPICE 传输键鼠操作,而虚拟机上同时只能有一个 SPICE 连接。这就意味着我们无法使用 Virt-Manager 来听声音了。

我们可以安装 Scream,一个 Windows 下的虚拟声卡软件,将声音通过虚拟机的网卡来传输,然后在宿主机上用 Scream 的客户端接收。

在虚拟机上,从 Scream 的下载页面下载 Scream 安装程序,解压后右键以管理员身份运行 Install-x64.bat 脚本安装驱动,然后重启。

在宿主机上安装 Scream 客户端,Arch Linux 用户可以安装 AUR 中的 scream 软件包。

在宿主机上开一个终端运行 scream -v,在虚拟机中播放音频,测试能不能听到。如果无法听到,尝试指定 Scream 客户端监听的网卡,例如 scream -i virbr0 -v,其中 virbr0 对应 Virt-Manager 默认的 NAT 网络,是你的虚拟机与宿主机通信的网卡。

最后,可以创建一个 SystemD 服务,来方便地启动 Scream 客户端。创建 ~/.config/systemd/user/scream.service,写入以下内容:

1
2
3
4
5
6
7
8
9
10
11
[Unit]
Description=Scream

[Service]
Type=simple
Restart=always
RestartSec=1
ExecStart=/usr/bin/scream -i virbr0 -v

[Install]
WantedBy=graphical-session.target

以后使用时就只需要运行 systemctl --user start scream 了。

直通键盘鼠标操作

(2023-05)新版 Looking Glass 已经可以稳定传输鼠标键盘操作。以下步骤保留以供参考。

Looking Glass 的键盘鼠标传输不太稳定,有时会丢失一些操作,因此如果你想在虚拟机里玩游戏,就需要用更稳定的方法将键鼠操作传进虚拟机。

我们有两种方法:让 Libvirt 虚拟机直接捕获宿主机的键鼠操作,或者把一套 USB 键鼠直接直通进虚拟机。

  1. 捕获宿主机键鼠操作。

    在 Linux 系统上,所有的键鼠操作都是通过 evdev(即 Event Device)框架传输给桌面环境的。Libvirt 可以监听你的键鼠操作,将你的操作传给虚拟机。同时,Libvirt 可以在你按下左 Ctrl + 右 Ctrl 这套组合键的时候,在虚拟机和宿主机之间切换,这样你就可以用同一套键盘鼠标同时操作宿主机和虚拟机了。

    首先在宿主机上运行 ls -l /dev/input/by-path 查看你现有的 evdev 设备,例如我就有:

    1
    2
    3
    4
    5
    6
    7
    8
    pci-0000:00:14.0-usb-0:1:1.1-event-mouse               # USB 外接鼠标
    pci-0000:00:14.0-usb-0:1:1.1-mouse
    pci-0000:00:14.0-usb-0:6:1.0-event
    pci-0000:00:15.0-platform-i2c_designware.0-event-mouse # 电脑内置的触摸板
    pci-0000:00:15.0-platform-i2c_designware.0-mouse
    pci-0000:00:1f.3-platform-skl_hda_dsp_generic-event
    platform-i8042-serio-0-event-kbd # 电脑内置的键盘
    platform-pcspkr-event-spkr

    名字中带有 event-mouse 的就是鼠标,带有 event-kbd 的就是键盘。

    然后,virsh edit Windows 编辑虚拟机配置,在 <devices> 中添加一段:

    1
    2
    3
    4
    5
    6
    7
    8
    <input type="evdev">
    <!-- 根据上面 ls 的结果,修改鼠标或键盘的路径 -->
    <source dev="/dev/input/by-path/platform-i8042-serio-0-event-kbd" grab="all" repeat="on"/>
    </input>
    <!-- 有多个鼠标键盘时,重复即可 -->
    <input type="evdev">
    <source dev="/dev/input/by-path/pci-0000:00:15.0-platform-i2c_designware.0-event-mouse" grab="all" repeat="on"/>
    </input>

    启动虚拟机,这时你会发现键鼠操作没反应了,因为它们被虚拟机捕获了。按下左 Ctrl + 右 Ctrl 组合键就可以恢复宿主机键鼠控制,再按一次就可以控制虚拟机。

    然后,我们就可以禁用 Looking Glass 的键鼠传输功能了。创建 /etc/looking-glass-client.ini,写入以下内容:

    1
    2
    [spice]
    enable=no
  2. USB 键鼠直通

    捕获键鼠操作并不是万能的,例如我的触摸板就无法被正常捕获,体现为无法移动虚拟机内的光标。

    如果你也遇到了这种情况,并且你有一套 USB 键鼠,就可以将它们直通进虚拟机,专门用它们控制虚拟机。虚拟机的 USB 直通技术非常成熟,你遇到问题的概率非常小。

    在 Virt-Manager 里选择添加硬件(Add Hardware) - USB 宿主设备(USB Host Device),选择你的鼠标键盘即可。

用内核模块加速 Looking Glass

这段内容大都来自 https://looking-glass.io/docs/B6/module/

Looking Glass 提供了一个内核模块,可以用于 IVSHMEM 共享内存设备,让 Looking Glass 能使用 DMA 技术高效地读取虚拟机画面,从而提高帧率。

  1. 安装 Linux 内核头文件和 DKMS,在 Arch Linux 上就是安装 linux-headersdkms 两个包。

  2. 从 AUR 安装 looking-glass-module-dkms

  3. 配置 Udev 规则:创建 /etc/udev/rules.d/99-kvmfr.rules,写入以下内容:

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

    lantian 替换成你自己的用户名。

  4. 配置内存大小:创建 /etc/modprobe.d/looking-glass.conf,写入以下内容:

    1
    2
    # 这里的内存大小计算方法和虚拟机的 shmem 一项相同。
    options kvmfr static_size_mb=64
  5. 开机自动加载模块:创建 /etc/modules-load.d/looking-glass.conf,写入一行 kvmfr

  6. 运行 sudo modprobe kvmfr 加载模块,此时 /dev 下会多出一个 kvmfr0 设备,就是 Looking Glass 的内存设备了。

  7. 修改 /etc/apparmor.d/local/abstractions/libvirt-qemu 文件增加一行:

    1
    /dev/kvmfr0 rw,

    以允许虚拟机访问这个设备。运行 sudo systemctl restart apparmor 重启 AppArmor。

  8. virsh edit Windows 编辑虚拟机配置:

    1. <devices> 中删除 <shmem> 一段:

      1
      2
      3
      4
      <shmem name='looking-glass'>
      <model type='ivshmem-plain'/>
      <size unit='M'>64</size>
      </shmem>
    2. <qemu:commandline> 中增加下面几行:

      1
      2
      3
      4
      5
      6
      <qemu:arg value="-device"/>
      <qemu:arg value="{&quot;driver&quot;:&quot;ivshmem-plain&quot;,&quot;id&quot;:&quot;shmem-looking-glass&quot;,&quot;memdev&quot;:&quot;looking-glass&quot;}"/>
      <qemu:arg value="-object"/>
      <!-- 下一行有一个 67108864,对应 64MB * 1048576 -->
      <!-- 如果你之前设置的内存大小不同请相应修改 -->
      <qemu:arg value="{&quot;qom-type&quot;:&quot;memory-backend-file&quot;,&quot;id&quot;:&quot;looking-glass&quot;,&quot;mem-path&quot;:&quot;/dev/kvmfr0&quot;,&quot;size&quot;:67108864,&quot;share&quot;:true}"/>
    3. 启动虚拟机。

  9. 修改 /etc/looking-glass-client.ini,添加以下内容:

    1
    2
    [app]
    shmFile=/dev/kvmfr0
  10. 启动 Looking Glass,此时应该可以看到虚拟机画面。

  11. (2023-05)如果你用的是 NixOS,可以直接使用下面的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
boot.extraModulePackages = with config.boot.kernelPackages; [
kvmfr
];
boot.extraModprobeConfig = ''
# 这里的内存大小计算方法和虚拟机的 shmem 一项相同。
options kvmfr static_size_mb=64
'';
boot.kernelModules = ["kvmfr"];
services.udev.extraRules = ''
SUBSYSTEM=="kvmfr", OWNER="root", GROUP="libvirtd", MODE="0660"
'';

environment.etc."looking-glass-client.ini".text = ''
[app]
shmFile=/dev/kvmfr0
'';
}

不使用虚拟机时给独立显卡断电

2022-01-26 更新:实测应用这个补丁后,NVIDIA 显卡仍未完全断电,耗电量与未使用补丁前相同。本段内容失效。

这一段只适用于 20 系及以上的 NVIDIA 显卡,当使用 NVIDIA 官方驱动时,它们也可以自动断电。10 系及以下的 NVIDIA 显卡不支持此功能。

这一段涉及自行编译内核,和使用未经严格检查和测试的内核补丁,不建议不熟悉 Linux 的用户操作。请自行衡量风险。

当你不使用虚拟机时,管理 PCIe 直通的 vfio-pci 驱动会将设备设置成 D3 模式,也就是 PCIe 设备的省电模式。但是 D3 模式也分两种:D3hot,此时设备仍然通电,和 D3cold,此时设备完全断电。现在内核中的 vfio-pci 驱动只支持 D3hot,此时 NVIDIA 独立显卡由于芯片未断电,仍会消耗 10W 左右的功率,从而导致笔记本电脑续航下降。

一位 NVIDIA 的工程师在 Linux 内核的邮件列表上发布了一组让 vfio-pci 支持 D3cold 模式的补丁。应用此补丁后,当虚拟机关机时,NVIDIA 独立显卡会被彻底断电,从而节省电量。

这组补丁可以在 https://lore.kernel.org/lkml/20211115133640.2231-1-abhsahu@nvidia.com/T/ 看到。它总共由三个补丁组成,我将三个补丁合并后上传到了 https://github.com/xddxdd/pkgbuild/blob/master/linux-xanmod-lantian/0007-vfio-pci-d3cold.patch

对于 Arch Linux 来说,给内核打补丁是比较简单的。AUR 中大部分内核的 PKGBUILD 都可以自动打补丁,只需要下载一个内核的 PKGBUILD,然后把这个补丁加入 PKGBUILD 的 source 部分就可以了。具体修改可以看我的这个 commit:https://github.com/xddxdd/pkgbuild/commit/406adb7bf5657cfe07bb17ff561d11ed97ebab39

要注意的是,这个补丁无法保证稳定。

根据邮件列表的讨论:

  1. 它是一个 RFC 补丁,也就是测试版补丁,邮件列表的标题上写着一个大大的 [RFC]
  2. 如果虚拟机中的显卡驱动想把显卡切换成 D3cold 模式,这个补丁存在将显卡 reset,导致状态丢失,继而导致虚拟机崩溃的风险。虽然目前我使用 Windows 11 虚拟机暂时没有发现类似的问题,但是你需要了解其中的隐患。
  3. 目前开发者只测试了 NVIDIA 的部分显卡,不保证对其它 PCIe 设备的支持。

风险自负。

资料来源

感谢前人在显卡直通上做出的努力,没有他们的努力本文不可能存在。

以下是我配置时参考的资料:

附录:最终 Libvirt XML 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
<domain xmlns:qemu="http://libvirt.org/schemas/domain/qemu/1.0" type="kvm">
<name>Windows11</name>
<uuid>5d5b00d8-475a-4b6c-8053-9dda30cd2f95</uuid>
<metadata>
<libosinfo:libosinfo xmlns:libosinfo="http://libosinfo.org/xmlns/libvirt/domain/1.0">
<libosinfo:os id="http://microsoft.com/win/11"/>
</libosinfo:libosinfo>
</metadata>
<memory unit="KiB">16777216</memory>
<currentMemory unit="KiB">16777216</currentMemory>
<vcpu placement="static">16</vcpu>
<os>
<type arch="x86_64" machine="pc-q35-8.0">hvm</type>
<loader readonly="yes" type="pflash">/run/libvirt/nix-ovmf/OVMF_CODE.fd</loader>
<nvram template="/run/libvirt/nix-ovmf/OVMF_VARS.fd">/var/lib/libvirt/qemu/nvram/Windows11_VARS.fd</nvram>
</os>
<features>
<acpi/>
<apic/>
<hyperv mode="custom">
<relaxed state="on"/>
<vapic state="on"/>
<spinlocks state="on" retries="8191"/>
<vpindex state="on"/>
<runtime state="on"/>
<synic state="on"/>
<stimer state="on"/>
<reset state="on"/>
<vendor_id state="on" value="GenuineIntel"/>
<frequencies state="on"/>
<tlbflush state="on"/>
</hyperv>
<kvm>
<hidden state="on"/>
</kvm>
<vmport state="off"/>
</features>
<cpu mode="host-passthrough" check="none" migratable="on">
<topology sockets="1" dies="1" cores="8" threads="2"/>
</cpu>
<clock offset="localtime">
<timer name="rtc" tickpolicy="catchup"/>
<timer name="pit" tickpolicy="delay"/>
<timer name="hpet" present="no"/>
<timer name="hypervclock" present="yes"/>
</clock>
<on_poweroff>destroy</on_poweroff>
<on_reboot>restart</on_reboot>
<on_crash>destroy</on_crash>
<pm>
<suspend-to-mem enabled="no"/>
<suspend-to-disk enabled="no"/>
</pm>
<devices>
<emulator>/run/libvirt/nix-emulators/qemu-system-x86_64</emulator>
<disk type="file" device="disk">
<driver name="qemu" type="qcow2" discard="unmap"/>
<source file="/var/lib/libvirt/images/Windows11.qcow2"/>
<target dev="vda" bus="virtio"/>
<boot order="1"/>
<address type="pci" domain="0x0000" bus="0x04" slot="0x00" function="0x0"/>
</disk>
<disk type="file" device="cdrom">
<driver name="qemu" type="raw"/>
<source file="/mnt/root/persistent/media/LegacyOS/Common/virtio-win-0.1.215.iso"/>
<target dev="sdb" bus="sata"/>
<readonly/>
<address type="drive" controller="0" bus="0" target="0" unit="1"/>
</disk>
<controller type="usb" index="0" model="qemu-xhci" ports="15">
<address type="pci" domain="0x0000" bus="0x02" slot="0x00" function="0x0"/>
</controller>
<controller type="pci" index="0" model="pcie-root"/>
<controller type="pci" index="1" model="pcie-root-port">
<model name="pcie-root-port"/>
<target chassis="1" port="0x10"/>
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x0" multifunction="on"/>
</controller>
<controller type="pci" index="2" model="pcie-root-port">
<model name="pcie-root-port"/>
<target chassis="2" port="0x11"/>
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x1"/>
</controller>
<controller type="pci" index="3" model="pcie-root-port">
<model name="pcie-root-port"/>
<target chassis="3" port="0x12"/>
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x2"/>
</controller>
<controller type="pci" index="4" model="pcie-root-port">
<model name="pcie-root-port"/>
<target chassis="4" port="0x13"/>
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x3"/>
</controller>
<controller type="pci" index="5" model="pcie-root-port">
<model name="pcie-root-port"/>
<target chassis="5" port="0x14"/>
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x4"/>
</controller>
<controller type="pci" index="6" model="pcie-root-port">
<model name="pcie-root-port"/>
<target chassis="6" port="0x15"/>
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x5"/>
</controller>
<controller type="pci" index="7" model="pcie-root-port">
<model name="pcie-root-port"/>
<target chassis="7" port="0x16"/>
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x6"/>
</controller>
<controller type="pci" index="8" model="pcie-root-port">
<model name="pcie-root-port"/>
<target chassis="8" port="0x17"/>
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x7"/>
</controller>
<controller type="pci" index="9" model="pcie-root-port">
<model name="pcie-root-port"/>
<target chassis="9" port="0x18"/>
<address type="pci" domain="0x0000" bus="0x00" slot="0x03" function="0x0" multifunction="on"/>
</controller>
<controller type="pci" index="10" model="pcie-root-port">
<model name="pcie-root-port"/>
<target chassis="10" port="0x19"/>
<address type="pci" domain="0x0000" bus="0x00" slot="0x03" function="0x1"/>
</controller>
<controller type="pci" index="11" model="pcie-root-port">
<model name="pcie-root-port"/>
<target chassis="11" port="0x1a"/>
<address type="pci" domain="0x0000" bus="0x00" slot="0x03" function="0x2"/>
</controller>
<controller type="pci" index="12" model="pcie-root-port">
<model name="pcie-root-port"/>
<target chassis="12" port="0x1b"/>
<address type="pci" domain="0x0000" bus="0x00" slot="0x03" function="0x3"/>
</controller>
<controller type="pci" index="13" model="pcie-root-port">
<model name="pcie-root-port"/>
<target chassis="13" port="0x1c"/>
<address type="pci" domain="0x0000" bus="0x00" slot="0x03" function="0x4"/>
</controller>
<controller type="pci" index="14" model="pcie-root-port">
<model name="pcie-root-port"/>
<target chassis="14" port="0x1d"/>
<address type="pci" domain="0x0000" bus="0x00" slot="0x03" function="0x5"/>
</controller>
<controller type="sata" index="0">
<address type="pci" domain="0x0000" bus="0x00" slot="0x1f" function="0x2"/>
</controller>
<controller type="virtio-serial" index="0">
<address type="pci" domain="0x0000" bus="0x03" slot="0x00" function="0x0"/>
</controller>
<interface type="network">
<mac address="52:54:00:f4:bf:15"/>
<source network="default"/>
<model type="virtio"/>
<address type="pci" domain="0x0000" bus="0x01" slot="0x00" function="0x0"/>
</interface>
<serial type="pty">
<target type="isa-serial" port="0">
<model name="isa-serial"/>
</target>
</serial>
<console type="pty">
<target type="serial" port="0"/>
</console>
<channel type="spicevmc">
<target type="virtio" name="com.redhat.spice.0"/>
<address type="virtio-serial" controller="0" bus="0" port="1"/>
</channel>
<input type="mouse" bus="ps2"/>
<input type="mouse" bus="virtio">
<address type="pci" domain="0x0000" bus="0x06" slot="0x00" function="0x0"/>
</input>
<input type="keyboard" bus="ps2"/>
<input type="keyboard" bus="virtio">
<address type="pci" domain="0x0000" bus="0x07" slot="0x00" function="0x0"/>
</input>
<tpm model="tpm-crb">
<backend type="passthrough">
<device path="/dev/tpm0"/>
</backend>
</tpm>
<graphics type="spice" autoport="yes">
<listen type="address"/>
<image compression="off"/>
</graphics>
<sound model="ich9">
<audio id="1"/>
<address type="pci" domain="0x0000" bus="0x00" slot="0x1b" function="0x0"/>
</sound>
<audio id="1" type="spice"/>
<video>
<model type="qxl" ram="65536" vram="65536" vgamem="16384" heads="1" primary="yes"/>
<address type="pci" domain="0x0000" bus="0x00" slot="0x01" function="0x0"/>
</video>
<hostdev mode="subsystem" type="pci" managed="yes">
<source>
<address domain="0x0000" bus="0x01" slot="0x00" function="0x0"/>
</source>
<address type="pci" domain="0x0000" bus="0x05" slot="0x00" function="0x0"/>
</hostdev>
<redirdev bus="usb" type="spicevmc">
<address type="usb" bus="0" port="2"/>
</redirdev>
<redirdev bus="usb" type="spicevmc">
<address type="usb" bus="0" port="3"/>
</redirdev>
<watchdog model="itco" action="reset"/>
<memballoon model="none"/>
</devices>
<qemu:commandline>
<qemu:arg value="-device"/>
<qemu:arg value="{&quot;driver&quot;:&quot;ivshmem-plain&quot;,&quot;id&quot;:&quot;shmem0&quot;,&quot;memdev&quot;:&quot;looking-glass&quot;}"/>
<qemu:arg value="-object"/>
<qemu:arg value="{&quot;qom-type&quot;:&quot;memory-backend-file&quot;,&quot;id&quot;:&quot;looking-glass&quot;,&quot;mem-path&quot;:&quot;/dev/kvmfr0&quot;,&quot;size&quot;:67108864,&quot;share&quot;:true}"/>
<qemu:arg value="-acpitable"/>
<qemu:arg value="file=/etc/ssdt1.dat"/>
</qemu:commandline>
</domain>