event-manager
The VMM thread does not poll. It sits in an epoll loop: it blocks in
epoll_wait until one of the file descriptors it cares about becomes ready — a
virtio device's ioeventfd (the guest kicked a queue), the API wake-up eventfd
(a control action arrived), a rate-limiter timer, host stdin (a console keystroke)
— and then dispatches to whoever owns that fd. Managing a set of fds, their
interest masks, and the "when this fd is ready, call this handler" mapping, cleanly
and safely, is the job of rust-vmm's event-manager. It is the
VMM threading model's engine and the
subject of the event manager deep dive — and,
like vm-superio and seccompiler, it is a crate Firecracker donated upstream:
it was derived from Firecracker's own EventManager.
After this chapter you can: name the core types (EventManager,
EventSubscriber/MutEventSubscriber, EventOps, Events, EventSet,
SubscriberId); explain the subscriber model and how registration works; implement
a subscriber; and find where Firecracker's VMM thread and devices use it.
Note: This is the third Firecracker donation. The subscriber pattern you implement here is the exact pattern every Firecracker device follows on the epoll loop. Learn it once and the whole device-dispatch story falls into place.
# Confirm the dependency and the pinned version (verify on your branch):
rg -n "event-manager" Cargo.toml src/vmm/Cargo.toml Cargo.lock
cargo doc -p event-manager --no-deps --open
docs.rs: docs.rs/event-manager.
The problem: an epoll loop with many owners
Raw epoll is awkward to share. One thread owns the epoll fd; many independent
components (each device, the API plumbing, the timers) each have their own fds and
their own logic for what to do when ready. Without an abstraction you get a giant
match on raw fd numbers and manual bookkeeping for which fd belongs to which
device, what interest mask each wants, and how to add/remove fds at runtime.
event-manager replaces that with a registry of subscribers: each component
registers the fds it cares about and an object that knows how to handle them; the
manager runs the loop and calls the right object when its fd fires.
flowchart TD
EM["EventManager (owns the epoll fd)"]
EM -->|epoll_wait fires fd| D1["block device subscriber"]
EM -->|epoll_wait fires fd| D2["net device subscriber"]
EM -->|epoll_wait fires fd| API["API wake-up eventfd subscriber"]
EM -->|epoll_wait fires fd| CON["serial/stdin subscriber"]
D1 -. "registered (eventfd, EventSet::IN, SubscriberId)" .-> EM
D2 -. register .-> EM
The core types
| Type | Is | Role |
|---|---|---|
EventManager<S> | the loop owner | holds the epoll fd + the subscriber registry; run() blocks in epoll_wait and dispatches |
EventSubscriber / MutEventSubscriber | the trait you implement | init(&self, ops) to register fds; process(&self, events, ops) to handle readiness |
EventOps | the registration handle | passed to your subscriber; add/modify/remove fds against the manager |
Events | one readiness notification | which fd fired + which EventSet |
EventSet | the interest/readiness flags | IN, OUT, ERROR, HANG_UP, … (bitflags over epoll events) |
SubscriberId | a handle to a registered subscriber | lets you later modify/remove its registration |
The trait split — EventSubscriber (shared &self) vs MutEventSubscriber
(&mut self) — exists because some subscribers need mutable state in process
(most devices do: they mutate their queues), and some don't. Firecracker's devices
are MutEventSubscriber.
find ~/.cargo/registry/src -maxdepth 2 -type d -name 'event-manager-*' \
-exec rg -n "trait EventSubscriber|trait MutEventSubscriber|struct EventOps|struct Events|struct EventSet|fn process|fn init|fn run\b|add_subscriber" {} +
Implementing a subscriber
A subscriber implements two methods: init, where it tells the manager which
fds it wants (via EventOps), and process, where it does the work when one
of those fds is ready. Here is a minimal subscriber that watches one EventFd and
reacts when it fires — the exact pattern a device follows.
#![allow(unused)] fn main() { use event_manager::{EventOps, Events, EventSet, MutEventSubscriber}; use vmm_sys_util::eventfd::EventFd; struct MyDevice { queue_evt: EventFd, // the ioeventfd the guest "kicks" (vmm-sys-util) } impl MutEventSubscriber for MyDevice { // Called once when registered: declare interest in our fd. fn init(&mut self, ops: &mut EventOps) { ops.add(Events::with_data( &self.queue_evt, 0, // a token you choose to identify this fd EventSet::IN, // readable )).expect("register queue_evt"); } // Called when one of our fds is ready. fn process(&mut self, events: Events, _ops: &mut EventOps) { if events.event_set().contains(EventSet::IN) { // Drain the eventfd (the guest kicked a queue)... let _ = self.queue_evt.read(); // ...then do the real work: pop the virtqueue, do host I/O, // add_used, raise the completion interrupt. (See virtio-queue.md.) } } } }
Registration returns a SubscriberId; the manager calls init once, then calls
process every time epoll reports one of that subscriber's fds ready. To add or
remove fds later (a device that gains/loses a queue at runtime), the subscriber
uses the EventOps handed to it. The loop driver is the manager:
#![allow(unused)] fn main() { use event_manager::{EventManager, SubscriberOps}; let mut mgr: EventManager<MyDevice> = EventManager::new().unwrap(); let _id = mgr.add_subscriber(MyDevice { queue_evt }); loop { // Block until something is ready, then dispatch to the matching subscriber's // process(). This is the VMM thread's entire life. mgr.run().unwrap(); } }
Warning:
processruns on the VMM thread, synchronously, holding up the whole loop until it returns. A device handler that blocks (a slow host syscall, a lock it can't get) stalls every device and the API wake-up too. This is the mechanical reason behind the threading-model rule that the VMM loop must never block — see the common bugs in the threading model.
How Firecracker uses it
# The VMM thread's EventManager and its run loop:
rg -n "EventManager|event_manager|add_subscriber|\.run\(\)|SubscriberOps" src/vmm/src/
# Devices implementing the subscriber trait:
rg -n "impl MutEventSubscriber|impl EventSubscriber|fn process\b|fn init\b|EventOps|EventSet::IN" src/vmm/src/devices/
# The API wake-up eventfd is also a subscriber on this loop:
rg -n "api_event_fd|EventFd|process.*api|EventSet" src/vmm/src/
| Subscriber on the VMM loop | Fd it watches | What process does |
|---|---|---|
| each virtio device | its queue ioeventfd(s) | pop the virtqueue, do host I/O, complete, interrupt |
| rate limiters | a timerfd | refill the token bucket, re-enable a throttled queue |
| the serial console | host stdin | enqueue input into the UART, raise the guest IRQ |
| the API wake-up | the api eventfd | drain the mpsc action channel, dispatch the VmmAction |
| metrics / others | timers / eventfds | periodic work |
The unifying picture: the VMM thread is one EventManager::run() loop, and every
device, timer, and the API plumbing is a subscriber on it. That is why a
Firecracker device is fundamentally "a struct that implements MutEventSubscriber
and owns some eventfds." When you write a virtio device in
Lab 7.3, the subscriber
boilerplate is event-manager.
The connection to KVM is the ioeventfd: Firecracker registers a device's queue
eventfd with KVM via KVM_IOEVENTFD so that a guest write to the device's
QueueNotify MMIO register signals the eventfd directly in the kernel — no VM exit
round-trip to userspace for the notification. The eventfd becomes ready, epoll
returns, the subscriber's process runs. That is the virtio fast path, and
event-manager is the userspace half of it.
sequenceDiagram
participant G as guest driver
participant K as KVM
participant E as ioeventfd (vmm-sys-util EventFd)
participant M as EventManager (VMM thread)
participant D as device subscriber
G->>K: write QueueNotify MMIO (kick)
K->>E: KVM_IOEVENTFD signals the eventfd (no VM exit to userspace)
E->>M: epoll_wait returns
M->>D: process(Events{IN})
D->>D: pop chain, host I/O, add_used, raise IRQ
Reading exercise
# 1. The dependency and pinned version.
rg -n "event-manager" Cargo.toml Cargo.lock
# 2. The traits and types (pinned version).
cargo doc -p event-manager --no-deps --open
# 3. Firecracker's EventManager and run loop.
rg -n "EventManager|add_subscriber|\.run\(\)" src/vmm/src/
# 4. A real device subscriber: its init + process.
rg -n "impl MutEventSubscriber|fn init\b|fn process\b" src/vmm/src/devices/virtio/
# 5. The API wake-up eventfd as a subscriber.
rg -n "api_event_fd|process.*Vmm|drain|mpsc" src/vmm/src/ src/firecracker/src/
# 6. The ioeventfd registration (the fast-path link to KVM).
rg -n "register_ioevent|KVM_IOEVENTFD|ioeventfd|queue_evt" src/vmm/src/
Answer:
- Why does the VMM thread use
event-managerinstead of a hand-rolledmatchon raw fd numbers? What does the manager own? - Name the six core types and what each is for. What is the difference between
EventSubscriberandMutEventSubscriber, and which do Firecracker devices use? - Implement (in pseudocode from memory) a subscriber that watches one eventfd:
what goes in
init, what goes inprocess? - Why must
processnever block, and what is the system-wide consequence if it does? - List four kinds of subscriber on Firecracker's VMM loop and what each one's
processdoes. - Explain the ioeventfd fast path: how a guest kick reaches a subscriber's
processwithout a VM exit to userspace, and which crate provides each piece.
Common bugs and symptoms
| Symptom | Root cause | Where to look |
|---|---|---|
| Whole microVM freezes (all devices + API) | a subscriber's process blocked and never returned to run() | the blocking process; the threading-model rule |
| A device never services I/O | its eventfd not registered in init, or KVM_IOEVENTFD not set up | init's ops.add; the ioeventfd registration |
| Console input ignored but output works | stdin subscriber missing or not requesting EventSet::IN | the serial/stdin subscriber registration |
| API actions delayed | the api eventfd subscriber starved behind a slow device handler | balance of work on the loop; a blocking handler |
process called but does nothing | wrong EventSet/token, or eventfd not drained (read) so it re-fires forever | the EventSet match; draining the eventfd |
Next: seccompiler — the crate (originally Firecracker's) that compiles JSON seccomp filters to BPF and applies them per-thread, locking down the exact threads whose loops you just learned.