Aller au contenu

Entrees/sorties et bus

Le CPU ne travaille jamais seul — il communique avec le monde extérieur à travers des bus et des mécanismes d'I/O.


Bus système

Un bus est un canal de communication partage entre le processeur, la mémoire et les périphériques. L'évolution des bus reflete l'explosion des débits nécessaires : les GPU, les SSD NVMe et les cartes réseau 100 Gbps exigent des bandes passantes que les bus classiques ne pouvaient pas fournir.

Évolution des bus

Les premiers PC utilisaient des bus parallèles partages (ISA, PCI) ou tous les périphériques se disputaient la même bande passante. Un bus PCI 32 bits a 33 MHz offrait 133 MB/s partages entre tous les périphériques — acceptable dans les années 1990, mais absurde pour un GPU moderne.

L'industrie a progressivement migré vers des liens serie point-à-point : chaque périphérique dispose de son propre lien vers le processeur, éliminant la contention. PCIe, USB 3.x, SATA et NVMe utilisent tous ce modèle.

PCIe (Peripheral Component Interconnect Express)

PCIe est le bus dominant pour les périphériques hautes performances. Contrairement aux anciens bus parallèles, PCIe utilise des liens serie point-à-point organises en voies (lanes). Chaque voie est un lien full-duplex (émission et reception simultanées).

Génération Débit par voie x4 (SSD NVMe) x16 (GPU) Année
PCIe 3.0 1 GB/s 4 GB/s 16 GB/s 2010
PCIe 4.0 2 GB/s 8 GB/s 32 GB/s 2017
PCIe 5.0 4 GB/s 16 GB/s 64 GB/s 2019
PCIe 6.0 8 GB/s 32 GB/s 128 GB/s 2022

Un SSD NVMe utilisé typiquement 4 voies PCIe (x4). En PCIe 4.0, cela donne ~8 GB/s de bande passante — largement supérieur à ce qu'un disque dur ou un SSD SATA peut fournir. Un GPU haut de gamme utilise 16 voies (x16) pour alimenter ses milliers de cœurs de calcul.

Topologie PCIe et CPU root complex

Le processeur contient un root complex PCIe qui géré les connexions vers les périphériques. Le nombre de voies PCIe disponibles est une ressource finie et critique :

  • Un processeur desktop typique offre 20-24 voies PCIe
  • Un processeur serveur (Xeon, EPYC) offre 64-128 voies PCIe

Si on connecte un GPU (x16), deux SSD NVMe (2x x4) et une carte réseau (x8), on consomme 32 voies — un processeur desktop ne suffit plus. C'est pourquoi les serveurs utilisent des chipsets ou des processeurs avec plus de voies PCIe.

NVMe vs SATA

Propriété NVMe (PCIe) SATA III
Bande passante max 7-14 GB/s (PCIe 4.0/5.0 x4) 600 MB/s
Latence ~10-20 us ~50-100 us
Files d'attente 65 535 queues x 65 536 commandes 1 queue x 32 commandes
Protocole Conçu pour le flash Conçu pour les disques rotatifs
CPU overhead Faible (moins d'interruptions) Modere

Note

Le gain de NVMe par rapport à SATA ne vient pas uniquement de la bande passante brute. Le nombre massif de files d'attente permet un parallélisme I/O que SATA ne peut pas offrir. Chaque cœur CPU peut soumettre des commandes dans sa propre queue sans contention. C'est particulièrement visible sur les charges de travail avec beaucoup de lectures aléatoires (bases de données, machines virtuelles).


DMA : Direct Memory Access

Dans le modèle le plus simple (Programmed I/O ou PIO), le CPU copie les données octet par octet entre le périphérique et la mémoire. Pour un transfert de 1 MB, le CPU est monopolise pendant toute l'opération — chaque octet nécessité une instruction IN ou OUT. Sur un serveur traitant des milliers de requêtes réseau simultanément, ce modèle est inacceptable.

Le DMA (Direct Memory Access) résout ce problème : un contrôleur DMA prend en charge le transfert, et le CPU est libre de faire autre chose pendant ce temps.

graph LR
    subgraph "Programmed I/O"
        CPU1["CPU"] --lit octet par octet--> DEV1["Peripherique"]
        DEV1 --donnees--> CPU1
        CPU1 --ecrit octet par octet--> MEM1["Memoire"]
    end
graph LR
    subgraph "DMA"
        CPU2["CPU"] --configure transfert--> DMA2["Controleur DMA"]
        DMA2 --transfert bloc--> MEM2["Memoire"]
        DEV2["Peripherique"] --donnees--> DMA2
        DMA2 --interruption fin--> CPU2
    end

Le flux DMA typique :

  1. Le CPU programme le contrôleur DMA : adresse source, adresse destination, taille du transfert
  2. Le DMA effectue le transfert sans intervention du CPU
  3. Le DMA envoie une interruption au CPU pour signaler la fin du transfert
  4. Le CPU reprend la main sur les données

Le gain est considérable : pendant un transfert DMA de 1 MB sur un lien PCIe 4.0 (qui prend ~125 us), le CPU peut exécuter environ 500 000 instructions au lieu de rester bloque.

Scatter-Gather DMA

Le DMA classique transfere un bloc continu. Le Scatter-Gather DMA permet de transferer des données dispersees en mémoire en une seule opération : le contrôleur reçoit une liste de fragments (scatter-gather list) et les assemble automatiquement. Les cartes réseau modernes utilisent cette technique pour construire des paquets à partir de headers et payloads stockes a des adresses différentes.

Zero-copy

Les techniques modernes vont plus loin avec le zero-copy : les données passent directement du périphérique à la mémoire utilisateur (ou entre deux périphériques) sans copie intermédiaire par le noyau.

Dans un serveur web classique sans zero-copy, envoyer un fichier implique 4 copies :

Disque → Buffer noyau → Buffer utilisateur → Buffer noyau (socket) → Carte reseau

Avec sendfile(), on réduit a 2 copies :

Disque → Buffer noyau → Carte reseau (via DMA)

Linux expose le zero-copy via sendfile(), splice(), et io_uring. Nginx et Kafka exploitent massivement le zero-copy pour leurs performances. Kafka, en particulier, utilisé sendfile() pour transferer les messages du disque vers le réseau sans que les données ne passent par l'espace utilisateur — c'est l'une des raisons de ses performances remarquables.


Interruptions

Les interruptions sont le mécanisme par lequel les périphériques signalent au CPU qu'un événement s'est produit — un paquet réseau est arrive, un transfert DMA est termine, une touche a été enfoncee.

Mécanisme

  1. Le périphérique leve une ligne d'interruption (IRQ — Interrupt Request)
  2. Le contrôleur d'interruptions (APIC sur x86) reçoit l'IRQ et la transmet au CPU
  3. Le CPU interrompt l'exécution en cours, sauvegarde le contexte (registres, compteur de programme)
  4. Le CPU exécuté la routine de traitement d'interruption (ISR — Interrupt Service Routine)
  5. L'ISR traite l'événement (lire les données, acquitter l'interruption)
  6. Le CPU restaure le contexte et reprend l'exécution

Le coût d'une interruption est significatif : 500 a 2000 ns sur un processeur moderne, principalement à cause de la sauvegarde/restauration de contexte et de la pollution du cache (l'ISR evince des données utiles du cache L1).

Types d'interruptions

Type Déclencheur Exemple
Matérielle (externe) Périphérique Carte réseau, disque, timer
Logicielle (trap) Instruction explicite Appel système (syscall)
Exception Erreur d'exécution Division par zero, page fault
NMI (Non-Maskable) Événement critique Erreur matérielle, watchdog

MSI/MSI-X

Les IRQ traditionnelles utilisent des lignes physiques dedicees — une ressource limitee (24 IRQ sur un système classique). Les interruptions MSI (Message Signaled Interrupts) et MSI-X remplacent les lignes physiques par des écritures mémoire : le périphérique écrit un message dans une adresse spécifique pour signaler l'interruption.

MSI-X supporte jusqu'à 2048 vecteurs d'interruption par périphérique, ce qui permet à une carte réseau d'avoir une interruption par file de reception, et donc une file par cœur CPU. C'est essentiel pour le scaling des cartes réseau multi-queue.

IRQ et affinite

Sur un système multicoeur, les interruptions peuvent être dirigees vers des cœurs spécifiques via l'affinite d'interruptions (IRQ affinity). C'est un levier de performance important :

  • Diriger les interruptions réseau vers les mêmes cœurs que les threads de traitement réduit les cache miss
  • Isoler certains cœurs des interruptions (CPU isolation) garantit une latence predictible pour les traitements temps-réel
  • Répartir les interruptions d'une carte réseau multi-queue entre les cœurs avec RSS (Receive Side Scaling) maximise le throughput

Tip

Sur un serveur à haute performance (trading, telecom), configurer l'affinite des IRQ et l'isolation CPU peut réduire la latence p99 de 30 a 50%. Les outils Linux irqbalance (répartition automatique), taskset (affinite processus), /proc/irq/N/smp_affinity (affinite IRQ) et les cgroups cpuset sont les leviers principaux.


Polling vs interruptions

Les interruptions ont un coût : la sauvegarde et restauration de contexte prend des centaines de nanosecondes, et chaque interruption pollue le cache. Sur une carte réseau a 100 Gbps qui reçoit des millions de paquets par seconde, les interruptions seules ne suffisent plus — le CPU passerait tout son temps en ISR.

La solution est le polling : le CPU interroge activement le périphérique en boucle, sans attendre d'interruption. DPDK (Data Plane Development Kit) et io_uring en mode polling utilisent cette approche.

Approche Avantage Inconvénient
Interruptions CPU libre entre les événements Coût de context switch, cache pollution
Polling Latence minimale, pas de context switch CPU consomme a 100% même sans trafic
Hybride (NAPI) Compromis adaptatif Complexité de configuration

Linux utilisé NAPI (New API) pour le réseau : interruptions quand le trafic est faible, basculement en polling quand le trafic est intense. La première interruption déclenché le mode polling ; quand le buffer est vide, on revient aux interruptions. Ce mode hybride est le défaut sur les serveurs modernes.

io_uring : le modèle d'I/O moderne

io_uring (depuis Linux 5.1) est un mécanisme d'I/O asynchrone qui utilise deux ring buffers partages entre l'espace utilisateur et le noyau :

  • Le Submission Queue (SQ) : l'application y depose les requêtes d'I/O
  • Le Completion Queue (CQ) : le noyau y depose les résultats

Ce modèle evite les appels système pour soumettre et récupérer les I/O — l'application écrit directement dans la mémoire partagee. En mode polling, le noyau vérifié les completions sans interruption. Le gain est significatif pour les workloads avec beaucoup de petites I/O (bases de données, serveurs HTTP).


I/O scheduling

Quand plusieurs processus demandent des I/O simultanément, le système d'exploitation doit ordonner ces requêtes. L'ordonnanceur d'I/O a un impact direct sur la latence et le débit.

Ordonnanceurs Linux

Ordonnanceur Stratégie Adapté a
none (noop) FIFO, pas de reordonnancement SSD, NVMe (pas de seeks a optimiser)
mq-deadline Garantit un délai maximal par requête Serveurs de base de données
BFQ (Budget Fair Queuing) Equite entre processus, interactivite Postes de travail, VM avec I/O mixtes
kyber Léger, conçu pour les devices rapides NVMe haute performance

Pour les HDD, l'ordonnanceur doit optimiser les déplacements de la tete de lecture. Le reordonnancement des requêtes par secteur (elevator algorithm) peut multiplier le débit par 2 a 5x. Pour les SSD et NVMe, il n'y a pas de tete de lecture — tout reordonnancement ajoute de la latence sans benefice.

Warning

Le choix de l'ordonnanceur d'I/O est rarement le bon endroit pour optimiser. Sur un SSD NVMe, none est presque toujours le bon choix. En revanche, sur des HDD partages entre plusieurs VM, mq-deadline peut faire la différence entre une latence acceptable et des I/O storms. Vérifier l'ordonnanceur actuel : cat /sys/block/sda/queue/scheduler.


MMIO et port-mapped I/O

Le CPU communique avec les périphériques par deux mécanismes :

  • Port-mapped I/O : des instructions spécifiques (IN, OUT sur x86) accedent a un espace d'adressage I/O séparé de 64 KB. Historique, encore utilisé pour les périphériques legacy (contrôleur clavier, port serie).
  • Memory-mapped I/O (MMIO) : les registres du périphérique sont mappes dans l'espace d'adressage mémoire. Le CPU accede au périphérique comme s'il accedait à la RAM, avec des instructions MOV standard. C'est l'approche dominante aujourd'hui.

MMIO simplifie la programmation (pas besoin d'instructions speciales), permet d'utiliser les mécanismes de protection mémoire du CPU pour isoler l'accès aux périphériques, et offre un espace d'adressage bien plus grand que les 64 KB du port I/O.

BAR (Base Address Register)

Chaque périphérique PCIe expose ses registres via un ou plusieurs BAR (Base Address Register). Le BIOS et l'OS assignent des plages d'adresses physiques à chaque BAR au démarrage. Le driver du périphérique mappe ensuite ces adresses physiques dans l'espace d'adressage virtuel du noyau pour y accéder.


Bande passante : ou se situent les goulots

graph LR
    CPU["CPU<br>~100 GB/s<br>vers cache"] --bus memoire--> RAM2["RAM<br>~50 GB/s"]
    RAM2 --PCIe 5.0 x16--> GPU["GPU<br>64 GB/s"]
    RAM2 --PCIe 4.0 x4--> SSD2["SSD NVMe<br>8 GB/s"]
    RAM2 --PCIe 3.0 x1--> NIC["Carte reseau<br>25 Gbps = 3 GB/s"]
    RAM2 --SATA III--> HDD2["HDD<br>200 MB/s"]

Chaque lien est un goulot potentiel. Un système qui transfere des données du SSD vers le GPU doit traverser le bus mémoire dans les deux sens — la bande passante effective est limitee par le maillon le plus faible. Les technologies comme GPUDirect (NVIDIA) permettent au SSD de transferer directement vers le GPU via PCIe, sans passer par la RAM.


Mesurer les I/O

Avant d'optimiser, il faut mesurer. Les outils Linux suivants permettent d'observer le comportement I/O en production.

# Latence et debit par disque, rafraichi toutes les secondes
iostat -xz 1

# Colonnes cles :
# r/s, w/s    : lectures/ecritures par seconde (IOPS)
# rkB/s, wkB/s: debit en KB/s
# await       : latence moyenne d'une requete I/O (ms)
# %util       : pourcentage d'utilisation du device
Outil Usage
iostat Débit, IOPS, latence par device
iotop I/O par processus (équivalent de top pour les I/O)
blktrace Trace détaillée des requêtes block I/O
fio Benchmark I/O synthetique (mesurer avant de déployer)
hdparm -t Test rapide de débit séquentiel

Un await supérieur à 10 ms sur un SSD NVMe (qui devrait être a ~0.1 ms) est le signe d'une saturation I/O ou d'un problème de configuration.


Implications pour l'architecte

Les I/O sont généralement le goulot d'étranglement des systèmes en production :

  1. I/O-bound vs CPU-bound : la majorité des applications serveur sont I/O-bound. Ajouter des cœurs CPU ne sert à rien si le disque ou le réseau est sature. Identifier le goulot est la première étape de tout dimensionnement. Les outils iostat, iotop et perf permettent de mesurer.

  2. Async I/O : les frameworks modernes (Node.js event loop, Go goroutines, Rust async/await) existent parce que le modèle "un thread par requête bloquant sur I/O" gaspille des ressources. L'I/O asynchrone permet à un seul thread de gérer des milliers de connexions concurrentes. io_uring est la prochaine étape de cette évolution.

  3. Choix de stockage : NVMe vs SATA vs réseau change la latence de 10x a 100x. Pour une base de données, le choix du stockage est souvent plus impactant que le choix du moteur. Un PostgreSQL sur NVMe avec 16 GB de shared_buffers surpasse souvent un PostgreSQL sur HDD avec 64 GB de RAM.

  4. Zero-copy et kernel bypass : pour les systèmes a ultra-haute performance, éliminer les copies inutiles et contourner le noyau (DPDK pour le réseau, SPDK pour le stockage) peut multiplier le débit par 10. Le coût est la complexité : ces techniques opèrent en espace utilisateur et ne beneficient plus de la protection du noyau.

  5. Budget I/O : dans un dimensionnement, comptabiliser le nombre d'IOPS nécessaires est aussi important que le CPU et la RAM. Un HDD offre ~150 IOPS aléatoires. Un SSD SATA offre ~50 000 IOPS. Un NVMe offre ~500 000+ IOPS. Le choix de stockage déterminé combien de requêtes le système peut traiter.


Chapitre suivant : Architectures multiprocesseurs — SMP, NUMA et les problèmes de cohérence qui préfigurent le distribué.