Skip to content

Latest commit

 

History

History
153 lines (100 loc) · 11.5 KB

File metadata and controls

153 lines (100 loc) · 11.5 KB

🔝 Retour au Sommaire

22.3.3 io_uring : I/O asynchrone haute performance (Linux 5.1+)

Introduction

io_uring est l'interface d'I/O asynchrone la plus avancée du noyau Linux. Introduite par Jens Axboe dans Linux 5.1 (mai 2019) et enrichie de manière significative à chaque version du noyau depuis, elle représente un changement de paradigme par rapport à epoll : on passe du modèle readiness notification (« ce descripteur est prêt, effectue l'opération toi-même ») au modèle completion notification (« voici l'opération à effectuer, préviens-moi quand c'est terminé »).

Ce changement peut sembler subtil, mais ses conséquences sont profondes. Avec epoll, chaque opération d'I/O nécessite au minimum deux appels système : un epoll_wait pour savoir qu'un descripteur est prêt, puis un read ou write pour effectuer l'opération. Avec io_uring, l'application dépose des requêtes dans une file partagée avec le noyau, et récupère les résultats dans une autre file — le tout sans aucun appel système dans le chemin critique, grâce à des buffers en mémoire partagée entre userspace et kernel.

🔥 io_uring est le mécanisme derrière les gains de performance observés dans les versions récentes de systèmes comme RocksDB, PostgreSQL 16+, Nginx (modules expérimentaux), Tigerbeetle, et de nombreux proxies et bases de données modernes.


Pourquoi un nouveau mécanisme après epoll ?

epoll a résolu le problème de scalabilité de select/poll pour le multiplexage réseau, et il reste excellent dans ce rôle. Mais il présente des limitations structurelles que io_uring adresse :

1. Le coût des appels système

Chaque appel système implique une transition userspace → kernel (context switch partiel, flush du pipeline CPU, vérification des permissions). Avec epoll, un cycle typique requiert au minimum deux appels système par opération d'I/O : epoll_wait + read/write. Sous forte charge (des centaines de milliers d'opérations par seconde), ce coût devient mesurable.

Après les vulnérabilités Spectre et Meltdown (2018), les mitigations noyau (KPTI, retpolines) ont significativement alourdi le coût de chaque transition user/kernel. Ce surcoût a rendu la réduction du nombre d'appels système encore plus critique pour les workloads I/O-intensifs.

io_uring élimine ce problème en permettant de soumettre et de récupérer des opérations sans appel système, via des ring buffers en mémoire partagée.

2. Le batching des opérations

Avec epoll, chaque opération est individuelle : un read ici, un write là. Il n'y a pas de mécanisme natif pour soumettre un lot d'opérations en une seule interaction avec le noyau.

io_uring permet de déposer plusieurs dizaines d'opérations dans la Submission Queue avant de notifier le noyau une seule fois (ou même sans le notifier, en mode polling). Le noyau peut alors les traiter en parallèle, optimiser leur ordonnancement, et retourner les résultats en lot.

3. L'unification des types d'I/O

epoll excelle pour le multiplexage réseau, mais ne couvre pas les I/O fichier de manière satisfaisante. Les opérations sur fichiers (lecture, écriture, fsync) ne sont pas réellement asynchrones avec epoll — elles bloquent souvent le thread malgré le mode non bloquant, car le système de fichiers ne supporte pas toujours O_NONBLOCK.

io_uring offre une API unifiée pour les opérations réseau (recv, send, accept, connect), les opérations fichier (read, write, fsync, fallocate), les opérations système (openat, close, statx, mkdir), et même les timeouts et annulations. Tout passe par la même interface de soumission/complétion.

4. Le zero-copy et les buffers pré-enregistrés

io_uring permet d'enregistrer des buffers à l'avance auprès du noyau (io_uring_register). Les opérations utilisant ces buffers pré-enregistrés évitent les copies entre userspace et kernel, et le noyau peut verrouiller les pages en mémoire pour éviter les fautes de page pendant l'I/O.


Le modèle conceptuel : deux files, zéro appel système

L'architecture d'io_uring repose sur deux files circulaires (ring buffers) en mémoire partagée entre l'application et le noyau :

    APPLICATION  (userspace)                    NOYAU  (kernel)
    ════════════════════════                    ═══════════════

    Préparer les requêtes                       
            │                                   
            ▼                                   
    ┌─────────────────────┐                     
    │  Submission Queue   │ ──── mémoire ────► Le noyau consomme
    │  (SQ)               │      partagée       les requêtes et
    │                     │                     exécute les I/O
    │  [recv fd=7       ] │                          │
    │  [send fd=12      ] │                          │
    │  [read fd=3       ] │                          │
    └─────────────────────┘                          │
                                                     ▼
    Récupérer les résultats                    ┌─────────────────────┐
            ▲                                  │  Completion Queue   │
            │                                  │  (CQ)               │
            └──────────── mémoire ──────────── │                     │
                          partagée             │  [fd=7: 2048 octets]│
                                               │  [fd=3: 4096 octets]│
                                               └─────────────────────┘

    ⚡ Aucun appel système dans le chemin critique :
       l'application écrit dans SQ et lit depuis CQ
       directement en mémoire.

L'application pousse des Submission Queue Entries (SQE) décrivant les opérations souhaitées, et récolte des Completion Queue Entries (CQE) contenant les résultats. Dans le meilleur cas, ces deux opérations sont de simples écritures et lectures en mémoire — pas d'appel système, pas de transition de privilège.

Le noyau est notifié de nouvelles soumissions soit par un appel io_uring_enter (le cas classique), soit automatiquement en mode SQPOLL (un kernel thread dédié scrute la SQ en permanence — zéro appel système, mais consommation CPU continue).


Évolution rapide : les jalons majeurs

io_uring évolue à chaque version du noyau Linux. Voici les ajouts les plus significatifs :

Version noyau Année Ajout majeur
5.1 2019 Introduction d'io_uring : read, write, fsync
5.4 2019 Timeout, annulation, linked SQEs
5.5 2020 IORING_OP_ACCEPT, IORING_OP_CONNECT — networking
5.6 2020 Buffers pré-enregistrés, IORING_OP_RECV, IORING_OP_SEND
5.7 2020 SQPOLL amélioré, plus d'opcodes réseau
5.11 2021 Restrictions et sandboxing (io_uring_register)
5.18 2022 Socket multishot accept, recv multishot
5.19 2022 IORING_OP_WAITID, améliorations zero-copy send
6.0 2022 Zero-copy networking (IORING_OP_SEND_ZC)
6.1 2022 Ring-mapped buffers, améliorations CQ overflow
6.7+ 2024 Maturité réseau, futex, waitid

La cadence d'évolution est intense : chaque version du noyau apporte de nouvelles opérations et des optimisations. C'est à la fois la force et la difficulté d'io_uring — les fonctionnalités disponibles dépendent directement de la version du noyau sur lequel vous déployez.

📝 Version minimale recommandée en 2026 : Linux 5.11+ pour un usage général, 6.0+ pour le zero-copy networking. Les distributions LTS courantes (Ubuntu 24.04 LTS avec noyau 6.8, RHEL 9 avec noyau 5.14) offrent un support suffisant pour la plupart des cas d'usage.


Considérations de sécurité

La surface d'attaque d'io_uring a été un sujet de préoccupation récurrent depuis son introduction. L'interface expose un grand nombre d'opérations noyau via un mécanisme de mémoire partagée, ce qui en fait une cible attractive pour les exploits de type escalade de privilèges. Plusieurs CVE critiques ont été associées à io_uring entre 2020 et 2023.

En conséquence :

  • Google a désactivé io_uring dans ChromeOS et dans les serveurs de production Google pendant une période, avant de le réactiver progressivement avec des restrictions via seccomp.
  • Docker et les runtimes de conteneurs bloquent io_uring par défaut dans les profils seccomp standards. Si votre application conteneurisée doit utiliser io_uring, il faut explicitement ajuster le profil seccomp.
  • Le noyau 5.11+ a introduit des mécanismes de restriction (io_uring_register avec IORING_REGISTER_RESTRICTIONS) qui permettent de limiter les opérations autorisées par instance.

Ces préoccupations ne remettent pas en cause la pertinence d'io_uring pour les workloads de production — la surface de bugs a été considérablement réduite — mais elles imposent de vérifier la politique de sécurité de votre environnement de déploiement.


io_uring vs epoll : quand migrer ?

io_uring n'est pas un remplacement systématique d'epoll. Voici un guide de décision :

Rester sur epoll quand :

  • Votre workload est principalement du multiplexage réseau avec des connexions longues et peu d'I/O fichier.
  • Vous déployez sur des noyaux anciens (< 5.1) ou dans des environnements conteneurisés qui bloquent io_uring.
  • La complexité additionnelle d'io_uring n'est pas justifiée par des gains mesurables dans votre cas.
  • Vous utilisez déjà une librairie comme Asio qui abstrait le mécanisme sous-jacent.

Adopter io_uring quand :

  • Vous combinez I/O réseau et I/O fichier (proxy, base de données, serveur de fichiers).
  • Le nombre d'appels système par seconde est un goulot d'étranglement identifié.
  • Vous avez besoin de zero-copy pour du transfert de données à très haut débit.
  • Vous développez une librairie ou un framework bas niveau qui doit offrir les meilleures performances possibles sous Linux.
  • Vous traitez des centaines de milliers d'opérations I/O par seconde.

En pratique, beaucoup de projets en 2026 utilisent io_uring à travers des librairies de plus haut niveau (liburing, Boost.Asio avec backend io_uring, ou des frameworks comme Seastar) plutôt que via l'interface brute du noyau.


Plan de la section

Les sous-sections suivantes détaillent io_uring en profondeur :

  • 22.3.3.1 — Architecture : Submission Queue et Completion Queue : fonctionnement interne des ring buffers, structure des SQE et CQE, modes de soumission (normal, SQPOLL), et interaction avec le noyau via io_uring_setup et io_uring_enter.

  • 22.3.3.2 — liburing : Interface C/C++ simplifiée : la librairie de Jens Axboe qui encapsule les appels système bruts derrière une API ergonomique. Installation, utilisation, et exemples complets.

  • 22.3.3.3 — Cas d'usage : networking, fichiers, timeouts : exemples concrets d'utilisation d'io_uring pour un serveur réseau, des opérations fichier asynchrones, et la gestion des timeouts et annulations.

⏭️ Architecture : Submission Queue et Completion Queue