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 2a : opérations filesystem

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

Position. Le Temps 2a spécifie les 26 opérations filesystem exposées via io_uring en 6.12 (cf. document maître io-uring-0-inventaire.md, axe B). Toutes réutilisent tel quel le cœur du Temps 1 (io-uring-1-core.md) : soumission, slab S1, complétion, téardown S2. Ce document ne re-spécifie ni le ring ni le slab — il spécifie les opérations.


1. Conventions transverses du Temps 2a

1.1 Renvoi aux familles synchrones (ADR-022 Décision 2)

Chaque opération io_uring a un équivalent syscall synchrone déjà spécifié dans family-fs.md / family-mem.md. Pour ne pas dupliquer (et risquer d’introduire des divergences), ce document :

  • identifie l’op par son opcode IORING_OP_* et son numéro (source de vérité : header uapi v6.12) ;
  • nomme l’équivalent syscall et renvoie à la famille pour son numéro x86_64/ARM64, ses flags et sa sémantique fine ;
  • réutilise les types partagés (OpenHow, StatxFlags, StatxMask, Statx, RenameFlags, Mode, DirFd, Advice, SpliceFlags, FallocateMode, FsyncFlags, XattrFlags…) définis avec les familles — un développeur qui connaît l’API synchrone retrouve les mêmes types.

Le « syscall sous-jacent » réel de toute op io_uring est io_uring_enter(2) (n° 426) ; les noms ci-dessous désignent l’équivalent sémantique.

1.2 Modèle de buffers (rappel ADR-022 Décision 3 + S1)

Trois mécanismes, transfert d’ownership par défaut :

  1. Ownership (défaut) : le buffer (Vec<u8>, Box<Statx>, CString…) est déplacé dans le slot S1 à la soumission, restitué à la complétion. Sûr par construction : impossible d’y toucher pendant que le kernel le détient.
  2. Buffer enregistré (READ_FIXED/WRITE_FIXED) : référence un buffer pré-enregistré par index → évite la traduction d’adresses. Type RegisteredBufferSlice défini au Temps 3a ; ici on expose seulement la forme submit_read_fixed.
  3. Raw unsafe : pointeur brut, validité garantie par l’appelant. Temps 4.

1.3 Offset : Option<u64> plutôt qu’une sentinelle (ADR-021 conv. 1)

Les opérations à offset (read, write, readv, writev, read_fixed, write_fixed) prennent offset: Option<u64> :

  • Some(n) : offset absolu n ;
  • None : « position courante du fichier » (transmis comme -1 au kernel, requiert IORING_FEAT_RW_CUR_POS, présent en 6.12).

On n’expose jamais le -1 magique : le None typé le remplace.

1.4 Descripteurs de répertoire

Les opérations *at prennent dirfd: DirFd (newtype partagé avec family-fs), où DirFd::CWD matérialise AT_FDCWD — encore une sentinelle kernel remplacée par un constructeur typé.

1.5 Chemins

Les chemins sont des CString (octets terminés NUL, pas forcément UTF-8 ; cohérent avec Principe 3 et l’usage de Path/OsStr côté couche 1). Le buffer de chemin est déplacé dans le slot (le kernel le lit de façon asynchrone, il doit rester valide jusqu’à la complétion — S1 le garantit).


2. Lecture / écriture

2.1 submit_read / submit_write

#![allow(unused)]
fn main() {
impl IoUring {
    pub fn submit_read(&mut self, fd: BorrowedFd<'_>, buffer: Vec<u8>, offset: Option<u64>)
        -> Result<SubmissionToken, Errno>;
    pub fn submit_write(&mut self, fd: BorrowedFd<'_>, buffer: Vec<u8>, offset: Option<u64>)
        -> Result<SubmissionToken, Errno>;
}
}
  • Opcode : IORING_OP_READ (22) / IORING_OP_WRITE (23).
  • Équivalent : pread/pwrite (offset Some) ou read/write (offset None). Cf. family-fs.md.
  • Buffer : buf déplacé dans le slot. Pour read, sa capacité borne le nombre d’octets lus (len = buf.len() ou capacité — préciser : on utilise la longueur logique du Vec). Pour write, buf.len() octets sont écrits.
  • Complétion : completion.into_buffer_result() -> (Vec<u8>, usize) rend le buffer et le nombre d’octets transférés. res < 0Err(Errno).
  • Préconditions : buf non vide pour un read utile (un read de 0 octet est légal et complète à 0). fd ouvert dans le bon mode.
  • Erreurs : EBUSY (slab plein, avant syscall), puis à la complétion EBADF, EINVAL, EFAULT, EIO, EAGAIN (FD non bloquant sans données).
  • Performance : zéro copie côté façade (move). Sur ring SQPOLL chaud, la soumission ≈ un store atomique.

Exemple.

#![allow(unused)]
fn main() {
let buf = vec![0u8; 4096];
let tok = ring.submit_read(file.as_fd(), buf, Some(0))?;
ring.submit_and_wait(1)?;
let cmp = ring.wait_completion()?;
let (buf, n) = cmp.into_buffer_result()?;   // buf récupéré, n octets lus
}

2.2 submit_readv / submit_writev

#![allow(unused)]
fn main() {
impl IoUring {
    pub fn submit_readv(&mut self, fd: BorrowedFd<'_>, buffers: Vec<Vec<u8>>, offset: Option<u64>)
        -> Result<SubmissionToken, Errno>;
    pub fn submit_writev(&mut self, fd: BorrowedFd<'_>, buffers: Vec<Vec<u8>>, offset: Option<u64>)
        -> Result<SubmissionToken, Errno>;
}
}
  • Opcode : IORING_OP_READV (1) / IORING_OP_WRITEV (2).
  • Équivalent : preadv/pwritev. Cf. family-fs.md.
  • Buffers : on possède un vecteur de buffers possédés (Vec<Vec<u8>>) pour rester sûr (transfert d’ownership). Le tableau d’iovec transmis au kernel est construit en interne et garé dans le slot ; il ne survit pas à l’appelant. (Le chemin haute perf vectorisé sans copie relève du Temps 4.)
  • Complétion : completion.into_vectored_result() -> (Vec<Vec<u8>>, usize).
  • Erreurs : comme read/write + EINVAL si nombre d’iovecs > IOV_MAX.

2.3 submit_read_fixed / submit_write_fixed

#![allow(unused)]
fn main() {
impl IoUring {
    pub fn submit_read_fixed(&mut self, fd: BorrowedFd<'_>, buf: RegisteredBufferSlice, offset: Option<u64>)
        -> Result<SubmissionToken, Errno>;
    pub fn submit_write_fixed(&mut self, fd: BorrowedFd<'_>, buf: RegisteredBufferSlice, offset: Option<u64>)
        -> Result<SubmissionToken, Errno>;
}
}
  • Opcode : IORING_OP_READ_FIXED (4) / IORING_OP_WRITE_FIXED (5).
  • Buffer : RegisteredBufferSlice désigne une tranche d’un buffer pré-enregistré (IORING_REGISTER_BUFFERS2, Temps 3a). Pas de transfert d’ownership par opération : le buffer appartient à l’enregistrement.
  • Gain : le kernel n’a pas à épingler/traduire les pages à chaque op (déjà fait à l’enregistrement). Cas chaud (data plane AirCom).
  • Complétion : completion.into_result() -> i32 (octets) ; le buffer reste détenu par l’enregistrement.
  • Renvoi : sémantique d’enregistrement détaillée au Temps 3a.

3. Synchronisation, allocation, troncature

3.1 submit_fsync

#![allow(unused)]
fn main() {
pub fn submit_fsync(&mut self, fd: BorrowedFd<'_>, flags: FsyncFlags)
    -> Result<SubmissionToken, Errno>;
}
  • Opcode : IORING_OP_FSYNC (3). Équivalent : fsync/fdatasync (FsyncFlags::DATASYNCIORING_FSYNC_DATASYNC).
  • Complétion : completed() -> Result<(), Errno>.
  • Erreurs : EBADF, EIO, EINVAL.

3.2 submit_sync_file_range

#![allow(unused)]
fn main() {
pub fn submit_sync_file_range(&mut self, fd: BorrowedFd<'_>, offset: u64, nbytes: u64, flags: SyncFileRangeFlags)
    -> Result<SubmissionToken, Errno>;
}
  • Opcode : IORING_OP_SYNC_FILE_RANGE (8). Équivalent : sync_file_range. Cf. family-fs.md pour les flags.
  • Complétion : completed().

3.3 submit_fallocate

#![allow(unused)]
fn main() {
pub fn submit_fallocate(&mut self, fd: BorrowedFd<'_>, mode: FallocateMode, offset: i64, length: i64)
    -> Result<SubmissionToken, Errno>;
}
  • Opcode : IORING_OP_FALLOCATE (17). Équivalent : fallocate.
  • Note : offset/len en i64 conformes à l’ABI ; validation amont (len > 0, pas de débordement) avant soumission (Principe 4).
  • Complétion : completed(). Erreurs : EBADF, EFBIG, ENOSPC, EINVAL.

3.4 submit_ftruncate

#![allow(unused)]
fn main() {
pub fn submit_ftruncate(&mut self, fd: BorrowedFd<'_>, length: u64)
    -> Result<SubmissionToken, Errno>;
}
  • Opcode : IORING_OP_FTRUNCATE (55). Équivalent : ftruncate.
  • Complétion : completed(). Erreurs : EBADF, EINVAL, EFBIG, EPERM.

4. Ouverture / fermeture

4.1 submit_openat2

#![allow(unused)]
fn main() {
pub fn submit_openat2(&mut self, dirfd: DirFd, path: CString, how: OpenHow)
    -> Result<SubmissionToken, Errno>;
}
  • Opcode : IORING_OP_OPENAT2 (28). Équivalent : openat2 (n° 437 x86_64/ARM64). Cf. family-fs.md pour OpenHow (flags, mode, resolve).
  • Note : IORING_OP_OPENAT (18) est évacué (UNSUPPORTED.md) au profit d’openat2, superset (cf. doc maître §4).
  • Complétion : completion.opened_fd() -> Result<OwnedFd, Errno> (CLOEXEC selon how). La variante descripteur direct (résultat placé dans un slot de table de FD enregistrée via file_index) relève du Temps 3a ; submit_openat2_direct y sera exposé.
  • Erreurs : ENOENT, EACCES, ELOOP, ENFILE, EINVAL

4.2 submit_close

#![allow(unused)]
fn main() {
pub fn submit_close(&mut self, fd: OwnedFd) -> Result<SubmissionToken, Errno>;
}
  • Opcode : IORING_OP_CLOSE (19). Équivalent : close.
  • Ownership : submit_close consomme l’OwnedFd (déplacé dans le slot) — il ne peut plus être réutilisé, garantissant l’absence de double close. Le FD n’est effectivement fermé qu’à l’exécution kernel.
  • Complétion : completed(). Erreur : EBADF (improbable, FD possédé).
  • Variante fixed : fermeture d’un slot de table de FD enregistrée → Temps 3a.

5. Métadonnées

5.1 submit_statx

#![allow(unused)]
fn main() {
pub fn submit_statx(&mut self, dirfd: DirFd, path: CString, flags: StatxFlags, mask: StatxMask, out: Box<Statx>)
    -> Result<SubmissionToken, Errno>;
}
  • Opcode : IORING_OP_STATX (21). Équivalent : statx (n° 332 x86_64 / 291 ARM64). Cf. family-fs.md pour StatxFlags, StatxMask, Statx.
  • Buffer de sortie : le kernel écrit la structure dans out ; on transfère l’ownership du Box<Statx> dans le slot (il doit rester valide jusqu’à la complétion — S1).
  • Complétion : completion.into_statx() -> Result<Box<Statx>, Errno>.
  • Erreurs : ENOENT, EACCES, EINVAL.

6. Conseils d’accès (advise)

6.1 submit_fadvise

#![allow(unused)]
fn main() {
pub fn submit_fadvise(&mut self, fd: BorrowedFd<'_>, offset: u64, length: u64, advice: Advice)
    -> Result<SubmissionToken, Errno>;
}
  • Opcode : IORING_OP_FADVISE (24). Équivalent : posix_fadvise.
  • Complétion : completed().

6.2 submit_madviseimplémenté (PR coordonnée family-mem)

#![allow(unused)]
fn main() {
pub fn submit_madvise(&mut self, region: &MmapRegion, range: Range<usize>, advice: MadviseAdvice)
    -> Result<SubmissionToken, Errno>;
}
  • Opcode : IORING_OP_MADVISE (25). Équivalent : madvise.
  • Sûreté (décision arrêtée, réalisée) : pas de plage brute (addr/len) ni d’API unsafe. submit_madvise prend une MmapRegion partageable (type de family-mem, cf. family-mem-mmap-region.md) et un sous-range validé contre les bornes de la région (Principe 4 ; range.end ≤ region.len() et range.start ≤ range.end sinon Err(EINVAL) avant soumission). La région doit rester mappée jusqu’à la complétion : le slot S1 retient une MmapRegionLiveness (clone de l’Arc interne, region.liveness_handle()), de sorte que munmap ne puisse pas survenir tant qu’une op madvise est en vol — sûreté par construction (ni use-after-unmap, ni fuite : munmap au dernier drop), prouvée Miri + loom. Le même handle est réutilisé par futex (Temps 2c §6.1) et le sera par l’enregistrement mémoire du Temps 3a.
  • Complétion : completed().

7. Zero-copy entre FDs

7.1 submit_splice

#![allow(unused)]
fn main() {
pub fn submit_splice(&mut self, fd_in: BorrowedFd<'_>, offset_in: Option<u64>,
                     fd_out: BorrowedFd<'_>, offset_out: Option<u64>,
                     length: u32, flags: SpliceFlags)
    -> Result<SubmissionToken, Errno>;
}
  • Opcode : IORING_OP_SPLICE (30). Équivalent : splice. Cf. family-ipc.md (la famille ipc documente déjà splice/tee).
  • Précondition : au moins un des deux FDs est un pipe (comme splice(2)). off_in/off_out en Option<u64> (None = position courante / FD non seekable).
  • Complétion : completion.into_result() -> i32 (octets transférés).
  • Note : SPLICE_F_FD_IN_FIXED (bit 31) permet d’utiliser un FD enregistré comme entrée → exposé proprement quand fd_in est un FD fixed (Temps 3a).

7.2 submit_tee

#![allow(unused)]
fn main() {
pub fn submit_tee(&mut self, fd_in: BorrowedFd<'_>, fd_out: BorrowedFd<'_>, length: u32, flags: SpliceFlags)
    -> Result<SubmissionToken, Errno>;
}
  • Opcode : IORING_OP_TEE (33). Équivalent : tee. Les deux FDs sont des pipes ; duplique sans consommer la source. Complétion : into_result.

8. Répertoires et liens

Opérations homogènes : chemins en CString déplacés dans le slot, dirfd: DirFd, complétion completed(). Équivalents et flags : cf. family-fs.md.

#![allow(unused)]
fn main() {
impl IoUring {
    pub fn submit_mkdirat(&mut self, dirfd: DirFd, path: CString, mode: Mode)
        -> Result<SubmissionToken, Errno>;                              // OP 37
    pub fn submit_unlinkat(&mut self, dirfd: DirFd, path: CString, flags: UnlinkFlags)
        -> Result<SubmissionToken, Errno>;                              // OP 36
    pub fn submit_renameat(&mut self, old_dirfd: DirFd, old_path: CString,
                           new_dirfd: DirFd, new_path: CString, flags: RenameFlags)
        -> Result<SubmissionToken, Errno>;                              // OP 35 (sémantique renameat2)
    pub fn submit_linkat(&mut self, old_dirfd: DirFd, old_path: CString,
                         new_dirfd: DirFd, new_path: CString, flags: LinkFlags)
        -> Result<SubmissionToken, Errno>;                              // OP 39
    pub fn submit_symlinkat(&mut self, target: CString, new_dirfd: DirFd, link_path: CString)
        -> Result<SubmissionToken, Errno>;                              // OP 38
}
}
  • renameat porte des RenameFlags (RENAME_NOREPLACE, RENAME_EXCHANGE, RENAME_WHITEOUT) — sémantique renameat2.
  • Erreurs typiques : ENOENT, EEXIST, ENOTEMPTY, EACCES, EXDEV (linkat cross-device), EINVAL (flags).
  • Note : chaque op porte deux CString pour rename/link → deux buffers déplacés dans le slot ; le slot S1 stocke un état composite documenté.

9. Attributs étendus (xattr)

#![allow(unused)]
fn main() {
impl IoUring {
    pub fn submit_setxattr(&mut self, path: CString, name: CString, value: Vec<u8>, flags: XattrFlags)
        -> Result<SubmissionToken, Errno>;                              // OP 42
    pub fn submit_getxattr(&mut self, path: CString, name: CString, value: Vec<u8>)
        -> Result<SubmissionToken, Errno>;                              // OP 44
    pub fn submit_fsetxattr(&mut self, fd: BorrowedFd<'_>, name: CString, value: Vec<u8>, flags: XattrFlags)
        -> Result<SubmissionToken, Errno>;                              // OP 41
    pub fn submit_fgetxattr(&mut self, fd: BorrowedFd<'_>, name: CString, value: Vec<u8>)
        -> Result<SubmissionToken, Errno>;                              // OP 43
}
}
  • Équivalents : setxattr/getxattr/fsetxattr/fgetxattr. Cf. family-fs.md.
  • Buffers : name (CString) et value (Vec<u8>, octets bruts — Principe 3) déplacés dans le slot. Pour les get*, value sert de buffer de sortie ; complétion completion.into_xattr_result() -> (Vec<u8>, usize) (la taille rendue permet de retailler).
  • Erreurs : ENODATA, ERANGE (buffer trop court), ENOTSUP, EACCES.

10. Récapitulatif des 26 opérations

# opcodeOpcodeFaçadeComplétion
1READVsubmit_readvinto_vectored_result
2WRITEVsubmit_writevinto_vectored_result
3FSYNCsubmit_fsynccompleted
4READ_FIXEDsubmit_read_fixedinto_result
5WRITE_FIXEDsubmit_write_fixedinto_result
8SYNC_FILE_RANGEsubmit_sync_file_rangecompleted
17FALLOCATEsubmit_fallocatecompleted
19CLOSEsubmit_closecompleted
21STATXsubmit_statxinto_statx
22READsubmit_readinto_buffer_result
23WRITEsubmit_writeinto_buffer_result
24FADVISEsubmit_fadvisecompleted
25MADVISEsubmit_madvisecompleted
28OPENAT2submit_openat2opened_fd
30SPLICEsubmit_spliceinto_result
33TEEsubmit_teeinto_result
35RENAMEATsubmit_renameatcompleted
36UNLINKATsubmit_unlinkatcompleted
37MKDIRATsubmit_mkdiratcompleted
38SYMLINKATsubmit_symlinkatcompleted
39LINKATsubmit_linkatcompleted
41FSETXATTRsubmit_fsetxattrcompleted
42SETXATTRsubmit_setxattrcompleted
43FGETXATTRsubmit_fgetxattrinto_xattr_result
44GETXATTRsubmit_getxattrinto_xattr_result
55FTRUNCATEsubmit_ftruncatecompleted

Absents volontairement (faux opcodes de l’ancien overview, inexistants dans l’ABI io_uring 6.12) : copy_file_range, fchmodat, fchownat. Les changements de permissions/propriété restent synchrones (family-fs).


11. Types ajoutés / partagés

Aucun nouveau type de ring ; réutilise le cœur (Temps 1). Types métier partagés avec les familles : OpenHow, Statx, StatxFlags, StatxMask, Mode, DirFd, RenameFlags, UnlinkFlags, LinkFlags, FsyncFlags, SyncFileRangeFlags, FallocateMode, Advice, SpliceFlags, XattrFlags. Nouveaux au Temps 2a : RegisteredBufferSlice (forme ; défini Temps 3a). madvise réutilise MmapRegion de family-mem (handle de vivacité partagé, spécifié conjointement). Nouvelles méthodes de Completion : into_vectored_result, into_statx, into_xattr_result.


12. Stratégie de tests

  • Intégration (kernel 6.12, tmpfs/ext4) : aller-retour read/write avec vérification du contenu, offset Some/None, readv multi-buffers, openat2 + read + close en chaîne, statx, rename/link/unlink, xattr set/get round-trip, splice fichier→pipe.
  • Property-based : pour read/write, octets transférés ≤ len et restitution fidèle du buffer ; pour les chemins, robustesse aux octets non-UTF-8.
  • Erreurs via simulateur : ENOENT, EACCES, ERANGE (xattr), ENOSPC (fallocate), EXDEV (linkat).
  • Soundness : Miri sur les chemins de transfert/restitution de buffer ; vérifier qu’un buffer en vol ne peut être ni libéré ni réutilisé (le type le rend impossible).
  • Fuzzing : décodage des résultats statx/getxattr (données venant du kernel = externes).
  • Couverture 100 % lignes + branches ; exceptions légitimes dans COVERAGE-EXCEPTIONS.md.

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

  1. offset: Option<u64> — application directe d’ADR-021 conv. 1 (None = position courante, pas de -1 magique).
  2. readv/writev en Vec<Vec<u8>> possédé — sûreté d’abord ; la voie vectorisée sans copie est au Temps 4, pas un raccourci ici.
  3. submit_close(OwnedFd) consomme le FD — interdit le double close par typage.
  4. Pas de copy_file_range/fchmodat/fchownat — ces opcodes n’existent pas dans io_uring 6.12 ; on ne fabrique pas de fausse façade. Les opérations correspondantes restent synchrones.
  5. Variantes « direct descriptor » et « fixed buffer » renvoyées au Temps 3a — on n’introduit pas l’enregistrement ici pour garder 2a centré sur les opérations à ownership simple.

14. Travail à reprendre

Spec suivante : io-uring-2b-network.md (12 opérations réseau, dont les variantes zero-copy send_zc/sendmsg_zc et bind/listen). La traduction anglaise globale est produite une fois les documents français validés.


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