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 :
- 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. - Buffer enregistré (
READ_FIXED/WRITE_FIXED) : référence un buffer pré-enregistré par index → évite la traduction d’adresses. TypeRegisteredBufferSlicedéfini au Temps 3a ; ici on expose seulement la formesubmit_read_fixed. - 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 absolun;None: « position courante du fichier » (transmis comme-1au kernel, requiertIORING_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(offsetSome) ouread/write(offsetNone). Cf.family-fs.md. - Buffer :
bufdéplacé dans le slot. Pourread, sa capacité borne le nombre d’octets lus (len = buf.len()ou capacité — préciser : on utilise la longueur logique duVec). Pourwrite,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 < 0⇒Err(Errno). - Préconditions :
bufnon vide pour un read utile (un read de 0 octet est légal et complète à 0).fdouvert dans le bon mode. - Erreurs :
EBUSY(slab plein, avant syscall), puis à la complétionEBADF,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’iovectransmis 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 +
EINVALsi 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 :
RegisteredBufferSlicedé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::DATASYNC⇒IORING_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.mdpour 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/leneni64conformes à 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.mdpourOpenHow(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 selonhow). La variante descripteur direct (résultat placé dans un slot de table de FD enregistrée viafile_index) relève du Temps 3a ;submit_openat2_directy 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_closeconsomme 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.mdpourStatxFlags,StatxMask,Statx. - Buffer de sortie : le kernel écrit la structure dans
out; on transfère l’ownership duBox<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_madvise — implé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’APIunsafe.submit_madviseprend uneMmapRegionpartageable (type defamily-mem, cf.family-mem-mmap-region.md) et un sous-rangevalidé contre les bornes de la région (Principe 4 ;range.end ≤ region.len()etrange.start ≤ range.endsinonErr(EINVAL)avant soumission). La région doit rester mappée jusqu’à la complétion : le slot S1 retient uneMmapRegionLiveness(clone de l’Arcinterne,region.liveness_handle()), de sorte quemunmapne puisse pas survenir tant qu’une opmadviseest en vol — sûreté par construction (ni use-after-unmap, ni fuite :munmapau dernier drop), prouvée Miri + loom. Le même handle est réutilisé parfutex(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 familleipcdocumente déjàsplice/tee). - Précondition : au moins un des deux FDs est un pipe (comme
splice(2)).off_in/off_outenOption<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 quandfd_inest 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
}
}
renameatporte desRenameFlags(RENAME_NOREPLACE,RENAME_EXCHANGE,RENAME_WHITEOUT) — sémantiquerenameat2.- Erreurs typiques :
ENOENT,EEXIST,ENOTEMPTY,EACCES,EXDEV(linkat cross-device),EINVAL(flags). - Note : chaque op porte deux
CStringpour 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) etvalue(Vec<u8>, octets bruts — Principe 3) déplacés dans le slot. Pour lesget*,valuesert de buffer de sortie ; complétioncompletion.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
| # opcode | Opcode | Façade | Complétion |
|---|---|---|---|
| 1 | READV | submit_readv | into_vectored_result |
| 2 | WRITEV | submit_writev | into_vectored_result |
| 3 | FSYNC | submit_fsync | completed |
| 4 | READ_FIXED | submit_read_fixed | into_result |
| 5 | WRITE_FIXED | submit_write_fixed | into_result |
| 8 | SYNC_FILE_RANGE | submit_sync_file_range | completed |
| 17 | FALLOCATE | submit_fallocate | completed |
| 19 | CLOSE | submit_close | completed |
| 21 | STATX | submit_statx | into_statx |
| 22 | READ | submit_read | into_buffer_result |
| 23 | WRITE | submit_write | into_buffer_result |
| 24 | FADVISE | submit_fadvise | completed |
| 25 | MADVISE | submit_madvise | completed |
| 28 | OPENAT2 | submit_openat2 | opened_fd |
| 30 | SPLICE | submit_splice | into_result |
| 33 | TEE | submit_tee | into_result |
| 35 | RENAMEAT | submit_renameat | completed |
| 36 | UNLINKAT | submit_unlinkat | completed |
| 37 | MKDIRAT | submit_mkdirat | completed |
| 38 | SYMLINKAT | submit_symlinkat | completed |
| 39 | LINKAT | submit_linkat | completed |
| 41 | FSETXATTR | submit_fsetxattr | completed |
| 42 | SETXATTR | submit_setxattr | completed |
| 43 | FGETXATTR | submit_fgetxattr | into_xattr_result |
| 44 | GETXATTR | submit_getxattr | into_xattr_result |
| 55 | FTRUNCATE | submit_ftruncate | completed |
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 ≤ lenet 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
offset: Option<u64>— application directe d’ADR-021 conv. 1 (None = position courante, pas de-1magique).readv/writevenVec<Vec<u8>>possédé — sûreté d’abord ; la voie vectorisée sans copie est au Temps 4, pas un raccourci ici.submit_close(OwnedFd)consomme le FD — interdit le double close par typage.- 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. - 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.