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 2b : opérations réseau

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

Position. Le Temps 2b spécifie les 12 opérations réseau d’io_uring en 6.12 (doc maître io-uring-0-inventaire.md, axe B). Elles réutilisent le cœur du Temps 1 et les conventions du Temps 2a (renvoi aux familles, modèle de buffers S1, offsets/sentinelles typés). Ce Temps est stratégique pour AirCom (ADR-001) : c’est ici que vivent le zero-copy (send_zc, sendmsg_zc) du data plane et le passage de FD (capabilities) via les messages de contrôle de sendmsg/recvmsg.


1. Conventions transverses du Temps 2b

  • Types scalaires partagés avec family-net (ADR-022 D2) : SocketAddr, SocketDomain, SocketType, MessageFlags, ShutdownMode, AcceptFlags. Renvoi à family-net.md pour leur définition et les numéros de syscall équivalents.
  • Messages : jumeaux possédés propres au Temps 2b (correction de spec, 2026-06-11). La rédaction initiale annonçait réutiliser SendMessageRequest/ReceiveMessageRequest/ControlMessages de family-net. C’est impossible : les types message de family-net sont à base d’emprunts (&[IoSlice], &[BorrowedFd]), adaptés au chemin synchrone (les données restent sur la pile pendant le syscall). io_uring est asynchrone : le kernel lit les données après le retour de la façade, ce qui exige un transfert d’ownership (S1). Le Temps 2b introduit donc des types possédés dédiésOwnedSendMessage / OwnedReceiveMessage — au lieu de redéfinir ou détourner les types empruntés. ControlMessages n’existe pas dans family-net (le passage de FD y est un champ fds direct) : on suit la même modélisation (OwnedSendMessage::fds: Vec<OwnedFd>). Les helpers de sérialisation sockaddr/cmsg de family-net sont réutilisés (rendus pub(crate)), sans duplication.
  • MSG_NOSIGNAL par défaut sur tous les envois : pas de SIGPIPE sur socket fermé (l’erreur remonte en EPIPE dans la complétion). C’est un invariant Air, surchargeable explicitement.
  • SOCK_CLOEXEC par défaut sur les FD créés (accept, socket) : cohérent avec la famille net et les autres familles couche 0.
  • Adresses possédées : une SocketAddr à transmettre est sérialisée en un stockage sockaddr possédé, déplacé dans le slot S1 (le kernel la lit de façon asynchrone — elle doit survivre jusqu’à la complétion).
  • Variantes « direct descriptor » (résultat rangé dans une table de FD enregistrée plutôt qu’un FD ordinaire) et multishot : renvoyées respectivement aux Temps 3a et Temps 3d. bind/listen existent précisément pour opérer sur ces FD directs sans FD ordinaire.

2. Établissement de connexion

2.1 submit_accept

#![allow(unused)]
fn main() {
impl IoUring {
    pub fn submit_accept(&mut self, listener: BorrowedFd<'_>, flags: AcceptFlags)
        -> Result<SubmissionToken, Errno>;
    pub fn submit_accept_with_peer(&mut self, listener: BorrowedFd<'_>, flags: AcceptFlags)
        -> Result<SubmissionToken, Errno>;
}
}
  • Opcode : IORING_OP_ACCEPT (13). Équivalent : accept4.
  • Complétion : completion.accepted_fd() -> Result<OwnedFd, Errno> (CLOEXEC par défaut). La variante _with_peer capture l’adresse pair dans un stockage possédé du slot ⇒ completion.into_accept_result() -> (OwnedFd, SocketAddr).
  • Multishot : submit_accept_multishot au Temps 3d (un seul SQE accepte en continu). Direct fd : variante _direct au Temps 3a.
  • Erreurs : EAGAIN (rien à accepter, FD non bloquant), EMFILE/ENFILE, ECONNABORTED, EINVAL.

2.2 submit_connect

#![allow(unused)]
fn main() {
pub fn submit_connect(&mut self, sock: BorrowedFd<'_>, address: SocketAddr)
    -> Result<SubmissionToken, Errno>;
}
  • Opcode : IORING_OP_CONNECT (16). Équivalent : connect.
  • Complétion : completed(). Erreurs : ECONNREFUSED, ETIMEDOUT, EINPROGRESS (ne devrait pas remonter en io_uring : l’op complète à la fin), EALREADY, EISCONN.

2.3 submit_socket

#![allow(unused)]
fn main() {
pub fn submit_socket(&mut self, domain: SocketDomain, ty: SocketType, protocol: i32)
    -> Result<SubmissionToken, Errno>;
}
  • Opcode : IORING_OP_SOCKET (45). Équivalent : socket.
  • Complétion : completion.into_socket_fd() -> Result<OwnedFd, Errno> (CLOEXEC par défaut). Direct fd : variante _direct au Temps 3a.
  • Intérêt : créer un socket dans le ring, chaînable (Temps 3c) avec connect/bind sans aller-retour synchrone.

2.4 submit_bind / submit_listen

#![allow(unused)]
fn main() {
pub fn submit_bind(&mut self, sock: BorrowedFd<'_>, address: SocketAddr)
    -> Result<SubmissionToken, Errno>;                       // OP 56
pub fn submit_listen(&mut self, sock: BorrowedFd<'_>, backlog: u32)
    -> Result<SubmissionToken, Errno>;                       // OP 57
}
  • Opcodes : IORING_OP_BIND (56) / IORING_OP_LISTEN (57) — ajoutés en 6.11. Équivalents : bind / listen.
  • Raison d’être : permettre bind/listen sur un socket créé en descripteur direct (submit_socket_direct), qui n’a pas de FD ordinaire utilisable par les bind(2)/listen(2) classiques.
  • Complétion : completed(). Erreurs : EADDRINUSE, EACCES, EINVAL.

3. Transfert de données (one-shot)

3.1 submit_send / submit_receive

#![allow(unused)]
fn main() {
pub fn submit_send(&mut self, sock: BorrowedFd<'_>, buffer: Vec<u8>, flags: MessageFlags)
    -> Result<SubmissionToken, Errno>;                       // OP 26
pub fn submit_receive(&mut self, sock: BorrowedFd<'_>, buffer: Vec<u8>, flags: MessageFlags)
    -> Result<SubmissionToken, Errno>;                       // OP 27
}
  • Équivalents : send / recv. MSG_NOSIGNAL ajouté par défaut au send.
  • Buffer : transfert d’ownership (S1). Complétion : into_buffer_result() -> (Vec<u8>, usize).
  • recv : completion.socket_has_pending_data() (CQE_F_SOCK_NONEMPTY) indique qu’il reste des données à lire — utile pour décider d’un nouveau recv.
  • Multishot recv et buffers fournis (BUFFER_SELECT) : Temps 3d/3b.
  • Erreurs : ECONNRESET, EPIPE (send sur socket fermé, grâce à NOSIGNAL), EAGAIN, EMSGSIZE.

3.2 submit_send_message / submit_receive_message

#![allow(unused)]
fn main() {
pub fn submit_send_message(&mut self, sock: BorrowedFd<'_>, request: OwnedSendMessage)
    -> Result<SubmissionToken, Errno>;                       // OP 9
pub fn submit_receive_message(&mut self, sock: BorrowedFd<'_>, request: OwnedReceiveMessage)
    -> Result<SubmissionToken, Errno>;                       // OP 10
}
  • Équivalents : sendmsg / recvmsg.
  • OwnedSendMessage (possédé, déplacé dans le slot — jumeau possédé, cf. §1) agrège : buffers de données (Vec<Vec<u8>>), adresse de destination optionnelle, et les FD à passer (fds: Vec<OwnedFd>, sérialisés en SCM_RIGHTS). C’est le mécanisme par lequel AirCom transporte une capability (un FD non-falsifiable) entre deux pairs (ADR-001). La façade construit le struct msghdr + cmsg en interne, garé dans le slot.
  • OwnedReceiveMessage (possédé) : buffers de réception + buffer de contrôle pré-dimensionné pour les cmsg entrants (FD reçus). Complétion : completion.into_receive_message_result() -> Result<(OwnedReceiveMessage, ReceiveMessageMeta), Errno>ReceiveMessageMeta (adossé à io_uring_recvmsg_out) donne namelen, controllen, payloadlen, flags (dont MSG_TRUNC/MSG_CTRUNC à vérifier), les FD reçus (OwnedFd CLOEXEC) et l’adresse pair. Aucune donnée n’est perdue : les buffers de réception sont restitués dans OwnedReceiveMessage, les FD et la méta dans ReceiveMessageMeta.
  • Sûreté FD passing : les FD reçus sont matérialisés en OwnedFd ; un cmsg tronqué (MSG_CTRUNC) est signalé et les FD partiels fermés proprement (pas de fuite).
  • Erreurs : comme send/recv + EINVAL (cmsg malformés en émission).

4. Zero-copy (data plane AirCom)

4.1 Cycle de vie à deux complétions — le point clé

send_zc/sendmsg_zc évitent la copie kernel du payload : le kernel référence directement les pages userspace. Conséquence majeure sur l’ownership :

Une soumission zero-copy produit deux complétions :

  1. Complétion de résultat — porte le nombre d’octets envoyés (res ≥ 0) et le flag CQE_F_MORE (une notification suivra).
  2. Complétion de notification — porte CQE_F_NOTIF ; elle signale que le kernel ne référence plus le buffer, qui peut être réutilisé/libéré.

Le ou les buffers doivent rester vivants jusqu’à la complétion NOTIF, pas seulement jusqu’au résultat. La façade gère cela via le slot S1 :

  • le slot retient tous les buffers jusqu’à réception du CQE_F_NOTIF ;
  • la complétion de résultat (F_MORE) expose les octets via into_result() sans restituer les buffers ;
  • la complétion NOTIF (is_notif()) restitue tous les buffers (via into_zero_copy_buffer() pour send_zc, into_zero_copy_buffers() pour sendmsg_zc) et libère le slot.

Principe « zéro perte de donnée » (à respecter partout). Une API Air ne discarde jamais une donnée que l’appelant lui a confiée : on restitue tout, proprement et sûrement. En zero-copy, into_zero_copy_buffers() rend l’intégralité des buffers d’un sendmsg_zc (un Vec<Vec<u8>>), jamais seulement le premier. (Ce principe transverse mérite d’être élevé en convention d’ingénierie — cf. §9.)

C’est l’unique cas du module où un slot survit à sa première complétion ; il est spécifié explicitement ici et testé (séquencement résultat→NOTIF, et échec précoce, cf. §4.2).

4.2 submit_send_zero_copy / submit_send_message_zero_copy

#![allow(unused)]
fn main() {
pub fn submit_send_zero_copy(&mut self, sock: BorrowedFd<'_>, buffer: Vec<u8>, flags: MessageFlags, zero_copy: ZeroCopyFlags)
    -> Result<SubmissionToken, Errno>;                       // OP 47
pub fn submit_send_message_zero_copy(&mut self, sock: BorrowedFd<'_>, request: OwnedSendMessage, zero_copy: ZeroCopyFlags)
    -> Result<SubmissionToken, Errno>;                       // OP 48
}
  • Équivalents : variantes zero-copy de send/sendmsg.
  • Restitution (zéro perte, ADR-032) : au NOTIF, send_zc rend son Vec<u8> via into_zero_copy_buffer() ; sendmsg_zc rend l’intégralité de ses buffers (Vec<Vec<u8>>) via into_zero_copy_buffers().
  • ZeroCopyFlags : expose REPORT_USAGE (IORING_SEND_ZC_REPORT_USAGE → la complétion NOTIF indique si le zero-copy a effectivement eu lieu ou si le kernel a dû copier ; lisible via Completion::zero_copy_copied()). Bundle (RECVSEND_BUNDLE, FEAT_RECVSEND_BUNDLE) avec buffers fournis : Temps 3b.
  • Quand l’utiliser : payloads volumineux (≳ 10 Ko, à mesurer) ; pour les petits messages, le send ordinaire est plus simple et souvent plus rapide (le zero-copy a un coût fixe de gestion de notification). Recommandation alignée sur le Principe 5 (mesurer).
  • Complétion et échec précoce (comportement réel kernel 6.12, corrigé). Voir §4.1. La rédaction initiale décrivait l’échec précoce comme « NOTIF sans F_MORE préalable ». En pratique sur 6.12, un échec se présente le plus souvent comme une complétion de résultat res < 0 portant F_MORE, suivie du NOTIF (le buffer reste tenu jusqu’au NOTIF, puis restitué) ; le cas du CQE unique (vrai échec immédiat sans NOTIF distinct) existe aussi et est géré. Dans tous les cas, les buffers sont restitués (jamais perdus) et l’erreur propagée.

5. Fermeture

5.1 submit_shutdown

#![allow(unused)]
fn main() {
pub fn submit_shutdown(&mut self, sock: BorrowedFd<'_>, how: ShutdownMode)
    -> Result<SubmissionToken, Errno>;                       // OP 34
}
  • Équivalent : shutdown. ShutdownMode : Read / Write / Both.
  • Complétion : completed(). Erreurs : ENOTCONN, EINVAL.
  • Note : fermer le FD lui-même se fait via submit_close (Temps 2a) ou la fermeture RAII de l’OwnedFd.

6. Récapitulatif des 12 opérations

# opcodeOpcodeFaçadeComplétion
9SENDMSGsubmit_send_messageinto_result
10RECVMSGsubmit_receive_messageinto_receive_message_result
13ACCEPTsubmit_accept / _with_peeraccepted_fd / into_accept_result
16CONNECTsubmit_connectcompleted
26SENDsubmit_sendinto_buffer_result
27RECVsubmit_receiveinto_buffer_result
34SHUTDOWNsubmit_shutdowncompleted
45SOCKETsubmit_socketinto_socket_fd
47SEND_ZCsubmit_send_zero_copyinto_result + NOTIF into_zero_copy_buffer (Vec<u8>)
48SENDMSG_ZCsubmit_send_message_zero_copyinto_result + NOTIF into_zero_copy_buffers (Vec<Vec<u8>>, tous)
56BINDsubmit_bindcompleted
57LISTENsubmit_listencompleted

7. Types ajoutés / partagés

Scalaires partagés avec family-net : SocketAddr, SocketDomain, SocketType, MessageFlags, ShutdownMode, AcceptFlags. Jumeaux possédés propres au Temps 2b (cf. §1) : OwnedSendMessage (dont fds: Vec<OwnedFd>), OwnedReceiveMessage — les types message de family-net, à base d’emprunts, sont inadaptés à l’ownership asynchrone S1. Helpers de sérialisation sockaddr/cmsg réutilisés de family-net (pub(crate)). Autres nouveaux au Temps 2b : ZeroCopyFlags, ReceiveMessageMeta (adossé à io_uring_recvmsg_out : méta + FD reçus + adresse pair). Nouvelles méthodes de Completion : into_socket_fd, into_accept_result, into_receive_message_result, into_zero_copy_buffer (send_zc, Vec<u8>), into_zero_copy_buffers (sendmsg_zc, Vec<Vec<u8>>, restitue tous les buffers), zero_copy_copied.


8. Stratégie de tests

  • Intégration (sockets locaux AF_UNIX + AF_INET loopback) : accept/connect, send/recv round-trip, sendmsg/recvmsg avec passage de FD (envoyer un FD, le recevoir, vérifier qu’il pointe le même objet — test cœur AirCom), shutdown, socket+bind+listen en chaîne (Temps 3c), accept d’un fd direct.
  • Zero-copy : vérifier la séquence résultat (F_MORE) → NOTIF (F_NOTIF), la non-restitution des buffers avant NOTIF, la restitution de TOUS les buffers au NOTIF (into_zero_copy_buffers, y compris un sendmsg_zc multi-buffers), le rapport zero_copy_copied, et l’échec précoce sous ses deux formes (res<0+F_MORE+NOTIF, et CQE unique) — buffers toujours restitués.
  • Sûreté FD passing : MSG_CTRUNC (buffer de contrôle trop petit) ⇒ pas de fuite de FD ; FD reçus bien en OwnedFd CLOEXEC.
  • Property-based : octets envoyés/reçus ≤ taille buffer ; robustesse du parsing recvmsg_out (données kernel = externes, Principe 3).
  • Erreurs via simulateur : ECONNRESET, EPIPE, EAGAIN, EADDRINUSE.
  • Soundness : Miri sur le cycle à deux complétions du zero-copy (le buffer ne doit être ni libéré ni lu entre soumission et NOTIF).
  • Couverture 100 % lignes + branches.

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

  1. MSG_NOSIGNAL et SOCK_CLOEXEC par défaut — invariants Air, cohérents couche 0 ; surcharge explicite possible.
  2. Cycle zero-copy à deux complétions explicite (S1 étendu) — un slot peut survivre à sa première complétion jusqu’au CQE_F_NOTIF. Seul cas du module ; documenté et testé.
  3. FD passing de première classe via sendmsg/recvmsg — c’est la matérialisation kernel des capabilities AirCom (ADR-001). Les FD reçus sont des OwnedFd, jamais des entiers bruts.
  4. bind/listen exposés bien qu’« évidents » — indispensables pour les sockets en descripteur direct (sans FD ordinaire).
  5. Variantes direct/multishot/bundle renvoyées aux Temps 3a/3d/3b — 2b reste centré sur le one-shot à ownership clair.
  6. Jumeaux possédés OwnedSendMessage/OwnedReceiveMessage (correction de spec, §1) — les types message de family-net sont à base d’emprunts (chemin synchrone) et incompatibles avec l’ownership asynchrone S1 ; on ne les réutilise pas, on en a des équivalents possédés. Helpers de sérialisation partagés (pub(crate)), zéro duplication.
  7. Zéro perte de donnée (into_zero_copy_buffers) — la restitution rend l’intégralité des buffers d’un sendmsg_zc, jamais seulement le premier. Application directe de l’ADR-032 (« Préservation des données confiées : zéro discard silencieux »), convention transverse gravée à partir de ce cas.
  8. Échec précoce zero-copy — comportement réel kernel 6.12 (§4.2) : souvent « résultat res<0 + F_MORE puis NOTIF » plutôt que « NOTIF seul » ; les deux formes sont gérées, les buffers toujours restitués.

10. Travail à reprendre

Spec suivante : io-uring-2c-async.md (15 opérations sans équivalent filesystem/réseau direct : nop, timeout, cancel, poll, futex, waitid, msg_ring, epoll_ctl, files_update, fixed_fd_install). Traduction anglaise globale après validation des documents français.


Licence du document : MPL 2.0 Statut : Spécification technique du Temps 2b (réseau) du module air-sys-syscall::io_uring, cible kernel 6.12 LTS.