Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Spec couche 0 — Module io_uring, Temps 3e : usage multi-thread (shared)

Spécification technique — Version 1.0 (cible kernel : Linux 6.12 LTS)

Position. Le Temps 3e spécifie les modèles multi-thread du ring. Sous-module air-sys-syscall::io_uring::shared. Repose sur l’invariant d’ADR-022 Décision 6 (IoUring est Send mais pas Sync), sur le flag IORING_SETUP_SQPOLL (Temps 1), ATTACH_WQ (Temps 3a) et msg_ring (Temps 2c). Réutilise le cœur du Temps 1. Aucun register opcode propre.


1. Le point de départ : Send mais pas Sync

IoUring peut être déplacé entre threads (Send) mais pas partagé par référence (!Sync). Raison : le protocole d’ordering du Temps 1 (§3.2) synchronise userspace ↔ kernel, pas userspace ↔ userspace. Les têtes/queues SQ/CQ et le slab S1 ne sont pas protégés contre des accès userspace concurrents. Trois réponses, par ordre de préférence Air.


2. Modèle recommandé : thread-per-core (un ring par thread)

C’est le modèle par défaut d’Air. Chaque thread (idéalement épinglé à un cœur) possède son propre IoUring, créé avec SINGLE_ISSUER | DEFER_TASKRUN (Temps 1 §5.1). Aucun partage sur le chemin chaud, donc aucun verrou, latence basse et prévisible.

  • SINGLE_ISSUER informe le kernel qu’un seul thread soumet → optimisations internes. DEFER_TASKRUN repousse le task-work au moment de l’attente → moins d’interruptions.
  • Communication entre rings : on ne partage pas le ring ; on passe des messages via msg_ring (Temps 2c, MSG_DATA pour réveiller un pair, MSG_SEND_FD pour lui transférer un descripteur). C’est le pendant, à l’échelle des reactors, du modèle peer-to-peer d’AirCom.
  • Sûreté : triviale — pas d’accès concurrent à un même ring. Le !Sync d’IoUring interdit par typage le partage accidentel.

Ce modèle n’introduit aucun type nouveau : c’est l’usage direct d’IoUring, un par thread. Les sections 3–5 couvrent les cas où il ne suffit pas.


3. LockedIoUring — partage simple par verrou

#![allow(unused)]
fn main() {
pub struct LockedIoUring { /* IoUring + verrou interne */ }

impl LockedIoUring {
    pub fn new(entries: NonZeroU32) -> Result<Self, Errno>;
    pub fn from_builder(builder: IoUringBuilder) -> Result<Self, Errno>;

    // API miroir d'IoUring, mais en &self (verrou interne) :
    pub fn submit_read(&self, fd: BorrowedFd<'_>, buffer: Vec<u8>, offset: Option<u64>)
        -> Result<SubmissionToken, Errno>;
    pub fn submit_and_wait(&self, want: u32) -> Result<u32, Errno>;
    pub fn wait_completion(&self) -> Result<Completion, Errno>;
    // … (l'ensemble des submit_*/complétions, en &self) …
}
// LockedIoUring : Send + Sync.
}
  • Un seul ring partagé entre threads, protégé par un verrou interne (sérialise les accès userspace au SQ/CQ/slab). Devient Sync.
  • Avertissement : le verrou est un point de contention — sous forte charge multi-thread, il sérialise tout. À réserver aux cas simples ou peu sollicités. Pour la perf, préférer le thread-per-core (§2).
  • Granularité : verrou unique au départ (clarté, Principe 7) ; un affinage (verrous séparés soumission/complétion) n’est envisagé qu’après mesure (Principe 5), jamais a priori.

4. RingPool — assistant thread-per-core

#![allow(unused)]
fn main() {
pub struct RingPool { /* N rings, io-wq partagé, ring fds enregistrés */ }

impl RingPool {
    /// Crée `workers` rings : le premier crée le pool io-wq, les suivants s'y
    /// attachent (ATTACH_WQ) → nombre total de threads workers borné.
    pub fn new(workers: NonZeroUsize, entries: NonZeroU32) -> Result<Self, Errno>;
    /// Distribue les rings : un `IoUring` possédé par worker (à déplacer dans
    /// chaque thread). Les ring fds sont mutuellement enregistrés pour msg_ring.
    pub fn into_rings(self) -> Vec<IoUring>;
    /// Poignée de routage vers un ring pair (pour msg_ring).
    pub fn handle(&self, worker: usize) -> Option<RingHandle>;
}

/// Référence à un ring pair, utilisable comme cible de msg_ring (Temps 2c).
pub struct RingHandle { /* ring fd enregistré du pair */ }
}
  • Rôle : créer N rings cohérents pour le modèle §2, en :
    • partageant le pool io-wq (ATTACH_WQ, Temps 3a) ⇒ le nombre total de threads workers kernel est borné quel que soit N — crucial sur matériel modeste (Pi 4, Charte principe 4) ;
    • enregistrant les ring fds mutuellement (Temps 3a) ⇒ msg_ring peut router vers un pair de façon découplée (réveil, transfert de FD).
  • Chaque IoUring rendu reste Send/!Sync : il est déplacé dans son thread, jamais partagé. RingPool ne crée pas de partage, il organise.

5. SqpollIoUring — thread kernel de poll de la SQ

#![allow(unused)]
fn main() {
pub struct SqpollIoUring { /* IoUring avec SETUP_SQPOLL */ }

impl SqpollIoUring {
    /// `idle` : délai d'inactivité avant que le thread kernel se mette en
    /// veille (NEED_WAKEUP). `cpu` : épinglage optionnel du thread (SQ_AFF).
    pub fn new(entries: NonZeroU32, idle: Duration, cpu: Option<u32>)
        -> Result<Self, Errno>;
    // submit_* habituels ; la soumission peut ne nécessiter AUCUN syscall.
}
}
  • Avec IORING_SETUP_SQPOLL, un thread kernel scrute la SQ : en régime établi, la soumission ne fait aucun io_uring_enter (juste la publication release). Latence minimale.
  • Réveil : si le thread s’est endormi (après idle), le drapeau IORING_SQ_NEED_WAKEUP est positionné ; la façade le détecte et émet io_uring_enter(.., SQ_WAKEUP) automatiquement à la soumission (c’est le seul cas de réveil géré pour le compte de l’appelant — documenté).
  • SQ_AFF : épinglage du thread kernel sur un cœur (cpu).
  • Coût : un cœur kernel dédié à scruter. Excellent pour la latence sous forte charge, mauvais choix sur Pi 4 où les cœurs sont rares. À réserver aux profils où la latence prime et le CPU abonde (mesure, Principe 5).
  • SQPOLL_NONFIXED (FEAT_SQPOLL_NONFIXED, présent en 6.12) lève l’ancienne contrainte d’usage exclusif de FD enregistrés.

6. Matrice Send / Sync

TypeSendSyncModèle
IoUringouinonun ring par thread (§2) — recommandé
LockedIoUringouiouiring partagé, verrou (§3) — simple, contention
RingPoolouinon (distribue)organise N rings thread-per-core (§4)
SqpollIoUringouinonpoll kernel de la SQ (§5) — latence vs CPU

7. Sûreté

  • L’invariant !Sync d’IoUring empêche par typage le partage d’un ring non protégé entre threads — pas de course possible sur le SQ/CQ/slab.
  • LockedIoUring ajoute la synchronisation userspace↔userspace manquante ; le protocole d’ordering userspace↔kernel du Temps 1 reste inchangé en dessous.
  • RingPool/thread-per-core : pas de partage ⇒ pas de synchronisation requise ; les seuls échanges passent par msg_ring (le kernel sérialise l’écriture dans la CQ cible).
  • Le téardown S2 s’applique par ring (chaque ring quiesce le sien).

8. Types ajoutés / partagés

Nouveaux : LockedIoUring, RingPool, RingHandle, SqpollIoUring. Réutilise : IoUring, IoUringBuilder, SetupFlags (SQPOLL/SQ_AFF/SINGLE_ISSUER/ DEFER_TASKRUN/ATTACH_WQ), msg_ring (Temps 2c), ring fd enregistré (Temps 3a).


9. Stratégie de tests

  • Thread-per-core : N threads, un ring chacun, charge indépendante ; hand-off via msg_ring (un thread réveille/transfère vers un pair, vérifié).
  • LockedIoUring : accès concurrent de plusieurs threads, cohérence des complétions ; loom sur le verrou + le protocole d’anneau (pas de course, pas d’interblocage).
  • RingPool : ATTACH_WQ borne bien le nombre de threads workers (observé via /proc ou compteur) ; routage inter-ring correct.
  • SqpollIoUring : soumission sans syscall en régime établi ; réveil correct après idle (NEED_WAKEUP) ; épinglage SQ_AFF.
  • Sûreté : compile-fail confirmant qu’IoUring ne traverse pas un & entre threads (!Sync) ; Miri/loom pour LockedIoUring.
  • Couverture 100 % lignes + branches.

10. Décisions de fond émergées au Temps 3e

  1. Thread-per-core comme modèle par défaut — un ring par thread, SINGLE_ISSUER + DEFER_TASKRUN, pas de verrou ; communication par msg_ring. Aligne le runtime async (ADR-023) sur le modèle peer-to-peer d’AirCom.
  2. !Sync assumé sur IoUring — la sûreté multi-thread est un choix explicite de l’appelant (LockedIoUring ou RingPool), pas un coût imposé à tous.
  3. RingPool organise, ne partage pasATTACH_WQ borne les threads workers (Pi 4) ; les rings restent possédés par thread.
  4. SqpollIoUring réservé — gain de latence réel mais coût d’un cœur ; déconseillé sur matériel modeste. Réveil NEED_WAKEUP géré pour l’appelant (seule exception au « pas de magie cachée », car indispensable et documentée).
  5. Verrou unique d’abord dans LockedIoUring — affinage seulement après mesure (Principe 5).

10 bis. Note d’implémentation (PR #49)

Implémentation : sous-module air-sys-syscall::io_uring::shared (PR #49, mergée). La surface validée ci-dessus n’a pas été modifiée ; trois choix d’implémentation sont consignés ici (jamais corrigés en silence — ADR-032) :

  1. unsafe impl Send for IoUring réalisé ici. L’invariant ADR-022 D6 (Send + !Sync) était documenté depuis le Temps 1 mais jamais implémenté : les pointeurs bruts des anneaux mmappés rendaient IoUring auto-!Send. Le unsafe impl Send est posé dans ce Temps, justifié // SAFETY: — mmaps et FD du ring sont process-global (valides quel que soit le thread), le slab et les charges utiles sont Send ; Send exprime un transfert d’ownership exclusif (pas un partage), et le !Sync est préservé (un PhantomData<Cell<…>> interdit &IoUring partagé). Aucune surface publique ajoutée — c’est une impl de trait auto. C’est l’invariant habilitant de tout le Temps 3e (sans lui, ni LockedIoUring ni le déplacement thread-per-core).
  2. LockedIoUring : primitive with_lock + raccourcis nommés. Plutôt que recopier ~50 forwarders &self (un par submit_*/accesseur d’IoUring), une primitive unique with_lock(|ring| …) couvre toute opération sous le verrou ; seuls les cas fréquents reçoivent un raccourci nommé (Principe 7, verbosité au service de la clarté sans duplication). Verrou insensible à l’empoisonnement (PoisonError::into_inner), cohérent avec la future synchro sans poisoning d’air-thread.
  3. submit_message_ring_data_to(&RingHandle) ajouté. Le msg_ring du Temps 2c prend une &IoUring (même contexte) — inadapté au routage cross-thread après into_rings (les rings sont déplacés dans leurs threads). Cette variante route par FD de ring (porté par RingHandle, capturé avant distribution), process-global donc valable entre threads. Le 2c reste inchangé.

Couverture (après PR #49 + complément 046) : shared.rs branches 100 %, « couvrable vide » (résidus = fallback SQPOLL-EPERM des tests, PRIVILEGE ; timeout du thread pair, TEST-HARNESS). Soundness prouvée : !Sync par doctest compile_fail, discipline de verrou par loom.


11. Travail à reprendre

Spec suivante : io-uring-3f-sandbox.md (confinement : R_DISABLED + REGISTER_RESTRICTIONS + ENABLE_RINGS, lien capabilities ADR-010/AirCom). Traduction anglaise globale après validation des documents français.


Licence du document : MPL 2.0 Statut : Spécification technique du Temps 3e (multi-thread) du module air-sys-syscall::io_uring, cible kernel 6.12 LTS.