Skip to main content

smolvm_network/
queues.rs

1//! Shared queues and wake notifications for the virtio-net backend.
2//!
3//! Context
4//! =======
5//!
6//! The host-side virtio runtime has several independently blocked workers:
7//! - the Unix-stream reader thread
8//! - the Unix-stream writer thread
9//! - the smoltcp poll loop
10//! - TCP relay threads
11//!
12//! They need two kinds of coordination:
13//! 1. lock-free frame handoff between threads
14//! 2. a way to wake a thread that is blocked waiting on socket readiness
15//!
16//! This module provides both:
17//! - `ArrayQueue<Vec<u8>>` for frame ownership transfer
18//! - `WakePipe` as a tiny readiness primitive built on a cross-platform poller
19//!   (`polling`, which maps to epoll/kqueue/IOCP). Its `notify()` is the
20//!   cross-thread wakeup, replacing the old self-pipe.
21//!
22//! Data flow:
23//!
24//! ```text
25//! guest_to_host queue : reader thread  -> smoltcp poll loop
26//! host_to_guest queue : smoltcp runtime -> writer thread
27//!
28//! guest_wake: reader thread / shutdown -> smoltcp poll loop
29//! host_wake : smoltcp runtime / shutdown -> Unix-stream writer
30//! relay_wake: TCP relay threads / shutdown -> smoltcp poll loop
31//! ```
32//!
33//! Thread interaction view:
34//!
35//! ```text
36//! FrameStream reader thread
37//!   -> guest_to_host.push(frame)
38//!   -> guest_wake.wake()
39//!
40//! smolvm-net-poll thread
41//!   -> guest_to_host.pop()
42//!   -> host_to_guest.push(frame)
43//!   -> host_wake.wake()
44//!   -> relay_wake.wait()/drain()
45//!
46//! FrameStream writer thread
47//!   -> host_wake.wait()
48//!   -> host_to_guest.pop()
49//!
50//! TCP relay thread
51//!   -> to_smoltcp.send(payload)
52//!   -> relay_wake.wake()
53//! ```
54
55use crossbeam_queue::ArrayQueue;
56use polling::{Events, Poller};
57use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
58use std::sync::Arc;
59use std::time::Duration;
60
61/// Default queue capacity for guest/host ethernet frames.
62pub const DEFAULT_FRAME_QUEUE_CAPACITY: usize = 1024;
63
64/// Shared queues and wake handles for the host-side virtio-net runtime.
65///
66/// One `NetworkFrameQueues` is shared across all helper threads for a single
67/// guest NIC.
68///
69/// A useful mental model is:
70///
71/// ```text
72/// queues  = ownership transfer for frame bytes
73/// wakes   = "go look at the queue now"
74/// shutdown= sticky flag + wake all blocked waiters
75/// ```
76pub struct NetworkFrameQueues {
77    /// Raw ethernet frames emitted by the guest and waiting for smoltcp.
78    pub guest_to_host: ArrayQueue<Vec<u8>>,
79    /// Raw ethernet frames emitted by smoltcp and waiting for libkrun.
80    pub host_to_guest: ArrayQueue<Vec<u8>>,
81    /// Wake the smoltcp poll loop when a guest frame arrives.
82    ///
83    /// `guest_wake` and `relay_wake` deliberately share one underlying poller:
84    /// the smoltcp loop blocks on that single poller and either wake unblocks
85    /// it. The loop re-runs its whole pipeline on every wakeup, so it does not
86    /// need to know which side fired.
87    pub guest_wake: WakePipe,
88    /// Wake the libkrun writer thread when a host frame is ready.
89    pub host_wake: WakePipe,
90    /// Wake the smoltcp poll loop when a TCP relay thread has new data.
91    pub relay_wake: WakePipe,
92    /// Signals that the helper process should shut down.
93    shutting_down: AtomicBool,
94    /// Cumulative guest-outbound (egress) bytes for this NIC since boot, at the
95    /// ethernet-frame level — every guest frame accepted into the stack is
96    /// counted. Used for per-machine egress billing/telemetry. Held behind an
97    /// `Arc` so the runtime owner can hand a cheap read handle to a flush thread
98    /// without exposing the rest of the queue set.
99    egress_bytes: Arc<AtomicU64>,
100}
101
102impl NetworkFrameQueues {
103    /// Create a new shared queue set wrapped in `Arc`.
104    pub fn shared(capacity: usize) -> Arc<Self> {
105        // The smoltcp poll loop waits on a single poller; both the guest-frame
106        // wake and the relay wake notify it, so they share one poller instance.
107        let poll_loop = WakePipe::new();
108        let relay_wake = poll_loop.share();
109        Arc::new(Self {
110            guest_to_host: ArrayQueue::new(capacity),
111            host_to_guest: ArrayQueue::new(capacity),
112            guest_wake: poll_loop,
113            host_wake: WakePipe::new(),
114            relay_wake,
115            shutting_down: AtomicBool::new(false),
116            egress_bytes: Arc::new(AtomicU64::new(0)),
117        })
118    }
119
120    /// Add `n` guest-outbound bytes to the egress counter. Relaxed ordering is
121    /// fine: the counter is a monotonic statistic, not a synchronization point.
122    pub fn add_egress_bytes(&self, n: u64) {
123        self.egress_bytes.fetch_add(n, Ordering::Relaxed);
124    }
125
126    /// Cumulative guest-outbound bytes for this NIC since boot.
127    pub fn egress_bytes(&self) -> u64 {
128        self.egress_bytes.load(Ordering::Relaxed)
129    }
130
131    /// A cheap, cloneable read handle to the egress counter, for a flush thread
132    /// owned by the launcher (the runtime itself is not `Clone`).
133    pub fn egress_counter(&self) -> Arc<AtomicU64> {
134        self.egress_bytes.clone()
135    }
136
137    /// Mark the runtime as shutting down and wake all waiters.
138    ///
139    /// The wakes are part of shutdown correctness. Without them, a thread
140    /// blocked waiting on socket readiness could sleep indefinitely even though
141    /// the shutdown flag was already set.
142    pub fn begin_shutdown(&self) {
143        self.shutting_down.store(true, Ordering::SeqCst);
144        self.guest_wake.wake();
145        self.host_wake.wake();
146        self.relay_wake.wake();
147    }
148
149    /// Whether shutdown has been requested.
150    pub fn is_shutting_down(&self) -> bool {
151        self.shutting_down.load(Ordering::SeqCst)
152    }
153}
154
155/// Cross-thread wake notification built on a cross-platform poller.
156///
157/// The pattern is:
158/// - one thread blocks waiting on the poller (optionally alongside registered
159///   socket sources)
160/// - another thread calls [`WakePipe::wake`] to unblock it
161/// - the waiter resumes; the wake is auto-cleared by the next `wait`
162///
163/// Why a poller rather than a self-pipe: `polling::Poller::notify()` is a
164/// portable cross-thread wakeup that works on Windows (where a `pipe(2)` is not
165/// pollable by the IOCP/wepoll backend) as well as Unix. The same poller can
166/// have socket sources registered on it, which lets the ICMP/UDP relay loops
167/// wait on "wake OR any flow socket readable" in a single blocking call.
168#[derive(Clone, Debug)]
169pub struct WakePipe {
170    poller: Arc<Poller>,
171}
172
173impl WakePipe {
174    /// Create a wake notification with its own poller.
175    pub fn new() -> Self {
176        Self {
177            poller: Arc::new(Poller::new().expect("create poller for wake notification")),
178        }
179    }
180
181    /// Create another handle that shares this wake's underlying poller.
182    ///
183    /// Two `WakePipe`s built this way notify the same waiter: useful when one
184    /// loop must wake on either of two logical events (the smoltcp loop wakes on
185    /// guest frames or relay data).
186    pub fn share(&self) -> Self {
187        Self {
188            poller: self.poller.clone(),
189        }
190    }
191
192    /// The underlying poller, so a caller can register socket sources on it and
193    /// block on "this wake OR a socket" in a single `wait`.
194    pub fn poller(&self) -> &Arc<Poller> {
195        &self.poller
196    }
197
198    /// Signal the waiting side.
199    ///
200    /// Multiple wakes coalesce: until the waiter next blocks, repeated notifies
201    /// collapse into a single "there is pending wake state".
202    pub fn wake(&self) {
203        // A failed notify only means the waiter will rely on its poll timeout;
204        // it is never fatal.
205        let _ = self.poller.notify();
206    }
207
208    /// Drain pending wake state.
209    ///
210    /// With a poller-backed waker the notification is consumed by `wait`
211    /// itself, so this is a no-op kept for API symmetry with the old self-pipe.
212    pub fn drain(&self) {}
213
214    /// Wait until woken or the timeout elapses.
215    ///
216    /// Returns `Ok(true)` if the wait ended because of a wake (a `notify` or a
217    /// registered source becoming ready), `Ok(false)` if the timeout elapsed.
218    ///
219    /// A `notify`-driven wakeup reports no events (the poller consumes the
220    /// notification internally), so a pure wake is detected as either a
221    /// non-empty event set or an early return: the wait unblocked before the
222    /// requested deadline.
223    pub fn wait(&self, timeout: Option<Duration>) -> std::io::Result<bool> {
224        let mut events = Events::new();
225        let start = std::time::Instant::now();
226        let count = self.poller.wait(&mut events, timeout)?;
227        if count > 0 {
228            return Ok(true);
229        }
230        match timeout {
231            // Without a deadline, the only way `wait` returns is a wake.
232            None => Ok(true),
233            // With a deadline, an early return means a `notify` woke us; a
234            // return at/after the deadline is a genuine timeout.
235            Some(timeout) => Ok(start.elapsed() < timeout),
236        }
237    }
238}
239
240impl Default for WakePipe {
241    fn default() -> Self {
242        Self::new()
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn wake_pipe_round_trip() {
252        let pipe = WakePipe::new();
253        pipe.wake();
254        assert!(pipe.wait(Some(Duration::from_millis(10))).unwrap());
255        pipe.drain();
256        assert!(!pipe.wait(Some(Duration::from_millis(1))).unwrap());
257    }
258
259    #[test]
260    fn shared_wake_notifies_same_waiter() {
261        let a = WakePipe::new();
262        let b = a.share();
263        // Waking the shared handle unblocks a waiter on the original.
264        b.wake();
265        assert!(a.wait(Some(Duration::from_millis(10))).unwrap());
266        a.drain();
267        assert!(!a.wait(Some(Duration::from_millis(1))).unwrap());
268    }
269
270    #[test]
271    fn queues_are_fifo() {
272        let queues = NetworkFrameQueues::shared(4);
273        queues.guest_to_host.push(vec![1, 2, 3]).unwrap();
274        queues.guest_to_host.push(vec![4, 5, 6]).unwrap();
275
276        assert_eq!(queues.guest_to_host.pop(), Some(vec![1, 2, 3]));
277        assert_eq!(queues.guest_to_host.pop(), Some(vec![4, 5, 6]));
278        assert_eq!(queues.guest_to_host.pop(), None);
279    }
280}