Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Spec couche 0 — Module io_uring, Temps 3a : registration (ressources fixes)

Spécification technique — Version 1.0 (cible kernel : Linux 6.12 LTS)

Position. Le Temps 3a spécifie l’enregistrement de ressources auprès du kernel (io_uring_register(2), n° 427) : tables de descripteurs fixes, buffers enregistrés, ring fd enregistré, eventfd, personality, réglage du pool io-wq, NAPI, horloge. Sous-module air-sys-syscall::io_uring::registration. C’est ce Temps qui débloque les variantes « fixed »/« direct » référencées depuis les Temps 2a–2c. Réutilise le cœur du Temps 1.

Périmètre : 21 register opcodes (doc maître, axe C — Temps 3a). Les variantes legacy (REGISTER_BUFFERS/FILES/FILES_UPDATE) sont évacuées au profit des variantes taguées *2 (doc maître §4).


1. Conventions transverses du Temps 3a

  • Registration explicite (ADR-022 D4) : Air n’enregistre jamais automatiquement. L’application qui veut le bénéfice (moins de traduction d’adresses, FD directs) le demande.
  • Ownership des ressources enregistrées : une ressource enregistrée (buffer, FD) doit rester valide tant qu’elle est enregistrée. Les types RegisteredBuffers/FixedFdTable possèdent la ressource ; les références d’usage (RegisteredBufferSlice, FixedSlot) y sont liées par lifetime, donc inutilisables après désenregistrement (sûreté par construction).
  • Lien avec le téardown S2 : un ring ne peut pas être détruit avec des ressources encore enregistrées sans les libérer ; shutdown()/Drop désenregistrent proprement (les buffers possédés sont restitués).
  • Tags et sparse : les variantes *2 acceptent un enregistrement sparse (IORING_RSRC_REGISTER_SPARSE : slots vides à remplir plus tard) et des tags (FEAT_RSRC_TAGS : une notification de CQE signale quand une ressource remplacée n’est plus référencée).
  • Ring fd enregistré : si le ring a enregistré son propre fd (Temps 1, REG_REG_RING), tous les register/enter l’utilisent de façon transparente (USE_REGISTERED_RING).

2. Table de descripteurs fixes — FixedFdTable

#![allow(unused)]
fn main() {
pub struct FixedFdTable { /* possède les OwnedFd des slots remplis */ }

impl FixedFdTable {
    /// REGISTER_FILES2 (13). `capacity` slots, sparse par défaut (slots vides).
    pub fn register(ring: &mut IoUring, capacity: NonZeroU32) -> Result<Self, Errno>;
    /// REGISTER_FILES2 avec un jeu initial de FD.
    pub fn register_with(ring: &mut IoUring, fds: Vec<OwnedFd>) -> Result<Self, Errno>;
    /// REGISTER_FILES_UPDATE2 (14) : place/remplace un FD dans un slot.
    pub fn set(&mut self, ring: &mut IoUring, slot: u32, fd: OwnedFd) -> Result<(), Errno>;
    /// Vide un slot (FD rendu à l'appelant).
    pub fn clear(&mut self, ring: &mut IoUring, slot: u32) -> Result<Option<OwnedFd>, Errno>;
    /// REGISTER_FILE_ALLOC_RANGE (25) : borne la plage d'auto-allocation.
    pub fn set_alloc_range(&mut self, ring: &mut IoUring, range: Range<u32>) -> Result<(), Errno>;
    /// UNREGISTER_FILES (3) : désenregistre tout, rend les FD restants.
    pub fn unregister(self, ring: &mut IoUring) -> Result<Vec<OwnedFd>, Errno>;
    /// Référence un slot pour usage en opération (IOSQE_FIXED_FILE).
    pub fn slot(&self, slot: u32) -> Option<FixedSlot<'_>>;
}

/// Référence empruntée à un slot rempli ; liée à la table par lifetime.
pub struct FixedSlot<'t> { /* index + emprunt */ }
}

2.1 Ce que FixedFdTable débloque

  • Opérations sur FD fixe : read/write/send/recv… acceptent un FixedSlot au lieu d’un BorrowedFd (drapeau IOSQE_FIXED_FILE) → le kernel évite la résolution du FD à chaque op.
  • Variantes « direct descriptor » des Temps 2a/2b — le résultat est rangé dans un slot plutôt que rendu en FD ordinaire :
#![allow(unused)]
fn main() {
impl IoUring {
    pub fn submit_openat2_direct(&mut self, dirfd: DirFd, path: CString, how: OpenHow,
        slot: FixedSlotTarget) -> Result<SubmissionToken, Errno>;
    pub fn submit_accept_direct(&mut self, listener: BorrowedFd<'_>, flags: AcceptFlags,
        slot: FixedSlotTarget) -> Result<SubmissionToken, Errno>;
    pub fn submit_socket_direct(&mut self, domain: SocketDomain, ty: SocketType, protocol: i32,
        slot: FixedSlotTarget) -> Result<SubmissionToken, Errno>;
}
/// Slot cible : indice précis, ou auto-allocation (IORING_FILE_INDEX_ALLOC).
pub enum FixedSlotTarget { Index(u32), Alloc }
}
  • FixedSlotTarget::Alloc ⇒ le kernel choisit un slot libre (dans la plage d’set_alloc_range) et le rend dans cqe->res ; sinon -ENFILE.
  • Pour revenir à un FD ordinaire : submit_fixed_fd_install (Temps 2c).
  • Mise à jour asynchrone dans le flux : submit_files_update (Temps 2c, op 20).

Intérêt Air : les services à fort taux de connexions (compositeur, AirCom) gardent leurs sockets en descripteurs directs — gain mesurable, et confinement (un FD direct n’est pas visible comme FD ordinaire du process).


3. Buffers enregistrés — RegisteredBuffers

#![allow(unused)]
fn main() {
pub struct RegisteredBuffers { /* possède les buffers épinglés */ }

impl RegisteredBuffers {
    /// REGISTER_BUFFERS2 (15). Épingle les buffers ; sparse/tags supportés.
    pub fn register(ring: &mut IoUring, buffers: Vec<Vec<u8>>) -> Result<Self, Errno>;
    /// Variante adossée à des MmapRegion possédées (data plane).
    pub fn register_mmap(ring: &mut IoUring, regions: Vec<MmapRegion>) -> Result<Self, Errno>;
    /// REGISTER_BUFFERS_UPDATE (16) : remplace un buffer (tag → notif quand
    /// l'ancien n'est plus référencé).
    pub fn update(&mut self, ring: &mut IoUring, index: u32, buffer: Vec<u8>) -> Result<(), Errno>;
    /// CLONE_BUFFERS (30) : clone les buffers enregistrés d'un autre ring.
    pub fn clone_from(ring: &mut IoUring, src: &IoUring) -> Result<Self, Errno>;
    /// UNREGISTER_BUFFERS (1) : rend les buffers.
    pub fn unregister(self, ring: &mut IoUring) -> Result<Vec<Vec<u8>>, Errno>;
    /// Tranche d'un buffer enregistré, pour read_fixed/write_fixed (Temps 2a).
    pub fn slice(&self, index: u32, range: Range<usize>) -> Option<RegisteredBufferSlice<'_>>;
}

/// Référence à une tranche d'un buffer enregistré ; liée par lifetime.
pub struct RegisteredBufferSlice<'b> { /* index + range + emprunt */ }
}
  • Gain : l’épinglage et la traduction d’adresses sont faits une fois à l’enregistrement → read_fixed/write_fixed (Temps 2a) et send_zc/recv fixed (Temps 2b) évitent ce coût par op. Cas chaud du data plane AirCom (register_mmap sur un memfd partagé).
  • clone_from : partage de buffers entre rings d’un même processus (thread-per-core) sans réépinglage.
  • Ownership : RegisteredBufferSlice ne peut survivre à unregister (lifetime) — read_fixed sur une tranche périmée ne compile pas.

4. Ring fd enregistré

#![allow(unused)]
fn main() {
impl IoUring {
    pub fn register_ring_fd(&mut self) -> Result<(), Errno>;     // RING_FDS (20)
    pub fn unregister_ring_fd(&mut self) -> Result<(), Errno>;   // UNREGISTER_RING_FDS (21)
}
}
  • Enregistre le fd du ring dans une table interne → les io_uring_enter suivants utilisent IORING_ENTER_REGISTERED_RING (pas de résolution de FD). Gain net sur les chemins à enter fréquents. Requiert/active FEAT_REG_REG_RING.
  • Utilisé aussi pour désigner un ring cible dans msg_ring (Temps 2c) de façon découplée.

5. Notification par eventfd

#![allow(unused)]
fn main() {
impl IoUring {
    pub fn register_eventfd(&mut self, efd: BorrowedFd<'_>) -> Result<(), Errno>;        // EVENTFD (4)
    pub fn register_eventfd_async(&mut self, efd: BorrowedFd<'_>) -> Result<(), Errno>;  // EVENTFD_ASYNC (7)
    pub fn unregister_eventfd(&mut self) -> Result<(), Errno>;                           // UNREGISTER_EVENTFD (5)
}
}
  • Lie le ring à un eventfd (famille ipc) : le kernel y écrit à chaque complétion postée → un reactor peut attendre les complétions via l’eventfd (intégration epoll, ou réveil cross-thread).
  • register_eventfd_async ne notifie que pour les complétions traitées en asynchrone (filtre le bruit des complétions inline). Désactivation ponctuelle via le drapeau IORING_CQ_EVENTFD_DISABLED (anneau CQ, Temps 1).

6. Personality (identités)

#![allow(unused)]
fn main() {
pub struct Personality(/* id */);

impl IoUring {
    /// REGISTER_PERSONALITY (9) : enregistre les credentials courants.
    pub fn register_personality(&mut self) -> Result<Personality, Errno>;
    /// UNREGISTER_PERSONALITY (10).
    pub fn unregister_personality(&mut self, p: Personality) -> Result<(), Errno>;
}
}
  • Enregistre les credentials du process appelant et rend un id ; une op peut ensuite s’exécuter avec ces credentials en plaçant l’id dans sqe->personality. Permet à un service privilégié d’effectuer une op pour le compte d’une identité moins privilégiée (et inversement) de façon contrôlée.
  • Lien sécurité : combiné aux restrictions (Temps 3f) et au modèle d’entitlements (ADR-010), c’est une brique de moindre privilège — exposé ici comme primitive, l’usage politique relève des couches supérieures.

7. Réglage du pool io-wq

#![allow(unused)]
fn main() {
impl IoUring {
    /// IOWQ_AFF (17) : affinité CPU des workers io-wq.
    pub fn set_work_queue_affinity(&mut self, cpus: &CpuSet) -> Result<(), Errno>;
    /// UNREGISTER_IOWQ_AFF (18).
    pub fn clear_work_queue_affinity(&mut self) -> Result<(), Errno>;
    /// IOWQ_MAX_WORKERS (19) : plafonds de workers bornés/non-bornés.
    pub fn set_work_queue_max_workers(&mut self, bounded: u32, unbounded: u32)
        -> Result<WorkQueueWorkerLimits, Errno>;
}
}
  • Le pool io-wq exécute les opérations qui ne peuvent pas être faites inline (ex. I/O bloquante). set_work_queue_max_workers borne le nombre de threads (catégories IO_WQ_BOUND/IO_WQ_UNBOUND) — important sur matériel modeste (Charte principe 4 : maîtriser l’empreinte sur Raspberry Pi 4).
  • CpuSet partagé avec family-system (affinités).

8. NAPI busy-poll

#![allow(unused)]
fn main() {
impl IoUring {
    pub fn register_napi(&mut self, cfg: NapiConfig) -> Result<NapiConfig, Errno>; // NAPI (27)
    pub fn unregister_napi(&mut self) -> Result<(), Errno>;                        // UNREGISTER_NAPI (28)
}
pub struct NapiConfig { /* busy_poll_to, prefer_busy_poll */ }
}
  • Active le busy-polling réseau (NAPI) pour réduire la latence sur les sockets à très haut débit, au prix de CPU. À réserver aux profils où la latence prime (mesure avant activation, Principe 5).

9. Source d’horloge

#![allow(unused)]
fn main() {
impl IoUring {
    pub fn register_clock(&mut self, clock: ClockSource) -> Result<(), Errno>; // CLOCK (29)
}
pub enum ClockSource { Monotonic, Boottime, Realtime }
}
  • Fixe l’horloge utilisée par les timeouts du ring (cohérent avec les TimeoutFlags du Temps 2c et family-time).

10. Récapitulatif des register opcodes (Temps 3a)

#Register opFaçade
1UNREGISTER_BUFFERSRegisteredBuffers::unregister
3UNREGISTER_FILESFixedFdTable::unregister
4REGISTER_EVENTFDregister_eventfd
5UNREGISTER_EVENTFDunregister_eventfd
7REGISTER_EVENTFD_ASYNCregister_eventfd_async
9REGISTER_PERSONALITYregister_personality
10UNREGISTER_PERSONALITYunregister_personality
13REGISTER_FILES2FixedFdTable::register(_with)
14REGISTER_FILES_UPDATE2FixedFdTable::set / clear
15REGISTER_BUFFERS2RegisteredBuffers::register(_mmap)
16REGISTER_BUFFERS_UPDATERegisteredBuffers::update
17REGISTER_IOWQ_AFFset_work_queue_affinity
18UNREGISTER_IOWQ_AFFclear_work_queue_affinity
19REGISTER_IOWQ_MAX_WORKERSset_work_queue_max_workers
20REGISTER_RING_FDSregister_ring_fd
21UNREGISTER_RING_FDSunregister_ring_fd
25REGISTER_FILE_ALLOC_RANGEFixedFdTable::set_alloc_range
27REGISTER_NAPIregister_napi
28UNREGISTER_NAPIunregister_napi
29REGISTER_CLOCKregister_clock
30REGISTER_CLONE_BUFFERSRegisteredBuffers::clone_from

Soit 21 register opcodes. (PROBE → Temps 1 ; SYNC_CANCEL → Temps 1/2c ; RESTRICTIONS/ENABLE_RINGS → Temps 3f ; PBUF_RING/PBUF_STATUS → Temps 3b.)


11. Types ajoutés / partagés

Nouveaux : FixedFdTable, FixedSlot<'t>, FixedSlotTarget, RegisteredBuffers, RegisteredBufferSlice<'b>, Personality, WorkQueueWorkerLimits, NapiConfig, ClockSource. Partagés : OpenHow, AcceptFlags, SocketDomain/Type (Temps 2a/2b), CpuSet (family-system), MmapRegion (family-mem).


12. Stratégie de tests

  • Intégration : enregistrer une FixedFdTable, faire un accept_direct/openat2_direct (slot Alloc puis Index), read/write via FixedSlot, fixed_fd_install retour en FD ordinaire ; RegisteredBuffers + read_fixed/write_fixed round-trip ; clone_from entre deux rings ; register_eventfd puis vérifier la notification ; register_personality + op avec personality ; set_work_queue_max_workers (lecture des valeurs rendues) ; register_napi ; register_clock.
  • Sûreté (compile-fail + Miri) : un RegisteredBufferSlice/FixedSlot ne doit pas survivre au désenregistrement (test trybuild compile-fail) ; pas de fuite de FD/buffer à l’unregister ni au téardown S2.
  • Property-based : enregistrement sparse + remplissage progressif, cohérence des tags (notif quand l’ancien buffer n’est plus référencé).
  • Erreurs via simulateur : ENFILE (slot Alloc plein), EINVAL, EBUSY, EFAULT.
  • Couverture 100 % lignes + branches.

13. Décisions de fond émergées au Temps 3a

  1. Variantes legacy abandonnées — seules les *2 (taguées) sont exposées (doc maître §4) ; cela simplifie l’API et impose les bénéfices du tagging.
  2. Ressources possédées + références liées par lifetime — sûreté par construction : impossible d’utiliser une ressource désenregistrée.
  3. FixedSlotTarget::{Index, Alloc} — l’auto-allocation est un enum typé, pas la sentinelle IORING_FILE_INDEX_ALLOC (~0U) exposée.
  4. register_mmap pour le data plane — buffers enregistrés adossés à des MmapRegion possédées (memfd partagé AirCom), réutilisant le handle de vivacité de family-mem.
  5. Plafonds io-wq exposés tôt — maîtrise de l’empreinte CPU sur matériel modeste, conformément à la Charte.

13 bis. Notes d’implémentation — écarts kernel 6.12/6.17 (PR #40)

Implémentation : sous-module air-sys-syscall::io_uring::registration (PR #40, mergée). La surface validée ci-dessus n’a pas été modifiée ; les écarts kernel observés à l’implémentation sont consignés ici (jamais corrigés en silence — ADR-032) :

  1. Opcodes 27-30 absents des headers exécuteurs. <linux/io_uring.h> (linux-libc-dev des exécuteurs) borne l’énumération à l’opcode 26 (IORING_REGISTER_LAST == 27) : REGISTER_NAPI/UNREGISTER_NAPI (27/28), REGISTER_CLOCK (29), REGISTER_CLONE_BUFFERS (30) — ainsi que les structures io_uring_napi/io_uring_clock_register/io_uring_clone_buffers — sont repris de l’uapi 6.12 amont (cible de la spec) et validés au runtime sur le kernel 6.17 des exécuteurs.
  2. register_clock(ClockSource::Realtime)EINVAL. io_uring (6.12/6.17) n’accepte que CLOCK_MONOTONIC/CLOCK_BOOTTIME pour l’horloge des timeouts ; REALTIME est rejeté par le kernel. Décision BDFL : la variante Realtime est conservée (surface inchangée) ; son rejet est testé et documenté.
  3. CLOEXEC refusé sur descripteur direct. Le kernel rend EINVAL si file_slot && (flags & CLOEXEC) (socket/accept/openat2 directs : descripteurs kernel-managed, non hérités à l’exec par nature). Les façades directes ne posent donc pas SOCK_CLOEXEC/O_CLOEXEC ; le O_CLOEXEC est appliqué à la matérialisation (fixed_fd_install, install_fd_flags = 0).
  4. register_napiEINVAL hors config NIC/NAPI. Le busy-poll NAPI exige CONFIG_NET_RX_BUSY_POLL et un NIC NAPI ; absent des exécuteurs, l’appel rend EINVAL (chemin d’erreur couvert). Le chemin succès est en exception de couverture FEATURE-KERNEL (cf. docs/COVERAGE-EXCEPTIONS.md).

Différés (dette, décision BDFL) : submit_files_update (op 20, mise à jour asynchrone de la table de FD fixes dans le flux) et msg_ring_fd (MSG_SEND_FD) — plomberie d’op async tangentielle à la registration. La mise à jour synchrone (FixedFdTable::set/clear, register opcode 14) couvre le besoin immédiat ; à reprendre avec le runtime async (ou un addendum 3a-bis).


14. Travail à reprendre

Spec suivante : io-uring-3b-provided.md (buffers fournis ring-mapped : PBUF_RING/PBUF_STATUS, consommation incrémentale, sélection automatique de buffer pour recv/read multishot). Traduction anglaise globale après validation des documents français.


Licence du document : MPL 2.0 Statut : Spécification technique du Temps 3a (registration) du module air-sys-syscall::io_uring, cible kernel 6.12 LTS.