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 {
531 use super::*;
532 use crate::test_support::sfd;
533 use core::num::{NonZeroU32, NonZeroUsize};
534 use std::sync::Arc;
535 use std::time::Duration;
536
537 fn nz32(n: u32) -> NonZeroU32 {
538 NonZeroU32::new(n).expect("n ≠ 0")
539 }
540
541 fn nzusize(n: usize) -> NonZeroUsize {
542 NonZeroUsize::new(n).expect("n ≠ 0")
543 }
544
545 /// Assertion **statique** : `LockedIoUring` est `Send + Sync` (le `!Sync`
546 /// d'`IoUring` est prouvé par le doctest `compile_fail` sur le type).
547 #[test]
548 fn locked_io_uring_is_send_sync() {
549 fn assert_send_sync<T: Send + Sync>() {}
550 assert_send_sync::<LockedIoUring>();
551 fn assert_send<T: Send>() {}
552 assert_send::<IoUring>(); // IoUring est Send (déplaçable entre threads)
553 assert_send::<RingPool>();
554 assert_send::<SqpollIoUring>();
555 }
556
557 // ── LockedIoUring : accès concurrent multi-thread ─────────────────────
558
559 #[test]
560 #[cfg_attr(miri, ignore = "io_uring non supporté par Miri")]
561 fn locked_io_uring_concurrent_nops() {
562 let ring = Arc::new(LockedIoUring::new(nz32(16)).expect("LockedIoUring"));
563 let threads: Vec<_> = (0..4)
564 .map(|_| {
565 let ring = Arc::clone(&ring);
566 std::thread::spawn(move || {
567 // Round-trip **atomique** sous le verrou : soumission, attente
568 // et moisson dans une **seule** section critique (`with_lock`).
569 // Indispensable — enchaîner les raccourcis `submit_nop` /
570 // `submit_and_wait` / `wait_completion` prend et relâche le
571 // verrou **trois fois**, ce qui laisserait un autre thread
572 // moissonner *ce* CQE entre-temps (course de complétion, pas
573 // de course SQ/CQ/slab). En tenant le verrou de bout en bout,
574 // la complétion récupérée est **forcément** la sienne.
575 ring.with_lock(|r| {
576 let tok = r.submit_nop().expect("nop");
577 r.submit_and_wait(1).expect("submit_and_wait");
578 let c = r.wait_completion().expect("completion");
579 assert_eq!(c.token(), tok);
580 c.completed().is_ok()
581 })
582 })
583 })
584 .collect();
585 for t in threads {
586 assert!(t.join().expect("thread"), "nop completed");
587 }
588 assert_eq!(ring.in_flight(), 0, "aucun slot perdu");
589 // Raccourcis nommés (`&self`), en **mono-thread** : chacun prend puis
590 // relâche le verrou ; sans concurrence, le token récupéré est le sien.
591 let tok = ring.submit_nop().expect("nop via mirror");
592 ring.submit_and_wait(1).expect("submit_and_wait via mirror");
593 assert_eq!(ring.wait_completion().expect("c").token(), tok);
594 assert_eq!(ring.in_flight(), 0);
595 }
596
597 // ── RingPool : ATTACH_WQ + routage msg_ring inter-ring ────────────────
598
599 #[test]
600 #[cfg_attr(miri, ignore = "io_uring non supporté par Miri")]
601 fn ring_pool_routes_msg_ring_between_peers() {
602 let pool = RingPool::new(nzusize(3), nz32(8)).expect("RingPool");
603 // Poignée vers le worker 1 (pair), capturée avant distribution.
604 let handle1 = pool.handle(1).expect("handle 1");
605 assert!(pool.handle(3).is_none(), "worker hors borne");
606 let mut rings = pool.into_rings();
607 assert_eq!(rings.len(), 3, "un ring par worker");
608 // worker 1 : attend (poll cq_ready) qu'un message arrive sur son CQ.
609 let ring1 = rings.remove(1);
610 let peer = std::thread::spawn(move || {
611 let ring1 = ring1;
612 for _ in 0..2000 {
613 if ring1.cq_ready() > 0 {
614 return true; // message reçu (CQE posté par msg_ring du pair)
615 }
616 std::thread::sleep(Duration::from_millis(1));
617 }
618 false
619 });
620 // worker 0 : route un msg_ring vers le pair (handle 1).
621 let mut ring0 = rings.remove(0);
622 let tok = ring0
623 .submit_message_ring_data_to(&handle1, 7, 0xABCD, MessageRingFlags::empty())
624 .expect("msg_ring vers le pair");
625 // La complétion de l'op msg_ring (côté émetteur) confirme l'envoi.
626 ring0.submit_and_wait(1).expect("submit");
627 let sent = ring0.wait_completion().expect("completion émetteur");
628 assert_eq!(sent.token(), tok);
629 sent.completed().expect("msg_ring envoyé");
630 assert!(
631 peer.join().expect("thread pair"),
632 "le pair a reçu le message"
633 );
634 }
635
636 // ── SqpollIoUring : soumission + réveil NEED_WAKEUP ───────────────────
637
638 #[test]
639 #[cfg_attr(miri, ignore = "io_uring SQPOLL non supporté par Miri")]
640 fn sqpoll_submits_and_wakes_after_idle() {
641 // idle court (10 ms) pour exercer le réveil NEED_WAKEUP après inactivité.
642 let mut ring = match SqpollIoUring::new(nz32(8), Duration::from_millis(10), None) {
643 Ok(r) => r,
644 // SQPOLL peut être refusé selon la config (privilège/cgroup) → on signale.
645 Err(e) => {
646 assert!(
647 matches!(e, Errno::EPERM),
648 "SQPOLL indisponible : {e:?} (attendu EPERM si refusé)"
649 );
650 return;
651 }
652 };
653 // 1ère soumission : thread SQPOLL éveillé (vient d'être créé) → pas de syscall.
654 let tok = ring.submit_nop().expect("nop");
655 ring.submit()
656 .expect("submit (SQPOLL, sans enter en régime établi)");
657 ring.submit_and_wait(1).expect("submit_and_wait");
658 let c = ring.wait_completion().expect("completion");
659 assert_eq!(c.token(), tok);
660 c.completed().expect("nop ok");
661 assert_eq!(ring.in_flight(), 0);
662 // Laisse le thread SQPOLL s'endormir (> idle), puis re-soumets : la façade
663 // doit détecter NEED_WAKEUP et réveiller le thread (chemin SQ_WAKEUP).
664 std::thread::sleep(Duration::from_millis(60));
665 let tok2 = ring.submit_nop().expect("nop 2");
666 // submit() après idle : NEED_WAKEUP détecté → réveil (chemin SQ_WAKEUP).
667 ring.submit().expect("submit (réveil NEED_WAKEUP)");
668 ring.submit_and_wait(1).expect("submit_and_wait après idle");
669 let c2 = ring.wait_completion().expect("completion 2");
670 assert_eq!(c2.token(), tok2);
671 c2.completed().expect("nop 2 ok (réveil NEED_WAKEUP)");
672 // Le thread SQPOLL est maintenant **éveillé** : une rafale de soumissions
673 // immédiates exerce le chemin **sans syscall** (`!NEED_WAKEUP`).
674 for _ in 0..16 {
675 let tok = ring.submit_nop().expect("nop rafale");
676 ring.submit().expect("submit éveillé (sans enter)");
677 ring.submit_and_wait(1).expect("drain");
678 assert_eq!(ring.wait_completion().expect("c").token(), tok);
679 }
680 }
681
682 #[test]
683 #[cfg_attr(miri, ignore = "io_uring SQPOLL non supporté par Miri")]
684 fn sqpoll_with_cpu_affinity_round_trips() {
685 // SQ_AFF : épingle le thread SQPOLL sur le CPU 0 (présent sur tout hôte).
686 let mut ring = match SqpollIoUring::new(nz32(8), Duration::from_millis(100), Some(0)) {
687 Ok(r) => r,
688 Err(e) => {
689 assert!(matches!(e, Errno::EPERM), "SQPOLL/SQ_AFF refusé : {e:?}");
690 return;
691 }
692 };
693 let tok = ring.submit_nop().expect("nop");
694 let n = ring.submit_and_wait(1).expect("submit_and_wait");
695 assert!(n <= 1);
696 assert_eq!(ring.wait_completion().expect("c").token(), tok);
697 assert_eq!(ring.in_flight(), 0);
698 }
699
700 // ── Thread-per-core : un ring par thread (modèle §2) ──────────────────
701
702 #[test]
703 #[cfg_attr(miri, ignore = "io_uring non supporté par Miri")]
704 fn thread_per_core_each_thread_owns_its_ring() {
705 // 3 threads, un IoUring déplacé (Send) dans chacun, charge indépendante,
706 // aucun partage ⇒ aucun verrou.
707 let threads: Vec<_> = (0..3)
708 .map(|_| {
709 std::thread::spawn(|| {
710 let mut ring = IoUring::new(nz32(8)).expect("ring");
711 let tok = ring.submit_nop().expect("nop");
712 ring.submit_and_wait(1).expect("submit");
713 let c = ring.wait_completion().expect("completion");
714 assert_eq!(c.token(), tok);
715 c.completed().is_ok()
716 })
717 })
718 .collect();
719 for t in threads {
720 assert!(t.join().expect("thread"), "ring indépendant OK");
721 }
722 }
723
724 // ── Couverture : mirrors LockedIoUring + read SQPOLL + Debug ──────────
725
726 #[test]
727 #[cfg_attr(miri, ignore = "io_uring non supporté par Miri")]
728 fn locked_io_uring_read_write_and_debug() {
729 let path = std::env::temp_dir().join(format!("air-iou3e-{}", std::process::id()));
730 {
731 std::fs::File::create(&path).expect("create");
732 }
733 let file = std::fs::OpenOptions::new()
734 .read(true)
735 .write(true)
736 .open(&path)
737 .expect("open rw");
738 // `from_builder` (couvre la construction via builder).
739 let ring = LockedIoUring::from_builder(IoUringBuilder::new(nz32(8)))
740 .expect("LockedIoUring from_builder");
741 // Debug ne prend pas le verrou et n'affiche pas l'état du ring.
742 assert_eq!(format!("{ring:?}"), "LockedIoUring { .. }");
743 // submit_write puis submit() explicite (mirror) puis wait.
744 ring.submit_write(sfd(&file), b"shared".to_vec(), Some(0))
745 .expect("submit_write");
746 assert!(ring.submit().expect("submit") >= 1);
747 let (_buf, n) = ring
748 .wait_completion()
749 .expect("completion")
750 .into_buffer_result()
751 .expect("write ok");
752 assert_eq!(n, 6);
753 // submit_read (mirror) + try_completion.
754 let tok = ring
755 .submit_read(sfd(&file), vec![0u8; 6], Some(0))
756 .expect("submit_read");
757 ring.submit_and_wait(1).expect("submit_and_wait");
758 // try_completion (mirror) récupère la complétion prête.
759 let c = ring.try_completion().expect("try_completion prête");
760 assert_eq!(c.token(), tok);
761 let (buf, n) = c.into_buffer_result().expect("read ok");
762 assert_eq!(n, 6);
763 assert_eq!(&buf, b"shared");
764 assert_eq!(ring.in_flight(), 0);
765 std::fs::remove_file(&path).ok();
766 }
767
768 #[test]
769 #[cfg_attr(miri, ignore = "io_uring non supporté par Miri")]
770 fn ring_pool_and_sqpoll_debug_and_extras() {
771 let pool = RingPool::new(nzusize(2), nz32(8)).expect("pool");
772 assert!(format!("{pool:?}").contains("RingPool"));
773 assert!(format!("{pool:?}").contains('2'), "workers affichés");
774 drop(pool.into_rings());
775 // SQPOLL : Debug + submit_read + submit_and_wait(0) (branche want == 0).
776 let mut sq = match SqpollIoUring::new(nz32(8), Duration::from_millis(100), None) {
777 Ok(r) => r,
778 Err(e) => {
779 assert!(matches!(e, Errno::EPERM), "SQPOLL refusé : {e:?}");
780 return;
781 }
782 };
783 assert_eq!(format!("{sq:?}"), "SqpollIoUring { .. }");
784 // submit_and_wait(0) : publie (sans rien) puis retourne sans attendre.
785 assert_eq!(sq.submit_and_wait(0).expect("submit_and_wait(0)"), 0);
786 // submit_read sur un fichier.
787 use std::io::Write as _;
788 let path = std::env::temp_dir().join(format!("air-iou3e-sq-{}", std::process::id()));
789 {
790 let mut f = std::fs::File::create(&path).expect("create");
791 f.write_all(b"sqpoll").expect("write");
792 }
793 let file = std::fs::File::open(&path).expect("open");
794 let tok = sq
795 .submit_read(sfd(&file), vec![0u8; 6], Some(0))
796 .expect("submit_read SQPOLL");
797 sq.submit_and_wait(1).expect("submit_and_wait");
798 let completion = sq.wait_completion().expect("completion");
799 assert_eq!(completion.token(), tok);
800 let (buf, n) = completion.into_buffer_result().expect("read ok");
801 assert_eq!(n, 6);
802 assert_eq!(&buf, b"sqpoll");
803 assert_eq!(sq.in_flight(), 0);
804 std::fs::remove_file(&path).ok();
805 }
806
807 // ── SQPOLL : bras d'erreur d'`io_uring_enter` (simulateur) ────────────
808 //
809 // Les deux `io_uring_enter` de `SqpollIoUring` — réveil `SQ_WAKEUP`
810 // (`submit`) et attente `GETEVENTS` (`submit_and_wait`) — passent par la
811 // **même couture instrumentable** `syscall::enter` que les autres syscalls
812 // io_uring (cf. `registration.rs`). Le simulateur y injecte un `-errno` :
813 // l'errno transite par le **même** `errno_from_negative_syscall_ret` que le
814 // vrai kernel et doit **remonter tel quel** (ADR-021 conv. 2 : `EINTR` non
815 // retenté). On couvre ainsi les bras d'erreur de façon **déterministe**.
816
817 #[test]
818 #[cfg_attr(miri, ignore = "io_uring SQPOLL non supporté par Miri")]
819 fn sqpoll_submit_propagates_enter_error_on_wakeup() {
820 use super::syscall::sim::{self, Syscall};
821 sim::clear();
822 let mut ring = match SqpollIoUring::new(nz32(8), Duration::from_millis(10), None) {
823 Ok(r) => r,
824 Err(e) => {
825 assert!(matches!(e, Errno::EPERM), "SQPOLL refusé : {e:?}");
826 return;
827 }
828 };
829 // Prépare un SQE **sans publier**, puis laisse le thread kernel SQPOLL
830 // s'endormir : `idle` vaut 10 ms et on attend bien plus — il pose alors
831 // `NEED_WAKEUP` sur la SQ. Délai **éprouvé**, identique à
832 // `sqpoll_submits_and_wakes_after_idle` (vert en CI sur les deux arches).
833 // `submit` empruntera donc le bras `SQ_WAKEUP` → `enter`. Pas de boucle
834 // conditionnelle (qui laisserait un corps non exécuté si `NEED_WAKEUP`
835 // était déjà posé) : si la précondition n'était pas tenue, le `submit`
836 // ci-dessous renverrait `Ok` et l'`expect_err` échouerait bruyamment.
837 let _token = ring.submit_nop().expect("nop");
838 std::thread::sleep(Duration::from_millis(60));
839 // Le simulateur fait échouer l'`enter(SQ_WAKEUP)` ⇒ l'errno doit remonter.
840 sim::inject(Syscall::Enter, -4); // -EINTR
841 let err = ring.submit().expect_err("enter SQ_WAKEUP en échec → Err");
842 assert!(
843 matches!(err, Errno::EINTR),
844 "EINTR remonté tel quel (conv. 2 ADR-021) : {err:?}"
845 );
846 sim::clear();
847 }
848
849 #[test]
850 #[cfg_attr(miri, ignore = "io_uring SQPOLL non supporté par Miri")]
851 fn sqpoll_submit_and_wait_propagates_enter_error_on_getevents() {
852 use super::syscall::sim::{self, Syscall};
853 sim::clear();
854 let mut ring = match SqpollIoUring::new(nz32(8), Duration::from_millis(100), None) {
855 Ok(r) => r,
856 Err(e) => {
857 assert!(matches!(e, Errno::EPERM), "SQPOLL refusé : {e:?}");
858 return;
859 }
860 };
861 // **Aucun** SQE en attente : `submit` (appelé en tête de
862 // `submit_and_wait`) court-circuite sur `to_submit == 0` (aucun
863 // `enter`). Le **seul** `enter` émis est donc le `GETEVENTS` d'attente
864 // → le simulateur le fait échouer et l'errno doit remonter.
865 // Entièrement déterministe (aucune dépendance au timing du thread).
866 sim::inject(Syscall::Enter, -4); // -EINTR
867 let err = ring
868 .submit_and_wait(1)
869 .expect_err("enter GETEVENTS en échec → Err");
870 assert!(
871 matches!(err, Errno::EINTR),
872 "EINTR remonté tel quel : {err:?}"
873 );
874 sim::clear();
875 }
876}