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 3b : buffers fournis (ring-mapped)

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

Position. Le Temps 3b spécifie les buffers fournis ring-mapped (IORING_REGISTER_PBUF_RING), qui remplacent l’ancien mécanisme op-based PROVIDE_BUFFERS/REMOVE_BUFFERS (évacué, doc maître §4). Sous-module air-sys-syscall::io_uring::provided. Réutilise le cœur du Temps 1 et la registration du Temps 3a.

Périmètre : register opcodes PBUF_RING (22), UNREGISTER_PBUF_RING (23), PBUF_STATUS (26) ; drapeau de soumission IOSQE_BUFFER_SELECT ; flags de complétion CQE_F_BUFFER et CQE_F_BUF_MORE.


1. Le problème résolu

Pour un recv classique, il faut pré-engager un buffer par opération — donc par connexion en attente. Un serveur à 10 000 connexions oisives gaspille 10 000 buffers immobilisés. Les buffers fournis inversent le modèle :

L’application enregistre un groupe de buffers ; elle soumet un recv/read avec IOSQE_BUFFER_SELECT sans buffer attaché ; le kernel choisit un buffer du groupe au moment où des données arrivent, et rend son identifiant dans le CQE (CQE_F_BUFFER, id dans les 16 bits hauts de cqe->flags).

Les buffers ne sont consommés que par les connexions effectivement actives. Combiné au recv multishot (Temps 3d), un seul SQE sert un flux entier, chaque datagramme atterrissant dans un buffer choisi par le kernel.


2. Le groupe de buffers — ProvidedBufferRing

#![allow(unused)]
fn main() {
pub struct ProvidedBufferRing { /* bgid, mémoire de l'anneau, buffers backing */ }

pub struct ProvidedBufferRingOptions {
    /// Mémoire de l'anneau allouée par le kernel (IOU_PBUF_RING_MMAP) puis
    /// mmappée par la façade, au lieu d'être fournie par l'application.
    pub kernel_mmap: bool,
    /// Consommation incrémentale (IOU_PBUF_RING_INC) : un buffer peut servir
    /// plusieurs complétions, consommé au fur et à mesure (cf. §5).
    pub incremental: bool,
}

impl ProvidedBufferRing {
    /// PBUF_RING (22). Enregistre un groupe `group_id` de `count` buffers de
    /// `buf_size` octets (count = puissance de 2). `count` borne aussi
    /// l'anneau de buffers.
    pub fn register(ring: &mut IoUring, group_id: u16, count: NonZeroU16,
                    buf_size: NonZeroU32, opts: ProvidedBufferRingOptions)
        -> Result<Self, Errno>;

    /// UNREGISTER_PBUF_RING (23). Rend la mémoire des buffers.
    pub fn unregister(self, ring: &mut IoUring) -> Result<(), Errno>;

    /// PBUF_STATUS (26). Tête courante du groupe (diagnostic / régulation).
    pub fn status(&self, ring: &IoUring) -> Result<ProvidedBufferRingStatus, Errno>;

    pub fn group_id(&self) -> u16;
    /// Nombre de buffers actuellement disponibles (non checkout).
    pub fn available(&self) -> u16;
}

pub struct ProvidedBufferRingStatus { pub head: u32 }
}
  • Deux modes mémoire : buffers fournis par l’application (par défaut, sur une MmapRegion possédée — data plane) ; ou anneau alloué par le kernel (kernel_mmap) puis mmappé à IORING_OFF_PBUF_RING | (group_id << IORING_OFF_PBUF_SHIFT).
  • Ownership : ProvidedBufferRing possède la mémoire des buffers tant que le groupe est enregistré ; unregister la restitue. Lien téardown S2 : désenregistrement avant destruction du ring.
  • Réapprovisionnement : l’application rend les buffers consommés au groupe (avance la queue de l’anneau de buffers) — cf. §4.

3. Soumission avec sélection automatique

#![allow(unused)]
fn main() {
impl IoUring {
    /// recv sur sélection auto de buffer dans `group` (IOSQE_BUFFER_SELECT).
    /// `bundle` ⇒ IORING_RECVSEND_BUNDLE (plusieurs buffers contigus en un recv).
    pub fn submit_receive_provided(&mut self, sock: BorrowedFd<'_>, group: &ProvidedBufferRing, flags: MessageFlags, bundle: bool)
        -> Result<SubmissionToken, Errno>;
    /// read sur sélection auto de buffer (`bundle` ⇒ IORING_RECVSEND_BUNDLE).
    pub fn submit_read_provided(&mut self, fd: BorrowedFd<'_>, group: &ProvidedBufferRing, length: u32, offset: Option<u64>, bundle: bool)
        -> Result<SubmissionToken, Errno>;
}
}
  • Aucun buffer n’est attaché à la soumission : seul le group_id est transmis, avec IOSQE_BUFFER_SELECT. Le slot S1 ne détient donc pas de buffer (le buffer appartient au groupe).
  • Variantes multishot (recv_multishot/read_multishot) : Temps 3d — elles s’appuient sur ce même groupe.
  • Bundle (IORING_RECVSEND_BUNDLE, FEAT_RECVSEND_BUNDLE) : un seul recv/send peut consommer plusieurs buffers contigus du groupe d’un coup (référencé en Temps 2b) — exposé via un paramètre bundle: bool ici.
  • Pénurie : si le groupe est vide quand des données arrivent, la complétion porte -ENOBUFS ; l’application doit réapprovisionner et resoumettre.

4. Consommation : le guard ProvidedBuffer

#![allow(unused)]
fn main() {
impl Completion {
    /// Récupère le buffer choisi par le kernel pour cette complétion.
    /// `None` si la complétion n'a pas utilisé de buffer fourni.
    pub fn into_provided_buffer<'r>(self, group: &'r mut ProvidedBufferRing)
        -> Option<ProvidedBuffer<'r>>;
}

/// Accès RAII aux données reçues dans un buffer fourni. Rend le buffer au
/// groupe (réapprovisionnement) à la libération du guard.
pub struct ProvidedBuffer<'r> { /* id, longueur utile, emprunt du groupe */ }

impl<'r> ProvidedBuffer<'r> {
    pub fn id(&self) -> u16;
    pub fn data(&self) -> &[u8];          // longueur = octets reçus (cqe->res)
    pub fn data_mut(&mut self) -> &mut [u8];
}
impl Drop for ProvidedBuffer<'_> { /* réapprovisionne le groupe */ }
}
  • Cycle checkout → traitement → return, rendu sûr par RAII : le kernel a « sorti » un buffer (id dans le CQE) ; into_provided_buffer matérialise un accès borné aux octets reçus ; la libération du guard rend le buffer au groupe (avance la queue de l’anneau). Oublier de rendre = buffer perdu pour le groupe → le guard le rend automatiquement.
  • L’emprunt &'r mut ProvidedBufferRing garantit qu’on ne désenregistre pas le groupe tant qu’un buffer est en cours de traitement.

5. Consommation incrémentale (incremental)

Avec ProvidedBufferRingOptions::incremental (IOU_PBUF_RING_INC) :

  • Un gros buffer peut être consommé partiellement sur plusieurs complétions. Le CQE porte alors CQE_F_BUF_MORE (Completion::flags()), signalant que le même buffer recevra d’autres complétions et n’est donc pas rendu automatiquement.
  • Le guard ProvidedBuffer reflète cela : si CQE_F_BUF_MORE est positionné, sa libération ne réapprovisionne pas (le kernel continue d’utiliser le buffer) ; le réapprovisionnement n’a lieu qu’à la complétion finale (sans BUF_MORE). L’application et la façade suivent ensemble l’index de consommation (contrat documenté).
  • Intérêt : enregistrer de grandes plages (un seul gros memfd) et ne consommer que ce qui est nécessaire par réception — économe en mémoire pour le data plane AirCom.

6. Récapitulatif

#ÉlémentFaçade
reg 22PBUF_RINGProvidedBufferRing::register
reg 23UNREGISTER_PBUF_RINGProvidedBufferRing::unregister
reg 26PBUF_STATUSProvidedBufferRing::status
flag SQEIOSQE_BUFFER_SELECTsubmit_receive_provided / submit_read_provided
flag CQECQE_F_BUFFERProvidedBuffer::id (via into_provided_buffer)
flag CQECQE_F_BUF_MOREconsommation incrémentale (§5)

Remplace IORING_OP_PROVIDE_BUFFERS (31) et REMOVE_BUFFERS (32), évacués vers UNSUPPORTED.md.


7. Types ajoutés / partagés

Nouveaux : ProvidedBufferRing, ProvidedBufferRingOptions, ProvidedBufferRingStatus, ProvidedBuffer<'r>. Nouvelle méthode de Completion : into_provided_buffer. Partagés : MmapRegion (family-mem), MessageFlags (Temps 2b).


8. Stratégie de tests

  • Intégration : enregistrer un groupe, recv_provided sur un socket loopback, vérifier l’id de buffer rendu et les octets reçus, libérer le guard, vérifier le réapprovisionnement (available() remonte) ; pénurie de buffers (-ENOBUFS) puis réapprovisionnement ; mode kernel_mmap ; bundle multi-buffers ; status().
  • Incrémental : gros buffer consommé sur plusieurs recv, séquence CQE_F_BUF_MORE puis complétion finale, réapprovisionnement uniquement à la fin.
  • Sûreté (compile-fail + Miri) : impossible de désenregistrer le groupe tant qu’un ProvidedBuffer est vivant (emprunt mut) ; pas de double rendu d’un buffer ; pas de lecture hors des res octets reçus.
  • Property-based : pour toute séquence checkout/return, available ≤ count, aucun id rendu deux fois, aucun buffer perdu.
  • Couverture 100 % lignes + branches.

9. Décisions de fond émergées au Temps 3b

  1. Seul le mécanisme ring-mapped — l’op-based classique est abandonné ; l’API est plus simple et sans syscall par lot de buffers.
  2. Guard RAII ProvidedBuffer — le réapprovisionnement est automatique à la libération ; on ne peut pas « oublier » de rendre un buffer.
  3. Emprunt &mut du groupe pendant le traitement — interdit le désenregistrement sous les pieds d’un buffer en cours de lecture.
  4. Incrémental traité explicitementCQE_F_BUF_MORE change la sémantique de rendu du guard ; documenté et testé, pas laissé implicite.
  5. Adossement MmapRegion — les buffers fournis du data plane vivent sur un memfd partagé possédé (réutilise le handle de vivacité de family-mem).

9 bis. Note d’implémentation — écarts kernel 6.12/6.17 (PR #42)

Implémentation : sous-module air-sys-syscall::io_uring::provided (PR #42, mergée). La surface validée n’a pas été modifiée (le bloc §3 a été aligné sur la prose : le paramètre bundle: bool, déjà décrit, est désormais visible dans les signatures). Écarts kernel observés (jamais corrigés en silence — ADR-032) :

  1. Constantes 6.11/6.12 absentes des headers exécuteurs. IOU_PBUF_RING_INC (2), IORING_RECVSEND_BUNDLE (1<<4 de ioprio) et IORING_CQE_F_BUF_MORE (1<<4 ; déjà porté par CompletionFlags) sont absents de <linux/io_uring.h> installé → valeurs reprises de l’uapi amont, validées au runtime (kernel 6.17). Consigné dans raw.rs (« Écart headers »).
  2. Modèle &mut group du guard. into_provided_buffer(self, &'r mut group) sérialise le traitement (un ProvidedBuffer vivant à la fois) ⇒ available() n’est pas observable pendant un checkout. La décrémentation est vérifiée via le chemin mem::forget. Conforme à la surface, pas une divergence.
  3. Accounting incrémental idempotent. En mode IOU_PBUF_RING_INC, un même buffer produit plusieurs complétions (BUF_MORE) ; available est décrémenté une seule fois au premier checkout et ré-incrémenté au rendu final (suivi par buffer, out_flags).

10. Travail à reprendre

Spec suivante : io-uring-3c-linked.md (chaînes d’opérations liées : soft/hard link, LinkedChainBuilder, intégration du link_timeout du Temps 2c). Traduction anglaise globale après validation des documents français.


Licence du document : MPL 2.0 Statut : Spécification technique du Temps 3b (buffers fournis ring-mapped) du module air-sys-syscall::io_uring, cible kernel 6.12 LTS.