Comme ce blog peut en témoigner, il m’arrive aussi bien de passer du temps sur de la technique que des jeux vidéos sur mon temps libre. Ces deux aspects sont malheureusement plutôt incompatibles quand on travaille avec Linux, les jeux tournant assez mal (quand ils tournent) sur Linux et les divers outils de sysadmin tournant assez mal (quand ils tournent) sous Windows. Le problème est d’autant plus délicat quand on a pris l’habitude de travailler avec 10 terminaux ouverts sur un gestionnaire de fenêtres en tuile au lieu du modèle de fenêtres flottantes omniprésent dans les gestionnaires de fenêtres populaires, dont celui de Windows.
Diverses solutions sont possibles et ont été tentées : des outils multi-plateforme pouvant tourner nativement sous Windows, WSL, Cygwin, Mingw, une machine virtuelle Hyper-V/VirtualBox depuis Windows et même la bonne vieille solution du dual-boot. Aucune ne m’a vraiment satisfait, toutes gardant une friction assez importante, posant des problèmes de performance ou d’utilisabilité. Le problème le plus difficile à surmonter est qu’au fond la partie Windows ne me sert vraiment que pour les jeux et la voir déborder sur tout le reste fini par être irritant
Cet article porte donc sur la dernière tentative de concilier les deux mondes, cette fois avec une VM depuis Linux. Historiquement, ce modèle tenait plutôt de la blague au vu des performances 3D dans une machine virtuelle typique. VFIO a toutefois permis de changer cet état de fait en contournant le problème de l’émulation, en envoyant n’importe quel périphérique PCI à la VM qui peut alors l’utiliser directement avec ses propres pilotes natifs. Le cas d’utilisation typique concerne évidemment les cartes graphiques, qui une fois transmises de cette façon permettent d’atteindre des performances très proches d’un fonctionnement natif.
Mon choix de distribution s’est porté sur Debian Testing (Buster), mais devrait pouvoir s’appliquer à Debian Stretch indifféremment. Côté matériel, il sera orienté vers un cas Intel/Nvidia avec l’iGPU Intel utilisé pour l’hôte, mais devrait rester assez générique pour d’autres configurations.
Choix du matériel
La première étape pour une organisation de ce type est de s’assurer que le matériel supporte ce qu’on va lui demander.
- Le CPU doit supporter la paravirtualisation matérielle (VT-x chez Intel, AMD-V chez AMD) et I/O MMU (VT-d chez Intel, AMD-Vi chez AMD)
- La carte mère, le chipset et leur firmware doivent supporter I/O MMU
- La carte graphique doit supporter UEFI GOP
Pour le CPU, la marche à suivre dépend du constructeur. Intel segmentant beaucoup ses gammes, le plus simple et d’aller voir la fiche technique sur leur Ark et d’y chercher les mots clé VT-x/VT-d. AMD ayant généralisé le support à partir de Bulldozer, n’importe quel CPU un tant soit peu récent devrait faire l’affaire.
Pour la carte mère, il est peu probable que la fiche technique le mentionne, le plus simple est de vérifier directement dans le firmware si l’option est proposée. À défaut et pour une vérification avant achat, Wikipedia et Xen proposent tous deux des listes.
Pour la carte graphique, L’UEFI GOP ayant été rendu obligatoire courant 2012 par Microsoft pour pousser le monde vers le boot UEFI et Secure Boot, sauf si vous comptez utiliser une vieille carte, vous devriez avoir ce qu’il faut. Vous pouvez éventuellement confirmer en lançant le système avec cette carte en adaptateur graphique principal, en UEFI sans mode de rétrocompatibilité BIOS (CSM). Vous pouvez également vérifier avec TechPowerup.
Pour des recommandations plus générales, faites attention à choisir une carte mère avec suffisamment d’I/O en tout genre pour vos besoins. Le point le plus évident sera les sorties vidéos si vous comptez vous appuyer sur l’iGPU pour l’hôte, mais les slots PCI Express utilisables ont également leur importance si vous comptez transmettre d’autres contrôleurs comme de l’USB ou du SATA.
Côté périphériques, plusieurs écrans ou un écran avec plusieurs entrées vidéos de type HDMI ou DisplayPort risquent d’être indispensables.
Accessoires facultatifs
En plus des pièces centrales (carte mère, CPU, etc), j’ai également utilisé le matériel suivant pour la partie USB.
- un contrôleur USB PCI-Express
- un switch USB (pour partager le clavier et la souris)
- un hub USB (pour rendre accessible des ports USB du contrôleur en passthrough)
Création de la VM VFIO de base
Activer le support dans le firmware
Selon votre carte mère, il est probable que tout ou partie des extensions de paravirtualisation soient désactivées par défaut. Faites un tour dans la configuration de l’UEFI pour les activer. Vous la trouverez généralement dans les paramètres avancés du CPU, mais il n’est pas impossible que le constructeur ait décidé de la planquer à un endroit pas forcément très logique (je l’ai trouvé dans les options Overclocking de ma carte MSI).
Activer le support dans Grub
Le switch à passer à Grub dépendra du constructeur du CPU, intel_iommu=on pour Intel et amd_iommu=on pour AMD. iommu=pt permet de protéger les périphériques incompatibles avec le passthrough. Ces options sont à passer dans /etc/default/grub.
GRUB_CMDLINE_LINUX_DEFAULT="quiet intel_iommu=on iommu=pt"
Mettre à jour Grub pour prendre en compte la modification et relancer le système pour activer le support.
update-grub
reboot
Analyser les groupes IOMMU
Une fois IOMMU activé, il va falloir s’assurer que les groupes d’isolation correspondent bien à ce qu’on cherche. il est possible d’aller farfouiller dans /sys pour trouver ce qu’on cherche, mais ce script affichera les informations de façon plus lisible.
#!/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;
Les groupes exactes dépendront de la façon dont la carte mère aura répartie les pistes PCI Express, en particulier celles du CPU et celles du chipset. Il n’est pas forcément problématique d’avoir plusieurs périphériques dans un même groupe, tant que vous pouvez tous les transmettre à l’invité. Vous devriez toutefois ignorer toute entrée désignant une section générique PCI Express (port racine, concentrateur PCI).
IOMMU Group 1 00:01.0 PCI bridge [0604]: Intel Corporation Skylake PCIe Controller (x16) [8086:1901] (rev 07) IOMMU Group 1 01:00.0 VGA compatible controller [0300]: NVIDIA Corporation GP104 [GeForce GTX 1070] [10de:1b81] (rev a1) IOMMU Group 1 01:00.1 Audio device [0403]: NVIDIA Corporation GP104 High Definition Audio Controller [10de:10f0] (rev a1)
Dans cet exemple (tiré de ma configuration), le groupe IOMMU n°1 correspond aux pistes CPU, tout le reste passe par le PCH. Les entrées 01:00.0 et 01:00.1 devront être transmises à la VM, mais pas la 00:01.0.
Isoler le GPU
Beaucoup de cartes PCI Express peuvent être passés à la volée sans autre forme de procès à la VM au lancement de cette dernière. Pour un GPU, l’opération est toutefois rendu délicate de part la complexité et la taille des pilotes, ainsi que leur importance pour le poste personnel typique dont vous perdrez tout contrôle si la sortie graphique n’est plus disponible. De ce fait, on va s’assurer de lier le plus tôt possible un pilote dédié pour l’isolation afin d’éviter que les habituels (nvidia, nouveau, amdgpu, etc) s’accaparent la carte et causent des conflits.
Il va de soit qu’à ce point, il est capital de s’assurer que l’hôte fonctionne bien sur un GPU alternatif à celui qui va être passé à l’invité, sinon vous allez perdre toute sortie graphique dès le boot du système.
Commencer par reprendre les ID PCI et les renseigner dans /etc/modprobe.d/vfio.conf.
options vfio-pci ids=10de:1b81,10de:10f0
Il vaut mieux s’assurer que les modules VFIO sont chargés dès que possible, dans l’initramfs, via le fichier /etc/initramfs-tools/modules.
vfio_pci vfio vfio_iommu_type1 vfio_virqfd
Mettre à jour l’initramfs et relancer le système pour application de la configuration.
update-initramfs
reboot
Constater si le pilote vfio-pci a bien pris possession des périphériques.
lspci -nnk -d 10de:1b81
La sortie devrait ressemble à quelque chose comme ci-dessous.
01:00.0 VGA compatible controller [0300]: NVIDIA Corporation GP104 [GeForce GTX 1070] [10de:1b81] (rev a1) Subsystem: ASUSTeK Computer Inc. GP104 [GeForce GTX 1070] [1043:8599] Kernel driver in use: vfio-pci Kernel modules: nvidia
Installation de libvirt
On peut à présent s’intéresser de plus près à la question de la virtualisation. La solution choisie pour cet article sera KVM/QEMU via libvirt.
apt install virt-manager libvirt-daemon-system virt-viewer bridge-utils qemu-kvm ovmf libvirt-clients
Outre les briques de base libvirt/QEMU, on notera l’ajout d’OVMF qui servira de firmware UEFI (à la place de SeaBIOS qui ne fait que du BIOS) et virt-manager qui va permettre d’élaborer un squelette de base pour la VM sans avoir à taper trop de XML.
Les connexions à virsh peuvent être simplifiées en rajoutant une variable d’environnement
LIBVIRT_DEFAULT_URI=qemu:///system
Configuration du pont
Cette partie est entièrement facultative, mais recommandée si vous voulez une VM autonome sur la partie réseau. Si vous l’omettez, vous risquez d’avoir des problèmes plus tard, en particulier avec la couche de NAT rajoutée par défaut sur l’hyperviseur, ou si vous voulez faire communiquer l’hôte avec l’invité.
ifupdown
Si vous avez effectué une installation type serveur sur votre hôte, votre réseau est probablement géré par les scripts ifupdown de Debian. Dans ce cas, adapter la configuration suivante depuis /etc/network/interfaces et rebooter (ou relancer le service networking)
allow-hotplug eno1 iface eno1 inet manual auto br0 iface br0 inet dhcp bridge_ports eno1 iface br0 inet6 auto
Network Manager
Si vous avez effectué une installation type bureau sur votre hôte, votre réseau est probablement géré par NetworkManager. Dans ce cas, suivre le chemin indiqué ci-dessous pour naviguer dans l’éditeur de connexion de NM.
Depuis l’éditeur de connexions :
- Supprimer l’interface Ethernet existante
- Créer une interface Virtual > Bridge
- Dans Bridged connections, créer une interface Ethernet
- Sur cette interface, dans Device, choisir la carte physique, éventuellement éditer le nom et sauvegarder
- Mettre en place la configuration IPv4/IPv6 nécessaire dans les onglets de l’interface virtuelle du pont
- Éventuellement, redémarrer Network Manager
Pour assurer une bonne intégration dans libvirt, il est nécessaire de déclarer le pont comme source de réseau. Commencer par coller le XML suivant dans un fichier.
<network>
<name>bridge0</name>
<forward mode="bridge"/>
<bridge name="bridge0" />
</network>
Puis, injecter ce fichier dans libvirt via virsh.
virsh -c qemu:///system net-define bridge.xml
virsh -c qemu:///system net-start bridge0
virsh -c qemu:///system net-autostart bridge0
Configuration libvirt
OVMF n’est pas déclaré par défaut à libvirt, il convient donc de le spécifier dans /etc/libvirt/qemu.conf.
nvram = [
"/usr/share/OVMF/OVMF_CODE.fd:/usr/share/OVMF/OVMF_VARS.fd"
]
Relancer le service libvirtd pour prise en compte.
systemctl restart libvirtd.service
Créer une machine standard avec virt-manager, préremplir les parties évidentes, ignorer le stockage pour l’instant, cocher “personnaliser” à la fin.
- Sur CPU, copier la configuration de l’hôte
- Sur Overview, passer la machine en UEFI et choisir le nvram OVMF précédement configuré
- Créer un contrôleur VirtIO SCSI
- Créer un disque SCSI
Conversion d’une installation Windows physique
Il est possible de convertir un Windows déjà installé en bare-metal, mais il faudra passer outre le problème de l’absence de drivers pour les périphériques VirtIO. Vous pouvez toujours les charger en avance avec la commande suivante, mais ça ne suffira pas pour le disque de boot.
pnputil /add-driver virtstor.inf
Pour contourner ce problème, monter le disque existant en SATA et ajouter un disque vide qcow2 avec le combo SCSI+VirtIO (la taille importe peu). Au premier boot virtualisé, vous pourrez trouver le disque en non-reconnu dans le Gestionnaire de périphériques et rajouter le pilote SCSI VirtIO Red Hat (si vous ne l’avez pas déjà chargé, sinon il devrait être reconnu automatiquement). Votre Windows pourra maintenant passer de bare-metal à virtualisé à la volée, mais demandera quand même un temps d’adaptation à chaque changement, avec un “Configuration des périphériques” qui prendra une ou deux minutes.
En alternative, il est également possible de passer à la VM un contrôleur SATA entier. Il vous faudra pour ça une carte PCI Express dédiée, mais la contrepartie sera que Windows n’y verra que du feu et ne fera pratiquement plus la différence avec son état en bare-metal. Un des avantages concrets sera une stabilisation de la licence qui courra moins le risque d’être invalidée.
Installation d’un nouveau Windows
Si l’installation est neuve, elle se fera via un adaptateur QXL standard. Pensez à récupérer l’ISO de Red Hat avec les pilotes VirtIO pour les passer à l’installeur, sinon vous serez bloqué lors du formatage du disque faute de block device utilisable.
Attacher les périphériques PCI
Une fois l’installation terminée, vous pouvez détacher toute ce qui a trait à Spice/QXL et remplacer par les périphériques PCI précédemment isolés.
Camouflage de l’hyperviseur pour les cartes Nvidia
Nvidia effectue des vérifications basiques pour s’assurer que le pilote ne tourne pas dans un hyperviseur. Cette contre-mesure ne vise pas vraiment les joueurs et enthousiastes, plus les utilisations professionnelles en centre de données, mais en pratique risquent de vous embêter tout autant. Le contournement n’a toutefois rien de compliqué avec un QEMU un peu récent.
virsh edit mavm
<features>
<hyperv>
...
<vendor_id state='on' value='whatever'/>
...
</hyperv>
...
<kvm>
<hidden state='on'/>
</kvm>
</features>
Sans cette partie, vous risquez de voir le pilote lâcher l’affaire et apparaître dans le Gestionnaire de périphériques avec l’erreur 43.
Input
Arrivé à ce point, vous devriez déjà avoir une VM quasiment utilisable, au détail prêt des périphériques d’entrée, en particulier le clavier et la souris. Vous avez trois solutions pour résoudre cette dernière barrière.
- Consacrer un couple clavier/souris à la VM
- Consacrer un contrôleur USB à la VM
- Partage des périphériques avec evdev
Périphériques dédiés
Le setup le plus simple est de passer les périphériques USB qui vous intéressent en USB Passthrough. Ces périphériques seront naturellement inutilisables depuis l’hôte dès le lancement de la VM et vous aurez besoin de deux claviers et deux souris pour piloter les deux en même temps.
N’espérez pas faire le malin en passant un hub USB, ça ne marchera pas. De par la nature d’USB, seul le hub sera exposé à la VM et le reste restera sur l’hôte.
Contrôleur USB dédié
Une solution alternative consiste à passer en PCI Passthrough tout un contrôleur USB. Cette solution est celle offrant le plus de souplesse à l’usage, étant donné que vous pourrez ensuite utiliser un hub USB pour démultiplier les possibilités de branchement et passer un périphérique de l’hôte à l’invité simplement en changeant de port, voir en utilisant un switch (très pratique pour échanger le clavier et la souris).
Vous pouvez vous arranger avec les contrôleurs présents de base sur votre carte mère si ça vous convient. Par exemple, vous pourriez en avoir un pour les ports boîtier/avant et un pour les ports arrière, mais à confirmer au cas par cas. Sinon, vous pouvez également vous appuyer sur une carte d’extension PCI Express et garder ceux de la carte mère pour l’hôte.
Le code suivant permet de visualiser un peu mieux quel périphérique USB correspond à quel périphérique PCI. Le reste viendra d’expérimentation à base de branchement/débranchement.
for usb_ctrl in $(find /sys/bus/usb/devices/usb* -maxdepth 0 -type l); do pci_path="$(dirname "$(realpath "${usb_ctrl}")")"; echo "Bus $(cat "${usb_ctrl}/busnum") --> $(basename $pci_path) (IOMMU group $(basename $(realpath $pci_path/iommu_group)))"; lsusb -s "$(cat "${usb_ctrl}/busnum"):"; echo; done
À noter que ce contrôleur n’aura pas besoin d’être assigné prématurément à vfio-pci, les chances étant grandes qu’il supporte le reset PCI. Vous pouvez vous en assurer avec le script suivant.
for iommu_group in $(find /sys/kernel/iommu_groups/ -maxdepth 1 -mindepth 1 -type d);do echo "IOMMU group $(basename "$iommu_group")"; for device in $(\ls -1 "$iommu_group"/devices/); do if [[ -e "$iommu_group"/devices/"$device"/reset ]]; then echo -n "[RESET]"; fi; echo -n $'\t';lspci -nns "$device"; done; done
evdev
La solution evdev permet de passer un clavier et une souris de l’hôte à l’invité simplement avec la combinaison des deux touches Control. Si elle semble en apparence simple, notez que vous aurez quand même une couche d’émulation qui peut provoquer des problèmes avec des jeux même modérément nerveux et demandant beaucoup d’inputs.
Pour commencer, listes les périphériques disponibles.
ls -l /dev/input/by-id
Vous pouvez tester si une device est bien ce que vous chercher avec un cat, puis en provoquant des évènements dessus (bouger la souris, taper sur le clavier). ^C vous fera sortir du cat. Si vous notez plusieurs ID similaires avec des versions en event, privilégiez les event.
Pour résoudre les problèmes de permission à venir, faire tourner la VM en tant qu’utilisateur ayant accès à input et rajouter les périphériques nécessaires au cgroup de QEMU (sans oublier ceux définis par défaut) dans /etc/libvirt/qemu.conf.
gpasswd -a monutilisateur input
user = "monutilisateur" group = "kvm" cgroup_device_acl = [ "/dev/input/by-id/usb-046d_Gaming_Keyboard_G110-event-kbd", "/dev/input/by-id/usb-Logitech_Gaming_Mouse_G600_9193F9B4F5E20017-event-mouse", "/dev/null", "/dev/full", "/dev/zero", "/dev/random", "/dev/urandom", "/dev/ptmx", "/dev/kvm", "/dev/kqemu", "/dev/rtc","/dev/hpet" ]
Ne pas oublier de relancer libvirtd.
systemctl restart libvirtd
Si votre système tourne avec une couche de protection type AppArmor ou SELinux, il sera également nécessaire de relâcher les permissions à ce niveau, soit avec des règles plus souples, soit en les désactivant complètement.
Reste à assigner ces périphériques à la VM. virt-manager ou libvirt ne supportent pas encore cette configuration, il faudra donc régler ça directement avec la ligne de commande QEMU.
Rajouter un espace de nom XML à l’élément racine
<domain type='kvm' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>
Rajouter les arguments QEMU à la fin, après le bloc <devices>.
<qemu:commandline>
<qemu:arg value='-object'/>
<qemu:arg value='input-linux,id=mouse1,evdev=/dev/input/by-id/MOUSE_NAME'/>
<qemu:arg value='-object'/>
<qemu:arg value='input-linux,id=kbd1,evdev=/dev/input/by-id/KEYBOARD_NAME,grab_all=on,repeat=on'/>
</qemu:commandline>
Audio
L’audio reste à ce jour le pire casse-tête dans notre cas d’utilisation, en partie car cette couche de QEMU a été passablement négligée. Vous avez deux options sur ce plan :
- utiliser la sortie HDMI/DisplayPort de la carte graphique passée à l’invité
- tenter de faire marcher la virtualisation tant bien que mal
Audio HDMI/DP
Étant donné qu’on passe déjà une carte graphique pour la vidéo, le plus simple est encore de passer l’audio par le même chemin et de tirer profit des sorties audio de votre écran, vous éviterez ainsi tous les problèmes qui viennent avec l’émulation d’une carte son. Le seul point noir est évidemment que sans matériel supplémentaire, vous aurez à faire un choix entre le son de l’hôte ou celui de l’invité.
Vous pouvez supprimer la carte son ICH6 installée par défaut sur votre VM dans virt-manager, elle ne servira plus et risque de vous poser des problèmes plus qu’autre chose.
Boucle avec l’entrée audio
Une solution au dilemme du mixage consiste à utiliser un câble pour relier la sortie audio de l’écran à l’entrée de la carte son de l’hôte. Faites bien attention à utiliser l’entrée (Line-in, qui devrait être bleue) et non le microphone (Mic, qui devrait être rose), cette dernière n’étant pas du tout adaptée à cette utilisation (prévue pour des niveaux beaucoup plus faibles, coincée en Mono).
Reste à demander au serveur de son de remixer l’entrée dans la sortie pour fusionner les flux. Pour Pulseaudio, il suffira de charger le module de loopback dans votre fichier /etc/pulse/default.pa.
load-module module-loopback
Puis relancer Pulseaudio
pulseaudio -k
Ou alors, pour charger et décharger le module dynamiquement.
pacmd load-module module-loopback pacmd unload-module module-loopback
Cette technique n’est toutefois pas exempte d’inconvénients. Le plus flagrant est qu’une entrée jack aura tendance à ajouter du bruit qui risque de devenir gênant à partir d’un certain volume.
Carte son virtuelle
Routage Pulseaudio
Le serveur Pulseaudio tournant généralement avec les droits de l’utilisateur, il faudra comme pour evdev indiquer à QEMU de lancer la VM avec votre utilisateur dans /etc/libvirt/qemu.conf.
user = "monutilisateur" # pour qemu 3.0 et plus nographics_allow_host_audio = 1
Puis relancer libvirtd
systemctl restart libvirtd
Si vous utilisez qemu 3.0 ou plus récent et que l’utilisateur de la VM est le même que celui du serveur pulseaudio, vous pouvez en rester là. Autrement, comme pour evdev, vous devrez toucher aux éléments QEMU dans le XML de votre VM.
virsh edit MAVM
<domain type='kvm' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>
...
<qemu:commandline>
<qemu:env name='QEMU_AUDIO_DRV' value='pa'/>
<qemu:env name='QEMU_PA_SERVER' value='/run/user/1000/pulse/native'/>
</qemu:commandline>
Le 1000 avec QEMU_PA_SERVER sera à ajuster selon l’UID de votre utilisateur.
Dans le cas d’utilisateurs différents pour Pulseaudio et VM, il faudra également pointer l’utilisateur VM vers le socket du serveur audio (via default-server = unix:/run/user/$ID/pulse/native dans la configuration client.conf), ainsi que partager le contenu du cookie d’authentification qui réside à ~/.config/pulse/cookie.
Message Signaled Interrupts
Cette configuration devrait déjà vous fournir un audio qui fonctionne. Si le son est déformé/ralenti/démoniaque, il faudra pousser un peu plus loin en passant les périphériques de l’invité en Message Signaled Interrupts (MSI) au lieu de Line-Based Interrupts (LSI).
Dans le cas des périphériques physiques passés à l’invité, vous pouvez les vérifier à tout moment avec lspci.
sudo lspci -vs 01:00.1
La sortie devrait ressemble à ce qui suit.
01:00.1 Audio device: NVIDIA Corporation GP104 High Definition Audio Controller (rev a1) Subsystem: ASUSTeK Computer Inc. GP104 High Definition Audio Controller Flags: bus master, fast devsel, latency 0, IRQ 146 Memory at df080000 (32-bit, non-prefetchable) [size=16K] Capabilities: [60] Power Management version 3 Capabilities: [68] MSI: Enable+ Count=1/1 Maskable- 64bit+ Capabilities: [78] Express Endpoint, MSI 00 Capabilities: [100] Advanced Error Reporting Kernel driver in use: vfio-pci Kernel modules: snd_hda_intel
Un Enable- indique que le périphérique supporte MSI mais qu’il est désactivé dans la VM, un Enable+ indique que MSI est supporté et actif.
Vous pouvez vérifier dans l’invité quels périphériques utilisent tel ou tel mode avec le gestionnaire de périphériques. Lancez-le (clic droit sur le menu démarrer), passer l’affichage en tri “Ressources par type”, et ouvrez la section “Requête d’interruption (IRQ)”. Les périphériques indexés positivement (en ascendant à partir de 00000000) utilisent LSI, ceux indexés négativement (en descendant à partir de FFFFFFFF) utilisent MSI.
Les basculer est une affaire assez compliquée qui passera par le registre HKLM. Vous aurez pour cela besoin du “chemin” du périphérique que vous cherchez à basculer. Ouvrez les propriétés du périphériques dans le gestionnaire, cherchez l’onglet Détails et Sélectionnez “Chemin d’accès à l’instance du périphérique”. Cette valeur est un chemin relatif sous la clé HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum. À partir de là, continuer à Device Parameters\Interrupt Management\MessageSignaledInterruptProperties (en le créant si nécessaire), la clé pour basculer sera MSISupported qui sera à 0 (DWORD 0x00000000) ou 1 (DWORD 0x00000001) selon le statut actuel du support MSI. Prenez les mesures de sécurité habituelles en modifiant le registre (sauvegarde, point de restauration), puis faites vos tests en redémarrant l’OS invité pour prise en compte. Vérifiez dans le gestionnaire si les IRQ ont changé et sont passées en négatif.
Si vous ne voulez pas vous embêter avec le registre, vous pouvez également vous appuyer sur un script PowerShell qui fera le boulot pour vous
Pour référence et sauvegarde, le script est également copié ci-dessous (crédits du code au forum Guru3D).
<#
.SYNOPSIS
MSI-mode utility
.DESCRIPTION
Main purpose of this script is turning on MSI-mode for all PCI devices in bulk. Script offers four modes which can be selected through following command line arguments:
'on' - turning MSI-mode on;
'off' - turning MSI-mode off;
'reg' - printing reg-file for backup purposes (to save text to reg-file use standard redirection or piplining to comandlet Out-File);
- started without command line argument script prints devices capable for MSI-mode.
.INPUTS
None. You cannot pipe objects to MSI_mode_utils.ps1.
.OUTPUTS
Script prints text which can be redirected to any text file by means of either standard redirection (>) or pipilining to comandlet Out-File.
.EXAMPLE
PS C:\Tools> .\MSI_mode_utils.ps1 'on'
.EXAMPLE
PS C:\Tools> .\MSI_mode_utils.ps1 'off'
.EXAMPLE
PS C:\Tools> .\MSI_mode_utils.ps1 'reg' > c:\tools\msi_mode_backup.reg
.LINK
http://forums.guru3d.com/showthread.php?t=378044
#>
#requires -version 2
[hashtable]$dev_descr_dict = @{}
[hashtable]$dev_irqman_dict = @{}
[hashtable]$dev_msiprops_dict = @{}
[hashtable]$dev_msisupported_dict = @{}
# enumerate all HW IDs - subkeys of hklm\system\CurrentControlSet\Enum\PCI
[Microsoft.Win32.RegistryKey]$pci_rk = gi registry::hklm\system\CurrentControlSet\Enum\PCI -ErrorAction Stop
try {
foreach($id in $pci_rk.GetSubKeyNames()) {
[Microsoft.Win32.RegistryKey]$id_rk = $null
try {
$id_rk = $pci_rk.OpenSubKey($id)
if(!$id_rk) { continue }
# enumerate all devices - subkeys of HW ID
foreach($dev in $id_rk.GetSubKeyNames()) {
[Microsoft.Win32.RegistryKey]$dev_rk = $null
try {
$dev_rk = $id_rk.OpenSubKey($dev)
if(!$dev_rk) { continue }
# skip devices without 'Device Parameters' and 'Device Parameters\Interrupt Management' subkeys
[Microsoft.Win32.RegistryKey]$dev_params_rk = $dev_rk.OpenSubKey('Device Parameters')
if(!$dev_params_rk) { continue }
[Microsoft.Win32.RegistryKey]$irq_management_rk = $dev_params_rk.OpenSubKey('Interrupt Management', $true)
$dev_params_rk.Close(); $dev_params_rk = $null
if(!$irq_management_rk) { continue }
[string]$devkey = $id+'\'+$dev
# store device`s description
$dev_descr_dict[$devkey] = $dev_rk.GetValue('DeviceDesc', $dev)
# store device`s 'Interrupt Management' registry key
$dev_irqman_dict[$devkey] = $irq_management_rk
# store device`s 'MessageSignaledInterruptProperties' registry key
[Microsoft.Win32.RegistryKey]$dev_msiprops_rk = $irq_management_rk.OpenSubKey('MessageSignaledInterruptProperties', $true)
$dev_msiprops_dict[$devkey] = $dev_msiprops_rk
# store device`s 'MSISupported' registry value
if($dev_msiprops_rk) { $dev_msisupported_dict[$devkey] = $dev_msiprops_rk.GetValue('MSISupported') }
else { $dev_msisupported_dict[$devkey] = $null }
}
finally { if($dev_rk) { $dev_rk.Close(); $dev_rk = $null } }
}
}
finally { if($id_rk) { $id_rk.Close(); $id_rk = $null } }
}
if($args.Count -eq 0) {
#print devices` info
filter print_devices_with_MSI_turned_on {
begin { '--------------------- Devices with MSI-mode turned on ---------------------' }
process {
if($dev_msiprops_dict[$_] -and $dev_msisupported_dict[$_] -eq 1) { $dev_descr_dict[$_] }
}
}
filter print_devices_with_MSI_turned_off {
begin { '--------------------- Devices with MSI-mode turned off ---------------------' }
process {
if($dev_msiprops_dict[$_] -and $dev_msisupported_dict[$_] -eq 0) { $dev_descr_dict[$_] }
}
}
filter print_devices_without_MSI {
begin { '--------------------- Devices with not configured MSI-mode ---------------------' }
process {
if(!$dev_msiprops_dict[$_] -or $dev_msisupported_dict[$_] -eq $null) { $dev_descr_dict[$_] }
}
}
$dev_descr_dict.Keys | print_devices_with_MSI_turned_on
$dev_descr_dict.Keys | print_devices_with_MSI_turned_off
$dev_descr_dict.Keys | print_devices_without_MSI
}
elseif($args[0] -eq 'off') {
# turning MSI-mode off
filter turn_MSI_off {
begin { '--------------------- Turning MSI-mode off ---------------------' }
process {
if($dev_msiprops_dict[$_] -and $dev_msisupported_dict[$_] -eq 1) {
$dev_descr_dict[$_]
[Microsoft.Win32.RegistryKey]$rk = $dev_msiprops_dict[$_]
$rk.SetValue('MSISupported', 0, 'DWord')
$dev_msisupported_dict[$_] = 0
}
}
}
$dev_descr_dict.Keys | turn_MSI_off
}
elseif($args[0] -eq 'on') {
# turning MSI-mode on
filter turn_MSI_on {
begin { '--------------------- Turning MSI-mode on ---------------------' }
process {
if(!$dev_msiprops_dict[$_]) {
$dev_descr_dict[$_]
[Microsoft.Win32.RegistryKey]$rk = $dev_irqman_dict[$_]
$rk = $rk.CreateSubKey('MessageSignaledInterruptProperties')
$dev_msiprops_dict[$_] = $rk
$rk.SetValue('MSISupported', 1, 'DWord')
$dev_msisupported_dict[$_] = 1
}
elseif($dev_msisupported_dict[$_] -ne 1) {
$dev_descr_dict[$_]
[Microsoft.Win32.RegistryKey]$rk = $dev_msiprops_dict[$_]
$rk.SetValue('MSISupported', 1, 'DWord')
$dev_msisupported_dict[$_] = 1
}
}
}
$dev_descr_dict.Keys | turn_MSI_on
}
elseif($args[0] -eq 'reg') {
# print backup reg-file content
filter print_MSI_registry {
begin { 'Windows Registry Editor Version 5.00'; '' }
process {
if(!$dev_msiprops_dict[$_]) {
'[-HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\PCI\{0}\Device Parameters\Interrupt Management\MessageSignaledInterruptProperties]' -f $_
}
elseif($dev_msisupported_dict[$_] -eq $null) {
'[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\PCI\{0}\Device Parameters\Interrupt Management\MessageSignaledInterruptProperties]' -f $_
'"MSISupported"=-'
}
else {
'[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\PCI\{0}\Device Parameters\Interrupt Management\MessageSignaledInterruptProperties]' -f $_
'"MSISupported"=dword:0000000{0}' -f $dev_msisupported_dict[$_]
}
''
}
}
$dev_descr_dict.Keys | print_MSI_registry
}
}
finally {
# clenaup
foreach($rk in $dev_irqman_dict.Values) { $rk.Close() }
foreach($rk in $dev_msiprops_dict.Values) { if($rk) { $rk.Close() } }
$dev_irqman_dict.Clear(); $dev_irqman_dict = $null
$dev_msiprops_dict.Clear(); $dev_msiprops_dict = $null
$dev_descr_dict.Clear(); $dev_descr_dict = $null
$dev_msisupported_dict.Clear(); $dev_msisupported_dict = $null
$pci_rk.Close(); $pci_rk = $null
Pour rappel, vous devez activer l’execution de script PowerShell avant de pouvoir l’utiliser.
Set-ExecutionPolicy -ExecutionPolicy Unrestricted
Crachotement de l’audio
Note: Toute cette section est à présent obsolète, la plupart des problèmes audio ayant été patchés avec les versions 3.0, 3.1, et dans une moindre mesure 4.1. Assurez-vous juste d’utiliser un modèle de machine virtuelle récent (ie, pc-i440fx-4.1).
L’audio devrait maintenant être écoutable, mais pas parfait, toujours parasité par des petits crachotements. Il est possible que cet état vous convienne, dans mon expérience ça restait assez mineur tant que je fonctionnais sur enceintes sans mettre le son trop fort. Par contre, en montant le son ou en mettant un casque, ces parasites peuvent devenir gênants.
On commence malheureusement à se frotter aux limites de QEMU, ces parasites étant dûs à des bugs dans son pilote Pulseaudio. Cet état de fait n’est toutefois pas sans solution, un pilote amélioré ayant été publié sur le Reddit VFIO. Ce pilote vous demandera toutefois de recompiler votre propre QEMU.
apt install build-essential
apt build-dep qemu-kvm
git clone https://github.com/spheenik/qemu.git
Le fork ne compile plus tel quel sur les dernières glibc à cause d’un changement relatif à memfd_create qui va vous renvoyer ce type d’insultes :
util/memfd.c:40:12: error: static declaration of memfd_create follows non-static declaration
Le problème peut être réglé avec ce patch.
diff --git a/configure b/configure
index 6587e8014b..0c0c610468 100755
--- a/configure
+++ b/configure
@@ -3847,7 +3847,7 @@ fi
# check if memfd is supported
memfd=no
cat > $TMPC << EOF
-#include <sys/memfd.h>
+#include <sys/mman.h>
int main(void)
{
diff --git a/util/memfd.c b/util/memfd.c
index 4571d1aba8..412e94a405 100644
--- a/util/memfd.c
+++ b/util/memfd.c
@@ -31,9 +31,7 @@
#include "qemu/memfd.h"
-#ifdef CONFIG_MEMFD
-#include <sys/memfd.h>
-#elif defined CONFIG_LINUX
+#if defined CONFIG_LINUX && !defined CONFIG_MEMFD
#include <sys/syscall.h>
#include <asm/unistd.h>
Vous pouvez à présent compiler le fork.
cd qemu
mkdir build
cd build
../configure --prefix=/opt/qemu-test --python=/usr/bin/python2 --target-list=x86_64-softmmu --audio-drv-list=pa --disable-werror
make
Vous pouvez utiliser le binaire en place dans le répertoire build, ou l’installer dans /opt/qemu-test.
sudo make install
Reste à indiquer dans le XML de la VM où trouver le nouvel émulateur.
<emulator>/opt/qemu-test/bin/qemu-system-x86_64</emulator>
Ce fork est basé sur QEMU 2.11, si vous utilisiez une version plus récente, il faudra aussi modifier votre machine QEMU pour qu’elle se lance.
<type arch='x86_64' machine='pc-i440fx-2.11'>hvm</type>
L’audio devrait à présent marcher comme attendu. On peut espérer que le patch finira upstream, les procédures par le créateur ont été suivies, le code vit maintenant dans une des branches expérimentales du mainteneur audio QEMU.
Performance
Arrivé à ce point, vous devriez avoir une VM complètement utilisable. Il reste toutefois quelques axes d’amélioration possibles pour optimiser un peu le tout.
Pin des CPU et threads IO
Épingler des cœurs CPU à la VM permettra de limiter la perte de temps lié au context switching. Si votre CPU supporte l’hyperthreading, soyez attentifs à la disposition de ces cœurs logiques supplémentaires et adressez les en conséquence. Passer les deux cœurs logiques d’un même cœur physique permet de mieux isoler la VM. Répartir la VM sur tous les cœurs physiques vous donnera peut-être plus de performances sous certaines conditions, mais peut tout autant vous en faire perdre si la VM fini par étouffer l’hôte.
Épingler les IO sur d’autres cœurs permet de laisser l’émulation des IO à part et d’éviter les blocages sur ce point.
<iothreads>2</iothreads>
<cputune>
<vcpupin vcpu='0' cpuset='0'/>
<vcpupin vcpu='1' cpuset='6'/>
<vcpupin vcpu='2' cpuset='1'/>
<vcpupin vcpu='3' cpuset='7'/>
<vcpupin vcpu='4' cpuset='2'/>
<vcpupin vcpu='5' cpuset='8'/>
<vcpupin vcpu='6' cpuset='3'/>
<vcpupin vcpu='7' cpuset='9'/>
<emulatorpin cpuset='5-11'/>
<iothreadpin iothread='1' cpuset='5'/>
<iothreadpin iothread='2' cpuset='11'/>
</cputune>
Huge pages mémoire
Afin de limiter la fragmentation de l’espace mémoire, il peut être préférable de s’appuyer sur les huge pages. Dans le cas d’une VM avec IOMMU, la mémoire devra être allouée d’un bloc sur des pages statiques allouées en avance. La taille d’une page par défaut étant de 2Mo, à vous de faire le calcul sur combien de ces pages vous avez besoin. Cette allocation peut se faire par sysctl.
sysctl vm.nr_hugepages=8192
Pour rendre cette allocation automatique au boot, par exemple dans /etc/sysctl.d/00-vfio.conf.
vm.nr_hugepages = 8192
Il faudra également indiquer à la VM de taper dans ces hugepages.
<memoryBacking>
<hugepages/>
</memoryBacking>
Patch NPT pour les CPU AMD
Un patch a été appliqué sur les noyaux Linux 4.9, 4.14 et plus récents pour corriger un bug touchant les tables de pages mémoires imbriquées sur les CPU AMD qui dégradait sérieusement les performances quand NPT était activé. Je n’ai pas vérifié si le noyaux standard de Stretch le propose, c’est possible étant donné qu’il est basé sur la branche LTS 4.9, mais à confirmer. Buster ne devrait pas poser de problème, étant basé sur la branche 4.16 à l’heure actuelle.
Code final
Pour référence, voici le code XML final de ma VM.
<domain type='kvm' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>
<name>Seath</name>
<uuid>7403c308-329c-4c82-9d8e-b03efff9feeb</uuid>
<memory unit='KiB'>16777216</memory>
<currentMemory unit='KiB'>16777216</currentMemory>
<memoryBacking>
<hugepages/>
</memoryBacking>
<vcpu placement='static'>8</vcpu>
<iothreads>2</iothreads>
<cputune>
<vcpupin vcpu='0' cpuset='0'/>
<vcpupin vcpu='1' cpuset='6'/>
<vcpupin vcpu='2' cpuset='1'/>
<vcpupin vcpu='3' cpuset='7'/>
<vcpupin vcpu='4' cpuset='2'/>
<vcpupin vcpu='5' cpuset='8'/>
<vcpupin vcpu='6' cpuset='3'/>
<vcpupin vcpu='7' cpuset='9'/>
<emulatorpin cpuset='5-11'/>
<iothreadpin iothread='1' cpuset='5'/>
<iothreadpin iothread='2' cpuset='11'/>
</cputune>
<os>
<type arch='x86_64' machine='pc-i440fx-2.11'>hvm</type>
<loader readonly='yes' type='pflash'>/usr/share/OVMF/OVMF_CODE.fd</loader>
<nvram>/var/lib/libvirt/qemu/nvram/Seath_VARS.fd</nvram>
<bootmenu enable='no'/>
</os>
<features>
<acpi/>
<apic/>
<hyperv>
<relaxed state='on'/>
<vapic state='on'/>
<spinlocks state='on' retries='8191'/>
<vendor_id state='on' value='6PqKaQHebsMg'/>
</hyperv>
<kvm>
<hidden state='on'/>
</kvm>
<vmport state='off'/>
</features>
<cpu mode='host-model' check='partial'>
<model fallback='allow'/>
<topology sockets='1' cores='4' 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>/opt/qemu-test/bin/qemu-system-x86_64</emulator>
<disk type='block' device='disk'>
<driver name='qemu' type='raw' cache='none' io='native'/>
<source dev='/dev/sdc'/>
<target dev='sda' bus='scsi'/>
<boot order='1'/>
<address type='drive' controller='0' bus='0' target='0' unit='0'/>
</disk>
<controller type='scsi' index='0' model='virtio-scsi'>
<driver queues='8'/>
<address type='pci' domain='0x0000' bus='0x00' slot='0x05' function='0x0'/>
</controller>
<controller type='pci' index='0' model='pci-root'/>
<controller type='usb' index='0' model='nec-xhci'>
<address type='pci' domain='0x0000' bus='0x00' slot='0x04' function='0x0'/>
</controller>
<controller type='virtio-serial' index='0'>
<address type='pci' domain='0x0000' bus='0x00' slot='0x09' function='0x0'/>
</controller>
<interface type='network'>
<mac address='52:54:00:1c:21:51'/>
<source network='bridge0'/>
<model type='virtio'/>
<address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x0'/>
</interface>
<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='keyboard' bus='ps2'/>
<sound model='ich9'>
<address type='pci' domain='0x0000' bus='0x00' slot='0x0b' function='0x0'/>
</sound>
<hostdev mode='subsystem' type='pci' managed='yes'>
<source>
<address domain='0x0000' bus='0x01' slot='0x00' function='0x0'/>
</source>
<address type='pci' domain='0x0000' bus='0x00' slot='0x06' 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='0x00' slot='0x07' function='0x0'/>
</hostdev>
<hostdev mode='subsystem' type='pci' managed='yes'>
<source>
<address domain='0x0000' bus='0x06' slot='0x00' function='0x0'/>
</source>
<address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/>
</hostdev>
<memballoon model='virtio'>
<address type='pci' domain='0x0000' bus='0x00' slot='0x08' function='0x0'/>
</memballoon>
</devices>
<qemu:commandline>
<qemu:env name='QEMU_AUDIO_DRV' value='pa'/>
<qemu:env name='QEMU_PA_SERVER' value='/run/user/1000/pulse/native'/>
</qemu:commandline>
</domain>
Alternative
Il est également possible d’utiliser Bumblebee pour récupérer l’usage de la carte Nvidia sur l’hôte quand la VM est éteinte. Dans ce cas de figure, tous les écrans sont branchés sur la carte graphique de l’hôte. Ce genre de montage ne marche toutefois probablement pas avec Gsync, ce pourquoi je ne l’ai pas recherché plus que ça.
Sources
À des fins d’exhaustivités, voici une compilation des sources sur lesquelles je me suis appuyé pour rédiger ce papier.
- https://wiki.archlinux.org/index.php/PCI_passthrough_via_OVMF
- https://www.reddit.com/r/VFIO/
- https://www.reddit.com/r/VFIO/comments/74vokw/improved_pulse_audio_driver_for_qemu/
- https://forums.guru3d.com/threads/windows-line-based-vs-message-signaled-based-interrupts.378044/
- https://gist.github.com/hflw/ed9590f4c79daaeb482c2419f74ed897
- https://passthroughpo.st/using-evdev-passthrough-seamless-vm-input/
- https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html-single/virtualization_tuning_and_optimization_guide/index
Conclusion
J’ai maintenant une VM Windows utilisable pour jouer. Pas pris le temps de benchamrker, mais sur mon jeu du moment (Mass Effect: Andromeda), les performances n’ont pris qu’un léger coup, environ 5fps. Le plus pénible maintenant que la configuration de la VM est au point reste la configuration de Windows à proprement parler, que j’aborderais peut-être plus tard quand j’en aurais marre de naviguer dans des clicodromes à chaque réinstallation.
Il est intéressant de noter que le PCI Passthrough pourrait n’être qu’une bricole temporaire. Un autre concept encore plus intéressant émerge ces dernières années, dit du vGPU, permettant de virtualiser le GPU comme on virtualise un CPU. L’idée est déjà implémentée par tous les grands fournisseurs de matériel, mais seul Intel semble pour l’instant décidé à le distribuer librement, nVidia et AMD gardant tous les deux la fonctionnalité pour leurs GPU orientés pro (Tesla, Fire Pro) dont l’étiquette de prix est bien au delà des moyens d’un particulier (sauf si des cartes à 7000-9000€ pour des performances 3D vaguement supérieures à celles d’un GPU grand public à 700-1000€ sont dans vos moyens). À voir si cet état de fait persistera sur le long terme, mais vu le passif des constructeurs en général et de Nvidia en particulier, il est fort probable que ça ne change pas de sitôt.