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;