Linux系統雖在 Steam Proton的支援下,能玩的Windows遊戲數目大幅增加,然而仍有許多遊戲和程式無法用Proton執行,必須得用Windows虛擬機跑。
本文Ivon將討論在Linux系統跑Windows虛擬機的方法。於虛擬機啟用硬體加速,順暢低延遲的玩遊戲。
設定過程不雙重開機,不接第二個螢幕,不準備二個滑鼠,不登出目前的使用者,直接在單一螢幕的Linux桌面環境操作Windows。
在英文圈,本文使用的技術可以概括的稱為"VFIO Gaming on Linux"。
以下為實際啟動過程的演示影片
1. 安裝前先論破#
為什麼不雙重開機就好?雙系統會影響穩定度,例如Windows更新有機率弄壞GRUB(雖然Arch也會自己弄壞就是了)。另,我不想讓Windows觸碰到我的系統,且虛擬機彈性大,方便備份。
GPU直通,乃利用Linux核心的VFIO功能,讓虛擬機存取實體顯示卡(GPU),增進圖形效能。GPU直通目前只有QEMU/KVM虛擬機技術支援。由於是直通給Windows虛擬機,不論顯示卡品牌為何,Windows應該都抓得到驅動。
此外,透過Libvirt的指令,虛擬機在關機後可解除綁定VFIO,將顯示卡「還給」host使用。
螢幕問題#
畫面部份,在GPU直通後,Virt Manager的QXL圖形效能僅是尚可,無法低延遲顯示Windows畫面,要玩遊戲或影片剪輯的話根本不敷需求,故我們要採用其他技術。
接兩個螢幕是最簡單的作法,畫面效率最高。不過我只想放一個螢幕,第二個螢幕插入HDMI欺騙器代替使顯示卡正常運作。在這樣的配置下,最終選用"Looking Glass"顯示Windows畫面,,它使用共享記憶體(IVSHMEM)繪製緩衝區,從而達到幾乎無延遲的畫面顯示效果。
Looking Glass的伺服端只適用Windows,客戶端則是支援Linux/Windows/macOS。
為何不裝 Moonlight然後遠端存取?Moonlight太吃系統的GPU資源,除非您真的有「遠端玩虛擬機遊戲」的需求再裝。至於Windows RDP,那是遠端桌面,用途不一樣。
音訊問題#
音訊部份有3個方案:Looking Glass內建的SPICE、Scream、PulseAudio。
Scream是一款虛擬音效卡程式,該程式會在Windows虛擬出一個音效輸出裝置,Linux Host再跑一個接收器來接受音訊。儘管Scream延遲最低,可是安裝有點複雜,最新版我還遇到驅動簽名過期的問題。
PulseAudio延遲跟SPICE半斤八兩,所以我採用SPICE,它可以讓虛擬機存取實體機的喇叭和麥克風,但延遲就不太樂觀。
其實,你還可以考慮音效卡直通的做法,買個USB音效卡,直通給虛擬機用就沒有延遲問題了。
2. 環境#
雖然這裡寫Arch Linux,但我測試Ubuntu 22.04環境一樣可以用。
- CPU:Intel I5-7400
- GPU:Intel UHD 630
- GPU:Nvidia GTX-1050Ti
- SSD:1TB
- Host OS:Arch Linux
- Guest OS:Windows 11 22H2
- Host OS桌面環境:KDE Plasma X11
- Host OS音訊伺服器:Pipewire
- QEMU版本:8.0.2
雖是雙GPU直通,但電腦只有一個螢幕,為了讓另一個直通給虛擬機的GPU正常運作,我購置了「HDMI欺騙器」插在獨顯上,使其能在虛擬機裡面正常運作。
使用HDMI顯卡欺騙器前務必先裝好Nvidia驅動,以及設定Looking Glass開機自動啟動。可以先用實體雙螢幕方案,確認Looking Glass能運作了之後再改回HDMI欺騙器。
3. 安裝Windows虛擬機#
首先,給電腦啟用虛擬化,安裝QEMU/KVM套件,再安裝Windows 11虛擬機,TPM可以用軟體模擬。
建議分配8GB RAM 、4核心以上CPU、128GB以上虛擬硬碟。必要情況下掛載SSHFS共享Host OS的目錄。
安裝虛擬機不需真的啟用Secure Boot,但建議隱藏KVM虛擬化狀態。
4. 直通GPU給Windows虛擬機#
請準備第二個實體螢幕,或是HDMI欺騙器,使顯示卡能正常運作。
依照個人情況,看要直通Intel內顯或Nvidia獨顯都可以,通常我是選擇後者。
參見:
5. 強化虛擬機CPU性能#
本節啟用的三個東西:1. Hugepage,增加記憶體利用效率。 2. Hyper-V保留系統資源 3. 讓vCPU獨佔實體CPU核心。
首先啟用hugepage,可以用
cat /proc/meminfo | grep Huge
查看有無啟用Hugepage,若Hugetlb為0kb代表沒啟用。編輯sysctl設定
sudo vim /etc/sysctl.d/99-sysctl.conf
- 將8GB RAM (1 hugepage = 2mb)保留給虛擬機使用(會直接從宿主機扣除)
vm.nr_hugepages=4096
vm.hugetlb_shm_group=48
- 重開機,開啟Virt Manager,編輯XML,在
<memory>
區塊的下方加入<memoryBacking>
:
<memory unit="KiB">8388608</memory>
<currentMemory unit="KiB">8388608</currentMemory>
<memoryBacking>
<hugepages/>
</memoryBacking>
- 再來調整Hyper-V設定,找到
<features></features>
區塊,在那之間填入如下內容:
<features>
...
<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="123456789123"/>
<frequencies state="on"/>
</hyperv>
...
</features>
開啟Virt Manager → 編輯CPU,勾選複製主機CPU配置。再依情況手動設定CPU拓樸,例如我是1通訊端4核心1執行緒
最後,讓vCPU獨佔實體CPU核心。找到
<vcpu></vcpu>
區塊,在該區塊下方填入要獨佔的核心,例如我這裡有4個核心。
<cpu mode="host-passthrough" check="none" migratable="on">
<topology sockets="1" dies="1" cores="4" threads="1"/>
</cpu>
<cputune>
<vcpupin vcpu="0" cpuset="0"/>
<vcpupin vcpu="1" cpuset="1"/>
<vcpupin vcpu="2" cpuset="2"/>
<vcpupin vcpu="3" cpuset="3"/>
</cputune>
- 有時候可能會遇到CPU佔用異常高的情況,這時請試著設定
migratable="off"
<cpu mode="host-passthrough" check="none" migratable="off">
<topology sockets="4" dies="1" cores="1" threads="1"/>
</cpu>
6. 設定Looking Glass,低延遲存取Windows桌面#
將Windows虛擬機關機。
7. 設定SPICE音訊#
此處的SPICE音訊為Looking Glass最新版內建的服務,支援存取麥克風。
將虛擬機關機。
安裝QEMU PulseAudio驅動套件
sudo pacman -S qemu-audio-pa
- 編輯QEMU設定檔
sudo vim /etc/libvirt/qemu.conf
- 找到此段,將Libvirt設定為以一般使用者執行(user為您的使用者名稱)
user = "user"
- 重新啟動Libvirt
sudo systemctl restart libvirtd
- 開啟Virt Manager,編輯Windows 11硬體,新增音效卡ICH9。
8. 防止遊戲反作弊機制偵測到虛擬機環境#
依實際情況做選擇,不是每個遊戲都有反作弊機制。
9. 測試連線到虛擬機#
您可能需要準備2個滑鼠,1個給虛擬機專用防止漂移。您可以用 KDE Connect的遠端控制功能虛擬一個滑鼠出來給宿主機使用。
開啟Virt Manager,編輯Windows 11虛擬機硬體,將顯示卡設定為「無」。這樣Windows開機後會進入只有獨顯輸出畫面的單螢幕狀態。
點選新增硬體 → 新增USB裝置,將滑鼠新增給虛擬機用
將虛擬機開機,關閉Virt Manager視窗。
從終端機啟動Looking Glass客戶端,參考 官方文件參數,我
-F
用讓Looking Glass全螢幕。加入-k
顯示FPS
looking-glass-client -F -k
- 此時就會看到Windows畫面,按
Scroll lock + F
開關全螢幕。
可以測試音效有無正常運作,如果Windows的音效有動畫,Host OS卻聽不到,請試著到KDE的系統設定調整音訊輸出模式(調整雙工模式)。
對音效點選右鍵 → 音效設定即可存取麥克風。
- 由於Looking Glass採用SPICE的緣故,按鍵盤的Windows鍵就能回到Host OS。非全螢幕模式下,用第二個滑鼠移動也能回到Host OS。
Looking Glass的視窗可以隨時關掉重開,Windows程式仍會繼續執行。
10. 建立快速啟動的桌面捷徑#
- 將自己加入libvirt群組
sudo usermod -a -G libvirt $(whoami)
- 編輯
/etc/libvirt/libvirtd.conf
,將unix_sock_rw_perms設定為0770
unix_sock_rw_perms = "0770"
- 重啟libvirtd服務
sudo systemctl restart libvirtd
- 新增指令稿
touch ~/.local/share/startlookinglass.sh
chmod +x ~/.local/share/startlookinglass.sh
vim ~/.local/share/startlookinglass.sh
- 貼上以下內容,啟動名為win11的虛擬機(用指令
virsh -c qemu:///system list --all
查看),等10秒後再啟動Looking Glass
#!/bin/bash
/usr/bin/virsh -c qemu:///system start win11
sleep 10
/usr/bin/looking-glass-client -F
- 新增桌面捷徑
vim ~/Desktop/Looking Glass.desktop
- 設定啟動指令稿。這樣桌面點二下該捷徑即會自動啟動虛擬機與Looking Glass。
[Desktop Entry]
Name=Looking Glass (Full Screen)
Comment=
Exec=bash /home/user/.local/share/startlookinglass.sh
Icon=
Terminal=false
Type=Application
MimeType=
附錄:我的完整虛擬機XML#
在這裡附上我Windows 11的虛擬機XML供參考,請勿完全照抄。
<domain type="kvm">
<name>Windows11</name>
<uuid>UUID</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">8388608</memory>
<currentMemory unit="KiB">8388608</currentMemory>
<memoryBacking>
<hugepages/>
</memoryBacking>
<vcpu placement="static">4</vcpu>
<cputune>
<vcpupin vcpu="0" cpuset="0"/>
<vcpupin vcpu="1" cpuset="1"/>
<vcpupin vcpu="2" cpuset="2"/>
<vcpupin vcpu="3" cpuset="3"/>
</cputune>
<sysinfo type="smbios">
<bios>
<entry name="vendor">LENOVO</entry>
</bios>
<system>
<entry name="manufacturer">Microsoft</entry>
<entry name="product">Windows11</entry>
<entry name="version">22H2</entry>
</system>
<baseBoard>
<entry name="manufacturer">LENOVO</entry>
<entry name="product">20BE0061MC</entry>
<entry name="version">0B98401 Pro</entry>
<entry name="serial">W1KS427111E</entry>
</baseBoard>
<chassis>
<entry name="manufacturer">Dell Inc.</entry>
<entry name="version">2.12</entry>
<entry name="serial">65X0XF2</entry>
<entry name="asset">40000101</entry>
<entry name="sku">Type3Sku1</entry>
</chassis>
<oemStrings>
<entry>myappname:some arbitrary data</entry>
<entry>otherappname:more arbitrary data</entry>
</oemStrings>
</sysinfo>
<os firmware="efi">
<type arch="x86_64" machine="pc-q35-8.0">hvm</type>
<firmware>
<feature enabled="no" name="enrolled-keys"/>
<feature enabled="yes" name="secure-boot"/>
</firmware>
<loader readonly="yes" secure="yes" type="pflash">/usr/share/edk2/x64/OVMF_CODE.secboot.fd</loader>
<nvram template="/usr/share/edk2/x64/OVMF_VARS.fd">/var/lib/libvirt/qemu/nvram/Windows11_VARS.fd</nvram>
<smbios mode="sysinfo"/>
</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="123456789123"/>
<frequencies state="on"/>
</hyperv>
<kvm>
<hidden state="on"/>
</kvm>
<vmport state="off"/>
<smm state="on"/>
<ioapic driver="kvm"/>
</features>
<cpu mode="host-passthrough" check="none" migratable="on">
<topology sockets="1" dies="1" cores="4" threads="1"/>
</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>/usr/bin/qemu-system-x86_64</emulator>
<disk type="file" device="disk">
<driver name="qemu" type="qcow2"/>
<source file="/var/lib/libvirt/images/windows11.qcow2"/>
<target dev="vda" bus="virtio"/>
<boot order="2"/>
<address type="pci" domain="0x0000" bus="0x04" slot="0x00" function="0x0"/>
</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="pci" index="15" model="pcie-root-port">
<model name="pcie-root-port"/>
<target chassis="15" port="0x1e"/>
<address type="pci" domain="0x0000" bus="0x00" slot="0x03" function="0x6"/>
</controller>
<controller type="pci" index="16" model="pcie-to-pci-bridge">
<model name="pcie-pci-bridge"/>
<address type="pci" domain="0x0000" bus="0x08" slot="0x00" function="0x0"/>
</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:25:0c:bf"/>
<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="tablet" bus="usb">
<address type="usb" bus="0" port="1"/>
</input>
<input type="mouse" bus="ps2"/>
<input type="keyboard" bus="ps2"/>
<tpm model="tpm-tis">
<backend type="emulator" version="2.0"/>
</tpm>
<graphics type="spice" autoport="yes">
<listen type="address"/>
<image compression="off"/>
</graphics>
<sound model="ich9">
<address type="pci" domain="0x0000" bus="0x00" slot="0x1b" function="0x0"/>
</sound>
<audio id="1" type="spice"/>
<video>
<model type="none"/>
</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="0x06" slot="0x00" function="0x0"/>
</hostdev>
<hostdev mode="subsystem" type="pci" managed="yes">
<source>
<address domain="0x0000" bus="0x01" slot="0x00" function="0x1"/>
</source>
<address type="pci" domain="0x0000" bus="0x07" 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="virtio">
<address type="pci" domain="0x0000" bus="0x05" slot="0x00" function="0x0"/>
</memballoon>
<shmem name="looking-glass">
<model type="ivshmem-plain"/>
<size unit="M">32</size>
<address type="pci" domain="0x0000" bus="0x10" slot="0x01" function="0x0"/>
</shmem>
</devices>
</domain>