Skip to main content

air_sys_syscall/io_uring/
shared.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! **Usage multi-thread** d'io_uring (Temps 3e). Sous-module
6//! `air-sys-syscall::io_uring::shared`. Repose sur l'invariant **ADR-022
7//! Décision 6** ([`IoUring`] est `Send` mais **`!Sync`**), sur `SETUP_SQPOLL`
8//! (Temps 1), `ATTACH_WQ` (Temps 3a) et `msg_ring` (Temps 2c). **Aucun register
9//! opcode propre.**
10//!
11//! Référence normative : `docs/specs/layer-0/io-uring-3e-shared.md`.
12//!
13//! ## Le point de départ : `Send` mais pas `Sync`
14//!
15//! Le protocole d'ordering du Temps 1 (§3.2) synchronise *userspace ↔ kernel*,
16//! **pas** *userspace ↔ userspace* : têtes/queues SQ/CQ et **slab S1** ne sont
17//! pas protégés contre des accès userspace concurrents. Le `!Sync` d'[`IoUring`]
18//! **interdit par typage** le partage accidentel d'un ring (prouvé par un
19//! doctest `compile_fail` sur [`LockedIoUring`]). Trois réponses, par ordre de
20//! préférence Air :
21//!
22//! 1. **Thread-per-core (§2, recommandé)** — un [`IoUring`] par thread
23//!    (`SINGLE_ISSUER | DEFER_TASKRUN`), **aucun verrou** ; communication par
24//!    `msg_ring`. Aucun type nouveau : usage direct d'[`IoUring`], déplacé
25//!    (`Send`) dans son thread. [`RingPool`] **organise** N tels rings.
26//! 2. **[`LockedIoUring`] (§3)** — un ring partagé derrière un verrou
27//!    (`Send + Sync`). Simple, mais le verrou est un **point de contention**.
28//! 3. **[`SqpollIoUring`] (§5)** — un thread kernel scrute la SQ (latence vs CPU).
29
30use super::{Completion, IoUring, IoUringBuilder, SetupFlags, SubmissionToken, raw, syscall};
31use air_sys_types::Errno;
32use air_sys_types::fd::BorrowedFd;
33use air_sys_types::io_uring::MessageRingFlags;
34use alloc::vec::Vec;
35use core::num::{NonZeroU32, NonZeroUsize};
36use core::time::Duration;
37
38use crate::sync::FutexMutex;
39
40// ───────────────────────────────────────────────────────────────────────────
41// LockedIoUring — partage simple par verrou (§3)
42// ───────────────────────────────────────────────────────────────────────────
43
44/// Un [`IoUring`] **partagé** entre threads derrière un **verrou interne**
45/// (`Send + Sync`). Le verrou sérialise les accès userspace au SQ/CQ/slab ; le
46/// protocole d'ordering userspace↔kernel du Temps 1 reste inchangé dessous.
47///
48/// **Avertissement (Principe 5)** : le verrou est un **point de contention** —
49/// sous forte charge multi-thread il sérialise tout. À réserver aux cas simples
50/// ou peu sollicités ; pour la performance, préférer le **thread-per-core** (§2,
51/// [`RingPool`]). **Verrou unique** d'abord (Principe 7) ; un affinage
52/// (soumission/complétion séparées) n'est envisagé qu'**après mesure**.
53///
54/// **Sûreté** : `Send + Sync` par construction — [`IoUring`] est `Send`, donc
55/// `FutexMutex<IoUring>` est `Send + Sync` :
56///
57/// ```
58/// fn assert_send_sync<T: Send + Sync>() {}
59/// assert_send_sync::<air_sys_syscall::io_uring::LockedIoUring>();
60/// ```
61///
62/// alors qu'un [`IoUring`] nu n'est **pas** `Sync` (partage refusé par typage) :
63///
64/// ```compile_fail
65/// fn assert_sync<T: Sync>() {}
66/// assert_sync::<air_sys_syscall::io_uring::IoUring>(); // ERREUR : IoUring: !Sync
67/// ```
68pub struct LockedIoUring {
69    /// `FutexMutex<IoUring>` : `IoUring` est `Send`, donc `FutexMutex<IoUring>`
70    /// est `Send + Sync` → `LockedIoUring` devient partageable (`Sync`). Le
71    /// verrou est un mutex futex-maison `std`-free (cf. [`crate::sync`]).
72    inner: FutexMutex<IoUring>,
73}
74
75impl core::fmt::Debug for LockedIoUring {
76    // `IoUring` n'est pas `Debug` (état de ring opaque) ; on n'affiche pas son
77    // contenu (et on ne prend pas le verrou pour formater).
78    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
79        f.write_str("LockedIoUring { .. }")
80    }
81}
82
83impl LockedIoUring {
84    /// Raccourci : `LockedIoUring::from_builder(IoUringBuilder::new(entries))`.
85    ///
86    /// # Errors
87    ///
88    /// Voir [`IoUringBuilder::build`].
89    pub fn new(entries: NonZeroU32) -> Result<Self, Errno> {
90        Ok(Self {
91            inner: FutexMutex::new(IoUring::new(entries)?),
92        })
93    }
94
95    /// Construit à partir d'un [`IoUringBuilder`] (flags, SQPOLL, etc.).
96    ///
97    /// # Errors
98    ///
99    /// Voir [`IoUringBuilder::build`].
100    pub fn from_builder(builder: IoUringBuilder) -> Result<Self, Errno> {
101        Ok(Self {
102            inner: FutexMutex::new(builder.build()?),
103        })
104    }
105
106    /// Exécute `f` sur le ring sous-jacent **sous le verrou** (accès exclusif
107    /// sérialisé). C'est la primitive complète : toute opération d'[`IoUring`]
108    /// (les ~50 `submit_*`, les accesseurs de complétion) est accessible ainsi en
109    /// `&self`, sans recopier 50 forwarders. Les méthodes nommées ci-dessous en
110    /// sont de simples raccourcis pour les cas les plus fréquents.
111    ///
112    /// Le verrou est **sans empoisonnement** ([`FutexMutex`] n'implémente pas le
113    /// poisoning) : un panic d'un autre thread sous le verrou ne le rend pas
114    /// inutilisable (cohérent avec la future synchro sans poisoning d'`air-thread`).
115    pub fn with_lock<R>(&self, f: impl FnOnce(&mut IoUring) -> R) -> R {
116        let mut guard = self.inner.lock();
117        f(&mut guard)
118    }
119
120    /// `submit_read` (Temps 2a) sous le verrou.
121    ///
122    /// # Errors
123    ///
124    /// Voir [`IoUring::submit_read`].
125    pub fn submit_read(
126        &self,
127        fd: BorrowedFd<'_>,
128        buffer: Vec<u8>,
129        offset: Option<u64>,
130    ) -> Result<SubmissionToken, Errno> {
131        self.with_lock(|ring| ring.submit_read(fd, buffer, offset))
132    }
133
134    /// `submit_write` (Temps 2a) sous le verrou.
135    ///
136    /// # Errors
137    ///
138    /// Voir [`IoUring::submit_write`].
139    pub fn submit_write(
140        &self,
141        fd: BorrowedFd<'_>,
142        buffer: Vec<u8>,
143        offset: Option<u64>,
144    ) -> Result<SubmissionToken, Errno> {
145        self.with_lock(|ring| ring.submit_write(fd, buffer, offset))
146    }
147
148    /// `submit_nop` (Temps 1) sous le verrou.
149    ///
150    /// # Errors
151    ///
152    /// Voir [`IoUring::submit_nop`].
153    pub fn submit_nop(&self) -> Result<SubmissionToken, Errno> {
154        self.with_lock(|ring| ring.submit_nop())
155    }
156
157    /// `submit` (Temps 1) sous le verrou.
158    ///
159    /// # Errors
160    ///
161    /// Voir [`IoUring::submit`].
162    pub fn submit(&self) -> Result<u32, Errno> {
163        self.with_lock(|ring| ring.submit())
164    }
165
166    /// `submit_and_wait` (Temps 1) sous le verrou.
167    ///
168    /// **Attention** : l'attente bloque **avec le verrou tenu** — un seul thread
169    /// attend à la fois. Pour de l'attente concurrente, préférer le thread-per-core.
170    ///
171    /// # Errors
172    ///
173    /// Voir [`IoUring::submit_and_wait`].
174    pub fn submit_and_wait(&self, want: u32) -> Result<u32, Errno> {
175        self.with_lock(|ring| ring.submit_and_wait(want))
176    }
177
178    /// `wait_completion` (Temps 1) sous le verrou.
179    ///
180    /// # Errors
181    ///
182    /// Voir [`IoUring::wait_completion`].
183    pub fn wait_completion(&self) -> Result<Completion, Errno> {
184        self.with_lock(|ring| ring.wait_completion())
185    }
186
187    /// `try_completion` (Temps 1) sous le verrou.
188    #[must_use]
189    pub fn try_completion(&self) -> Option<Completion> {
190        self.with_lock(|ring| ring.try_completion())
191    }
192
193    /// Nombre d'opérations en vol (sous le verrou).
194    #[must_use]
195    pub fn in_flight(&self) -> u32 {
196        self.with_lock(|ring| ring.in_flight())
197    }
198}
199
200// ───────────────────────────────────────────────────────────────────────────
201// RingPool — assistant thread-per-core (§4)
202// ───────────────────────────────────────────────────────────────────────────
203
204/// Assistant **thread-per-core** : crée N [`IoUring`] cohérents en **partageant
205/// le pool io-wq** (`ATTACH_WQ`, Temps 3a) — le nombre total de threads workers
206/// kernel est **borné** quel que soit N (**crucial sur matériel modeste**, Pi 4)
207/// — et en **enregistrant les ring fds** (Temps 3a) pour le routage `msg_ring`.
208///
209/// `RingPool` **organise, ne partage pas** : chaque [`IoUring`] rendu reste
210/// `Send`/`!Sync` et est **déplacé** dans son thread, jamais partagé.
211pub struct RingPool {
212    /// Un ring possédé par worker ; le ring 0 détient le pool io-wq, les autres
213    /// s'y attachent. Ring fds enregistrés (msg_ring découplé).
214    rings: Vec<IoUring>,
215}
216
217impl core::fmt::Debug for RingPool {
218    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
219        f.debug_struct("RingPool")
220            .field("workers", &self.rings.len())
221            .finish_non_exhaustive()
222    }
223}
224
225impl RingPool {
226    /// Crée `workers` rings : le **premier** crée le pool io-wq, les **suivants**
227    /// s'y attachent (`ATTACH_WQ`) → nombre total de threads workers kernel borné.
228    /// Chaque ring fd est enregistré (`REGISTER_RING_FDS`, Temps 3a).
229    ///
230    /// # Errors
231    ///
232    /// Erreurs d'`io_uring_setup`/`io_uring_register` (cf. [`IoUringBuilder::build`],
233    /// [`IoUring::register_ring_fd`]).
234    pub fn new(workers: NonZeroUsize, entries: NonZeroU32) -> Result<Self, Errno> {
235        let count = workers.get();
236        let mut rings: Vec<IoUring> = Vec::with_capacity(count);
237        // Premier ring : crée le pool io-wq.
238        rings.push(IoUring::new(entries)?);
239        // Suivants : s'attachent au pool du premier (workers kernel bornés).
240        for _ in 1..count {
241            let attached = {
242                let first = &rings[0];
243                IoUringBuilder::new(entries)
244                    .attach_work_queue(first)
245                    .build()?
246            };
247            rings.push(attached);
248        }
249        // Enregistre chaque ring fd (routage msg_ring découplé, Temps 3a).
250        for ring in &mut rings {
251            ring.register_ring_fd()?;
252        }
253        Ok(Self { rings })
254    }
255
256    /// Distribue les rings : un [`IoUring`] **possédé** par worker, à **déplacer**
257    /// dans chaque thread.
258    #[must_use]
259    pub fn into_rings(self) -> Vec<IoUring> {
260        self.rings
261    }
262
263    /// Poignée de routage vers le ring du worker `worker` (cible `msg_ring`).
264    /// `None` si `worker` est hors borne.
265    #[must_use]
266    pub fn handle(&self, worker: usize) -> Option<RingHandle> {
267        self.rings.get(worker).map(|ring| RingHandle {
268            target_fd: ring.fd_raw(),
269        })
270    }
271}
272
273/// Référence à un ring **pair**, utilisable comme **cible de `msg_ring`**
274/// ([`IoUring::submit_message_ring_data_to`]). Porte le FD du ring pair (valide
275/// tant que ce ring vit dans son thread — l'appelant garantit sa vivacité).
276#[derive(Debug, Clone, Copy)]
277pub struct RingHandle {
278    /// FD du ring cible (process-global ; valable cross-thread).
279    target_fd: i32,
280}
281
282impl IoUring {
283    /// `msg_ring` (Temps 2c, `MSG_DATA`) ciblant un **pair** par sa [`RingHandle`]
284    /// (FD), pour réveiller/transmettre une donnée vers un ring **d'un autre
285    /// thread** (modèle thread-per-core / [`RingPool`]). Variante de
286    /// [`IoUring::submit_message_ring_data`] (qui prend une `&IoUring`, même
287    /// contexte).
288    ///
289    /// # Errors
290    ///
291    /// [`Errno::EBUSY`] si la SQ/le slab sont pleins ; à la complétion, `EBADF`
292    /// si le ring cible n'existe plus.
293    pub fn submit_message_ring_data_to(
294        &mut self,
295        target: &RingHandle,
296        res: i32,
297        user_data: u64,
298        flags: MessageRingFlags,
299    ) -> Result<SubmissionToken, Errno> {
300        let target_fd = target.target_fd;
301        let len = res.cast_unsigned();
302        let op_flags = flags.bits();
303        self.submit_op(None, |sqe| {
304            sqe.opcode = raw::IORING_OP_MSG_RING;
305            sqe.fd = target_fd;
306            sqe.addr_or_splice_off_in = raw::IORING_MSG_DATA;
307            sqe.len = len;
308            sqe.off_or_addr2 = user_data;
309            sqe.op_flags = op_flags;
310        })
311    }
312}
313
314// ───────────────────────────────────────────────────────────────────────────
315// SqpollIoUring — thread kernel de poll de la SQ (§5)
316// ───────────────────────────────────────────────────────────────────────────
317
318/// [`IoUring`] créé avec `IORING_SETUP_SQPOLL` : un **thread kernel** scrute la
319/// SQ. En régime établi, la **soumission ne fait aucun `io_uring_enter`** (juste
320/// la publication release) → latence minimale.
321///
322/// **Coût** : un **cœur kernel** dédié à scruter. Excellent sous forte charge où
323/// le CPU abonde ; **mauvais choix sur Pi 4** (cœurs rares). À réserver aux
324/// profils où la latence prime (mesure, Principe 5).
325pub struct SqpollIoUring {
326    inner: IoUring,
327}
328
329impl core::fmt::Debug for SqpollIoUring {
330    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
331        f.write_str("SqpollIoUring { .. }")
332    }
333}
334
335impl SqpollIoUring {
336    /// Crée un ring `SQPOLL`. `idle` : délai d'inactivité avant que le thread
337    /// kernel ne s'endorme (`NEED_WAKEUP`). `cpu` : épinglage optionnel du thread
338    /// (`SQ_AFF`).
339    ///
340    /// # Errors
341    ///
342    /// [`Errno::EPERM`] si `SQPOLL` n'est pas autorisé par la config ; autres
343    /// erreurs de [`IoUringBuilder::build`].
344    pub fn new(entries: NonZeroU32, idle: Duration, cpu: Option<u32>) -> Result<Self, Errno> {
345        let mut flags = SetupFlags::SQPOLL;
346        let mut builder = IoUringBuilder::new(entries);
347        if let Some(cpu) = cpu {
348            // `SQ_AFF` : épingle le thread kernel sur `cpu`.
349            flags |= SetupFlags::SQ_AFF;
350            builder = builder.with_sqpoll_cpu(cpu);
351        }
352        builder = builder.with_flags(flags).with_sqpoll_idle(idle);
353        Ok(Self {
354            inner: builder.build()?,
355        })
356    }
357
358    /// Soumet les SQE en attente. En `SQPOLL`, **aucun `io_uring_enter`** n'est
359    /// émis si le thread kernel est éveillé (il consommera la SQ) ; s'il s'est
360    /// endormi (`IORING_SQ_NEED_WAKEUP`), la façade le **réveille** automatiquement
361    /// via `io_uring_enter(.., SQ_WAKEUP)` — **seule magie cachée**, indispensable
362    /// et documentée. Retourne le nombre de SQE en attente.
363    ///
364    /// # Errors
365    ///
366    /// [`Errno::EINTR`] remonté tel quel (ADR-021 conv. 2) ; autres errno d'`enter`.
367    pub fn submit(&mut self) -> Result<u32, Errno> {
368        let to_submit = self.inner.sq.publish_and_pending();
369        if to_submit == 0 {
370            return Ok(0);
371        }
372        if self.inner.sq.flags() & raw::IORING_SQ_NEED_WAKEUP != 0 {
373            // Thread SQPOLL endormi : le réveiller.
374            // SAFETY: `fd` est un ring fd valide ; pas d'argument étendu.
375            let ret = unsafe {
376                syscall::enter(
377                    self.inner.fd_raw(),
378                    to_submit,
379                    0,
380                    raw::IORING_ENTER_SQ_WAKEUP,
381                    0,
382                    0,
383                )
384            };
385            if ret < 0 {
386                return Err(raw::errno_from_negative_syscall_ret(ret));
387            }
388            Ok(u32::try_from(ret).unwrap_or(0))
389        } else {
390            // Thread SQPOLL éveillé : il consommera la SQ, aucun syscall requis.
391            Ok(to_submit)
392        }
393    }
394
395    /// `submit_nop` (Temps 1) : prépare un SQE (sans publier — [`Self::submit`] ou
396    /// [`Self::submit_and_wait`] publie).
397    ///
398    /// # Errors
399    ///
400    /// Voir [`IoUring::submit_nop`].
401    pub fn submit_nop(&mut self) -> Result<SubmissionToken, Errno> {
402        self.inner.submit_nop()
403    }
404
405    /// `submit_read` (Temps 2a) en mode `SQPOLL`.
406    ///
407    /// # Errors
408    ///
409    /// Voir [`IoUring::submit_read`].
410    pub fn submit_read(
411        &mut self,
412        fd: BorrowedFd<'_>,
413        buffer: Vec<u8>,
414        offset: Option<u64>,
415    ) -> Result<SubmissionToken, Errno> {
416        self.inner.submit_read(fd, buffer, offset)
417    }
418
419    /// Publie (avec réveil `SQPOLL` si nécessaire) **puis** attend au moins `want`
420    /// complétions (`io_uring_enter(GETEVENTS)`). L'attente requiert un syscall
421    /// (la soumission, elle, peut n'en demander aucun).
422    ///
423    /// # Errors
424    ///
425    /// [`Errno::EINTR`] remonté tel quel ; autres errno d'`enter`.
426    pub fn submit_and_wait(&mut self, want: u32) -> Result<u32, Errno> {
427        self.submit()?;
428        if want == 0 {
429            return Ok(0);
430        }
431        // SAFETY: `fd` valide ; on attend `want` complétions, pas d'argument étendu.
432        let ret = unsafe {
433            syscall::enter(
434                self.inner.fd_raw(),
435                0,
436                want,
437                raw::IORING_ENTER_GETEVENTS,
438                0,
439                0,
440            )
441        };
442        if ret < 0 {
443            return Err(raw::errno_from_negative_syscall_ret(ret));
444        }
445        Ok(u32::try_from(ret).unwrap_or(0))
446    }
447
448    /// `wait_completion` (Temps 1).
449    ///
450    /// # Errors
451    ///
452    /// Voir [`IoUring::wait_completion`].
453    pub fn wait_completion(&mut self) -> Result<Completion, Errno> {
454        self.inner.wait_completion()
455    }
456
457    /// Nombre d'opérations en vol.
458    #[must_use]
459    pub fn in_flight(&self) -> u32 {
460        self.inner.in_flight()
461    }
462}
463
464// ───────────────────────────────────────────────────────────────────────────
465// Preuve loom — discipline de verrou de LockedIoUring (sous `cfg(loom)`)
466// ───────────────────────────────────────────────────────────────────────────
467//
468// Exécution : `RUSTFLAGS="--cfg loom" cargo test -p air-sys-syscall --lib shared::loom_proof`.
469// `LockedIoUring` enveloppe l'`IoUring` (SQ/CQ/slab non protégés) dans un `Mutex` :
470// la sûreté repose entièrement sur l'**exclusion mutuelle** du verrou. Comme un
471// `IoUring` réel n'est pas constructible sous `loom::model` (pas de syscall), on
472// modélise la **discipline** : deux threads accèdent à un état partagé gardé
473// (représentant SQ-tail/slab) UNIQUEMENT sous le verrou. loom explore tous les
474// entrelacements et prouve l'absence de course (cellule gardée) et de mise à
475// jour perdue. Si l'accès gardé fuyait hors du verrou, loom le détecterait.
476#[cfg(loom)]
477mod loom_proof {
478    use loom::cell::UnsafeCell;
479    use loom::sync::{Arc, Mutex};
480
481    /// Deux threads incrémentent un état partagé **sous le verrou** : aucune mise
482    /// à jour perdue (somme exacte), aucun accès concurrent à la cellule.
483    #[test]
484    fn mutex_serializes_shared_ring_state() {
485        loom::model(|| {
486            // `Mutex<()>` garde l'accès à la cellule (état userspace du ring).
487            let state = Arc::new((Mutex::new(()), UnsafeCell::new(0u32)));
488
489            let writer = {
490                let state = Arc::clone(&state);
491                loom::thread::spawn(move || {
492                    let (lock, cell) = &*state;
493                    let _guard = lock.lock().unwrap();
494                    // SAFETY: accès exclusif garanti par le verrou tenu ; loom
495                    // vérifie qu'aucun autre thread ne touche la cellule sans le
496                    // verrou (modèle de `LockedIoUring::with_lock`).
497                    cell.with_mut(|p| unsafe { *p += 1 });
498                })
499            };
500
501            {
502                let (lock, cell) = &*state;
503                let _guard = lock.lock().unwrap();
504                // SAFETY: idem — sous le verrou, accès exclusif à la cellule.
505                cell.with_mut(|p| unsafe { *p += 1 });
506            }
507
508            writer.join().unwrap();
509
510            let (_lock, cell) = &*state;
511            // SAFETY: les deux threads ont terminé (join) ; lecture finale exclusive.
512            let total = cell.with(|p| unsafe { *p });
513            assert_eq!(total, 2, "aucune mise à jour perdue sous le verrou");
514        });
515    }
516}
517
518// ───────────────────────────────────────────────────────────────────────────
519// Tests
520// ───────────────────────────────────────────────────────────────────────────
521//
522// Intégration kernel (io_uring non modélisé par Miri) → `#[cfg_attr(miri, ignore)]`.
523// La sûreté multi-thread est prouvée à **trois** niveaux : (1) `!Sync` d'`IoUring`
524// et `Send + Sync` de `LockedIoUring` par **doctests `compile_fail`/compile-pass**
525// (sur le type `LockedIoUring`) + assertion statique ci-dessous ; (2) **loom** sur
526// la discipline de verrou (`mod loom_proof`, sous `cfg(loom)`) ; (3) tests
527// d'intégration concurrents réels (threads).
528
529#[cfg(test)]
530mod tests;