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}