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 (IoUringestSendmais pasSync), sur le flagIORING_SETUP_SQPOLL(Temps 1),ATTACH_WQ(Temps 3a) etmsg_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éé avecSINGLE_ISSUER | DEFER_TASKRUN(Temps 1 §5.1). Aucun partage sur le chemin chaud, donc aucun verrou, latence basse et prévisible.
SINGLE_ISSUERinforme le kernel qu’un seul thread soumet → optimisations internes.DEFER_TASKRUNrepousse 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_DATApour réveiller un pair,MSG_SEND_FDpour 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
!Syncd’IoUringinterdit 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_ringpeut router vers un pair de façon découplée (réveil, transfert de FD).
- partageant le pool io-wq (
- Chaque
IoUringrendu resteSend/!Sync: il est déplacé dans son thread, jamais partagé.RingPoolne 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 aucunio_uring_enter(juste la publication release). Latence minimale. - Réveil : si le thread s’est endormi (après
idle), le drapeauIORING_SQ_NEED_WAKEUPest positionné ; la façade le détecte et émetio_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
| Type | Send | Sync | Modèle |
|---|---|---|---|
IoUring | oui | non | un ring par thread (§2) — recommandé |
LockedIoUring | oui | oui | ring partagé, verrou (§3) — simple, contention |
RingPool | oui | non (distribue) | organise N rings thread-per-core (§4) |
SqpollIoUring | oui | non | poll kernel de la SQ (§5) — latence vs CPU |
7. Sûreté
- L’invariant
!Syncd’IoUringempêche par typage le partage d’un ring non protégé entre threads — pas de course possible sur le SQ/CQ/slab. LockedIoUringajoute 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 parmsg_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_WQborne bien le nombre de threads workers (observé via/procou compteur) ; routage inter-ring correct. - SqpollIoUring : soumission sans syscall en régime établi ; réveil correct
après
idle(NEED_WAKEUP) ; épinglageSQ_AFF. - Sûreté : compile-fail confirmant qu’
IoUringne traverse pas un&entre threads (!Sync) ; Miri/loom pourLockedIoUring. - Couverture 100 % lignes + branches.
10. Décisions de fond émergées au Temps 3e
- Thread-per-core comme modèle par défaut — un ring par thread,
SINGLE_ISSUER + DEFER_TASKRUN, pas de verrou ; communication parmsg_ring. Aligne le runtime async (ADR-023) sur le modèle peer-to-peer d’AirCom. !Syncassumé surIoUring— la sûreté multi-thread est un choix explicite de l’appelant (LockedIoUring ou RingPool), pas un coût imposé à tous.RingPoolorganise, ne partage pas —ATTACH_WQborne les threads workers (Pi 4) ; les rings restent possédés par thread.SqpollIoUringré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).- 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) :
unsafe impl Send for IoUringré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 rendaientIoUringauto-!Send. Leunsafe impl Sendest 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 sontSend;Sendexprime un transfert d’ownership exclusif (pas un partage), et le!Syncest préservé (unPhantomData<Cell<…>>interdit&IoUringpartagé). Aucune surface publique ajoutée — c’est une impl de trait auto. C’est l’invariant habilitant de tout le Temps 3e (sans lui, niLockedIoUringni le déplacement thread-per-core).LockedIoUring: primitivewith_lock+ raccourcis nommés. Plutôt que recopier ~50 forwarders&self(un parsubmit_*/accesseur d’IoUring), une primitive uniquewith_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.submit_message_ring_data_to(&RingHandle)ajouté. Lemsg_ringdu Temps 2c prend une&IoUring(même contexte) — inadapté au routage cross-thread aprèsinto_rings(les rings sont déplacés dans leurs threads). Cette variante route par FD de ring (porté parRingHandle, capturé avant distribution), process-global donc valable entre threads. Le 2c reste inchangé.
Couverture (après PR #49 + complément
046) :shared.rsbranches 100 %, « couvrable vide » (résidus = fallbackSQPOLL-EPERMdes tests,PRIVILEGE; timeout du thread pair,TEST-HARNESS). Soundness prouvée :!Syncpar doctestcompile_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.