一年前,为了能够一边用 Arch Linux 浏览网页、写代码,一边用 Windows 运行游戏等没法在 Linux 上方便地完成的任务,我试着在我的联想 R720 游戏本上进行了显卡直通。但是由于那台电脑是 Optimus MUXless 架构(前文有各种架构的介绍),也就是独显没有输出端口、全靠核显显示画面,那套配置的应用受到了很大的阻碍,最后被我放弃。
但是现在,我换了台新电脑。这台电脑的 HDMI 输出接口是直连 NVIDIA 独立显卡的,也就是 Optimus MUXed 架构。在这种架构下,有办法让虚拟机识别到一个「独显上的显示器」,从而正常启用大部分功能。于是,我终于可以配置出一套可以长期使用的显卡直通配置。
更新日志
- 2023-05-08:针对新版 Looking Glass B6 更新部分内容。
- 2022-01-26:PCIe 省电补丁实测无效。
准备工作
在按照本文进行操作前,你需要准备好:
-
一台 Optimus MUXed 架构的笔记本电脑。我的电脑型号是 HP OMEN 17t-ck000(i7-11800H,RTX 3070)。
- (2022-01)我用的操作系统是 Arch Linux,更新到最新版本。
- (2023-05)本次更新时我用的操作系统是 NixOS,但大部分步骤同样适用于其它 Linux 发行版。
- 建议关闭安全启动功能,但既然你已经装上了 Linux,你大概率已经关掉了。安全启动理论上可能会对 PCIe 直通功能造成一定的限制。
-
用 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 虚拟显卡,保证自己可以看得到虚拟机的视频输出。
-
(可选)根据电脑视频输出接口的不同,一个 HDMI,DP,或 USB Type-C 接口的假显示器(诱骗接头),淘宝上一般几块到十几块钱一个。
- (2023-05)或者你也可以选择安装虚拟显示器驱动。
-
(可选)外接一套 USB 键鼠套装。
开始操作之前,预先提醒:
- 整个步骤中会多次重启宿主系统,同时一些操作存在导致宿主系统崩溃的风险,请备份好你的数据。
- 整个步骤中你不需要手动下载任何 NVIDIA 显卡驱动,交给 Windows 自动下载就好。
- 如果 Windows 自动下载失败,手动安装驱动的底线是下载驱动 EXE 然后双击安装。
- 千万不要在设备管理器中手动指定设备安装。
- 手动安装显卡驱动有时反而会干扰判断。
购买 Optimus MUXed 架构的新电脑
如果你有兴趣尝试显卡直通,并正准备购买一台新电脑,你可以参考以下我的选择方法。
显卡直通的前提条件是:
- NVIDIA 独立显卡本身要具有视频输出功能
- 机身上至少有一个连接到独立显卡的视频接口
但是,游戏本厂商很少会在宣传页上写明视频接口连接的是独显还是核显。因此我们只能根据常见的参数进行推测:
-
优先选择支持「独显直连内屏」的电脑,因为这种情况下独显一定具有视频输出功能,并且厂家大概率会将机身视频接口连接到独显上。
- 典型的例子包括:2020 和 2021 款的联想拯救者系列、惠普暗影精灵系列,以及戴尔游侠 G15。
- **但我不保证这些例子是准确的!**请自行查阅资料或询问客服,确保电脑支持「独显直连」功能。
-
或者选择带有中高端独立显卡的电脑,一般 NVIDIA 显卡型号要以 60 或以上结尾。
- 中高端 NVIDIA 显卡一般都有视频输出功能,此时厂家大概率会将机身视频接口连接到独显上。
- 请勿购买显卡型号以 50 或以下结尾的电脑,例如 RTX 3050,GTX 1650 Ti 等等。它们大概率不支持视频输出。
-
用好七天无理由退货服务。
- 因为厂家不会宣传、甚至不会特别在意视频输出接口的连接方式,我们只能看配置参数和宣传页盲猜。因此,你完全可能按照以上规则挑选到一台无法进行显卡直通的电脑,此时可以考虑退货或者转卖。
- 在一些国家(包括中国),笔记本电脑厂家对无理由退货的要求都是「电脑自带的 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 直通的内核模块管理的步骤如下:
-
运行
lspci -nn | grep NVIDIA
,获得类似如下输出: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
驱动接管。 -
创建
/etc/modprobe.d/lantian.conf
,添加如下内容:options vfio-pci ids=10de:249d,10de:228b
给
vfio-pci
这个负责 PCIe 直通的内核驱动一个配置,让它去管理独显。ids
参数就是要直通的独显的制造商 ID 和设备 ID。 -
修改
/etc/mkinitcpio.conf
,在MODULES
中添加以下内容:MODULES=(vfio_pci vfio vfio_iommu_type1 vfio_virqfd)
删除
nvidia
等与 NVIDIA 驱动相关的内核模块,或者确保它们排在 VFIO 驱动后面。这样 PCIe 直通模块就会在系统启动的早期抢占独显,阻止 NVIDIA 驱动后续占用独显。 -
运行
mkinitcpio -P
更新 initramfs。 -
重启电脑。
(2023-05)如果你用的是 NixOS 系统,可以直接使用下面的配置:
{
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 解除了大部分的限制,理论上来说现在直接把显卡直通进虚拟机就能用。
但也只是理论上而已。
我依然建议大家做完所有的隐藏虚拟机的步骤,因为:
-
(2022-01)对于笔记本电脑来说,NVIDIA 并没有解除所有的限制。
至少在我测试时,显卡的 PCIe 总线位置和系统是否存在电池依然会导致直通失败、驱动报错代码 43。- (2023-05)这次测试时,PCIe 总线位置和是否存在电池不再影响直通结果。
-
即使 NVIDIA 驱动不检测虚拟机,你运行的程序也会检测虚拟机,隐藏虚拟机特征可以提高成功运行这些程序的概率。
- 典型例子包括带有反作弊系统的网游,或者部分需要联网激活的商业软件。
那么,开始操作:
-
与 Optimus MUXless 架构不同,我这次没有手动提取显卡 BIOS、修改 UEFI 固件就成功进行了显卡直通。
- 如果你的显卡直通进虚拟机后无法安装驱动,包括 Windows 不会自动下载安装、手动下载 NVIDIA 官网驱动安装器也提示找不到兼容的显卡,那么你大概率仍然需要提取显卡 BIOS。
- 为了二次确认,你可以在虚拟机里进入设备管理器,找到你的显卡,查看它的硬件
ID,类似
PCI\VEN_10DE&DEV_1C8D&SUBSYS_39D117AA&REV_A1
。如果SUBSYS
后面跟着的是一串 0,这就意味着显卡 BIOS 加载失败,你需要手动提取显卡 BIOS。 - 具体步骤请看去年的文章的「配置 NVIDIA 独显直通」一段。
-
编辑你的虚拟机配置,
virsh edit Windows
,做如下修改:<!-- 把 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 解码网站转换成二进制文件,放在根目录,或者从本站下载。
U1NEVKEAAAAB9EJPQ0hTAEJYUENTU0RUAQAAAElOVEwYEBkgoA8AFVwuX1NCX1BDSTAGABBMBi5f U0JfUENJMFuCTwVCQVQwCF9ISUQMQdAMCghfVUlEABQJX1NUQQCkCh8UK19CSUYApBIjDQELcBcL cBcBC9A5C1gCCywBCjwKPA0ADQANTElPTgANABQSX0JTVACkEgoEAAALcBcL0Dk=
-
修改共享内存文件的权限。
-
修改
/etc/apparmor.d/local/abstractions/libvirt-qemu
文件增加一行:/dev/shm/looking-glass rw,
然后运行
sudo systemctl restart apparmor
重启 AppArmor。 -
创建
/etc/tmpfiles.d/looking-glass.conf
,写入以下内容,把lantian
换成你的用户名:f /dev/shm/looking-glass 0660 lantian kvm -
然后运行
sudo systemd-tmpfiles /etc/tmpfiles.d/looking-glass.conf --create
生效。
-
-
启动虚拟机,等一会,Windows 会自动装好 NVIDIA 驱动。
- 如果设备管理器里显卡打感叹号,显示代码 43,即驱动程序加载失败,你需要检查上面的步骤有没有遗漏,所有配置是否正确。
- (2022-01)将设备管理器切换到
Device by Connection
(按照连接方式显示设备),确认显卡的地址是总线 Bus 1,接口 Slot 0,功能 Function 0,并且确认显卡上级的 PCIe 接口是总线 Bus 0,接口 Slot 1,功能 Function 0。 - 如果对不上,你需要按上面的方法重新分配一遍设备的 PCIe 地址。
- (2023-05)我这次尝试时不再需要进行这一步骤。
- (2022-01)将设备管理器切换到
- 如果系统没有自动安装 NVIDIA 驱动,并且你手动下载的也显示系统不兼容/找不到显卡,那么你需要查看显卡的属性,其硬件 ID 中,
SUBSYS
后是否跟着一串 0。- 如果是一串 0,请参照第一步。
- 如果设备管理器里显卡打感叹号,显示代码 43,即驱动程序加载失败,你需要检查上面的步骤有没有遗漏,所有配置是否正确。
-
关闭虚拟机并再次启动,注意不是直接重启,再次在设备管理器里确认显卡工作正常。
- 如果此时出现代码 43 了,检查你有没有添加好第二步最后的模拟电池。
- 我第一次尝试用的是 Windows 10 LTSC 2019,也是重启后出现了代码 43。但因为当时我没有添加模拟电池,我无法确认是 NVIDIA 驱动不兼容系统版本,还是模拟电池的原因。建议使用最新版本的 Windows 10 或 Windows 11。
-
以下步骤二选一:
- (2022-01)把你的 HDMI 假显示器插入电脑,虚拟机应该识别到一个新的显示器。
- (2023-05)安装虚拟显示器驱动:
- 下载 ge9/IddSampleDriver 这份虚拟显示器驱动,解压到
C:\IddSampleDriver
。注意这个文件夹不能移动到其它位置! - 打开
C:\IddSampleDriver\option.txt
,你会看到第一行是一个数字 1(不要修改),然后是分辨率/刷新率列表。只保留你想要的一项分辨率/刷新率,把其它的分辨率/刷新率都删掉。 - 打开设备管理器,在菜单中选择「操作 - 添加过时硬件」,点击「从列表中选择 -
全部 - 我有驱动磁盘」,然后选择
C:\IddSampleDriver\IddSampleDriver.inf
并一路下一步完成安装。 - 此时 Windows 系统应该检测到了一个新的显示器。
- 在我的测试中,使用虚拟显示器时,Looking Glass 显示的内容会有部分像素出错。有条件的话,还是建议使用 HDMI 假显示器。
- 下载 ge9/IddSampleDriver 这份虚拟显示器驱动,解压到
-
(2023-05)现在新版 Looking Glass 会自动安装 IVSHMEM 驱动(虚拟机和宿主机共享内存的驱动),你无需再手动安装驱动。这里保留手动安装步骤以供参考:
-
(2022-01)下载这份 Virtio 驱动复制到虚拟机内解压,注意一定是这份,其它的版本大都没有 IVSHMEM 驱动:
-
在虚拟机里进入设备管理器,找到系统设备 - PCI 标准内存控制器(
PCI standard RAM controller
):- 右键选择「更新驱动」
- 点击「浏览我的电脑查找驱动程序」
- 点击「从列表中选择」
- 点击「我有驱动磁盘」按钮
- 选择
Virtio 驱动/Win10/amd64/ivshmem.inf
文件 - 一路下一步安装驱动,此时它的名字应该已经变成了
IVSHMEM
-
-
安装 Looking Glass,这是一个将虚拟机的显示画面传输到宿主机的工具。
- 我们插入的假显示器将成为虚拟机唯一能识别到的显示器。如果不安装 Looking Glass,就看不到虚拟机的画面了。
- 在上面的链接点击「Windows Host Binary」下载,在虚拟机内双击安装。
-
(2023-05)如果按照 2022-01 的步骤操作,虚拟机开机过程中、Looking Glass 启动前你将无法看到开机画面。因此我推荐在设备管理器中直接禁用 QXL 虚拟显卡。以下旧版步骤保留以供参考。
-
(2022-01)关闭虚拟机,
virsh edit Windows
编辑虚拟机配置。找到
<video><model type="qxl" ...></video>
,将type
改为none
,以禁用 QXL 虚拟显卡:<video> <model type="none"/> </video>
-
-
在宿主机上安装 Looking Glass 的客户端,Arch Linux 用户可以直接从 AUR 安装
looking-glass
包。运行looking-glass-client
命令启动客户端。 -
回到 Virt-Manager,关掉虚拟机的窗口(就是查看虚拟机桌面、编辑配置的窗口),在 Virt-Manager 主界面右键选择你的虚拟机,点击启动。
-
稍等片刻,Looking Glass 的客户端就会显示出虚拟机的画面,此时显卡直通就配置完成了。
性能和体验优化
虽然显卡直通已经完成,但是虚拟机的体验还需要优化。具体来说:
- (2022-01)Looking Glass 可以传输鼠标键盘操作,但无法传输声音,意味着虚拟机无法发声;
- (2023-05)最新的 Looking Glass 已经可以传输声音。
- (2022-01)Looking Glass 传输鼠标键盘操作有时会丢键;
- (2023-05)最新的 Looking Glass 已经可以稳定传输鼠标键盘操作。
- IVSHMEM 共享内存功能其实有一个宿主机的内核模块,可以让宿主机的 Looking Glass 使用 DMA 模式提高性能;
- 关闭虚拟机后,独立显卡会被设置成 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
,写入以下内容:
[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 键鼠直接直通进虚拟机。
-
捕获宿主机键鼠操作。
在 Linux 系统上,所有的键鼠操作都是通过
evdev
(即Event Device
)框架传输给桌面环境的。Libvirt 可以监听你的键鼠操作,将你的操作传给虚拟机。同时,Libvirt 可以在你按下左 Ctrl + 右 Ctrl 这套组合键的时候,在虚拟机和宿主机之间切换,这样你就可以用同一套键盘鼠标同时操作宿主机和虚拟机了。首先在宿主机上运行
ls -l /dev/input/by-path
查看你现有的evdev
设备,例如我就有: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>
中添加一段:<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
,写入以下内容:[spice] enable=no
-
USB 键鼠直通
捕获键鼠操作并不是万能的,例如我的触摸板就无法被正常捕获,体现为无法移动虚拟机内的光标。
如果你也遇到了这种情况,并且你有一套 USB 键鼠,就可以将它们直通进虚拟机,专门用它们控制虚拟机。虚拟机的 USB 直通技术非常成熟,你遇到问题的概率非常小。
在 Virt-Manager 里选择添加硬件(
Add Hardware
) - USB 宿主设备(USB Host Device
),选择你的鼠标键盘即可。
用内核模块加速 Looking Glass
Looking Glass 提供了一个内核模块,可以用于 IVSHMEM 共享内存设备,让 Looking Glass 能使用 DMA 技术高效地读取虚拟机画面,从而提高帧率。
-
安装 Linux 内核头文件和 DKMS,在 Arch Linux 上就是安装
linux-headers
和dkms
两个包。 -
从 AUR 安装
looking-glass-module-dkms
。 -
配置 Udev 规则:创建
/etc/udev/rules.d/99-kvmfr.rules
,写入以下内容:SUBSYSTEM=="kvmfr", OWNER="lantian", GROUP="kvm", MODE="0660"
将
lantian
替换成你自己的用户名。 -
配置内存大小:创建
/etc/modprobe.d/looking-glass.conf
,写入以下内容:# 这里的内存大小计算方法和虚拟机的 shmem 一项相同。 options kvmfr static_size_mb=64
-
开机自动加载模块:创建
/etc/modules-load.d/looking-glass.conf
,写入一行kvmfr
。 -
运行
sudo modprobe kvmfr
加载模块,此时/dev
下会多出一个kvmfr0
设备,就是 Looking Glass 的内存设备了。 -
修改
/etc/apparmor.d/local/abstractions/libvirt-qemu
文件增加一行:/dev/kvmfr0 rw,
以允许虚拟机访问这个设备。运行
sudo systemctl restart apparmor
重启 AppArmor。 -
virsh edit Windows
编辑虚拟机配置:-
在
<devices>
中删除<shmem>
一段:<shmem name='looking-glass'> <model type='ivshmem-plain'/> <size unit='M'>64</size> </shmem>
-
在
<qemu:commandline>
中增加下面几行:<qemu:arg value="-device"/> <qemu:arg value="{"driver":"ivshmem-plain","id":"shmem-looking-glass","memdev":"looking-glass"}"/> <qemu:arg value="-object"/> <!-- 下一行有一个 67108864,对应 64MB * 1048576 --> <!-- 如果你之前设置的内存大小不同请相应修改 --> <qemu:arg value="{"qom-type":"memory-backend-file","id":"looking-glass","mem-path":"/dev/kvmfr0","size":67108864,"share":true}"/>
-
启动虚拟机。
-
-
修改
/etc/looking-glass-client.ini
,添加以下内容:[app] shmFile=/dev/kvmfr0
-
启动 Looking Glass,此时应该可以看到虚拟机画面。
-
(2023-05)如果你用的是 NixOS,可以直接使用下面的配置:
{
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
要注意的是,这个补丁无法保证稳定。
根据邮件列表的讨论:
- 它是一个 RFC 补丁,也就是测试版补丁,邮件列表的标题上写着一个大大的
[RFC]
。 - 如果虚拟机中的显卡驱动想把显卡切换成
D3cold
模式,这个补丁存在将显卡 reset,导致状态丢失,继而导致虚拟机崩溃的风险。虽然目前我使用 Windows 11 虚拟机暂时没有发现类似的问题,但是你需要了解其中的隐患。 - 目前开发者只测试了 NVIDIA 的部分显卡,不保证对其它 PCIe 设备的支持。
风险自负。
资料来源
感谢前人在显卡直通上做出的努力,没有他们的努力本文不可能存在。
以下是我配置时参考的资料:
- NVIDIA 独显直通
- GitHub Misairu-G 的 NVIDIA Optimus MUXed 直通教程 https://gist.github.com/Misairu-G/616f7b2756c488148b7309addc940b28
- Reddit VFIO 版块的虚拟电池补丁 https://www.reddit.com/r/VFIO/comments/ebo2uk/nvidia_geforce_rtx_2060_mobile_success_qemu_ovmf/
- Arch Linux Wiki https://wiki.archlinux.org/title/PCI_passthrough_via_OVMF
- Looking Glass 的文档
- 虚拟显示器驱动
- 文中使用的可以修改分辨率和刷新率的版本 https://github.com/ge9/IddSampleDriver
- 分辨率和刷新率固定的原版 https://github.com/roshkins/IddSampleDriver
- VFIO D3cold 模式补丁
附录:最终 Libvirt XML 文件
<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="{"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}"/>
<qemu:arg value="-acpitable"/>
<qemu:arg value="file=/etc/ssdt1.dat"/>
</qemu:commandline>
</domain>