之前Ivon討論過Linux 虛擬機雙GPU直通的做法,並透過Looking Glass存取Windows桌面。但是,如果你真的生不出第二個螢幕,連HDMI欺騙器都不想買,那麼我們還有「單GPU直通」這招。
本文Ivon講述,如何在只有一個GPU、CPU無內顯、只有一台螢幕的情況下,進行單GPU直通(Single GPU Passthrough)
1. 單GPU直通原理#
參考網路上的許多做法後,我採用Libvirt hook的做法,即讓虛擬機開關機後自動觸發指令。
構想是:啟動Windows虛擬機之後,讓Linux斷開螢幕連結,把螢幕交給Windows虛擬機使用,這樣只有一個螢幕也能直通。
以我的配備來說,過程會變成這樣:
- 螢幕接在電腦主機的Nvidia顯示卡上,開機進入Linux
- 進入SDDM登入畫面,登入KDE桌面
- 開啟Virt Manager,啟動Windows 11虛擬機
- 中止顯示管理器和桌面環境行程,卸除Linux的圖形驅動程式(Nvidia核心模組),將GPU與宿主機取消連結,載入VFIO核心模組
- 螢幕稍微黑一下,隨後變成Windows的畫面
- Windows關機後,回到Linux,SDDM重新啟動,回到SDDM登入畫面。
以上過程可以透過Libvirt的hook功能自動化執行,設定好一次後就行。
缺點:沒辦法同時使用二個系統,因為螢幕被Windows搶走了。在Windows虛擬機執行的時候,Linux宿主機只能透過SSH存取
…例如從Windows虛擬機裡面,SSH回Linux宿主機,跳脫Matrix,反察自身。
2. 系統環境#
- 主機板:ASUS K31CD-K
- CPU: Intel® Core™ i5-7400
- GPU:Intel® UHD Graphics 630 (內顯)
- GPU:NVIDIA GTX 1050 Ti(獨顯),已安裝閉源驅動
- 宿主機Host OS:Ubuntu 22.04 LTS
- Linux核心版本:6.5.0
- 桌面環境:KDE Plasma 5.24 (X11)
- 虛擬機Guest OS:Windows 11 23H2
- QEMU版本:6.2
- Libvirt版本:8.0.0
3. 安裝Libvirt與Windows虛擬機#
4. 啟用IOMMU#
- 給Intel啟用IOMMU,
nvidia-drm.modeset=0
防止DRM載入,後面的kvm.ignore_msrs
防止Windows BSOD
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash intel_iommu=on iommu=pt nvidia-drm.modeset=0 kvm.ignore_msrs=1 kvm.report_ignored_msrs=0"
- 更新initramfs和GRUB
sudo update-initramfs -u
sudo update-grub
到這裡就可以了,不需要解除Nvidia驅動,不用黑名單Nvidia核心模組,也不用綁VFIO裝置,剩下的交給Libvirt hook處理即可。
5. 撰寫Libvirt hook指令稿#
- 用
lscpi -nnk
指令查看Nvidia顯示卡的匯流排位址,如下所示,二個裝置位址轉譯為:pci_0000_01_00_0
和pci_0000_01_00_1
01:00.0 VGA compatible controller [0300]: NVIDIA Corporation GP107 [GeForce GTX 1050 Ti] [10de:1c82] (rev a1)
Subsystem: ASUSTeK Computer Inc. GP107 [GeForce GTX 1050 Ti] [1043:85d6]
Kernel driver in use: nvidia
Kernel modules: nvidiafb, nouveau, nvidia_drm, nvidia
01:00.1 Audio device [0403]: NVIDIA Corporation GP107GL High Definition Audio Controller [10de:0fb9] (rev a1)
Subsystem: ASUSTeK Computer Inc. GP107GL High Definition Audio Controller [1043:85d6]
Kernel driver in use: snd_hda_intel
Kernel modules: snd_hda_intel
- 用
sudo lsmod | grep nvidia
確認目前有用到哪些核心模組
nvidia_uvm 1794048 0
nvidia_drm 94208 8
nvidia_modeset 1314816 20 nvidia_drm
nvidia 56786944 1198 nvidia_uvm,nvidia_modeset
- 建立Hooks Helper script,注意我的虛擬機名稱叫做
Windows11
所以建立的目錄也要叫Windows11
sudo mkdir /etc/libvirt/hooks
sudo touch /etc/libvirt/hooks/qemu
sudo chmod +x /etc/libvirt/hooks/qemu
- 在
/etc/libvirt/hooks/qemu
填入以下內容:
#!/bin/bash
GUEST_NAME="$1"
HOOK_NAME="$2"
STATE_NAME="$3"
MISC="${@:4}"
BASEDIR="$(dirname $0)"
HOOKPATH="$BASEDIR/qemu.d/$GUEST_NAME/$HOOK_NAME/$STATE_NAME"
set -e # If a script exits with an error, we should as well.
if [ -f "$HOOKPATH" ]; then
eval \""$HOOKPATH"\" "$@"
elif [ -d "$HOOKPATH" ]; then
while read file; do
eval \""$file"\" "$@"
done <<< "$(find -L "$HOOKPATH" -maxdepth 1 -type f -executable -print;)"
fi
- 建立虛擬機開機啟動的hook
sudo mkdir -p /etc/libvirt/hooks/qemu.d/Windows11/prepare/begin
sudo touch /etc/libvirt/hooks/qemu.d/Windows11/prepare/begin/start.sh
sudo chmod +x /etc/libvirt/hooks/qemu.d/Windows11/prepare/begin/start.sh
- 在
/etc/libvirt/hooks/qemu.d/Windows11/prepare/begin/start.sh
填入以下內容:
#!/bin/bash
set -x
# 停止顯示管理器
systemctl stop sddm
# Wayland下需要停止KDE Plasama服務
#systemctl --user -M "你的使用者名稱" stop plasma-plasmashell.service
# Unbind VTconsoles
echo 0 > /sys/class/vtconsole/vtcon0/bind
echo 0 > /sys/class/vtconsole/vtcon1/bind
# Unbind EFI Framebuffer
echo efi-framebuffer.0 > /sys/bus/platform/drivers/efi-framebuffer/unbind
# 停止Nvidia服務
systemctl stop nvidia-persistenced.service
sleep 2
# 取消載入NVIDIA核心模組
modprobe -r nvidia_drm nvidia_modeset nvidia_uvm nvidia
sleep 2
# 從宿主機移除GPU裝置和GPU音訊裝置
virsh nodedev-detach pci_0000_01_00_0
virsh nodedev-detach pci_0000_01_00_1
# 載入VFIO核心模組
modprobe vfio-pci
- 建立虛擬機關機後的hook:
sudo mkdir -p /etc/libvirt/hooks/qemu.d/Windows11/release/end
sudo touch /etc/libvirt/hooks/qemu.d/Windows11/release/end/stop.sh
sudo chmod +x /etc/libvirt/hooks/qemu.d/Windows11/release/end/stop.sh
- 在
/etc/libvirt/hooks/qemu.d/Windows11/release/end/stop.sh
填入以下內容
#!/bin/bash
set -x
# 將GPU裝置加回宿主機
virsh nodedev-reattach pci_0000_01_00_0
virsh nodedev-reattach pci_0000_01_00_1
# 取消載入VFIO核心模組
modprobe -r vfio-pci
# Rebind framebuffer to host
echo "efi-framebuffer.0" > /sys/bus/platform/drivers/efi-framebuffer/bind
# 載入NVIDIA核心模組
modprobe nvidia_drm
modprobe nvidia_modeset
modprobe nvidia_uvm
modprobe nvidia
# 啟動Nvidia服務
systemctl start nvidia-persistenced.service
# Bind VTconsoles
echo 1 > /sys/class/vtconsole/vtcon0/bind
echo 1 > /sys/class/vtconsole/vtcon1/bind
# 啟動顯示管理器
systemctl start sddm
- 重新啟動Libvritd服務
sudo systemctl restart libvirtd
你可以先在SSH模式下,從另一台電腦登入,一條一條的測試指令,確認指令稿的指令都能執行後再繼續。Libvirt的虛擬機在SSH下用
virsh start "虛擬機名稱"
指令開機。如果虛擬機啟動後指令稿沒啟動,你可以用
sudo dmesg
看錯誤訊息。
6. 將鍵鼠全部直通給虛擬機#
設定好Linux宿主機的SSH服務,確保你可以在緊急時用SSH登入。手機SSH客戶端可以用 Termux。
關閉電腦的休眠機制,KDE可以到系統設定 → 電源管理,關閉休眠
編輯虛擬機硬體,將Nvidia顯示卡的PCI裝置加入至虛擬機
接著將QXL顯示卡改為None,這樣開機後就只有一個螢幕。
因為開機後無法使用SPICE,點選新增USB裝置,把滑鼠和鍵盤全部加入到虛擬機。
音效卡部分,最簡單的方法是準備一個USB音效卡,或是含有3.5mm耳機孔的Type-C擴充埠,並將其直通進去。不然的話開機後Linux宿主機的桌面行程被幹掉,SPICE無法透過PipeWire輸出音訊。