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 desendmsg/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.mdpour 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/ControlMessagesdefamily-net. C’est impossible : les types message defamily-netsont à 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és —OwnedSendMessage/OwnedReceiveMessage— au lieu de redéfinir ou détourner les types empruntés.ControlMessagesn’existe pas dansfamily-net(le passage de FD y est un champfdsdirect) : on suit la même modélisation (OwnedSendMessage::fds: Vec<OwnedFd>). Les helpers de sérialisationsockaddr/cmsg defamily-netsont réutilisés (renduspub(crate)), sans duplication. MSG_NOSIGNALpar défaut sur tous les envois : pas deSIGPIPEsur socket fermé (l’erreur remonte enEPIPEdans la complétion). C’est un invariant Air, surchargeable explicitement.SOCK_CLOEXECpar défaut sur les FD créés (accept,socket) : cohérent avec la famillenetet les autres familles couche 0.- Adresses possédées : une
SocketAddrà transmettre est sérialisée en un stockagesockaddrpossé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/listenexistent 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_peercapture l’adresse pair dans un stockage possédé du slot ⇒completion.into_accept_result() -> (OwnedFd, SocketAddr). - Multishot :
submit_accept_multishotau Temps 3d (un seul SQE accepte en continu). Direct fd : variante_directau 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_directau Temps 3a. - Intérêt : créer un socket dans le ring, chaînable (Temps 3c) avec
connect/bindsans 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/listensur un socket créé en descripteur direct (submit_socket_direct), qui n’a pas de FD ordinaire utilisable par lesbind(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_NOSIGNALajouté par défaut ausend. - 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 nouveaurecv.- 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 enSCM_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 lestruct msghdr+cmsgen 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>oùReceiveMessageMeta(adossé àio_uring_recvmsg_out) donnenamelen,controllen,payloadlen,flags(dontMSG_TRUNC/MSG_CTRUNCà vérifier), les FD reçus (OwnedFdCLOEXEC) et l’adresse pair. Aucune donnée n’est perdue : les buffers de réception sont restitués dansOwnedReceiveMessage, les FD et la méta dansReceiveMessageMeta.- 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 :
- Complétion de résultat — porte le nombre d’octets envoyés (
res ≥ 0) et le flagCQE_F_MORE(une notification suivra).- 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 viainto_result()sans restituer les buffers ; - la complétion NOTIF (
is_notif()) restitue tous les buffers (viainto_zero_copy_buffer()poursend_zc,into_zero_copy_buffers()poursendmsg_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’unsendmsg_zc(unVec<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_zcrend sonVec<u8>viainto_zero_copy_buffer();sendmsg_zcrend l’intégralité de ses buffers (Vec<Vec<u8>>) viainto_zero_copy_buffers(). ZeroCopyFlags: exposeREPORT_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 viaCompletion::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
sendordinaire 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_MOREpréalable ». En pratique sur 6.12, un échec se présente le plus souvent comme une complétion de résultatres < 0portantF_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
| # opcode | Opcode | Façade | Complétion |
|---|---|---|---|
| 9 | SENDMSG | submit_send_message | into_result |
| 10 | RECVMSG | submit_receive_message | into_receive_message_result |
| 13 | ACCEPT | submit_accept / _with_peer | accepted_fd / into_accept_result |
| 16 | CONNECT | submit_connect | completed |
| 26 | SEND | submit_send | into_buffer_result |
| 27 | RECV | submit_receive | into_buffer_result |
| 34 | SHUTDOWN | submit_shutdown | completed |
| 45 | SOCKET | submit_socket | into_socket_fd |
| 47 | SEND_ZC | submit_send_zero_copy | into_result + NOTIF into_zero_copy_buffer (Vec<u8>) |
| 48 | SENDMSG_ZC | submit_send_message_zero_copy | into_result + NOTIF into_zero_copy_buffers (Vec<Vec<u8>>, tous) |
| 56 | BIND | submit_bind | completed |
| 57 | LISTEN | submit_listen | completed |
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 unsendmsg_zcmulti-buffers), le rapportzero_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 enOwnedFdCLOEXEC. - 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
MSG_NOSIGNALetSOCK_CLOEXECpar défaut — invariants Air, cohérents couche 0 ; surcharge explicite possible.- 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é. - 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 desOwnedFd, jamais des entiers bruts. bind/listenexposés bien qu’« évidents » — indispensables pour les sockets en descripteur direct (sans FD ordinaire).- Variantes direct/multishot/bundle renvoyées aux Temps 3a/3d/3b — 2b reste centré sur le one-shot à ownership clair.
- Jumeaux possédés
OwnedSendMessage/OwnedReceiveMessage(correction de spec, §1) — les types message defamily-netsont à 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. - Zéro perte de donnée (
into_zero_copy_buffers) — la restitution rend l’intégralité des buffers d’unsendmsg_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. - Échec précoce zero-copy — comportement réel kernel 6.12 (§4.2) : souvent
« résultat
res<0+F_MOREpuis 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.