Skip to main content

agave_io_uring/
ring.rs

1use {
2    crate::slab::FixedSlab,
3    io_uring::{
4        IoUring, cqueue, squeue,
5        types::{SubmitArgs, Timespec},
6    },
7    smallvec::{SmallVec, smallvec},
8    std::{io, os::fd::RawFd, time::Duration},
9};
10
11/// Trait for accessing the context and pushing operations to the [Ring].
12///
13/// Enables generic operations on [Ring] or [Completion].
14pub trait RingAccess {
15    type Context;
16    type Operation;
17
18    /// Returns a reference to the context value stored in a [Ring].
19    fn context(&self) -> &Self::Context;
20
21    /// Returns a mutable reference to the context value stored in a [Ring].
22    fn context_mut(&mut self) -> &mut Self::Context;
23
24    /// Pushes an operation for execution in io_uring.
25    ///
26    /// Once completed, [RingOp::complete] will be called with the result.
27    ///
28    /// Note that the exact moment the operation is submitted to the kernel is implementation
29    /// specific.
30    fn push(&mut self, op: Self::Operation) -> io::Result<()>;
31}
32
33/// An io_uring instance.
34pub struct Ring<T, E: RingOp<T>> {
35    ring: IoUring,
36    entries: FixedSlab<E>,
37    context: T,
38}
39
40impl<T, E: RingOp<T>> Ring<T, E> {
41    /// Creates a new ring with the provided io_uring instance and context.
42    ///
43    /// The context `T` is a user defined value that will be passed to entries `E` once they
44    /// complete. This value can be used to update state or perform additional actions as operations
45    /// complete asynchronously.
46    pub fn new(ring: IoUring, ctx: T) -> Self {
47        Self {
48            entries: FixedSlab::with_capacity(ring.params().cq_entries() as usize),
49            ring,
50            context: ctx,
51        }
52    }
53
54    /// Registers in-memory fixed buffers for I/O with the kernel.
55    ///
56    /// # Safety
57    ///
58    /// Callers must ensure that the iov_base and iov_len values are valid and will be valid until
59    /// buffers are unregistered or the ring destroyed, otherwise undefined behaviour may occur.
60    ///
61    /// See
62    /// [Submitter::register_buffers](https://docs.rs/io-uring/0.6.3/io_uring/struct.Submitter.html#method.register_buffers).
63    pub unsafe fn register_buffers(&self, iovecs: &[libc::iovec]) -> io::Result<()> {
64        unsafe { self.ring.submitter().register_buffers(iovecs) }
65    }
66
67    /// Registers file descriptors as fixed for I/O with the kernel.
68    ///
69    /// Operations may then use `types::Fixed(index)` for index in `fds` to refer to the
70    /// registered file descriptor.
71    ///
72    /// `-1` values can be used as slots for kernel managed fixed file descriptors (created by
73    /// open operation).
74    pub fn register_files(&self, fds: &[RawFd]) -> io::Result<()> {
75        self.ring.submitter().register_files(fds)
76    }
77
78    /// Submits all pending operations to the kernel.
79    ///
80    /// If the ring can't accept any more submissions because the completion
81    /// queue is full, this will process completions and retry until the
82    /// submissions are accepted.
83    ///
84    /// See also [Ring::process_completions].
85    pub fn submit(&mut self) -> io::Result<()> {
86        self.submit_and_wait(0, None).map(|_| ())
87    }
88
89    /// Submits all pending operations to the kernel and waits for completions.
90    ///
91    /// If no `timeout` is passed this will block until `want` completions are available. If a
92    /// timeout is passed, this will block until `want` completions are available or the timeout is
93    /// reached.
94    ///
95    /// Returns the number of completions received.
96    pub fn submit_and_wait(&mut self, want: usize, timeout: Option<Duration>) -> io::Result<usize> {
97        let mut args = SubmitArgs::new();
98        let ts;
99        if let Some(timeout) = timeout {
100            ts = Timespec::from(timeout);
101            args = args.timespec(&ts);
102        }
103
104        loop {
105            match self.ring.submitter().submit_with_args(want, &args) {
106                Ok(n) => return Ok(n),
107                Err(e) if e.raw_os_error() == Some(libc::ETIME) => return Ok(0),
108                Err(e) if e.raw_os_error() == Some(libc::EBUSY) => {
109                    // the completion queue is full, process completions and retry
110                    self.process_completions()?;
111                    continue;
112                }
113                Err(e) if e.raw_os_error() == Some(libc::EINTR) => return Ok(0),
114                Err(e) => return Err(e),
115            }
116        }
117    }
118
119    /// Processes completions from the kernel.
120    ///
121    /// This will process all completions currently available in the completion
122    /// queue and invoke [RingOp::complete] for each completed operation.
123    pub fn process_completions(&mut self) -> io::Result<()> {
124        let mut completion = self.ring.completion();
125        let mut new_entries = smallvec![];
126        while let Some(cqe) = completion.next() {
127            let completed_key = cqe.user_data() as usize;
128            let entry = self.entries.get_mut(completed_key).unwrap();
129            let result = entry.result(cqe.result());
130            let mut comp_ctx = Completion {
131                context: &mut self.context,
132                new_entries,
133            };
134            let res = entry.complete(&mut comp_ctx, result);
135            if !cqueue::more(cqe.flags()) {
136                self.entries.remove(completed_key);
137            }
138            res?;
139            new_entries = std::mem::take(&mut comp_ctx.new_entries);
140            if !new_entries.is_empty() {
141                completion.sync();
142                drop(completion);
143                for new_entry in new_entries.drain(..) {
144                    self.push(new_entry)?;
145                }
146                completion = self.ring.completion();
147            }
148        }
149
150        Ok(())
151    }
152
153    /// Drains the ring.
154    ///
155    /// This will submit all pending operations to the kernel and process all
156    /// completions until the ring is empty.
157    pub fn drain(&mut self) -> io::Result<()> {
158        loop {
159            self.process_completions()?;
160
161            if self.entries.is_empty() {
162                break;
163            }
164
165            match self.ring.submitter().submit_with_args(
166                1,
167                &SubmitArgs::new().timespec(&Timespec::from(Duration::from_millis(10))),
168            ) {
169                Ok(_) => {}
170                Err(e) if e.raw_os_error() == Some(libc::ETIME) => {}
171                Err(e) => return Err(e),
172            }
173        }
174
175        Ok(())
176    }
177}
178
179/// Trait for operations that can be submitted to a [Ring].
180pub trait RingOp<T> {
181    fn entry(&mut self) -> squeue::Entry;
182    fn complete(&mut self, ctx: &mut Completion<T, Self>, res: io::Result<i32>) -> io::Result<()>
183    where
184        Self: Sized;
185    fn result(&self, res: i32) -> io::Result<i32> {
186        if res < 0 {
187            Err(io::Error::from_raw_os_error(res.wrapping_neg()))
188        } else {
189            Ok(res)
190        }
191    }
192}
193
194/// Context object passed to [RingOp::complete].
195pub struct Completion<'a, T, E: RingOp<T>> {
196    // Give new_entries a stack size of 2 to avoid heap allocations in the common case where only
197    // one or two ops are queued from a completion handler.
198    //
199    // It's common to want to queue some extra work after a completion, for instance if you've
200    // completed a read and want to close the file descriptor, or if you're doing chained operations
201    // and want to push the next one. It's less common to want to queue many operations.
202    new_entries: SmallVec<[E; 2]>,
203    context: &'a mut T,
204}
205
206impl<T, E: RingOp<T>> RingAccess for Ring<T, E> {
207    type Context = T;
208    type Operation = E;
209
210    fn context(&self) -> &T {
211        &self.context
212    }
213
214    fn context_mut(&mut self) -> &mut T {
215        &mut self.context
216    }
217
218    /// Pushes an operation to the submission queue.
219    ///
220    /// Note that the operation is not submitted to the kernel until [Ring::submit] is called. If
221    /// the submission queue is full, submit will be called internally to make room for the new
222    /// operation.
223    ///
224    /// See also [Ring::submit].
225    fn push(&mut self, op: E) -> io::Result<()> {
226        loop {
227            self.process_completions()?;
228
229            if !self.entries.is_full() {
230                break;
231            }
232            // if the entries slab is full, we need to submit and poll
233            // completions to make room
234            self.submit_and_wait(1, None)?;
235        }
236        let key = self.entries.insert(op);
237        let entry = self.entries.get_mut(key).unwrap().entry();
238        let entry = entry.user_data(key as u64);
239        // Safety: the entry is stored in self.entries and guaranteed to be valid for the lifetime
240        // of the operation. E implementations must still ensure that the entry
241        // remains valid until the last E::complete call.
242        while unsafe { self.ring.submission().push(&entry) }.is_err() {
243            self.submit()?;
244            self.process_completions()?;
245        }
246
247        Ok(())
248    }
249}
250
251impl<T, E: RingOp<T>> RingAccess for Completion<'_, T, E> {
252    type Context = T;
253    type Operation = E;
254
255    fn context(&self) -> &T {
256        self.context
257    }
258
259    fn context_mut(&mut self) -> &mut T {
260        self.context
261    }
262
263    /// This can be used to push new operations from within [RingOp::complete].
264    ///
265    /// Note that the operations are buffered until completion is finished and then pushed
266    /// to the parent [Ring].
267    fn push(&mut self, op: E) -> io::Result<()> {
268        self.new_entries.push(op);
269        Ok(())
270    }
271}