Skip to content

Commit 95c6a5d

Browse files
committed
core: add the io_uring buffer pool uAPI of protocol 7.46
Linux 7.3 lets a FUSE-over-io_uring server give each queue a pool of payload buffers instead of a buffer per ring entry. abi::fuse_uring gets the ADD_QUEUE and ADD_BUFPOOL commands, the FUSE_HAS_IO_URING_BUFPOOL INIT flag, the offset field of the entry header and the bufpool field of the command. FsOptions doesn't know the INIT flag, so the server records it from the raw INIT flags, together with the max_write and max_pages of its reply. Server::uring_bufpool_supported() reports the flag. Server::uring_payload_size() returns the size of the kernel's payload buffers, computed the way fuse_uring_create() does. Signed-off-by: Ilan Joselevich <personal@ilanjoselevich.com>
1 parent c4efc19 commit 95c6a5d

4 files changed

Lines changed: 177 additions & 12 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
- [269](https://github.com/cloud-hypervisor/fuse-backend-rs/pull/269): `FuseSession::detach()` lazily unmounts a Linux session through `&self`,
3838
so the owner of a serving layer can start the teardown while another thread waits in
3939
`FuseServing::wait()`.
40+
- `abi::fuse_uring` has the commands, INIT flag and fields of protocol 7.46 for buffer pools.
41+
`Server::uring_bufpool_supported()` tells whether the kernel offered them at INIT, and
42+
`Server::uring_payload_size()` returns the size of the kernel's payload buffers.
4043

4144
### Changed
4245
- Bump the declared minimum supported Rust version (MSRV) from 1.80 to 1.86.
@@ -49,6 +52,7 @@
4952
- The FUSE-over-io_uring transport answers opcodes it doesn't know with ENOSYS instead of EIO.
5053
Kernels that send `FUSE_STATX` only fall back to `FUSE_GETATTR` after ENOSYS, so `statx()`
5154
calls that ask for more than the basic fields, like the birth time `stat(1)` shows, failed.
55+
- `FuseUringEntInOut::padding` is now `offset`, and `FuseUringCmdReq` has a new `bufpool` field.
5256
- [254](https://github.com/cloud-hypervisor/fuse-backend-rs/pull/254): `fuse-backend-rs` is now a thin facade re-exporting the sub-crates. Every
5357
historical `fuse_backend_rs::{abi, api, buffer, common, transport, passthrough,
5458
overlayfs}` import path and cargo feature name (`fusedev`, `virtiofs`,

‎crates/fuse-backend-core/src/abi/fuse_uring.rs‎

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
// Copyright (C) 2026 Alibaba Cloud. All rights reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
//! Linux FUSE-over-io_uring ABI definitions, protocol version 7.42 (kernel 6.14+).
4+
//! Linux FUSE-over-io_uring ABI definitions, protocol version 7.42 (kernel 6.14+),
5+
//! and the buffer pools of protocol 7.46 (kernel 7.3).
56
//!
6-
//! Experimental: the kernel interface is still evolving (reduced queue counts,
7-
//! bufpool/zero-copy extensions are being developed upstream). Only the minimal
8-
//! 7.42 interface is defined here; in-flight extensions are intentionally left
9-
//! out.
7+
//! Experimental: the kernel interface is still evolving.
108
119
#![allow(missing_docs)]
1210

@@ -15,6 +13,9 @@ use vm_memory::ByteValued;
1513
/// INIT flag: client supports serving FUSE requests over io_uring (protocol 7.42).
1614
pub const FUSE_OVER_IO_URING: u64 = 1_u64 << 41;
1715

16+
/// INIT flag the kernel sets when it supports io_uring buffer pools (protocol 7.46).
17+
pub const FUSE_HAS_IO_URING_BUFPOOL: u64 = 1_u64 << 43;
18+
1819
/// Size of the fuse_in_header/fuse_out_header slot inside `FuseUringReqHeader`.
1920
pub const FUSE_URING_IN_OUT_HEADER_SZ: usize = 128;
2021

@@ -34,6 +35,11 @@ pub const FUSE_URING_IOV_PAYLOAD: usize = 1;
3435
pub const FUSE_IO_URING_CMD_INVALID: u32 = 0;
3536
pub const FUSE_IO_URING_CMD_REGISTER: u32 = 1;
3637
pub const FUSE_IO_URING_CMD_COMMIT_AND_FETCH: u32 = 2;
38+
/// Create a queue (protocol 7.46). Buffer pools need it.
39+
pub const FUSE_IO_URING_CMD_ADD_QUEUE: u32 = 3;
40+
/// Give a queue a buffer pool (protocol 7.46). The queue must exist and must
41+
/// not have registered entries with their own payload buffers yet.
42+
pub const FUSE_IO_URING_CMD_ADD_BUFPOOL: u32 = 4;
3743

3844
/// Shared in/out descriptor of one ring entry, embedded in `FuseUringReqHeader`.
3945
#[repr(C)]
@@ -44,7 +50,9 @@ pub struct FuseUringEntInOut {
4450
pub commit_id: u64,
4551
/// Size of the payload in bytes (request: set by kernel, reply: set by daemon).
4652
pub payload_sz: u32,
47-
pub padding: u32,
53+
/// Offset of the request's payload buffer in the queue's buffer pool, if
54+
/// the queue has one (protocol 7.46). Before that it was padding.
55+
pub offset: u32,
4856
pub reserved: u64,
4957
}
5058

@@ -72,8 +80,9 @@ impl Default for FuseUringReqHeader {
7280
/// Command payload of an uring command SQE, placed in the 80-byte command
7381
/// area of an SQE128.
7482
///
75-
/// Layout mirrors `struct fuse_uring_cmd_req`. For REGISTER and
76-
/// COMMIT_AND_FETCH only `commit_id` and `qid` are used.
83+
/// Layout mirrors `struct fuse_uring_cmd_req`. REGISTER and
84+
/// COMMIT_AND_FETCH use `commit_id` and `qid`, ADD_QUEUE uses `qid`, and
85+
/// ADD_BUFPOOL uses `qid` and `bufpool`.
7786
#[repr(C)]
7887
#[derive(Clone, Copy, Debug, Default)]
7988
pub struct FuseUringCmdReq {
@@ -83,13 +92,28 @@ pub struct FuseUringCmdReq {
8392
/// Index of the per-CPU queue the command is for.
8493
pub qid: u16,
8594
pub padding: [u8; 6],
95+
/// The buffer pool of an ADD_BUFPOOL command (protocol 7.46).
96+
pub bufpool: FuseUringBufpool,
97+
}
98+
99+
/// Buffer pool of a queue, `fuse_uring_cmd_req.bufpool`. The kernel cuts it
100+
/// into buffers of the connection's maximum payload size.
101+
#[repr(C)]
102+
#[derive(Clone, Copy, Debug, Default)]
103+
pub struct FuseUringBufpool {
104+
/// Address of the pool in the daemon's memory.
105+
pub uaddr: u64,
106+
/// Length of the pool in bytes.
107+
pub len: u32,
108+
pub reserved: u32,
86109
}
87110

88-
// SAFETY: all three structs are `repr(C)` plain data types without padding
111+
// SAFETY: all four structs are `repr(C)` plain data types without padding
89112
// holes or pointer members, matching their kernel counterparts.
90113
unsafe impl ByteValued for FuseUringEntInOut {}
91114
unsafe impl ByteValued for FuseUringReqHeader {}
92115
unsafe impl ByteValued for FuseUringCmdReq {}
116+
unsafe impl ByteValued for FuseUringBufpool {}
93117

94118
#[cfg(test)]
95119
mod tests {
@@ -99,7 +123,9 @@ mod tests {
99123
fn test_layouts() {
100124
assert_eq!(std::mem::size_of::<FuseUringEntInOut>(), 32);
101125
assert_eq!(std::mem::size_of::<FuseUringReqHeader>(), 288);
102-
assert_eq!(std::mem::size_of::<FuseUringCmdReq>(), 24);
126+
assert_eq!(std::mem::size_of::<FuseUringCmdReq>(), 40);
127+
assert_eq!(std::mem::offset_of!(FuseUringCmdReq, bufpool), 24);
128+
assert_eq!(std::mem::offset_of!(FuseUringEntInOut, offset), 20);
103129
assert_eq!(
104130
std::mem::offset_of!(FuseUringReqHeader, ring_ent_in_out),
105131
FUSE_URING_IN_OUT_HEADER_SZ + FUSE_URING_OP_IN_OUT_SZ

‎crates/fuse-backend-core/src/api/server/mod.rs‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ use std::io::{self, Read};
2121
use std::marker::PhantomData;
2222
use std::mem::size_of;
2323
use std::sync::atomic::AtomicU64;
24+
#[cfg(all(target_os = "linux", feature = "fusedev-uring"))]
25+
use std::sync::atomic::{AtomicBool, AtomicU32};
2426

2527
use crate::abi::fuse_abi::*;
2628
use crate::api::filesystem::{Context, FileSystem, ZeroCopyReader, ZeroCopyWriter};
@@ -65,6 +67,15 @@ pub struct Server<F: FileSystem + Sync> {
6567
/// Capability flags actually enabled by the INIT exchange.
6668
#[cfg(all(target_os = "linux", feature = "fusedev-uring"))]
6769
negotiated_init_flags: AtomicU64,
70+
/// Whether the kernel offered io_uring buffer pools in INIT.
71+
#[cfg(all(target_os = "linux", feature = "fusedev-uring"))]
72+
uring_bufpool: AtomicBool,
73+
/// `max_write` of the INIT reply.
74+
#[cfg(all(target_os = "linux", feature = "fusedev-uring"))]
75+
init_max_write: AtomicU32,
76+
/// `max_pages` of the INIT reply, 0 if it doesn't set `FUSE_MAX_PAGES`.
77+
#[cfg(all(target_os = "linux", feature = "fusedev-uring"))]
78+
init_max_pages: AtomicU32,
6879
}
6980

7081
impl<F: FileSystem + Sync> Server<F> {
@@ -78,6 +89,12 @@ impl<F: FileSystem + Sync> Server<F> {
7889
extra_init_flags: AtomicU64::new(0),
7990
#[cfg(all(target_os = "linux", feature = "fusedev-uring"))]
8091
negotiated_init_flags: AtomicU64::new(0),
92+
#[cfg(all(target_os = "linux", feature = "fusedev-uring"))]
93+
uring_bufpool: AtomicBool::new(false),
94+
#[cfg(all(target_os = "linux", feature = "fusedev-uring"))]
95+
init_max_write: AtomicU32::new(0),
96+
#[cfg(all(target_os = "linux", feature = "fusedev-uring"))]
97+
init_max_pages: AtomicU32::new(0),
8198
}
8299
}
83100

@@ -104,6 +121,51 @@ impl<F: FileSystem + Sync> Server<F> {
104121
self.negotiated_init_flags.load(Ordering::Acquire) & FsOptions::OVER_IO_URING.bits() != 0
105122
}
106123

124+
/// Report whether the kernel offered buffer pools for FUSE-over-io_uring
125+
/// in INIT (`FUSE_HAS_IO_URING_BUFPOOL`, Linux 7.3). Returns false before
126+
/// INIT completes.
127+
#[cfg(all(target_os = "linux", feature = "fusedev-uring"))]
128+
pub fn uring_bufpool_supported(&self) -> bool {
129+
self.uring_bufpool.load(Ordering::Acquire)
130+
}
131+
132+
/// The size of the payload buffer the kernel needs for one
133+
/// FUSE-over-io_uring request on this connection, and the size of each
134+
/// buffer it cuts from a buffer pool. It follows `fuse_uring_create()`:
135+
/// the largest of `FUSE_MIN_READ_BUFFER`, `max_write` and `max_pages`
136+
/// pages. The kernel can lower `max_pages` to its `max_pages_limit`
137+
/// sysctl, but the INIT reply sets `max_write` to at least `max_pages`
138+
/// pages, so that doesn't change the size. Meaningful only after INIT.
139+
#[cfg(all(target_os = "linux", feature = "fusedev-uring"))]
140+
pub fn uring_payload_size(&self) -> usize {
141+
// The defaults of process_init_reply() and fuse_conn_init().
142+
const MIN_MAX_WRITE: usize = 4096;
143+
const DEFAULT_MAX_PAGES: usize = 32;
144+
let max_write = self.init_max_write.load(Ordering::Acquire) as usize;
145+
let max_pages = match self.init_max_pages.load(Ordering::Acquire) as usize {
146+
0 => DEFAULT_MAX_PAGES,
147+
n => n,
148+
};
149+
(MIN_READ_BUFFER as usize)
150+
.max(max_write.max(MIN_MAX_WRITE))
151+
.max(max_pages * crate::buffer::pagesize())
152+
}
153+
154+
/// Remember what FUSE-over-io_uring needs from INIT: whether the kernel
155+
/// offered buffer pools in `kernel_flags`, and the `max_write` and
156+
/// `max_pages` of the reply.
157+
#[cfg(all(target_os = "linux", feature = "fusedev-uring"))]
158+
fn record_uring_init(&self, kernel_flags: u64, max_write: u32, max_pages: u16) {
159+
use crate::abi::fuse_uring::FUSE_HAS_IO_URING_BUFPOOL;
160+
self.uring_bufpool.store(
161+
kernel_flags & FUSE_HAS_IO_URING_BUFPOOL != 0,
162+
Ordering::Release,
163+
);
164+
self.init_max_write.store(max_write, Ordering::Release);
165+
self.init_max_pages
166+
.store(u32::from(max_pages), Ordering::Release);
167+
}
168+
107169
/// OR the extra INIT flags requested through `set_uring()` into the
108170
/// enabled capability set and remember the negotiation outcome.
109171
#[cfg(all(target_os = "linux", feature = "fusedev-uring"))]

‎crates/fuse-backend-core/src/api/server/sync_io.rs‎

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -977,6 +977,8 @@ impl<F: FileSystem + Sync> Server<F> {
977977
out.max_pages = MAX_REQ_PAGES;
978978
out.max_write = MAX_REQ_PAGES as u32 * pagesize() as u32; // 1MB
979979
}
980+
#[cfg(all(target_os = "linux", feature = "fusedev-uring"))]
981+
self.record_uring_init(flags_u64, out.max_write, out.max_pages);
980982
self.vers.store(
981983
encode_version(version.major, version.minor),
982984
Ordering::Release,
@@ -1726,10 +1728,10 @@ mod tests {
17261728
use super::super::test_util::{MockFS, TestWriter};
17271729
use super::*;
17281730

1729-
fn prepare_srvcontext<'a>(
1731+
fn prepare_srvcontext<'a, F: FileSystem>(
17301732
read_buf: &'a mut [u8],
17311733
write_buf: &'a mut [u8],
1732-
) -> SrvContext<'a, MockFS, (), TestWriter<'a>> {
1734+
) -> SrvContext<'a, F, (), TestWriter<'a>> {
17331735
let reader = Reader::<()>::from_slice(read_buf);
17341736
let writer = TestWriter::new(write_buf);
17351737
let in_header = InHeader::default();
@@ -2019,6 +2021,77 @@ mod tests {
20192021
);
20202022
}
20212023

2024+
#[cfg(feature = "fusedev-uring")]
2025+
#[test]
2026+
fn test_server_init_uring_bufpool() {
2027+
use crate::abi::fuse_uring::FUSE_HAS_IO_URING_BUFPOOL;
2028+
2029+
/// A file system that wants everything the kernel offers.
2030+
struct WantAll;
2031+
impl FileSystem for WantAll {
2032+
type Inode = u64;
2033+
type Handle = u64;
2034+
fn init(&self, capable: FsOptions) -> io::Result<FsOptions> {
2035+
Ok(capable)
2036+
}
2037+
}
2038+
2039+
// Run INIT with `flags` and `flags2` from the kernel, and return the
2040+
// server and the reply.
2041+
let init = |flags: FsOptions, flags2: u64| {
2042+
let mut read_buf = [0u8; size_of::<InitIn>() + size_of::<InitIn2>()];
2043+
let init_in = InitIn {
2044+
major: KERNEL_VERSION,
2045+
minor: KERNEL_MINOR_VERSION,
2046+
max_readahead: 0,
2047+
flags: (flags | FsOptions::INIT_EXT).bits() as u32,
2048+
};
2049+
read_buf[..size_of::<InitIn>()].copy_from_slice(init_in.as_slice());
2050+
let init_in2 = InitIn2 {
2051+
flags2: ((FsOptions::OVER_IO_URING.bits() | flags2) >> 32) as u32,
2052+
unused: [0; 11],
2053+
};
2054+
read_buf[size_of::<InitIn>()..].copy_from_slice(init_in2.as_slice());
2055+
let server = Server::new(WantAll);
2056+
server.set_uring(true);
2057+
let mut write_buf = [0u8; 4096];
2058+
let res = {
2059+
let ctx = prepare_srvcontext(&mut read_buf, &mut write_buf);
2060+
server.init(ctx, |_| {}).unwrap()
2061+
};
2062+
let mut out = InitOut::default();
2063+
out.as_mut_slice()
2064+
.copy_from_slice(&write_buf[size_of::<OutHeader>()..res]);
2065+
(server, out)
2066+
};
2067+
2068+
// The kernel's flag for buffer pools isn't an FsOptions flag, but
2069+
// the server still sees it.
2070+
let (server, _) = init(FsOptions::empty(), FUSE_HAS_IO_URING_BUFPOOL);
2071+
assert!(server.uring_bufpool_supported());
2072+
let (server, out) = init(FsOptions::empty(), 0);
2073+
assert!(!server.uring_bufpool_supported());
2074+
// Without FUSE_MAX_PAGES the kernel keeps 32 pages per request.
2075+
assert_eq!(out.max_pages, 0);
2076+
assert_eq!(
2077+
server.uring_payload_size(),
2078+
(MIN_READ_BUFFER as usize)
2079+
.max(out.max_write as usize)
2080+
.max(32 * pagesize())
2081+
);
2082+
2083+
// With FUSE_MAX_PAGES the kernel takes max_pages up to its limit.
2084+
let (server, out) = init(FsOptions::MAX_PAGES | FsOptions::BIG_WRITES, 0);
2085+
assert_eq!(out.max_pages, MAX_REQ_PAGES);
2086+
assert_eq!(
2087+
server.uring_payload_size(),
2088+
MAX_REQ_PAGES as usize * pagesize()
2089+
);
2090+
// The kernel's max_pages_limit can't change the size, because the
2091+
// reply's max_write covers max_pages pages.
2092+
assert!(out.max_write as usize >= MAX_REQ_PAGES as usize * pagesize());
2093+
}
2094+
20222095
#[cfg(feature = "fusedev-uring")]
20232096
#[test]
20242097
fn test_server_init_uring_negotiation() {

0 commit comments

Comments
 (0)