Skip to main content

miri/concurrency/
blocking_io.rs

1use std::cell::RefMut;
2use std::collections::BTreeMap;
3use std::io;
4use std::time::Duration;
5
6use mio::event::Source;
7use mio::{Events, Interest, Poll, Token};
8
9use crate::shims::{FdId, FileDescription, FileDescriptionRef, WeakFileDescriptionRef};
10use crate::*;
11
12/// Capacity of the event queue which can be polled at a time.
13/// Since we don't expect many simultaneous blocking I/O events
14/// this value can be set rather low.
15const IO_EVENT_CAPACITY: usize = 16;
16
17/// Trait for file descriptions that contain a mio [`Source`].
18pub trait SourceFileDescription: FileDescription {
19    /// Invoke `f` on the source inside `self`.
20    fn with_source(&self, f: &mut dyn FnMut(&mut dyn Source) -> io::Result<()>) -> io::Result<()>;
21
22    /// Get a mutable reference to the readiness of the source.
23    fn get_readiness_mut(&self) -> RefMut<'_, Readiness>;
24}
25
26/// An I/O interest for a blocked thread. Note that all threads are always considered
27/// to be interested in "error" events.
28#[derive(Debug, Clone, Copy)]
29pub enum BlockingIoInterest {
30    /// The blocked thread is interested in [`Interest::READABLE`].
31    Read,
32    /// The blocked thread is interested in [`Interest::WRITABLE`].
33    Write,
34    /// The blocked thread is interested in [`Interest::READABLE`] and
35    /// [`Interest::WRITABLE`].
36    ReadWrite,
37}
38
39impl BlockingIoInterest {
40    /// Check whether the [`Readiness`] fulfills this blocking I/O interest.
41    /// This function also returns `true` if the error readiness is set
42    /// even when the requested interest might not be fulfilled.
43    fn is_fulfilled_by(&self, readiness: &Readiness) -> bool {
44        match self {
45            BlockingIoInterest::Read => readiness.readable || readiness.error,
46            BlockingIoInterest::Write => readiness.writable || readiness.error,
47            BlockingIoInterest::ReadWrite =>
48                readiness.readable || readiness.writable || readiness.error,
49        }
50    }
51}
52
53impl From<&mio::event::Event> for Readiness {
54    fn from(event: &mio::event::Event) -> Self {
55        Self {
56            readable: event.is_readable(),
57            writable: event.is_writable(),
58            read_closed: event.is_read_closed(),
59            write_closed: event.is_write_closed(),
60            error: event.is_error(),
61        }
62    }
63}
64
65struct BlockingIoSource {
66    /// The source file description which is registered into the poll. We only store weak references
67    /// such that source file descriptions can be destroyed whilst they are registered. They will
68    /// later be cleaned up on the next GC run.
69    fd: WeakFileDescriptionRef<dyn SourceFileDescription>,
70    /// The threads which are blocked on the I/O source, and the interest indicating
71    /// when they should be unblocked.
72    blocked_threads: BTreeMap<ThreadId, BlockingIoInterest>,
73}
74
75/// Manager for managing blocking host I/O in a non-blocking manner.
76/// We use [`Poll`] to poll for new I/O events from the OS for sources
77/// registered using this manager.
78///
79/// The semantics of this manager are that host I/O sources are registered
80/// to a [`Poll`] for their entire lifespan. Once host readiness events happen
81/// on a registered source, its internal readiness gets updated -- even when
82/// the source isn't part of an active [`ReadinessWatcher`]. Also, for the entire
83/// lifespan of the source, threads can be added which should be unblocked
84/// once a certain [`Readiness`] for an I/O source is satisfied.
85///
86/// Since blocking host I/O is inherently non-deterministic, no method on this
87/// manager should be called when isolation is enabled. The only exception is
88/// the [`BlockingIoManager::new`] function to create the manager. Everywhere else,
89/// we assert that isolation is disabled!
90pub struct BlockingIoManager {
91    /// Poll instance to monitor I/O events from the OS.
92    /// This is only [`None`] when Miri is run with isolation enabled.
93    poll: Option<Poll>,
94    /// Buffer used to store the ready I/O events when calling [`Poll::poll`].
95    /// This is not part of the state and only stored to avoid allocating a
96    /// new buffer for every poll.
97    events: Events,
98    /// Map from source file description ids to the actual sources and their
99    /// blocked threads.
100    sources: BTreeMap<FdId, BlockingIoSource>,
101}
102
103impl BlockingIoManager {
104    /// Create a new blocking I/O manager instance based on the availability
105    /// of communication with the host.
106    pub fn new(communicate: bool) -> Result<Self, io::Error> {
107        let manager = Self {
108            poll: communicate.then_some(Poll::new()?),
109            events: Events::with_capacity(IO_EVENT_CAPACITY),
110            sources: BTreeMap::default(),
111        };
112        Ok(manager)
113    }
114
115    /// Poll for new I/O events from the OS or wait until the timeout expired.
116    /// The timeout semantics are the same as described in [`Poll::poll`].
117    /// The events also immediately get processed: threads get unblocked, and fd readiness gets updated.
118    fn poll<'tcx>(
119        ecx: &mut MiriInterpCx<'tcx>,
120        timeout: Option<Duration>,
121    ) -> InterpResult<'tcx, Result<(), io::Error>> {
122        let poll = ecx
123            .machine
124            .blocking_io
125            .poll
126            .as_mut()
127            .expect("Blocking I/O should not be called with isolation enabled");
128
129        // Poll for new I/O events from OS and store them in the events buffer.
130        if let Err(err) = poll.poll(&mut ecx.machine.blocking_io.events, timeout) {
131            return interp_ok(Err(err));
132        };
133
134        let event_fds = ecx
135            .machine
136            .blocking_io
137            .events
138            .iter()
139            .map(|event| {
140                let token = event.token();
141                // We know all tokens are valid `FdId`.
142                let fd_id = FdId::new_unchecked(token.0);
143                let source = ecx
144                    .machine
145                    .blocking_io
146                    .sources
147                    .get(&fd_id)
148                    .expect("Source should be registered");
149                let Some(fd) = source.fd.upgrade() else {
150                    panic!("Should not receive readiness event for closed source file description")
151                };
152
153                assert_eq!(fd.id(), fd_id);
154                // Update the readiness of the source.
155                *fd.get_readiness_mut() |= Readiness::from(event);
156                // Put FD into `event_fds` list.
157                fd
158            })
159            .collect::<Vec<_>>();
160
161        // Update the readiness for all source file descriptions which received an event. Also,
162        // unblock the threads which are blocked on such a source and whose interests are now fulfilled.
163        for fd in event_fds.into_iter() {
164            // Update readiness for the `fd` source. This is no a "release" event since it was
165            // not triggered by the current thread, it was triggered by the outside world.
166            ecx.update_fd_readiness(fd.clone(), ReadinessUpdateFlags::NO_RELEASE_CLOCK)?;
167
168            // The `update_fd_readiness` can't cause the source to be deregistered since we still
169            // hold a strong reference to the file description with `fd`.
170            let source = ecx.machine.blocking_io.sources.get(&fd.id()).unwrap();
171
172            // List of all thread id's whose interests are currently fulfilled
173            // and which are blocked on the `fd` source. This also includes
174            // threads whose interests were already fulfilled before the
175            // `poll` invocation.
176            let threads = source
177                .blocked_threads
178                .iter()
179                .filter_map(|(thread_id, interest)| {
180                    interest.is_fulfilled_by(&fd.get_readiness_mut()).then_some(*thread_id)
181                })
182                .collect::<Vec<_>>();
183
184            // Unblock all threads whose interests are currently fulfilled and
185            // which are blocked on the `fd` source.
186            threads
187                .into_iter()
188                .try_for_each(|thread_id| ecx.unblock_thread(thread_id, BlockReason::IO))?;
189        }
190
191        interp_ok(Ok(()))
192    }
193
194    /// Register a source file description to the blocking I/O poll.
195    pub fn register(&mut self, source_fd: FileDescriptionRef<dyn SourceFileDescription>) {
196        let poll =
197            self.poll.as_ref().expect("Blocking I/O should not be called with isolation enabled");
198
199        let id = source_fd.id();
200        let token = Token(id.to_usize());
201
202        // All possible interests.
203        // We only care about the readable and writable interests because those are the only
204        // interests which are available on all platforms. Internally, mio also
205        // registers an error interest.
206        let interest = Interest::READABLE | Interest::WRITABLE;
207
208        // Treat errors from registering as fatal. On UNIX hosts this can only
209        // fail due to system resource errors (e.g. ENOMEM or ENOSPC) or when the source is already registered.
210        source_fd
211            .with_source(&mut |source| poll.registry().register(source, token, interest))
212            .unwrap();
213
214        let source = BlockingIoSource {
215            fd: FileDescriptionRef::downgrade(&source_fd),
216            blocked_threads: BTreeMap::default(),
217        };
218
219        self.sources
220            .try_insert(id, source)
221            .unwrap_or_else(|_| panic!("Source should not already be registered"));
222    }
223
224    /// Add a new blocked thread to a registered source. The thread gets unblocked
225    /// once its [`BlockingIoInterest`] is fulfilled when calling
226    /// [`BlockingIoManager::poll`].
227    ///
228    /// It's assumed that the thread of `thread_id` isn't already blocked on
229    /// the source with id `source_id` and that this source is currently
230    /// registered.
231    fn add_blocked_thread(
232        &mut self,
233        source_id: FdId,
234        thread_id: ThreadId,
235        interest: BlockingIoInterest,
236    ) {
237        let source = self.sources.get_mut(&source_id).expect("Source should be registered");
238
239        source
240            .blocked_threads
241            .try_insert(thread_id, interest)
242            .expect("Thread cannot be blocked multiple times on the same source");
243    }
244
245    /// Remove a blocked thread from a registered source.
246    ///
247    /// It's assumed that the thread of `thread_id` is blocked on the
248    /// source with id `source_id` and that this source is currently
249    /// registered.
250    pub fn remove_blocked_thread(&mut self, source_id: FdId, thread_id: ThreadId) {
251        let source = self.sources.get_mut(&source_id).expect("Source should be registered");
252        source.blocked_threads.remove(&thread_id).expect("Thread should be blocked on source");
253    }
254
255    /// Run garbage collector on blocking I/O manager to remove all closed source file
256    /// descriptions from the registered sources map.
257    pub fn run_gc(&mut self) {
258        // For hosts where mio uses `kqueue`, `epoll` or `IOCP` (Windows), we know that
259        // the source doesn't need to be deregistered from the `Poll` before being dropped:
260        // See <https://github.com/tokio-rs/mio/issues/1972>
261        // Thus, just removing the closed source file descriptions from the `sources` map
262        // is enough.
263        self.sources.retain(|_id, source| !source.fd.is_closed());
264    }
265}
266
267impl<'tcx> EvalContextExt<'tcx> for MiriInterpCx<'tcx> {}
268pub trait EvalContextExt<'tcx>: MiriInterpCxExt<'tcx> {
269    /// Block the current thread until some interests on an I/O source
270    /// are fulfilled or the optional timeout exceeded.
271    /// The callback will be invoked when the thread gets unblocked.
272    ///
273    /// Note that an error interest is implicitly added to `interest`.
274    /// This means that the thread will also be unblocked when the error
275    /// readiness gets set for the source even when the requested interest
276    /// might not be fulfilled.
277    ///
278    /// The callback function will immediately be executed with [`UnblockKind::Ready`]
279    /// when `interest` is already fulfilled for `source_fd`.
280    ///
281    /// There can also be spurious wake-ups by the OS and thus it's the callers
282    /// responsibility to verify that the requested I/O interests are
283    /// really ready and to block again if they're not.
284    ///
285    /// It's the callers responsibility to remove the [`BlockingIoInterest`]
286    /// from the blocking I/O manager in the provided callback function.
287    #[inline]
288    fn block_thread_for_io(
289        &mut self,
290        source_fd: FileDescriptionRef<dyn SourceFileDescription>,
291        interest: BlockingIoInterest,
292        deadline: Option<Deadline>,
293        callback: DynUnblockCallback<'tcx>,
294    ) -> InterpResult<'tcx> {
295        let this = self.eval_context_mut();
296
297        // We always have to do this since the thread will de-register itself.
298        this.machine.blocking_io.add_blocked_thread(source_fd.id(), this.active_thread(), interest);
299
300        if interest.is_fulfilled_by(&source_fd.get_readiness_mut()) {
301            // The requested readiness is currently already fulfilled for the provided source.
302            // Instead of actually blocking the thread, we just run the callback function.
303            callback.call(this, UnblockKind::Ready)
304        } else {
305            // The I/O readiness is currently not fulfilled. We block the thread
306            // until the readiness is fulfilled and execute the callback then.
307            this.block_thread(BlockReason::IO, deadline, callback);
308            interp_ok(())
309        }
310    }
311
312    /// Poll for I/O events until either an I/O event happened or the timeout expired.
313    ///
314    /// - If the timeout is [`Some`] and contains [`Duration::ZERO`], the poll doesn't block and just
315    ///   reads all events since the last poll.
316    /// - If the timeout is [`Some`] and contains a non-zero duration, it blocks at most for the
317    ///   specified duration.
318    /// - If the timeout is [`None`] the poll blocks indefinitely until an event occurs.
319    ///
320    /// Unblocks all threads which are blocked on I/O and whose I/O interests
321    /// are currently fulfilled.
322    fn poll_and_unblock(&mut self, timeout: Option<Duration>) -> InterpResult<'tcx> {
323        let this = self.eval_context_mut();
324
325        match BlockingIoManager::poll(this, timeout)? {
326            Ok(_) => interp_ok(()),
327            // We can ignore errors originating from interrupts; that's just a spurious wakeup.
328            Err(e) if e.kind() == io::ErrorKind::Interrupted => interp_ok(()),
329            // For other errors we panic. On Linux and BSD hosts this should only be
330            // reachable when a system resource error (e.g. ENOMEM or ENOSPC) occurred.
331            Err(e) => panic!("unexpected error while polling: {e}"),
332        }
333    }
334
335    /// Returns whether there exists any thread that is blocked on host I/O.
336    fn any_thread_blocked_on_host(&self) -> bool {
337        let this = self.eval_context_ref();
338        this.machine.blocking_io.sources.values().any(|source| {
339            // There's two ways something could be blocked on this: directly,
340            // or indirectly via a readiness watcher.
341            source.blocked_threads.len() > 0
342                || source.fd.upgrade().is_some_and(|fd| {
343                    fd.readiness_watched().is_some_and(|w| w.has_watcher_with_blocked_thread())
344                })
345        })
346    }
347}