Spec couche 0 — Module io_uring, Temps 1 : cœur de l’API
Spécification technique — Version 1.0 (cible kernel : Linux 6.12 LTS)
Position. Ce document spécifie le Temps 1 du module
air-sys-syscall::io_uring: le cœur sur lequel reposent tous les autres Temps. Il dérive du document maître d’inventaire (io-uring-0-inventaire.md) et de l’ADR-022. Les décisions de soundness S1 (slab pré-alloué), S2 (téardown sûr) et S3 (sandbox) y sont rendues opérationnelles. Les opérations métier (submit_read, etc.) relèvent des Temps 2a–2d et 3a–3f ; ici on spécifie le ring, la soumission, la complétion, et leur sûreté.Conventions couche 0 (ADR-021) appliquées sans exception :
Result<_, Errno>partout, EINTR remonté à l’appelant (jamais de retry automatique),Option<T>au lieu des sentinelles kernel, FD typés (OwnedFd/BorrowedFd), pas d’allocation heap dans le happy path (S1),// SAFETY:sur chaque blocunsafe.
1. Périmètre du Temps 1
Le Temps 1 couvre :
- Le modèle mémoire : les trois zones mmappées (SQ, CQ, SQE) et le protocole de publication par têtes/queues avec ordering acquire/release (§3). C’est la fondation de correction de tout le module.
- La construction du ring :
IoUringBuilder,IoUring::new,io_uring_setup(2), négociation des features, application des setup flags (§5). - Le slab d’opérations en vol (S1) : stockage sans allocation des buffers
transférés et des métadonnées, encodage
SubmissionTokenslot+génération, back-pressure (§4). - La soumission :
submit,submit_and_wait,with(SubmitOptions),io_uring_enter(2)(§6). - La complétion :
wait_completion,wait_completion_timeout,try_completion,completions(), et le typeCompletionavec ses interprétations typées génériques (§7). - Le téardown sûr (S2) :
shutdown()explicite +Dropquiescent, bâtis sursync_cancel(§8). - L’introspection :
supports_op(probe) etcapabilities(§9).
Les trois syscalls sous-jacents (numéros identiques x86_64 / ARM64) :
| Syscall | n° x86_64 | n° ARM64 | Usage au Temps 1 |
|---|---|---|---|
io_uring_setup | 425 | 425 | construction (§5) |
io_uring_enter | 426 | 426 | soumission + attente (§6, §7) |
io_uring_register | 427 | 427 | probe, sync_cancel, eventfd… (§8, §9) |
2. Types fondamentaux introduits au Temps 1
| Type | Rôle | air-sys-types ? |
|---|---|---|
IoUring | l’anneau (FD + mmaps + slab + capabilities). Send, !Sync. | oui |
IoUringBuilder | construction configurée (flags, tailles, restrictions). | oui |
SetupFlags | bitflags des flags de io_uring_setup (axe A). | oui |
IoUringParams | miroir typé de io_uring_params (entrées/sorties du setup). | oui |
IoUringCapabilities | features négociées (axe G), prédicats stables. | oui |
IoUringOpcode | énumération des 55 opcodes retenus (probe, restrictions). | oui |
SubmissionToken | jeton opaque slot+génération (S1). | oui |
SubmitOptions | options par-op (drain, async, skip-cqe, link — exposé Temps 3c). | oui |
Completion | une complétion typée (CQE décodé). | oui |
CompletionFlags | bitflags des flags de CQE (axe E). | oui |
CompletionIter<'ring> | itérateur sur les complétions disponibles. | non (emprunt) |
CancelTarget | cible d’annulation (token / fd / op / any). | oui |
3. Modèle mémoire et protocole d’anneau
Le Temps 1 implémente ce protocole mais ne l’expose pas crûment (le brut est au Temps 4,
::raw). Cette section fixe le contrat de correction.
3.1 Les trois zones mmappées
io_uring_setup retourne un FD et remplit io_uring_params avec deux jeux
d’offsets (io_sqring_offsets, io_cqring_offsets). On mmappe :
- Anneau SQ à l’offset
IORING_OFF_SQ_RING: contienthead,tail,ring_mask,ring_entries,flags,dropped, et (saufNO_SQARRAY) le tableau d’indexarray. - Tableau SQE à l’offset
IORING_OFF_SQES:ring_entriesentrées de 64 o (ou 128 o siSQE128). - Anneau CQ à l’offset
IORING_OFF_CQ_RING:head,tail,ring_mask,ring_entries,overflow,flags, et le tableaucqes(16 o, ou 32 o siCQE32).
Avec IORING_FEAT_SINGLE_MMAP (toujours présent en 6.12), SQ et CQ partagent
une seule mmap ; on le détecte et on adapte (deux mmaps sinon). Avec NO_MMAP,
l’appelant fournit la mémoire — non couvert au Temps 1 (option avancée, gated).
3.2 Protocole de publication — ordering mémoire
Les têtes/queues sont des compteurs 32 bits monotones partagés avec le kernel ;
l’index réel = compteur & ring_mask. L’ordering est non négociable :
Soumission (userspace producteur de la SQ) :
- Écrire le SQE complet dans
sqes[tail & mask]. - (Sauf
NO_SQARRAY) écriretail & maskdansarray[tail & mask]. - Publier la nouvelle
tailpar un store release (AtomicU32::store(.., Release)) — garantit que le kernel voit les écritures du SQE avant de voir la queue avancer. headde la SQ est lue par un load acquire pour calculer la place disponible.
Complétion (userspace consommateur de la CQ) :
- Lire
tailde la CQ par un load acquire — garantit que les CQE écrits par le kernel sont visibles. - Lire les CQE entre
headettail. - Publier la nouvelle
headpar un store release pour rendre les entrées au kernel.
Toute implémentation doit respecter ce protocole ; il est testé par modèle
(loom, §11) en plus des tests fonctionnels. Référence : io_uring(7) et
io_uring_setup(2).
3.3 Réveil et drapeaux d’anneau
IORING_SQ_NEED_WAKEUP(flags SQ) : en modeSQPOLL, si positionné, il faut réveiller le thread kernel viaio_uring_enter(.., SQ_WAKEUP)(géré au Temps 3e ; au Temps 1 on lit/expose le drapeau).IORING_SQ_CQ_OVERFLOW: la CQ a débordé ; avecIORING_FEAT_NODROP(présent en 6.12) le kernel retient les complétions et les re-livre, mais signale l’état. Le Temps 1 le surface (IoUring::completion_queue_overflowed()).IORING_SQ_TASKRUN: du task-work est en attente (avecTASKRUN_FLAG) ; indique qu’une entrée kernel est utile.
4. Le slab d’opérations en vol (décision S1)
4.1 Problème et forme
Entre une soumission et sa complétion, le kernel détient potentiellement un
buffer (modèle de transfert d’ownership, ADR-022 Décision 3) et la façade
doit retrouver, à la complétion, quelle opération et quel buffer
correspondent au CQE. Le lien est user_data (u64, posé dans le SQE, rendu
dans le CQE).
Une table dynamique allouerait par opération → interdit (CLAUDE.md). On utilise un slab pré-alloué :
struct InflightSlab {
slots: Box<[Slot]>, // alloué UNE fois à la construction
free_head: Option<u32>,
in_flight: u32,
}
struct Slot {
generation: u32, // incrémenté à chaque réutilisation (compteur de génération anti-réutilisation)
state: SlotState, // Free | Inflight { kind, buffer } | Multishot { .. }
}
- Capacité = nombre maximal d’opérations simultanément en vol, par défaut
cq_entries(le kernel ne peut pas avoir plus de complétions pendantes que la CQ, modulo overflow géré par NODROP). Configurable via le builder. - Le buffer transféré est déplacé (move) dans le slot : aucune copie, aucune réallocation dans le happy path.
4.2 Encodage du SubmissionToken
SubmissionToken encapsule { slot: u32, generation: u32 }. Le user_data
kernel = ((generation as u64) << 32) | (slot as u64). À la complétion :
- décoder
user_data→(generation, slot); - si
slots[slot].generation != generation→ complétion périmée (op déjà annulée/recyclée, p. ex. CQE multishot tardif) : on l’ignore proprement ; - sinon, on consomme le slot et on restitue le buffer à l’appelant.
4.3 Back-pressure
submit_* réserve un slot avant d’écrire le SQE. Slab plein (in_flight == capacity) ⇒ retour Err(Errno::EBUSY) sans toucher au kernel. C’est la
back-pressure structurelle d’Air : on refuse poliment plutôt que de déborder.
4.4 Multishot
Une opération multishot (Temps 3d) occupe un slot mais produit plusieurs
CQE. Le slot reste vivant tant que les complétions portent
IORING_CQE_F_MORE ; il est libéré à la complétion finale (sans F_MORE) ou à
l’annulation. La génération protège contre les CQE arrivant après annulation.
5. Construction du ring
5.1 IoUringBuilder
#![allow(unused)]
fn main() {
pub struct IoUringBuilder { /* ... */ }
impl IoUringBuilder {
pub fn new(entries: NonZeroU32) -> Self;
pub fn with_completion_queue_entries(self, entries: NonZeroU32) -> Self; // SETUP_CQSIZE
pub fn max_inflight(self, n: NonZeroU32) -> Self; // capacité du slab
pub fn with_flags(self, flags: SetupFlags) -> Self;
pub fn with_sqpoll_idle(self, dur: Duration) -> Self; // SQPOLL (Temps 3e)
pub fn with_sqpoll_cpu(self, cpu: u32) -> Self; // SQ_AFF
pub fn attach_work_queue(self, other: &IoUring) -> Self; // ATTACH_WQ
pub fn restrict(self, restrictions: &[Restriction]) -> Self; // S3 (Temps 3f)
pub fn build(self) -> Result<IoUring, Errno>;
}
}
build — comportement.
- Traduit la configuration en
io_uring_params, appelleio_uring_setup(2). entriesest arrondi par le kernel à la puissance de 2 supérieure ; on lit les tailles effectives en retour (sq_entries,cq_entries).- mmappe SQ/CQ/SQE selon §3.1 (RAII : les mmaps sont possédées par
IoUring). - alloue le slab (§4) une fois, dimensionné à
max_inflightoucq_entries. - lit
params.features→IoUringCapabilities. - si
REGISTERED_FD_ONLY/REG_REG_RING: enregistre le ring fd et bascule en mode fd enregistré de façon transparente. - si
restrict(..)non vide : poseR_DISABLED, appliqueREGISTER_RESTRICTIONS, laisse le ring désactivé (l’appelant appelleenable(); cf. Temps 3f).
Préconditions. entries ≤ limite kernel (sinon EINVAL). Combinaisons
de flags incompatibles refusées (p. ex. IOPOLL + SQPOLL selon contexte) :
on remonte l’EINVAL du kernel sans le masquer.
Erreurs. EINVAL (params invalides), ENOMEM (mémoire), EPERM
(SQPOLL/affinité sans privilège selon config), EFAULT, ENOSYS (io_uring
indisponible — sandbox/container : cf. §10).
Performance. Coût dominé par io_uring_setup + mmaps + alloc unique du
slab : ~quelques dizaines de µs. Fait une fois, hors chemin chaud.
Exemple.
#![allow(unused)]
fn main() {
use core::num::NonZeroU32;
let ring = IoUringBuilder::new(NonZeroU32::new(256).unwrap())
.with_flags(SetupFlags::SINGLE_ISSUER | SetupFlags::DEFER_TASKRUN)
.build()?;
}
Tests. unit (tailles arrondies, lecture features), property (entries
arbitraires → invariants tailles), intégration (création réelle sur 6.12),
simulateur de syscall pour forcer ENOSYS/ENOMEM, fuzzing des combinaisons
de flags.
5.2 IoUring::new et enable
#![allow(unused)]
fn main() {
impl IoUring {
pub fn new(entries: NonZeroU32) -> Result<Self, Errno>; // = builder.build()
pub fn enable(&mut self) -> Result<(), Errno>; // REGISTER_ENABLE_RINGS
pub fn completion_queue_overflowed(&self) -> bool; // lit SQ_CQ_OVERFLOW
pub fn submission_queue_space_left(&self) -> u32;
pub fn in_flight(&self) -> u32; // slots occupés (S1)
}
}
enable n’est utile que pour un ring créé désactivé (via restrict) ; sur un
ring déjà actif il retourne Err(EINVAL) (remonté tel quel).
6. Soumission
6.1 submit et submit_and_wait
#![allow(unused)]
fn main() {
impl IoUring {
pub fn submit(&mut self) -> Result<u32, Errno>;
pub fn submit_and_wait(&mut self, want: u32) -> Result<u32, Errno>;
}
}
Comportement. submit publie la queue SQ (§3.2) puis appelle
io_uring_enter(fd, to_submit, 0, 0, ..) ; retourne le nombre de SQE consommés
par le kernel. submit_and_wait(want) ajoute IORING_ENTER_GETEVENTS et
min_complete = want.
En mode SQPOLL, si le thread kernel tourne, submit peut n’avoir aucun
syscall à faire (juste la publication release) ; il ne réveille le thread que
si SQ_NEED_WAKEUP.
EINTR. Remonté tel quel (ADR-021 conv. 2). L’appelant qui veut réessayer écrit sa boucle.
Erreurs. EINTR, EAGAIN (ressources momentanées), EBUSY (CQ pleine non
drainée selon contexte), EINVAL, EFAULT, EBADF.
Performance. Le gain d’io_uring est ici : on batch N opérations pour un
seul io_uring_enter. Un submit sans attente sur ring SQPOLL chaud ≈ coût
d’un store atomique.
6.2 Options par-opération
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, Default)]
pub struct SubmitOptions { /* ... */ }
impl SubmitOptions {
pub fn drain(self) -> Self; // IOSQE_IO_DRAIN
pub fn force_async(self) -> Self; // IOSQE_ASYNC
pub fn skip_cqe_on_success(self) -> Self; // IOSQE_CQE_SKIP_SUCCESS (FEAT_CQE_SKIP)
// link / hardlink exposés via LinkedChainBuilder (Temps 3c)
}
impl IoUring {
pub fn with(&mut self, opts: SubmitOptions) -> &mut Self;
}
}
with applique les options à la prochaine soumission submit_*. Note :
skip_cqe_on_success libère le slot S1 à la soumission (pas de CQE attendu en
cas de succès) — la spec du slot documente ce cas particulier de libération.
7. Complétion
7.1 Récupération
#![allow(unused)]
fn main() {
impl IoUring {
pub fn wait_completion(&mut self) -> Result<Completion, Errno>;
pub fn wait_completion_timeout(&mut self, timeout: Duration)
-> Result<Option<Completion>, Errno>;
pub fn try_completion(&mut self) -> Option<Completion>;
pub fn completions(&mut self) -> CompletionIter<'_>;
}
}
wait_completion: bloque jusqu’à au moins une complétion (io_uring_enter(.., GETEVENTS, min_complete=1)), la décode et la consomme.wait_completion_timeout: utiliseIORING_ENTER_EXT_ARG(io_uring_getevents_arg+__kernel_timespec) ouABS_TIMER(FEAT_MIN_TIMEOUT) ;Ok(None)à l’expiration.try_completion: non bloquant, lit la CQ sans syscall si des CQE sont présents (load acquire de la queue).completions(): draine ce qui est disponible, avancehead(store release) à la fin de l’itération ou par lot.
Complétion périmée. Si le user_data décodé pointe un slot dont la
génération ne correspond pas (§4.2), la complétion est filtrée : ces
fonctions passent à la suivante sans la rendre à l’appelant.
7.2 Le type Completion
#![allow(unused)]
fn main() {
pub struct Completion { /* token, res, flags, accès au slot pour reprise buffer */ }
impl Completion {
pub fn token(&self) -> SubmissionToken;
pub fn raw_result(&self) -> i32; // brut, sémantique selon l'op
pub fn flags(&self) -> CompletionFlags;
pub fn has_more(&self) -> bool; // CQE_F_MORE (multishot)
pub fn is_notif(&self) -> bool; // CQE_F_NOTIF (zero-copy)
pub fn buffer_id(&self) -> Option<u16>; // CQE_F_BUFFER
pub fn socket_has_pending_data(&self) -> bool; // CQE_F_SOCK_NONEMPTY
// Interprétation générique (les variantes typées riches sont aux Temps 2x) :
pub fn into_result(self) -> Result<i32, Errno>; // res<0 => Err(-res)
pub fn into_buffer_result(self) -> Result<(Vec<u8>, usize), Errno>; // reprise buffer S1
pub fn completed(&self) -> Result<(), Errno>; // succès sans valeur
}
}
Convention de résultat. Un res négatif est un -errno ; les méthodes
into_* le convertissent en Err(Errno). Un res ≥ 0 est la valeur utile
(octets, fd, etc.), interprétée par la méthode appropriée — le développeur sait
quelle opération il a soumise (ADR-022 Décision 9).
Reprise du buffer. into_buffer_result récupère le buffer déplacé hors
du slot (S1) et le rend à l’appelant avec le nombre d’octets — zéro copie.
7.3 CompletionFlags
bitflags des 5 flags de l’axe E : BUFFER, MORE, SOCK_NONEMPTY, NOTIF,
BUF_MORE.
8. Téardown sûr (décision S2)
8.1 sync_cancel (brique)
#![allow(unused)]
fn main() {
pub enum CancelTarget {
Token(SubmissionToken),
Fd(BorrowedFd<'_>),
Op(IoUringOpcode),
Any,
}
impl IoUring {
pub fn sync_cancel(&mut self, target: CancelTarget) -> Result<u32, Errno>;
}
}
S’appuie sur IORING_REGISTER_SYNC_CANCEL (io_uring_sync_cancel_reg, avec
timeout). Mappe CancelTarget sur les flags IORING_ASYNC_CANCEL_*
(USERDATA / FD / OP / ANY). Retourne le nombre d’opérations annulées.
8.2 shutdown et Drop
#![allow(unused)]
fn main() {
impl IoUring {
pub fn shutdown(self) -> Result<(), Errno>;
}
impl Drop for IoUring { fn drop(&mut self); }
}
shutdown (voie propre).
sync_cancel(Any)avec timeout borné.- Drainer la CQ jusqu’à
in_flight() == 0(les buffers des slots sont libérés proprement à mesure). munmapdes anneaux, fermeture du FD (RAII), libération du slab.- Retourne
Errsi le drainage échoue/expire (l’appelant décide).
Drop (filet de sécurité). Si in_flight() > 0, exécute la même séquence de
quiescence mais de façon bloquante et best-effort (boucle bornée). Le coût
(blocage potentiel) est documenté : sur chemin chaud, préférer shutdown().
Justification : « sur-sécuriser puis dégraisser » (Principe 5) — un Drop qui
laisse le kernel écrire dans de la mémoire libérée est un défaut de soundness
inacceptable en couche 0.
Invariant de sûreté. Tant qu’une opération est en vol, le buffer associé vit dans son slot (S1) et n’est pas libérable par l’appelant ; le ring ne peut pas être détruit sans quiescence. Ces deux invariants garantissent qu’aucune écriture kernel ne tombe sur de la mémoire libérée.
9. Introspection : probe et capabilities
#![allow(unused)]
fn main() {
impl IoUring {
pub fn supports_op(&self, op: IoUringOpcode) -> bool;
pub fn capabilities(&self) -> IoUringCapabilities;
}
}
supports_op: interrogeIORING_REGISTER_PROBE(mis en cache à la construction). Permet le fallback vers les syscalls synchrones quand une op n’est pas supportée par le kernel courant (ADR-022 Décision 8).capabilities: expose les 16 features de l’axe G via des prédicats stables (single_mmap,nodrop,cqe_skip,min_timeout,recvsend_bundle,reg_reg_ring, …).
Exemple de fallback.
#![allow(unused)]
fn main() {
if ring.supports_op(IoUringOpcode::Openat2) {
let tok = ring.submit_openat2(/* ... */)?; // Temps 2a
} else {
// bascule sur air-sys-syscall::fs::openat2 synchrone
}
}
10. Indisponibilité d’io_uring (ADR-022 Décision 10)
Si le kernel ne supporte pas io_uring ou si l’environnement l’a désactivé
(seccomp, sandbox, container durci), build() retourne Err(Errno::ENOSYS)
(ou EPERM selon le filtre). Aucun fallback automatique caché : l’appelant
choisit de basculer en synchrone ou de refuser de démarrer.
11. Stratégie de tests (couche 0 — 100 % lignes + branches)
- Unitaires : encodage/décodage
SubmissionToken(slot+génération), arithmétique des têtes/queues (masquage, wrap), back-pressureEBUSY, filtrage des complétions périmées. - Property-based (proptest) : pour toute séquence de submit/complete, les
invariants
in_flight ≤ capacity,head ≤ tail, aucun slot doublement libéré, aucun buffer perdu. - Modèle de concurrence (loom) : le protocole acquire/release de §3.2 sur un modèle producteur(userspace)/consommateur(kernel simulé) ; détecte tout ordering manquant.
- Intégration : sur kernel 6.12 réel — création, nop, submit/wait, timeout, shutdown propre, Drop avec ops en vol, overflow CQ (NODROP).
- Simulateur de syscalls : harnais injectant
EINTR,EAGAIN,ENOSYS,EFAULTaux frontièresenter/setup/registerpour couvrir les branches d’erreur sans dépendre du kernel. - Fuzzing (cargo-fuzz) : décodage des CQE et des
io_uring_paramsretournés (toute donnée venant du kernel est traitée comme externe, Principe 3). - Tests de Drop : Miri/valgrind pour confirmer l’absence d’usage après libération sur les chemins de quiescence.
Branches non couvrables (p. ex. erreurs kernel impossibles à provoquer) :
consignées dans docs/COVERAGE-EXCEPTIONS.md avec justification.
12. Récapitulatif des erreurs (Temps 1)
| Fonction | Erreurs notables |
|---|---|
build / new | EINVAL, ENOMEM, EPERM, ENOSYS, EFAULT |
enable | EINVAL, EBADF |
submit / submit_and_wait | EINTR, EAGAIN, EBUSY, EINVAL, EBADF, EFAULT |
wait_completion* | EINTR, ETIME (timeout interne, selon mapping), EBADF |
submit_* (réservation slot) | EBUSY (slab plein) avant tout syscall |
sync_cancel | EINTR, EALREADY, ENOENT, EINVAL |
shutdown | propage les erreurs de drainage/cancel |
13. Types ajoutés à air-sys-types (Temps 1)
IoUring, IoUringBuilder, IoUringParams, SetupFlags,
IoUringCapabilities, IoUringOpcode, SubmissionToken, SubmitOptions,
Completion, CompletionFlags, CancelTarget. Soit ~11 types publics (le
slab et les wrappers de mmap sont internes, non exposés).
14. Décisions de fond émergées au Temps 1
SubmissionToken= slot+génération, pasuser_databrut. L’API ne laisse jamais l’appelant manipuler unuser_dataarbitraire : la génération protège contre l’ABA et les complétions périmées (multishot/cancel).- Slab dimensionné par défaut à
cq_entries. Aligne la back-pressure applicative sur la capacité réelle de complétions du kernel. Dropbloquant assumé. Choix de soundness sur performance, réversible après mesure (mais jamais dans le sens dangereux).- Pas de trait
Submittableau Temps 1. Méthodes inhérentes ; on n’introduit l’abstraction que si les Temps 3c/3d en prouvent le besoin (Principe 7). - Timeouts via
EXT_ARG/ABS_TIMER, pas via une opérationTIMEOUTliée — celle-ci reste disponible au Temps 2c pour les cas explicites.
15. Travail à reprendre
Specs suivantes, sur ce modèle : io-uring-2a-filesystem.md (les 26 opérations
FS s’appuyant sur le cœur ci-dessus), puis 2b, 2c, 2d, 3a–3f, 4. La traduction
anglaise globale est produite une fois les documents français validés.
Licence du document : MPL 2.0
Statut : Spécification technique du Temps 1 (cœur) du module air-sys-syscall::io_uring, cible kernel 6.12 LTS.