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-moduleair-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/FixedFdTablepossè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()/Dropdésenregistrent proprement (les buffers possédés sont restitués). - Tags et sparse : les variantes
*2acceptent 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 lesregister/enterl’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 unFixedSlotau lieu d’unBorrowedFd(drapeauIOSQE_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 danscqe->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) etsend_zc/recv fixed (Temps 2b) évitent ce coût par op. Cas chaud du data plane AirCom (register_mmapsur unmemfdpartagé). clone_from: partage de buffers entre rings d’un même processus (thread-per-core) sans réépinglage.- Ownership :
RegisteredBufferSlicene peut survivre àunregister(lifetime) —read_fixedsur 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_entersuivants utilisentIORING_ENTER_REGISTERED_RING(pas de résolution de FD). Gain net sur les chemins àenterfréquents. Requiert/activeFEAT_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(familleipc) : 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_asyncne notifie que pour les complétions traitées en asynchrone (filtre le bruit des complétions inline). Désactivation ponctuelle via le drapeauIORING_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_workersborne le nombre de threads (catégoriesIO_WQ_BOUND/IO_WQ_UNBOUND) — important sur matériel modeste (Charte principe 4 : maîtriser l’empreinte sur Raspberry Pi 4). CpuSetpartagé avecfamily-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
TimeoutFlagsdu Temps 2c etfamily-time).
10. Récapitulatif des register opcodes (Temps 3a)
| # | Register op | Façade |
|---|---|---|
| 1 | UNREGISTER_BUFFERS | RegisteredBuffers::unregister |
| 3 | UNREGISTER_FILES | FixedFdTable::unregister |
| 4 | REGISTER_EVENTFD | register_eventfd |
| 5 | UNREGISTER_EVENTFD | unregister_eventfd |
| 7 | REGISTER_EVENTFD_ASYNC | register_eventfd_async |
| 9 | REGISTER_PERSONALITY | register_personality |
| 10 | UNREGISTER_PERSONALITY | unregister_personality |
| 13 | REGISTER_FILES2 | FixedFdTable::register(_with) |
| 14 | REGISTER_FILES_UPDATE2 | FixedFdTable::set / clear |
| 15 | REGISTER_BUFFERS2 | RegisteredBuffers::register(_mmap) |
| 16 | REGISTER_BUFFERS_UPDATE | RegisteredBuffers::update |
| 17 | REGISTER_IOWQ_AFF | set_work_queue_affinity |
| 18 | UNREGISTER_IOWQ_AFF | clear_work_queue_affinity |
| 19 | REGISTER_IOWQ_MAX_WORKERS | set_work_queue_max_workers |
| 20 | REGISTER_RING_FDS | register_ring_fd |
| 21 | UNREGISTER_RING_FDS | unregister_ring_fd |
| 25 | REGISTER_FILE_ALLOC_RANGE | FixedFdTable::set_alloc_range |
| 27 | REGISTER_NAPI | register_napi |
| 28 | UNREGISTER_NAPI | unregister_napi |
| 29 | REGISTER_CLOCK | register_clock |
| 30 | REGISTER_CLONE_BUFFERS | RegisteredBuffers::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 unaccept_direct/openat2_direct(slotAllocpuisIndex),read/writeviaFixedSlot,fixed_fd_installretour en FD ordinaire ;RegisteredBuffers+read_fixed/write_fixedround-trip ;clone_fromentre deux rings ;register_eventfdpuis 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/FixedSlotne doit pas survivre au désenregistrement (testtrybuildcompile-fail) ; pas de fuite de FD/buffer à l’unregisterni 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
- 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. - Ressources possédées + références liées par lifetime — sûreté par construction : impossible d’utiliser une ressource désenregistrée.
FixedSlotTarget::{Index, Alloc}— l’auto-allocation est unenumtypé, pas la sentinelleIORING_FILE_INDEX_ALLOC(~0U) exposée.register_mmappour le data plane — buffers enregistrés adossés à desMmapRegionpossédées (memfd partagé AirCom), réutilisant le handle de vivacité defamily-mem.- 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) :
- 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 structuresio_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. register_clock(ClockSource::Realtime)→EINVAL. io_uring (6.12/6.17) n’accepte queCLOCK_MONOTONIC/CLOCK_BOOTTIMEpour l’horloge des timeouts ;REALTIMEest rejeté par le kernel. Décision BDFL : la varianteRealtimeest conservée (surface inchangée) ; son rejet est testé et documenté.- CLOEXEC refusé sur descripteur direct. Le kernel rend
EINVALsifile_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 pasSOCK_CLOEXEC/O_CLOEXEC; leO_CLOEXECest appliqué à la matérialisation (fixed_fd_install,install_fd_flags = 0). register_napi→EINVALhors config NIC/NAPI. Le busy-poll NAPI exigeCONFIG_NET_RX_BUSY_POLLet un NIC NAPI ; absent des exécuteurs, l’appel rendEINVAL(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.