2323use std:: os:: unix:: io:: AsRawFd ;
2424use std:: sync:: Arc ;
2525
26- use io_uring:: IoUring ;
2726use io_uring:: opcode;
2827use io_uring:: types:: Fd ;
29- use parking_lot:: Mutex ;
3028
3129use crate :: Result ;
3230use crate :: errors:: PagedbError ;
3331use crate :: vfs:: blocking:: offload;
32+ use crate :: vfs:: iouring:: ring:: { SharedRing , SubmitError } ;
3433use crate :: vfs:: traits:: {
35- VfsFile , checked_indexed_completion , checked_iouring_positioned_offset, checked_read_count,
36- checked_signed_file_len , write_all_at,
34+ VfsFile , checked_iouring_positioned_offset, checked_read_count, checked_signed_file_len ,
35+ write_all_at,
3736} ;
3837use crate :: vfs:: types:: { ReadReq , WriteReq } ;
3938
4039/// Per-file handle backed by an `std::fs::File` fd and the shared `io_uring`.
4140///
4241/// `Send` and `Sync` are both derived, not asserted: `Arc<std::fs::File>` is
43- /// thread-safe, and `io_uring::IoUring` is `Send + Sync` , so `Arc<Mutex<_>>`
44- /// around it is too. Nothing about this type needs a hand-written auto-trait
45- /// impl, and it must not grow one — a manual impl would silently keep holding
46- /// once a future field stopped qualifying.
42+ /// thread-safe, and `SharedRing` wraps the ring in a `Mutex` , so an `Arc` of
43+ /// it is too. Nothing about this type needs a hand-written auto-trait impl,
44+ /// and it must not grow one — a manual impl would silently keep holding once a
45+ /// future field stopped qualifying.
4746pub struct IouringFile {
4847 /// Shared so a blocking-pool call can own a reference to the descriptor
4948 /// for its whole duration, independently of when this handle drops.
5049 file : Arc < std:: fs:: File > ,
5150 writable : bool ,
52- ring : Arc < Mutex < IoUring > > ,
51+ ring : Arc < SharedRing > ,
5352}
5453
5554impl IouringFile {
56- pub ( crate ) fn new ( file : std:: fs:: File , writable : bool , ring : Arc < Mutex < IoUring > > ) -> Self {
55+ pub ( crate ) fn new ( file : std:: fs:: File , writable : bool , ring : Arc < SharedRing > ) -> Self {
5756 Self {
5857 file : Arc :: new ( file) ,
5958 writable,
@@ -63,7 +62,7 @@ impl IouringFile {
6362
6463 /// The two owned handles a blocking-pool cycle needs. Both are `Arc`
6564 /// clones, and both keep their target alive for as long as the cycle runs.
66- fn shared ( & self ) -> ( Arc < std:: fs:: File > , Arc < Mutex < IoUring > > ) {
65+ fn shared ( & self ) -> ( Arc < std:: fs:: File > , Arc < SharedRing > ) {
6766 ( Arc :: clone ( & self . file ) , Arc :: clone ( & self . ring ) )
6867 }
6968
@@ -91,121 +90,32 @@ impl IouringFile {
9190 . map_err ( |_| PagedbError :: Io ( std:: io:: Error :: other ( "buffer too large for u32" ) ) )
9291 }
9392
94- /// Submit a single SQE, wait for exactly one CQE with matching
95- /// `user_data`, and return the CQE result.
93+ /// Run one submit + drain cycle, translating the ring's memory-safety
94+ /// verdict into a plain error while leaking whatever the kernel may still
95+ /// own.
9696 ///
97- /// # Safety
98- ///
99- /// The caller must ensure that any buffers referenced by the SQE remain
100- /// valid for the duration of this call (i.e. until `submit_and_wait`
101- /// returns and the CQE is drained). Because the ring lock is held across
102- /// the entire submit+drain sequence and we wait for the exact CQE before
103- /// returning, this invariant is satisfied for any buffer whose lifetime
104- /// outlasts this function.
105- unsafe fn submit_one (
106- ring : & mut IoUring ,
107- entry : & io_uring:: squeue:: Entry ,
108- user_data : u64 ,
109- ) -> std:: io:: Result < i32 > {
110- // SAFETY: caller guarantees the buffers referenced by `entry` are live.
111- unsafe {
112- ring. submission ( )
113- . push ( entry)
114- . map_err ( |_| std:: io:: Error :: other ( "submission queue full" ) ) ?;
115- }
116- ring. submit_and_wait ( 1 ) ?;
117- let mut result = None ;
118- {
119- let mut cq = ring. completion ( ) ;
120- cq. sync ( ) ;
121- for cqe in cq. by_ref ( ) {
122- if cqe. user_data ( ) == user_data {
123- result = Some ( cqe. result ( ) ) ;
124- break ;
125- }
126- // Stale CQEs from prior submissions are discarded.
127- }
128- }
129- let res =
130- result. ok_or_else ( || std:: io:: Error :: other ( "io_uring: expected CQE not found" ) ) ?;
131- if res < 0 {
132- Err ( std:: io:: Error :: from_raw_os_error ( -res) )
133- } else {
134- Ok ( res)
135- }
136- }
137-
138- /// Submit a batch of SQEs and wait for all of them. Each SQE must carry
139- /// its index (0..n) as `user_data`. Returns CQE results in submission order.
140- ///
141- /// # Safety
142- ///
143- /// All buffers referenced by every entry in `entries` must remain valid
144- /// until this function returns (same contract as `submit_one`).
145- unsafe fn submit_batch (
146- ring : & mut IoUring ,
97+ /// `buffers` is every allocation the SQEs point at. On the abandoned path
98+ /// it is forgotten rather than dropped: entries are still outstanding, and
99+ /// the kernel writing into reclaimed memory is the one outcome that must
100+ /// not be possible. Leaking a bounded amount on a failing device is the
101+ /// cheap side of that trade.
102+ fn run_cycle < B > (
103+ ring : & SharedRing ,
147104 entries : & [ io_uring:: squeue:: Entry ] ,
148- ) -> std:: io:: Result < Vec < i32 > > {
149- let total = entries. len ( ) ;
150- if total == 0 {
151- return Ok ( Vec :: new ( ) ) ;
152- }
153- // Cap each submission at the ring's SQ depth. Larger callers
154- // (a full B+ tree flush) get chunked across multiple ring round-trips.
155- // Each chunk re-tags `user_data` with the index within the chunk so
156- // the CQE drain can match results into the global results vector.
157- let chunk_size = crate :: vfs:: iouring:: ring:: RING_DEPTH as usize ;
158- let mut results = vec ! [ 0i32 ; total] ;
159- let mut base = 0usize ;
160- while base < total {
161- let end = ( base + chunk_size) . min ( total) ;
162- let chunk_len = end - base;
163- {
164- let mut sq = ring. submission ( ) ;
165- for ( i, entry) in entries[ base..end] . iter ( ) . enumerate ( ) {
166- // Re-tag with the in-chunk index. The caller-assigned
167- // `user_data` is overwritten because the outer `for cqe`
168- // loop needs a stable 0..chunk_len keyspace.
169- let tagged = entry. clone ( ) . user_data ( i as u64 ) ;
170- // SAFETY: caller guarantees buffers are live for `entries`.
171- unsafe {
172- sq. push ( & tagged)
173- . map_err ( |_| std:: io:: Error :: other ( "submission queue full" ) ) ?;
174- }
175- }
105+ buffers : B ,
106+ ) -> Result < ( Vec < i32 > , B ) > {
107+ // SAFETY: `buffers` owns every allocation the entries reference and is
108+ // moved into this function, so the buffers outlive the call; on the
109+ // `Abandoned` path it is leaked instead of dropped, which extends that
110+ // lifetime for the rest of the process as the contract requires.
111+ match unsafe { ring. submit_and_collect ( entries) } {
112+ Ok ( results) => Ok ( ( results, buffers) ) ,
113+ Err ( SubmitError :: Settled ( error) ) => Err ( PagedbError :: Io ( error) ) ,
114+ Err ( abandoned @ SubmitError :: Abandoned ( _) ) => {
115+ std:: mem:: forget ( buffers) ;
116+ Err ( PagedbError :: Io ( abandoned. into_io ( ) ) )
176117 }
177- ring. submit_and_wait ( chunk_len) ?;
178- let mut chunk_results = vec ! [ None ; chunk_len] ;
179- let mut found = 0usize ;
180- {
181- let mut cq = ring. completion ( ) ;
182- cq. sync ( ) ;
183- for cqe in cq. by_ref ( ) {
184- if checked_indexed_completion ( & mut chunk_results, cqe. user_data ( ) , cqe. result ( ) )
185- . map_err ( |error| match error {
186- PagedbError :: Io ( io) => io,
187- other => std:: io:: Error :: other ( other. to_string ( ) ) ,
188- } ) ?
189- {
190- found += 1 ;
191- }
192- if found == chunk_len {
193- break ;
194- }
195- }
196- }
197- if found < chunk_len {
198- return Err ( std:: io:: Error :: other (
199- "io_uring: fewer CQEs returned than submitted" ,
200- ) ) ;
201- }
202- for ( index, result) in chunk_results. into_iter ( ) . enumerate ( ) {
203- results[ base + index] = result
204- . ok_or_else ( || std:: io:: Error :: other ( "io_uring: missing indexed CQE result" ) ) ?;
205- }
206- base = end;
207118 }
208- Ok ( results)
209119 }
210120}
211121
@@ -227,15 +137,9 @@ impl VfsFile for IouringFile {
227137 . offset ( offset)
228138 . build ( )
229139 . user_data ( 0 ) ;
230- let n = {
231- let mut guard = ring. lock ( ) ;
232- // SAFETY: `scratch` is owned by this closure and outlives the
233- // submit+drain below, and the ring lock is held across both, so
234- // the kernel is finished with the buffer before this returns.
235- unsafe { IouringFile :: submit_one ( & mut guard, & entry, 0 ) }
236- }
237- . map_err ( PagedbError :: Io ) ?;
238- // n >= 0 guaranteed by submit_one (negative becomes Err).
140+ let ( results, scratch) = IouringFile :: run_cycle ( & ring, & [ entry] , scratch) ?;
141+ let n = single_result ( & results) ?;
142+ // `single_result` rejects a negative CQE, so this is non-negative.
239143 #[ allow( clippy:: cast_sign_loss) ]
240144 let read = checked_read_count ( n as usize , len) ?;
241145 Ok ( ( scratch, read) )
@@ -273,15 +177,7 @@ impl VfsFile for IouringFile {
273177 ) ;
274178 }
275179
276- let results = {
277- let mut guard = ring. lock ( ) ;
278- // SAFETY: every buffer referenced by `entries` lives in
279- // `buffers`, owned by this closure, and the ring lock is held
280- // across submit+drain so the kernel cannot touch them after
281- // `submit_batch` returns.
282- unsafe { IouringFile :: submit_batch ( & mut guard, & entries) }
283- }
284- . map_err ( PagedbError :: Io ) ?;
180+ let ( results, buffers) = IouringFile :: run_cycle ( & ring, & entries, buffers) ?;
285181 drop ( entries) ; // buf raw-ptrs no longer needed
286182
287183 let mut out: Vec < ( Vec < u8 > , usize ) > = Vec :: with_capacity ( plan. len ( ) ) ;
@@ -328,14 +224,10 @@ impl VfsFile for IouringFile {
328224 . offset ( offset)
329225 . build ( )
330226 . user_data ( 0 ) ;
331- let n = {
332- let mut guard = ring. lock ( ) ;
333- // SAFETY: `data` is owned by this closure and outlives the
334- // submit+drain below; the ring lock is held across both.
335- unsafe { IouringFile :: submit_one ( & mut guard, & entry, 0 ) }
336- }
337- . map_err ( PagedbError :: Io ) ?;
338- // n >= 0 guaranteed by submit_one.
227+ let ( results, data) = IouringFile :: run_cycle ( & ring, & [ entry] , data) ?;
228+ let n = single_result ( & results) ?;
229+ drop ( data) ;
230+ // `single_result` rejects a negative CQE, so this is non-negative.
339231 #[ allow( clippy:: cast_sign_loss) ]
340232 let written = n as usize ;
341233 Ok ( written)
@@ -388,14 +280,7 @@ impl VfsFile for IouringFile {
388280 ) ;
389281 }
390282
391- let results = {
392- let mut guard = ring. lock ( ) ;
393- // SAFETY: every buffer referenced by `entries` lives in `plan`,
394- // owned by this closure, and the ring lock is held across
395- // submit+drain.
396- unsafe { IouringFile :: submit_batch ( & mut guard, & entries) }
397- }
398- . map_err ( PagedbError :: Io ) ?;
283+ let ( results, plan) = IouringFile :: run_cycle ( & ring, & entries, plan) ?;
399284 drop ( entries) ;
400285
401286 let mut short_writes = Vec :: new ( ) ;
@@ -448,13 +333,11 @@ impl VfsFile for IouringFile {
448333 offload ( move || {
449334 let fd = Fd ( file. as_raw_fd ( ) ) ;
450335 let entry = opcode:: Fsync :: new ( fd) . build ( ) . user_data ( 0 ) ;
451- let completed = {
452- let mut guard = ring. lock ( ) ;
453- // SAFETY: `Fsync` carries no buffer pointer; there is nothing
454- // to alias. The `Arc` keeps the fd open for the whole call.
455- unsafe { IouringFile :: submit_one ( & mut guard, & entry, 0 ) }
456- } ;
457- completed. map_err ( PagedbError :: Io ) ?;
336+ // `Fsync` carries no buffer pointer, so there is nothing the
337+ // kernel could still be reading; the unit payload makes that
338+ // explicit rather than leaving an empty allocation to leak.
339+ let ( results, ( ) ) = IouringFile :: run_cycle ( & ring, & [ entry] , ( ) ) ?;
340+ single_result ( & results) ?;
458341 Ok ( ( ) )
459342 } )
460343 . await
@@ -495,3 +378,14 @@ impl VfsFile for IouringFile {
495378 true
496379 }
497380}
381+
382+ /// Unwrap a one-entry cycle's result, turning a negative CQE into its errno.
383+ fn single_result ( results : & [ i32 ] ) -> Result < i32 > {
384+ let result = * results
385+ . first ( )
386+ . ok_or_else ( || PagedbError :: Io ( std:: io:: Error :: other ( "io_uring: no CQE for entry" ) ) ) ?;
387+ if result < 0 {
388+ return Err ( PagedbError :: Io ( std:: io:: Error :: from_raw_os_error ( -result) ) ) ;
389+ }
390+ Ok ( result)
391+ }
0 commit comments