Compréhension de l'API KVM via rust-vmm
Pour explorer les mécanismes internes des machines virtuelles (VM) sous Linux, rust-vmm fournit une base moderne écrite en Rust pour construire des hyperviseurs légers. L’un des composants centraux est kvm-ioctls, qui permet d’interagir directement avec le module noyau KVM via des appels système.
Démarrage rapide : exécution et débogage d’un test simple
Un bon point d’entrée pour comprendre le fonctionnement d’une VM minimaliste est la fonction test_vm présente dans le dépôt vmm. Voici comment localiser et exécuter ce test :
FILE_LINE=$(git grep -n "pub fn test_vm" | head -1)
echo ${FILE_LINE%:*} # Affiche le chemin du fichier
cargo test test_vm
Cette commande identifie l'emplacement de la fonction de test et lance l’exécution. Pour un débogage plus fin, on peut utiliser rust-gdb :
rust-gdb target/debug/deps/vmm-*
(gdb) info functions test_vm
(gdb) b src/vm.rs:854
(gdb) run --test test_vm
Exploration du démarrage d’un hyperviseur complet : Cloud Hypervisor
Cloud Hypervisor est un exemple concret d’hyperviseur utilisant rust-vmm. Pour déboguer son point d’entrée principal :
rust-gdb cloud-hypervisor/target/debug/cloud-hypervisor
(gdb) info functions main
(gdb) b cloud_hypervisor::main::h9d806d3576285a29
(gdb) run --kernel ./vmlinux.bin --disk ./clear-29160-kvm.img --cpus 4 --memory 512 --rng
Le backtrace lors du lancement révèle le flux d’exécution typique : configuration → création de la VM → chargement du noyau → démarrage des vCPU.
Analyse du périphérique Virtio-Rng
Les dispositifs Virtio sont au cœur de la paravirtualisation. Prenons l’exemple du générateur de nombres aléatoires (Rng) :
Rng::new("/dev/urandom")
En plaçant un point d’arrêt sur cette méthode :
(gdb) b vm_virtio::rng::Rng::new::h79792d122294607e
On observe que Rng::new est appelé durant l’initialisation du DeviceManager, qui configure tous les périphériques avent le démarrage de la VM.
Lorsque le pilote invité active le périphérique, la méthode activate est invoquée :
(gdb) info functions "Rng.*as.*VirtioDevice.*activate"
(gdb) b _$LT$vm_virtio..rng..Rng$u20$as$u20$vm_virtio..device..VirtioDevice$GT$::activate::hdedcc2490e8a5155
À l’intérieur de activate, on vérifie que le nombre de files d’attente correspond à ce que s’attend le périphérique :
if queues.len() != NUM_QUEUES || queue_evts.len() != NUM_QUEUES {
return Err(ActivateError::BadQueue);
}
Un backtrace montre que l’activation est déclenchée par une écriture sur une BAR PCI via VirtioPciDevice::write_bar, illustrant le lien entre configuration PCI et activation Virtio.
Structure interne des composants Virtio
La communication entre le frontend (invité) et le backend (hôte) repoce sur des structures partagées en mémoire physique. Les principales structures incluent :
- Descriptor : représente un bloc de données dans la chaîne de descripteurs.
struct Descriptor {
addr: u64,
len: u32,
flags: u16,
next: u16,
}
- DescriptorChain : encapsule un descripteur actif avec accès à la mémoire invité.
pub struct DescriptorChain<'a> {
mem: &'a GuestMemoryMmap,
desc_table: GuestAddress,
queue_size: u16,
ttl: u16,
pub index: u16,
pub addr: GuestAddress,
pub len: u32,
pub flags: u16,
pub next: u16,
}
- Queue : gère les tables available et used, ainsi que l’état de progression.
pub struct Queue {
max_size: u16,
pub size: u16,
pub ready: bool,
pub vector: u16,
pub desc_table: GuestAddress,
pub avail_ring: GuestAddress,
pub used_ring: GuestAddress,
next_avail: Wrapping<u16>,
next_used: Wrapping<u16>,
}
- AvailIter : itérateur sur les chaînes disponibles dans la file.
pub struct AvailIter<'a, 'b> {
mem: &'a GuestMemoryMmap,
desc_table: GuestAddress,
avail_ring: GuestAddress,
next_index: Wrapping<u16>,
last_index: Wrapping<u16>,
queue_size: u16,
next_avail: &'b mut Wrapping<u16>,
}
Gestion des adresses BAR PCI
Les périphériques PCI exposent des régions mémoire ou E/S via des BAR (Base Address Registers). Dans rust-vmm :
- Un
SystemAllocatorest instancié pour gérer l’espace d’adresses MMIO. - Le
DeviceManagerappelleallocate_barssur chaqueVirtioPciDevice. - L’allocation est réalisée par
AddressAllocator::allocatedansvm-allocator. - La méthode
SystemAllocator::allocate_mmio_addressesréserve une plage d’adresses MMIO pour le périphérique.
Support du passage direct PCI (VFIO)
rust-vmm intègre VFIO pour permettre l’affectation directe de périphériques matériels aux VMs. Le support est implémenté en plusieurs étapes :
- Génération des liaisons C : permettent d’appeler les API noyau VFIO.
- Interface utilisateur VFIO : fonctions comme
setup_dma_map,enable_msi,region_read/write. - Implémentation PCI : gession des BAR, des registres de configuration, des interruptions MSI/MSI-X.
- Intégration KVM : configuration des routes d’interruption via KVM.
- Mappage MMIO : exposition des régions mémoire du périphérique au contexte invité.
Communication inter-processus avec transfert de descripteurs
Un mécanisme clé pour la sécurité et l’isolation consiste à passer des descripteurs de fichiers entre processus via des sockets Unix. Exemple en C :
// client.c
struct msghdr msg;
struct cmsghdr *cmsg;
union { char cmsgbuf[CMSG_SPACE(sizeof(int))]; } control_un;
msg.msg_control = control_un.cmsgbuf;
msg.msg_controllen = sizeof(control_un.cmsgbuf);
cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
*((int *)CMSG_DATA(cmsg)) = fd;
sendmsg(sockfd, &msg, 0);
Le serveur récupère le descripteur via recvmsg et peut l’utiliser localement, sans avoir besoin d’un accès direct au fichier.
Synthèse du fonctionnement Virtio
Le flux de données typique dans un périphérique Virtio suit ces étapes :
- Le frontend remplit les descripteurs avec les adresses GPA et longueurs des buffers.
- Il inscrit l’index de la chaîne dans le ring available.
- Il notifie le backend via une interruption ou un événement.
- Le backend lit l’index depuis available, traite les données, puis place l’index traité dans used.
- Le frontend consomme les entrées de used pour récupérer les résultats.