Analyse approfondie de rust-vmm et de l’architecture de virtualisation basée sur KVM

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 :

  1. Un SystemAllocator est instancié pour gérer l’espace d’adresses MMIO.
  2. Le DeviceManager appelle allocate_bars sur chaque VirtioPciDevice.
  3. L’allocation est réalisée par AddressAllocator::allocate dans vm-allocator.
  4. La méthode SystemAllocator::allocate_mmio_addresses ré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 :

  1. Le frontend remplit les descripteurs avec les adresses GPA et longueurs des buffers.
  2. Il inscrit l’index de la chaîne dans le ring available.
  3. Il notifie le backend via une interruption ou un événement.
  4. Le backend lit l’index depuis available, traite les données, puis place l’index traité dans used.
  5. Le frontend consomme les entrées de used pour récupérer les résultats.

Étiquettes: kvm rust-vmm virtio vfio pci-passthrough

Publié le 8 août à 18h43