Skip to main content

pal_uring/
uring.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Driver implementation for the `pal` crate's io-uring threadpool.
5
6use super::threadpool::Io;
7use super::threadpool::IoInitiator;
8use futures::FutureExt;
9use io_uring::opcode;
10use io_uring::squeue;
11use io_uring::types::TimeoutFlags;
12use io_uring::types::Timespec;
13use pal_async::fd::FdReadyDriver;
14use pal_async::fd::PollFdReady;
15use pal_async::interest::InterestSlot;
16use pal_async::interest::PollEvents;
17use pal_async::interest::SLOT_COUNT;
18use pal_async::timer::Instant;
19use pal_async::timer::PollTimer;
20use pal_async::timer::TimerDriver;
21use pal_async::wait::MAXIMUM_WAIT_READ_SIZE;
22use pal_async::wait::PollWait;
23use pal_async::wait::WaitDriver;
24use std::fmt::Debug;
25use std::io;
26use std::os::unix::prelude::*;
27use std::sync::OnceLock;
28use std::task::Context;
29use std::task::Poll;
30use std::task::Waker;
31
32/// An object that can be used to initiate an IO, by returning a reference to an
33/// [`IoInitiator`].
34pub trait Initiate: 'static + Send + Sync + Unpin {
35    /// Returns a reference to the initiator to use for IO operations.
36    ///
37    /// A different initiator may be returned each time this is called, allowing
38    /// an object (timer, socket, etc.) to be moved between initiators.
39    fn initiator(&self) -> &IoInitiator;
40}
41
42impl Initiate for IoInitiator {
43    fn initiator(&self) -> &IoInitiator {
44        self
45    }
46}
47
48/// A [`pal_async::fd::PollFdReady`] implementation for io_uring.
49#[derive(Debug)]
50pub struct FdReady<T: Initiate> {
51    fd: RawFd,
52    initiator: T,
53    interests: [Interest; SLOT_COUNT],
54}
55
56impl<T: Initiate> FdReady<T> {
57    /// Creates a new `FdReady` for the given file descriptor and initiator.
58    pub fn new(initiator: T, fd: RawFd) -> Self {
59        FdReady {
60            fd,
61            initiator,
62            interests: Default::default(),
63        }
64    }
65}
66
67impl FdReadyDriver for IoInitiator {
68    type FdReady = FdReady<Self>;
69
70    fn new_fd_ready(&self, fd: RawFd) -> io::Result<Self::FdReady> {
71        Ok(FdReady::new(self.clone(), fd))
72    }
73}
74
75#[derive(Debug, Default)]
76struct Interest {
77    io: Option<Io<()>>,
78    cancelled: bool,
79    events: PollEvents,
80    revents: PollEvents,
81}
82
83impl<T: Initiate> PollFdReady for FdReady<T> {
84    fn poll_fd_ready(
85        &mut self,
86        cx: &mut Context<'_>,
87        slot: InterestSlot,
88        events: PollEvents,
89    ) -> Poll<PollEvents> {
90        let interest = &mut self.interests[slot as usize];
91        loop {
92            if !(interest.revents & events).is_empty() {
93                break Poll::Ready(interest.revents & events);
94            } else if let Some(io) = &mut interest.io {
95                // Cancel the current operation if not all the requested events
96                // are included in the current IO.
97                //
98                // FUTURE: just update the current poll operation. This requires
99                // >= Linux 5.11.
100                if interest.events & events != events && !interest.cancelled {
101                    io.cancel_poll();
102                    interest.cancelled = true;
103                }
104                let result = std::task::ready!(io.poll_unpin(cx));
105                interest.io = None;
106                match result {
107                    Ok(poll_revents) => {
108                        interest.revents |= PollEvents::from_poll_events(poll_revents as i16);
109                    }
110                    Err(err) if err.raw_os_error() == Some(libc::ECANCELED) => {}
111                    Err(err) => panic!("poll failed: {}", err),
112                }
113            } else {
114                interest.events = events;
115                let sqe = opcode::PollAdd::new(
116                    io_uring::types::Fd(self.fd),
117                    events.to_poll_events() as u32,
118                )
119                .build();
120                // SAFETY: the PollAdd entry does not reference any external
121                // memory.
122                let io = unsafe { Io::new(self.initiator.initiator().clone(), sqe, ()) };
123                interest.io = Some(io);
124                interest.cancelled = false;
125            }
126        }
127    }
128
129    fn clear_fd_ready(&mut self, slot: InterestSlot) {
130        let interest = &mut self.interests[slot as usize];
131        interest.revents = PollEvents::EMPTY;
132    }
133}
134
135/// A [`pal_async::wait::PollWait`] implementation for io_uring.
136#[derive(Debug)]
137pub struct FdWait<T: Initiate> {
138    inner: FdWaitInner<T>,
139}
140
141#[derive(Debug)]
142enum FdWaitInner<T: Initiate> {
143    ViaPoll(pal_async::unix::FdWait<FdReady<T>>),
144    ViaRead(FdWaitViaRead<T>),
145}
146
147impl WaitDriver for IoInitiator {
148    type Wait = FdWait<Self>;
149
150    fn new_wait(&self, fd: RawFd, read_size: usize) -> io::Result<Self::Wait> {
151        Ok(FdWait::new(self.clone(), fd, read_size))
152    }
153}
154
155impl<T: Initiate> FdWait<T> {
156    /// Creates a new instance for the given file descriptor and initiator.
157    pub fn new(initiator: T, fd: RawFd, read_size: usize) -> Self {
158        static SUPPORTS_NONBLOCK_READ: OnceLock<bool> = OnceLock::new();
159        // There is no easy way to detect whether the ring supports nonblocking
160        // reads, but the functionality was added in the same release as linkat
161        // (5.15), so that's probably as close as we're getting.
162        const LINKAT: u8 = 39;
163        let supports_nonblock_read =
164            *SUPPORTS_NONBLOCK_READ.get_or_init(|| initiator.initiator().probe(LINKAT));
165
166        let inner = if supports_nonblock_read {
167            assert!(read_size <= MAXIMUM_WAIT_READ_SIZE);
168            FdWaitInner::ViaRead(FdWaitViaRead {
169                fd,
170                read_size,
171                initiator,
172                state: FdWaitViaReadState::Idle(Box::new(0)),
173            })
174        } else {
175            FdWaitInner::ViaPoll(pal_async::unix::FdWait::new(
176                fd,
177                FdReady::new(initiator, fd),
178                read_size,
179            ))
180        };
181        FdWait { inner }
182    }
183}
184
185impl<T: Initiate> PollWait for FdWait<T> {
186    fn poll_wait(&mut self, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
187        match &mut self.inner {
188            FdWaitInner::ViaPoll(wait) => wait.poll_wait(cx),
189            FdWaitInner::ViaRead(wait) => wait.poll_wait(cx),
190        }
191    }
192
193    fn poll_cancel_wait(&mut self, cx: &mut Context<'_>) -> Poll<bool> {
194        match &mut self.inner {
195            FdWaitInner::ViaPoll(wait) => wait.poll_cancel_wait(cx),
196            FdWaitInner::ViaRead(wait) => wait.poll_cancel_wait(cx),
197        }
198    }
199}
200
201#[derive(Debug)]
202struct FdWaitViaRead<T: Initiate> {
203    fd: RawFd,
204    read_size: usize,
205    initiator: T,
206    state: FdWaitViaReadState,
207}
208
209#[derive(Debug)]
210enum FdWaitViaReadState {
211    Idle(Box<u64>),
212    ReadPending { io: Io<Box<u64>>, cancelling: bool },
213    Invalid,
214}
215
216impl<T: Initiate> PollWait for FdWaitViaRead<T> {
217    fn poll_wait(&mut self, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
218        loop {
219            match std::mem::replace(&mut self.state, FdWaitViaReadState::Invalid) {
220                FdWaitViaReadState::Idle(mut buf) => {
221                    assert!(self.read_size <= 8);
222                    let sqe = opcode::Read::new(
223                        io_uring::types::Fd(self.fd),
224                        std::ptr::from_mut(&mut *buf).cast(),
225                        self.read_size as u32,
226                    )
227                    .build();
228                    // SAFETY: the sqe's buffer is kept alive in `buf` for the
229                    // lifetime of the IO.
230                    let io = unsafe { Io::new(self.initiator.initiator().clone(), sqe, buf) };
231                    self.state = FdWaitViaReadState::ReadPending {
232                        io,
233                        cancelling: false,
234                    };
235                }
236                FdWaitViaReadState::ReadPending { mut io, cancelling } => match io.poll_unpin(cx) {
237                    Poll::Ready(r) => {
238                        self.state = FdWaitViaReadState::Idle(io.into_mem());
239                        match r {
240                            Ok(_) => break Poll::Ready(Ok(())),
241                            Err(err) if err.raw_os_error() == Some(libc::ECANCELED) => {}
242                            Err(err) => return Poll::Ready(Err(err)),
243                        }
244                    }
245                    Poll::Pending => {
246                        self.state = FdWaitViaReadState::ReadPending { io, cancelling };
247                        return Poll::Pending;
248                    }
249                },
250                FdWaitViaReadState::Invalid => unreachable!(),
251            }
252        }
253    }
254
255    fn poll_cancel_wait(&mut self, cx: &mut Context<'_>) -> Poll<bool> {
256        loop {
257            match std::mem::replace(&mut self.state, FdWaitViaReadState::Invalid) {
258                FdWaitViaReadState::Idle(buf) => {
259                    self.state = FdWaitViaReadState::Idle(buf);
260                    break Poll::Ready(false);
261                }
262                FdWaitViaReadState::ReadPending { mut io, cancelling } => {
263                    if cancelling {
264                        match io.poll_unpin(cx) {
265                            Poll::Ready(r) => {
266                                self.state = FdWaitViaReadState::Idle(io.into_mem());
267                                // If `r` is an error, it was either `ECANCELED`
268                                // (so do nothing), or it was a real error. We
269                                // assume that subsequent reads will return the
270                                // same error, so we can ignore those here to
271                                // keep the cancel contract simple for the
272                                // caller.
273                                break Poll::Ready(r.is_ok());
274                            }
275                            Poll::Pending => {
276                                self.state = FdWaitViaReadState::ReadPending { io, cancelling };
277                                break Poll::Pending;
278                            }
279                        }
280                    } else {
281                        io.cancel();
282                        self.state = FdWaitViaReadState::ReadPending {
283                            io,
284                            cancelling: true,
285                        };
286                    }
287                }
288                FdWaitViaReadState::Invalid => unreachable!(),
289            }
290        }
291    }
292}
293
294impl<T: Initiate> Drop for FdWaitViaRead<T> {
295    fn drop(&mut self) {
296        let _ = self.poll_cancel_wait(&mut Context::from_waker(Waker::noop()));
297    }
298}
299
300/// A [`pal_async::timer::PollTimer`] implementation for io_uring.
301#[derive(Debug)]
302pub struct Timer<T: Initiate> {
303    initiator: T,
304    target_deadline: Instant,
305    state: Option<TimerState>,
306}
307
308impl<T: Initiate> Timer<T> {
309    /// Creates a new instance for the given initiator.
310    pub fn new(initiator: T) -> Self {
311        Timer {
312            initiator,
313            target_deadline: Instant::from_nanos(0),
314            state: None,
315        }
316    }
317}
318
319#[derive(Debug)]
320struct TimerState {
321    io: Io<Box<Timespec>>,
322    cancelled: bool,
323}
324
325impl TimerDriver for IoInitiator {
326    type Timer = Timer<Self>;
327
328    fn new_timer(&self) -> Self::Timer {
329        Timer::new(self.clone())
330    }
331}
332
333impl pal_async::io_uring::IoUringSubmit for IoInitiator {
334    fn probe(&self, opcode: u8) -> bool {
335        self.probe(opcode)
336    }
337
338    async unsafe fn submit(&self, sqe: squeue::Entry) -> io::Result<i32> {
339        // SAFETY: the caller guarantees the SQE only references memory that is
340        // valid for the lifetime of the returned future.
341        unsafe { self.issue_io((), |_| sqe).await.0 }
342    }
343}
344
345impl pal_async::io_uring::IoUringDriver for IoInitiator {
346    type Submitter = Self;
347
348    fn io_uring_submitter(&self) -> Option<&Self> {
349        Some(self)
350    }
351}
352
353impl<T: Initiate> PollTimer for Timer<T> {
354    fn poll_timer(&mut self, cx: &mut Context<'_>, deadline: Option<Instant>) -> Poll<Instant> {
355        if let Some(deadline) = deadline {
356            self.set_deadline(deadline);
357        }
358        loop {
359            let now = Instant::now();
360            if self.target_deadline <= now {
361                break Poll::Ready(now);
362            } else if let Some(state) = &mut self.state {
363                let _ = std::task::ready!(state.io.poll_unpin(cx));
364                self.state = None;
365            } else {
366                // Compute an absolute timeout. Note that pal's Instant is
367                // CLOCK_MONOTONIC, which is exactly what io_uring supports.
368                let absolute_timeout = self.target_deadline - Instant::from_nanos(0);
369                let timespec = Box::new(
370                    Timespec::new()
371                        .sec(absolute_timeout.as_secs())
372                        .nsec(absolute_timeout.subsec_nanos()),
373                );
374                let sqe = {
375                    opcode::Timeout::new(&*timespec)
376                        .flags(TimeoutFlags::ABS)
377                        .build()
378                };
379                // SAFETY: the operation references timespec, which is boxed for
380                // the duration of the IO.
381                let io = unsafe { Io::new(self.initiator.initiator().clone(), sqe, timespec) };
382                let state = TimerState {
383                    io,
384                    cancelled: false,
385                };
386                self.state = Some(state);
387            }
388        }
389    }
390
391    fn set_deadline(&mut self, deadline: Instant) {
392        if let Some(state) = &mut self.state {
393            // Cancel the current operation if the deadline is later than
394            // the current one.
395            //
396            // FUTURE: just update the current operation. This requires >=
397            // Linux 5.11.
398            if self.target_deadline > deadline && !state.cancelled {
399                state.io.cancel_timeout();
400                state.cancelled = true;
401            }
402        }
403        self.target_deadline = deadline;
404    }
405}
406
407#[cfg(test)]
408pub(crate) mod tests {
409    use crate::IoInitiator;
410    use crate::IoUringPool;
411    use futures::executor::block_on;
412    use once_cell::sync::OnceCell;
413    use pal_async::executor_tests;
414    use pal_async::task::Spawn;
415    use std::future::Future;
416    use std::io;
417    use std::thread::JoinHandle;
418
419    pub struct SingleThreadPool {
420        _thread: JoinHandle<()>,
421        initiator: IoInitiator,
422    }
423
424    impl SingleThreadPool {
425        pub fn new() -> io::Result<Self> {
426            let pool = IoUringPool::new("test", 16)?;
427            let initiator = pool.client().initiator().clone();
428            let thread = std::thread::spawn(move || pool.run());
429            Ok(Self {
430                _thread: thread,
431                initiator,
432            })
433        }
434
435        pub fn initiator(&self) -> &IoInitiator {
436            &self.initiator
437        }
438    }
439
440    fn test_pool() -> io::Result<&'static SingleThreadPool> {
441        // TODO: switch to std::sync::OnceLock once `get_or_try_init` is stable
442        static POOL: OnceCell<SingleThreadPool> = OnceCell::new();
443        POOL.get_or_try_init(SingleThreadPool::new)
444    }
445
446    macro_rules! get_pool_or_skip {
447        () => {
448            match test_pool() {
449                Ok(pool) => pool,
450                Err(err) if err.raw_os_error() == Some(libc::ENOSYS) => {
451                    println!("Test case skipped (no IO-Uring support)");
452                    return;
453                }
454                Err(err) => panic!("{}", err),
455            }
456        };
457    }
458
459    fn run_until<F>(pool: &SingleThreadPool, fut: F) -> F::Output
460    where
461        F: 'static + Future + Send,
462        F::Output: Send,
463    {
464        block_on(pool.initiator().spawn("test", fut))
465    }
466
467    #[test]
468    fn waker_works() {
469        run_until(get_pool_or_skip!(), executor_tests::waker_tests());
470    }
471
472    #[test]
473    fn spawn_works() {
474        let pool = get_pool_or_skip!();
475        executor_tests::spawn_tests(|| (pool.initiator(), || ()))
476    }
477
478    #[test]
479    fn sleep_works() {
480        let pool = get_pool_or_skip!();
481        run_until(pool, executor_tests::sleep_tests(pool.initiator().clone()))
482    }
483
484    #[test]
485    fn wait_works() {
486        let pool = get_pool_or_skip!();
487        run_until(pool, executor_tests::wait_tests(pool.initiator().clone()))
488    }
489
490    #[test]
491    fn socket_works() {
492        let pool = get_pool_or_skip!();
493        run_until(pool, executor_tests::socket_tests(pool.initiator().clone()))
494    }
495
496    #[test]
497    fn uring_works() {
498        let pool = get_pool_or_skip!();
499        run_until(
500            pool,
501            executor_tests::io_uring_tests::uring_tests(pool.initiator().clone()),
502        )
503    }
504}