Linux för arbete, Windows VM för spel - How-to
Tja alla,
Jag har länge inväntat en uppgradering från min trogne 4670K, och ett av kraven var att jag ville ha möjligheten att köra Linux på bare metal, med ett dedikerat grafikkort till en Windows VM. På det sättet kan jag spela alla spel jag vill, utan problem med Anti-cheat, DRM eller att det helt enkelt inte stöds för Linux, men samtidigt ha Linux så jag kan använda min dator som jag vill. Vi alla vet varför vi föredrar Linux så tänker inte gå in på det. Jag vet att dual-boot är ett alternativ, men hur kul är det?
Jag tänkte här beskriva hur jag gjorde det, vad jag lärde mig, och vad jag ska göra om. Ni får ursäkta om jag hoppar lite mellan engelska och svenska. Jag kommer anta att det finns basic kunskaper inom Linux för att installera paket, redigera filer och navigera runt i terminalen och köra skript. Eftersom jag bara har AMD hårdvara med NVIDIA-gpuer kommer en del av detta vara aningen specifikt, men det är i grund och botten samma sak man behöver göra på ett system med Intel CPU och AMD GPU.
Först och främst, hur ser mitt system ut?
Moderkort: Asrock X570 Taichi
CPU: AMD Ryzen 3700X
Minne: 32GB
GPU0: NVIDIA GT1030
GPU1: NVIDIA RTX2070
Lagring: 1TB NVMe SSD
Som ni ser kräver detta två grafikkort, ett för vårt primära OS, och en för vår virtuella maskin. Har man ett Intel-baserat system kan man använda den integrerade GPUn och därmed frigöra en PCIe x16 slot. Jag köpte personligen ett passivt kylt GT1030 då jag inte behöver någon GPU-kraft i Linux (än).
Ett annat krav är att moderkortet har stöd för IOMMU gruppering, men även att den hanterar IOMMU-grupper på ett bra sätt. Dvs, att PCIe-enheterna får addresserbart minne inom samma IOMMU grupp. Detta möjliggör passthrough för en total enhet. RTX-korten specifikt har 4 olika PCIe device IDs som alla behöver befinna sig på samma IOMMU-grupp.
Vi antar att vi börjar från en fräsch ny installation av Ubuntu 20.04 (eller något som är baserat på det som PopOS!).
Vi kan börja med att installera de nödvändiga paketen vi behöver:
# apt install libvirt-bin bridge-utils virt-manager qemu-kvm ovmf
Innan vi startar om maskinen för att konfigurera UEFI så kan vi passa på och ställa in maskinen så att den alltid bootar med IOMMU påslaget. Det vi faktiskt gör här är att vi specificierar linux kernel-moduler och parametrar som vi vill att linuxkärnan laddar vid boot.
Editera följande fil med er favorit redigerare, /etc/default/grub och ändra raden som börjar med GRUB_CMDLINE_LINUX_DEFAULT och lägg till följande moduler och modulparametrar:
amd_iommu=on
kvm.ignore_msrs=1
iommu=pt
Så här såg min ut efter jag lade till det ovan:
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash amd_iommu=on kvm.ignore_msrs=1 iommu=pt"
OBS! Flaggorna kommer heta något annat om du har Intel.
Sedan behöver vi uppdatera vår Grub-konfiguration:
# update-grub
Nu kan vi starta om maskinen och gå in i UEFI för att aktivera ett par saker på vårt system.
AMD-SRV
IOMMU
Nu när vi bootar in i Linux så bör vi kunna se de olika IOMMU-grupperna under:
/sys/kernel/iommu_groups/*/devices/*
För att verifiera att IOMMU är påslaget så kan vi titta i dmesg och söka efter AMD-Vi.
# dmesg | grep -i amd-vi
[ 0.734955] pci 0000:00:00.2: AMD-Vi: IOMMU performance counters supported
[ 0.736312] pci 0000:00:00.2: AMD-Vi: Found IOMMU cap 0x40
[ 0.736313] pci 0000:00:00.2: AMD-Vi: Extended features (0x58f77ef22294ade):
[ 0.736315] AMD-Vi: Interrupt remapping enabled
[ 0.736315] AMD-Vi: Virtual APIC enabled
[ 0.736315] AMD-Vi: X2APIC enabled
[ 0.736387] AMD-Vi: Lazy IO/TLB flushing enabled
Här är ett skript för att enkelt se IOMMU-grupper:
#!/bin/bash
shopt -s nullglob
for d in /sys/kernel/iommu_groups/*/devices/*
do
n=${d#*/iommu_groups/*}; n=${n%%/*}
printf 'IOMMU Group %s ' "$n"
lspci -nns "${d##*/}"
done
Jag kan rekommendera moderkortet Asrock X570 Taichi då den har bra IOMMU-gruppering. Jag hade inga problem själv, men har läst att om inte hela kortet hamnar i samma grupp så får man testa andra PCI-e slots. Finns säkert andra trick man kan göra, men som tur är så slapp jag lära mig dem.
När jag kör ovanstående skript ser jag att samtliga 4-PCI ID's för kortet är i samma IOMMU-grupp.
# bash iommu.sh | grep 'TU106'
IOMMU Group 27 0e:00.0 VGA compatible controller [0300]: NVIDIA Corporation TU106 [GeForce RTX 2070 Rev. A] [10de:1f07] (rev a1)
IOMMU Group 27 0e:00.1 Audio device [0403]: NVIDIA Corporation TU106 High Definition Audio Controller [10de:10f9] (rev a1)
IOMMU Group 27 0e:00.2 USB controller [0c03]: NVIDIA Corporation TU106 USB 3.1 Host Controller [10de:1ada] (rev a1)
IOMMU Group 27 0e:00.3 Serial bus controller [0c80]: NVIDIA Corporation TU106 USB Type-C UCSI Controller [10de:1adb] (rev a1)
(Note: Jag greppade efter 'TU106' för att göra det lite snyggare här, kör skriptet utan grep)
Nu har vi säkertställt att vi har IOMMU påslaget, att det är aktiverat och igenkänt av vårt operativsystem och även att samtliga del-enheter av vår GPU är del av samma IOMMU-grupp.
Nästa steg är att isolera GPUn som ska användas till vår VM från vår host genom att specificera VFIO som drivrutin för kortet. Detta gör vi lättast genom Grub igen genom att redigera samma fil och lägga till samtliga 4 PCI IDs som parametrar till VFIO-drivern i våra Grub-options. Detta gör kortet oanvändbart i Linux, men ger KVM möjligheten att använda GPUn i en virtuell maskin.
PCI IDn är det näst sista vi ser på varje rad av outputen ovan, så för mitt system blir det:
10de:1f07
10de:10f9
10de:1ada
10de:1adb
Redigera /etc/default/grub och lägg till följande till samma rad som tidigare:
vfio-pci.ids=10de:1f07,10de:10f9,10de:1ada,10de:1adb
OBS!!! Klipp inte och klistra in dessa värden. Dina ID's kommer säkerligen se annorlunda ut!
Så här ser min grub config ut efter jag lade till det ovan:
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash amd_iommu=on kvm.ignore_msrs=1 iommu=pt vfio-pci.ids=10de:1f07,10de:10f9,10de:1ada,10de:1adb"
Sedan behöver vi uppdatera vår Grub-konfiguration igen:
# update-grub
Starta om datorn så att kärnan laddas om med VFIO drivern:
# reboot
För att verifiera att vår VM-GPU nu använder sig utav VFIO som driver kan vi köra:
# lspci -k | grep -B3 vfio
Kernel modules: snd_hda_intel
0e:00.0 VGA compatible controller: NVIDIA Corporation TU106 [GeForce RTX 2070 Rev. A] (rev a1)
Subsystem: ASUSTeK Computer Inc. TU106 [GeForce RTX 2070 Rev. A]
Kernel driver in use: vfio-pci
Kernel modules: nvidiafb, nouveau, nvidia_drm, nvidia
0e:00.1 Audio device: NVIDIA Corporation TU106 High Definition Audio Controller (rev a1)
Subsystem: ASUSTeK Computer Inc. TU106 High Definition Audio Controller
Kernel driver in use: vfio-pci
Kernel modules: snd_hda_intel
0e:00.2 USB controller: NVIDIA Corporation TU106 USB 3.1 Host Controller (rev a1)
Subsystem: ASUSTeK Computer Inc. TU106 USB 3.1 Host Controller
Kernel driver in use: vfio-pci
Kernel modules: xhci_pci
0e:00.3 Serial bus controller [0c80]: NVIDIA Corporation TU106 USB Type-C UCSI Controller (rev a1)
Subsystem: ASUSTeK Computer Inc. TU106 USB Type-C UCSI Controller
Kernel driver in use: vfio-pci
Perfekt. Nu är vår maskin förberedd för GPU-passthrough!
Dags att skapa en VM. Detta kan vi göra med virt-manager.
Följ stegen i GUI't för att skapa en Windows VM och välj resurser så att Linux kan köras parallelt med Windows, så lämna åtminstone 2 kärnor och 4GB RAM, helst mer. Personligen har jag gett maskinen 8 vCPUs som består av 4 fysiska kärnor med 2 logiska per kärna.
Ni behöver en Windows ISO som går att hämta från Microsoft gratis. Kör inte med någon egen variant om ni inte vet vad ni gör. Ladda hem den officiella ISOn från Microsoft, annars kan det uppstå problem med kompatibilitet.
Innan ni går vidare med sista steget (steg 5) men innan ni går vidare till att installera, välj Customize configuration before install
Under Overview, välj Q35 som chipset och UEFI x86_64: /usr/share/OVMF/OVMF_CODE.fd som firmware:
Innan vi kan köra måste vi sätta vår virtualisering i ett hidden-mode, och även lura NVIDIA's drivrutiner så att den inte detekterar att vi kör i en VM. Detta gör vi via terminalen via virsh.
# virsh edit <VM-namn>
Nu behöver vi lägga till lite rader i denna XML-filen:
...
<features>
...
<hyperv>
...
<vendor_id state='on' value='randomid'/>
...
</hyperv>
...
</features>
...
Samt:
...
<features>
...
<kvm>
<hidden state='on'/>
</kvm>
...
</features>
...
Nu finns det lite olika sätt att fortsätta. Antingen kan vi lägga till vår GPU innan vi installerar Windows, eller efteråt. Jag upplevde det lättare att lägga till grafikkortet efteråt. Testa lite som ni vill, jag var lite disträ under tiden jag gjorde det så jag kan ha upplevt fel som inte var relaterade till ordningen.
Jag rekommenderar starkt att använda VirtIO som drivrutiner på Windows-gästen för mycket bättre prestanda för lagring (VirtIO SCSI) och nätverk.
Under diskens options i virt-manager finns det Disk Bus, där man kan ändra till VirtIO. För att kunna installera Windows på en disk som har VirtIO som sin kontroller, så behöver vi installera drivrutiner innan vi installerar Windows (annars kan inte Windows detektera diskarna).
Ladda hem följande fil från Red Hat:
virtio-win.iso
Här är en bra guide för hur man installerar VirtIO-drivrutinerna för en Windows VM innan installation. I princip behöver man lägga till ytterligare en CD-ROM-läsare och montera ovanstående .ISO-fil där och under installation välja:
Browse:
Och sist amd64 --> w10
När Windows är färdiginstallerat så installera virtio-win-guest-tools.exe från ISOn på VMen och starta om. Guest-tools är kritiskt. Den hjälper KVM att hantera filskrivningar, suspend mode, mer effektiv minneshantering osv.
När Windows senare är installerat så är det dags att lägga till GPUn! Starta virt-manager, och sen Add hardware --> PCI Host Device --> och lägg till alla komponenter av kortet (4 i mitt fall).
Sist men inte minst behöver vi mus/tangentbord till vår VM. Som tur är har qemu en feature där man via evdev kan pass through kontrollenheter. För att göra det behöver vi först identifiera vårt tangentbord och mus under /dev/input/by-id/.
sagan ~ $ ls -l /dev/input/by-id/
total 0
lrwxrwxrwx 1 root root 9 Mar 25 20:01 usb-04d9_USB_Keyboard-event-if01 -> ../event6
lrwxrwxrwx 1 root root 9 Mar 25 20:01 usb-04d9_USB_Keyboard-event-kbd -> ../event3
lrwxrwxrwx 1 root root 9 Mar 25 20:01 usb-Kingsis_Peripherals_ZOWIE_Gaming_mouse-event-mouse -> ../event2
lrwxrwxrwx 1 root root 9 Mar 25 20:01 usb-Kingsis_Peripherals_ZOWIE_Gaming_mouse-mouse -> ../mouse0
lrwxrwxrwx 1 root root 9 Mar 25 20:01 usb-Kingston_HyperX_Cloud_Flight_Wireless_Headset-event-if03 -> ../event7
sagan ~ $
Filerna vi är ute efter är dessa:
usb-04d9_USB_Keyboard-event-kbd
usb-Kingsis_Peripherals_ZOWIE_Gaming_mouse-event-mouse
Dvs filerna som har event med i namnet. För att lägga till denna funktion till vår VM behöver vi redigera XML-filen igen:
# virsh edit <VM-namn>
Och i slutet, av filen, innan </domain> lägger vi till:
<qemu:commandline>
<qemu:arg value="-object"/>
<qemu:arg value="input-linux,id=mouse1,evdev=/dev/input/by-id/usb-Kingsis_Peripherals_ZOWIE_Gaming_mouse-event-mouse"/>
<qemu:arg value="-object"/>
<qemu:arg value="input-linux,id=kbd1,evdev=/dev/input/by-id/usb-04d9_USB_Keyboard-event-kbd,grab_all=on,repeat=on"/>
</qemu:commandline>
Notera att det är den absoluta filvägen för muset och tangentbordet som behövs. Så fort VMen startas kommer tangentbordet och musen att gå över till att kontrollera VMen. Genom att trycka på båda Ctrl på tangentbordet samtidigt så kan man toggla mellan Host <-> VM. Glöm inte att ansluta en display-kabel mellan VM-GPUn och din skärm och byt input när VMen startas.
Nu har vi en fungerande VM med GPU-passthrough! Starta den via virt-manager eller terminalen med:
$ virsh start <vm-namn>
Prestanda-förbättringar
Om ni kör AMD så rekommenderar jag starkt att pinna CPU-kärnorna så att den virtuella maskinens alla kärnor befinner sig på en sida av CCX. Detta reducerar latency enormt, eftersom all data som en kärna kan tänkas behöva finns i den delade L3-cachen. För att hjälpa er få en överblick på er CPU's topografi kan ni köra kommandot lstopo. För att installera det behövs det ett paket som heter hwloc
# apt install hwloc
# lstopo
Här kan vi se att fysiska kärnor 0-3 är ett set av kärnor med sin egna cache, med 4-7 på den andre. Jag valde att dedikera de logiska kärnorna på 4-7 för VMen, vilket ledde till en förminsking av overhead och därmed närmre native prestanda.
<vcpu placement='static'>8</vcpu>
<iothreads>1</iothreads>
<cputune>
<vcpupin vcpu='0' cpuset='8'/>
<vcpupin vcpu='1' cpuset='9'/>
<vcpupin vcpu='2' cpuset='10'/>
<vcpupin vcpu='3' cpuset='11'/>
<vcpupin vcpu='4' cpuset='12'/>
<vcpupin vcpu='5' cpuset='13'/>
<vcpupin vcpu='6' cpuset='14'/>
<vcpupin vcpu='7' cpuset='15'/>
<emulatorpin cpuset='0-1'/>
<iothreadpin iothread='1' cpuset='0-1'/>
</cputune>
<cpu mode='host-passthrough' check='none'>
<topology sockets='1' cores='4' threads='2'/>
<cache mode='passthrough'/>
<feature policy='require' name='topoext'/>
</cpu>
Överklockning!
Vi är ju trots allt på Sweclockers. Eftersom Linux-kärnan inte riktigt hanterar rejäla överklockningar lika bra som Windows-kärnan så kändes det mer safe att köra på moderkortets integrerade auto-OC funktion på alla kärnor. GPUn går att överklocka i VMen precis som på baremetal så med lite OC med MSI Afterburner så kan man kräma ur mer prestanda.
För övrigt, VMens konfiguration är något som inte är etsat i sten. Man kan ändra CPU-parameterar och nästan hela VMens konfiguration senare. Allt förutom chipset och UEFI/BIOS-firmware kan ändras och justeras i efterhand.
Prestandatester
Tankar på förbättringar
Något jag hade velat göra annorlunda skulle vara att partionerna min NVMe SSD med LVM, och skapa en volym för min VM-data, för att sedan passa genom den partitionen till VMen. Då hade jag fått blockenhets-lagring med lite overhead istället för att skriva till en fil på ett filsystem. Fördelen med att filbaserad lagring är att backups blir väldigt smidigt. Kör man sen ett filsystem som ZFS för sina backups kan man enkelt ta snapshots.
Lagrings-prestandan i VMen är OK, men kan vara lite bättre. GPU prestandan är utmärkt, ungefär 5% prestanda är tappad ungefär. Eftersom inte hela CPUn är virtualiserad blir det svårt att testa men det är mer än tillräckligt bra med 8 vCPUs. Väldigt låg latens när jag spelar och jag spelar CS:GO och Rocket League utan konstigheter. Jag har ofta bättre ping än mina vänner, vilket är lite kul också.
För övrigt så har jag även lagt till en bluetooth-dongel så jag kan ansluta min PS4-kontroller, och även mitt trådlösa USB headset och allt bara funkar. Jag är shockad hur bra det fungerat hittils och jag har väl kört denna setupen i snart 2 månader.
-------------------------------------------------------------------
Proper dokumentation: https://wiki.archlinux.org/index.php/PCI_passthrough_via_OVMF
Min XML för referens (uppdaterad 2021-03-26): https://pastebin.com/NiRaB6Ad