The vCPU Run Loop and VM Exits
A vCPU thread's entire working life is one loop: call ioctl(vcpufd, KVM_RUN), let the guest execute on
the physical CPU until it does something the VMM must handle, read the exit reason, dispatch it, and
loop. This is the data plane's hot path — the place where a guest's I/O instruction becomes a host
pread, where a halt becomes a clean idle, where a triple fault becomes a shutdown. This chapter dissects
that loop in src/vmm/src/vstate/vcpu/: the VcpuExit taxonomy KVM returns, how PIO and MMIO exits are
dispatched to the device buses, how the kvm_run shared page carries the exit data, how the vCPU thread
coordinates with the VMM thread through VcpuEvent/VcpuResponse, and how EINTR and pause interact
with the loop.
After this chapter you will be able to: read the run loop without a guide; name every common VcpuExit
variant and what the handler does with it; explain the difference between how PIO and MMIO data reach you;
and trace a guest device access from KVM_RUN to a bus to a device and back.
Note: A VM exit is synchronous and on the vCPU thread. While the handler runs, the guest is stopped. That is why exit handlers must be fast and why the expensive parts of device work are pushed to the VMM thread via eventfds (see kvm-fundamentals.md on
KVM_IOEVENTFD). A slow exit handler is a stalled guest.
The loop
# Find the run loop. Do not trust a line number.
rg -n "fn run\b|fn run_emulation|KVM_RUN|vcpu.run\(|VcpuExit|loop \{" src/vmm/src/vstate/vcpu/
In skeleton, the loop is:
#![allow(unused)] fn main() { // Shape only — read the real thing with the rg above; names drift. loop { match self.fd.run() { // ioctl(KVM_RUN): blocks while guest runs Ok(VcpuExit::IoIn(addr, data)) => self.pio_bus.read(addr, data), Ok(VcpuExit::IoOut(addr, data)) => self.pio_bus.write(addr, data), Ok(VcpuExit::MmioRead(addr, data)) => self.mmio_bus.read(addr, data), Ok(VcpuExit::MmioWrite(addr, data)) => self.mmio_bus.write(addr, data), Ok(VcpuExit::Hlt) => break, // guest idled/halted Ok(VcpuExit::Shutdown | VcpuExit::SystemEvent(..)) => /* reset/shutdown */, Err(e) if e == EINTR => { /* signal: check for pause/exit */ }, Err(e) => /* fatal: report and stop */, _ => /* other exits */, } // between iterations: respond to any pending VcpuEvent (Pause/Resume/...) } }
vcpu.run() is kvm-ioctls' wrapper around KVM_RUN. It returns a VcpuExit (success) or an errno
(notably EINTR when a signal interrupted the ioctl). The match arms are the heart of device emulation.
The VcpuExit taxonomy
# The full set of variants comes from kvm-ioctls; see which ones Firecracker handles.
rg -n "VcpuExit::" src/vmm/src/vstate/vcpu/
cargo doc -p kvm-ioctls --open # then search "VcpuExit"
VcpuExit variant | KVM exit reason | Meaning | Firecracker's response |
|---|---|---|---|
IoIn(port, data) | KVM_EXIT_IO (in) | Guest executed IN on a PIO port | Read from the PIO bus into data |
IoOut(port, data) | KVM_EXIT_IO (out) | Guest executed OUT to a PIO port | Write data to the PIO bus |
MmioRead(addr, data) | KVM_EXIT_MMIO (read) | Guest read an MMIO address | Read from the MMIO bus into data |
MmioWrite(addr, data) | KVM_EXIT_MMIO (write) | Guest wrote an MMIO address | Write data to the MMIO bus |
Hlt | KVM_EXIT_HLT | Guest executed HLT (idle) | End this emulation round; the guest is idle |
Shutdown / SystemEvent | KVM_EXIT_SHUTDOWN | Triple fault / reset / poweroff | Initiate reset/shutdown; signals-shutdown-and-reset.md |
FailEntry | KVM_EXIT_FAIL_ENTRY | The CPU could not enter the guest | Fatal: bad vCPU state (regs/sregs); report |
InternalError | KVM_EXIT_INTERNAL_ERROR | KVM hit an internal error | Fatal: report and stop |
Most guest device interaction shows up as MmioRead/MmioWrite (virtio-mmio registers,
the-mmio-bus-and-device-manager.md) or IoIn/IoOut (the serial
console and legacy x86 ports). Note what is not here on the fast path: a virtio "kick" that has been
wired to an ioeventfd does not appear as an MMIO exit at all — KVM signals an eventfd instead, and
the VMM thread handles it. The exits you see are the ones not (or not yet) offloaded.
PIO vs MMIO: how the data reaches you
This distinction trips people up because the raw kvm_run shared page lays the two out differently. KVM
mmaps a struct kvm_run page per vCPU (size from KVM_GET_VCPU_MMAP_SIZE); on exit, the relevant union
member is populated.
PIO (KVM_EXIT_IO) | MMIO (KVM_EXIT_MMIO) | |
|---|---|---|
| Triggered by | IN/OUT instructions, 16-bit port space | loads/stores to a physical address |
| Data location | a separate buffer at (char*)run + run->io.data_offset | inline in run->mmio.data[8] |
| Address | run->io.port | run->mmio.phys_addr |
| Size/direction | run->io.size, run->io.direction, run->io.count | run->mmio.len, run->mmio.is_write |
kvm-ioctls hides this: VcpuExit::IoOut(port, data) already hands you the data slice (resolved from
io.data_offset), and VcpuExit::MmioWrite(addr, data) hands you the inline mmio.data. You rarely
touch the raw page in Firecracker — but you must know the distinction, because a bug where PIO data is
read from the wrong offset, or an MMIO write of the wrong len, manifests as garbage in/out of a device.
# If you need the raw page (rare): the mmap and accessors.
rg -n "kvm_run|get_vcpu_mmap_size|VcpuFd::run|io.data_offset|mmio" src/vmm/src/vstate/vcpu/
Dispatch to the buses
The PIO and MMIO arms each call into a Bus — a sorted map from address range to device — that finds the
device owning the address and forwards the access. This is the boundary between the run loop and device
emulation.
rg -n "Bus|pio_bus|mmio_bus|fn read\b|fn write\b|BusDevice|insert" src/vmm/src/vstate/vcpu/ src/vmm/src/device_manager/
flowchart TD
Run["ioctl(KVM_RUN)"] --> Exit{VcpuExit?}
Exit -->|IoIn/IoOut| PIO["PortIODeviceManager.bus"]
Exit -->|MmioRead/MmioWrite| MMIO["MMIODeviceManager.bus"]
Exit -->|Hlt| Idle["end round (guest idle)"]
Exit -->|Shutdown/SystemEvent| Shut["reset / shutdown"]
Exit -->|FailEntry/InternalError| Fatal["report fatal, stop"]
PIO --> Dev1["serial / i8042 / legacy"]
MMIO --> Dev2["virtio-mmio device registers"]
Dev1 --> Run
Dev2 --> Run
The bus lookup, the device's read/write, and any side effect (e.g. the serial device emitting a byte)
all happen synchronously, on the vCPU thread, with the guest stopped. The device handler must be
quick. Heavy work — actually reading a block from disk, sending a network frame — is not done here; the
virtio device only updates ring state and the real I/O is driven later by the VMM thread off an eventfd.
This split is the whole performance argument. See
the-mmio-bus-and-device-manager.md for the bus internals.
Coordinating with the VMM thread: VcpuEvent / VcpuResponse
A vCPU thread cannot be left to spin forever with no way to control it. The VMM thread holds a
VcpuEvent sender per vCPU and receives VcpuResponses; between (or interrupting) KVM_RUN calls, the
vCPU thread checks for control events.
rg -n "enum VcpuEvent|enum VcpuResponse|Pause|Resume|Exit|VcpuHandle|send_event" src/vmm/src/vstate/vcpu/
VcpuEvent | The vCPU thread does | Replies |
|---|---|---|
Pause | Exit the run loop into a paused state (used by snapshot, clean shutdown) | VcpuResponse::Paused |
Resume | Re-enter the KVM_RUN loop | VcpuResponse::Resumed |
Exit / Finish | Tear down and end the thread | terminal |
| (state save/restore) | Dump/load KVM state for snapshots | state payload |
To make a running vCPU notice a Pause, the VMM thread sends the event and sends the vCPU thread a
signal. The signal interrupts the in-flight KVM_RUN ioctl, which returns EINTR; the loop's
EINTR arm then checks the VcpuEvent channel and transitions to paused. This is exactly how snapshot
create gets every vCPU to a stable, readable state before serializing it
(snapshotting.md).
rg -n "EINTR|VCPU_RTSIG_OFFSET|kill|pthread_kill|signal" src/vmm/src/vstate/vcpu/ src/vmm/src/signal_handler.rs
Tip:
EINTRonKVM_RUNis normal, not an error — it is the mechanism by which the VMM thread reaches into a running vCPU. Treat it as "check for control events and continue," not "something broke."
Reading exercise
# 1. The loop and the exit match.
rg -n "fn run\b|VcpuExit::|vcpu.*run\(" src/vmm/src/vstate/vcpu/
# 2. The PIO/MMIO bus dispatch.
rg -n "pio_bus|mmio_bus|Bus|\.read\(|\.write\(" src/vmm/src/vstate/vcpu/
# 3. The control channel.
rg -n "enum VcpuEvent|enum VcpuResponse|Pause|Resume" src/vmm/src/vstate/vcpu/
# 4. The EINTR / signal interaction.
rg -n "EINTR|signal|pthread_kill|VCPU_RTSIG" src/vmm/src/vstate/vcpu/ src/vmm/src/signal_handler.rs
# 5. (optional) the raw kvm_run page accessors.
rg -n "kvm_run|get_vcpu_mmap_size|data_offset" src/vmm/src/vstate/vcpu/
# 6. Boot a microVM and watch exits indirectly: a guest that spams the serial console drives IoOut.
Answer:
- Write the run loop's match arms from memory and say what each does.
- Why is a VM exit handler required to be fast? What stops while it runs?
- How do PIO and MMIO differ in where their data lives in
kvm_run, and how doeskvm-ioctlshide that? - Trace an MMIO write to a virtio device register:
KVM_RUN→ which bus → what the device does → back. - A virtio kick wired to an
ioeventfddoes not show up as aVcpuExit. Why, and where is it handled instead? - Explain how
Pause+ a signal +EINTRcombine to stop a running vCPU for a snapshot.
Common bugs and symptoms
| Symptom | Root cause | Where to look |
|---|---|---|
| Guest hangs, one vCPU pinned at 100% | Exit handler loops or blocks; never returns to KVM_RUN | the matching Bus device read/write |
KVM_EXIT_FAIL_ENTRY on first run | Initial regs/sregs wrong (not long mode) | the-boot-sequence.md; set_sregs |
| Device reads/writes garbage | Wrong len/offset handling of MMIO/PIO data | exit arm + device read/write width handling |
| Snapshot create hangs | A vCPU never reaches paused; signal/EINTR path broken | VcpuEvent::Pause; signal delivery; EINTR arm |
| Spurious "vCPU error" logs under load | EINTR treated as fatal instead of "check control events" | the Err(EINTR) arm |
| Guest reset does nothing | Shutdown/SystemEvent not routed to the reset path | the shutdown arm; signals-shutdown-and-reset.md |
Validation: prove you understand this
- Draw the run loop and the dispatch fan-out to the PIO/MMIO buses, halt, and shutdown.
- List the
VcpuExitvariants Firecracker handles and the action for each. - Explain the PIO-vs-MMIO data layout in
kvm_runand why a width/offset bug corrupts a device. - Explain, with the
ioeventfdmechanism, why some virtio activity never appears as a VM exit. - Walk the Pause sequence: how does the VMM thread stop a vCPU that is inside
KVM_RUN? - Why is
EINTRonKVM_RUNexpected behaviour and not an error?
Next: Guest Memory Management — the host memory that backs everything the guest, the loader, and the devices touch.