The MMIO Bus and the Device Manager
When a guest reads or writes a device register, the CPU exits to KVM with a physical address, and that
address arrives at a vCPU thread as a VcpuExit::MmioRead/MmioWrite (or IoIn/IoOut for port I/O).
Something has to turn that bare address into "the virtio-block device's QueueNotify register" and call the
right handler. That something is the Bus — an address-range-to-device map — and the device
managers that populate it. Firecracker's DeviceManager wraps an MMIODeviceManager (the virtio-mmio
and serial devices on x86 and aarch64), a PortIODeviceManager (x86 legacy PIO devices), and an
ACPIDeviceManager. This chapter shows how devices are placed at fixed MMIO windows, advertised to the
guest so its drivers can find them, registered during build, and how an MMIO exit is routed back to the
owning device.
After this chapter you will be able to: explain how a VM-exit address becomes a device method call; locate
the bus and the device managers; describe how a device's MMIO window and IRQ are allocated and advertised;
and trace device registration during build_microvm_for_boot.
Note: Firecracker's default transport is virtio-mmio: each device lives at a fixed MMIO register block with a single IRQ, and the guest is told where each device is (there is no PCI bus to enumerate). The
Busis what connects a guest's access to that block back to the device object. Hold the two halves together — placement (host side) and advertisement (guest side) must agree exactly.
The Bus: address → device
# The Bus abstraction and how exits are routed through it.
rg -n "struct Bus|BusDevice|fn insert|fn read\b|fn write\b|get_device|BusRange" src/vmm/src/devices/ src/vmm/src/device_manager/
A Bus is essentially a sorted map from [start, start+len) address ranges to device handles. On an MMIO
exit, the vCPU thread asks the MMIO bus "who owns this address?", gets the device and the offset within
its range, and calls read/write on it.
flowchart TD
Exit["VcpuExit::MmioWrite(addr, data)"] --> Lookup["mmio_bus.get_device(addr)"]
Lookup -->|found| Dev["BusDevice + offset = addr - base"]
Lookup -->|not found| Err["no device -> ignored / error"]
Dev --> Call["device.write(offset, data)"]
Call --> Effect["e.g. QueueNotify -> kick the virtqueue"]
| Concept | Meaning |
|---|---|
BusRange / range key | A device's [base, base+len) address window |
BusDevice | The trait a device implements: read(offset, data) / write(offset, data) |
get_device(addr) | Binary search for the range containing addr; returns the device + base |
| offset | addr - base — the register within the device's block |
There are two buses: the MMIO bus (driven by MmioRead/MmioWrite) and the PIO bus (driven by
IoIn/IoOut, x86 only). The vCPU run loop dispatches to the right one by exit type — see
vcpu-run-loop-and-vm-exits.md.
The device managers
DeviceManager is the umbrella; under it sit the specific managers.
rg -n "struct DeviceManager|MMIODeviceManager|PortIODeviceManager|ACPIDeviceManager" src/vmm/src/device_manager/
| Manager | Owns | Arch |
|---|---|---|
MMIODeviceManager | The MMIO bus; virtio-mmio devices (block/net/vsock/balloon/rng), the serial console, the i8042; MMIO window + IRQ allocation | x86_64 + aarch64 |
PortIODeviceManager | The PIO bus; legacy x86 port devices (serial COM ports, i8042 ports, the PIT/PIC ports owned by the irqchip) | x86_64 |
ACPIDeviceManager | ACPI device objects (for the ACPI tables that advertise topology and some devices) | x86_64 |
rg -n "fn register_mmio_virtio|fn register_virtio_device|register_mmio_serial|allocate|MMIO_LEN|mmio_base" src/vmm/src/device_manager/
The MMIODeviceManager is where the real action is for virtio. It does three things per device:
- Allocate an MMIO window (a fixed-size block from the MMIO gap region) and an IRQ line (see interrupts-and-irqchip.md).
- Register the device on the MMIO bus at that window, and register its interrupt eventfd with KVM via
KVM_IRQFD, and its queue-notify withKVM_IOEVENTFD. - Record the window/IRQ so they can be advertised to the guest.
Fixed MMIO windows and advertising them to the guest
Because there is no PCI enumeration, the guest cannot discover devices — it must be told. Each virtio-mmio device occupies a fixed-size window in the MMIO gap (the reserved address range below 4 GiB — see the-boot-sequence.md), and Firecracker tells the guest about each window and IRQ.
guest physical address space
┌─────────────────────────────────────────────────────────────┐
│ low RAM │ ... │ MMIO gap (below 4 GiB) │ high RAM │
│ │ │ ┌────────┐ ┌────────┐ ┌────────┐│ (>4 GiB) │
│ │ │ │virtio0 │ │virtio1 │ │virtio2 ││ │
│ │ │ │@ A0:I0 │ │@ A1:I1 │ │@ A2:I2 ││ │
│ │ │ └────────┘ └────────┘ └────────┘│ │
└─────────────────────────────────────────────────────────────┘
| Arch | How the guest learns device location |
|---|---|
| x86_64 | A kernel-cmdline token per device: virtio_mmio.device=<SIZE>@<ADDR>:<IRQ> (appended at boot-config time) |
| x86_64 (topology/some devices) | ACPI tables (RSDP/MADT) — see acpi-and-mptable.md |
| aarch64 | An FDT virtio_mmio node with reg (address/size) and interrupts properties |
rg -n "virtio_mmio.device|add_virtio_mmio|cmdline.*virtio|fdt.*virtio|MMIO_CFG|add_mmio_node" src/vmm/src/ src/vmm/src/arch/
The two sides must match: the bus registration (host) and the advertisement (guest) must use the same
base address and IRQ, or the guest's driver will probe an address the bus doesn't route to that device,
or wait on an interrupt line the device never raises. This is the single most common device-bring-up bug,
and it is why the MMIODeviceManager records the window/IRQ at registration and replays them into the
cmdline/FDT.
Registration during build
Device registration happens in the-boot-sequence.md, step 8, after memory, the VM fd, the irqchip, and the vCPUs exist, and before the vCPUs are released.
rg -n "attach_block_devices|attach_net_devices|attach_vsock|attach_balloon|attach_entropy|attach_legacy|register" src/vmm/src/builder.rs src/vmm/src/device_manager/
flowchart LR
B["build_microvm_for_boot"] --> L["create legacy devices (serial, i8042)"]
L --> Blk["attach block devices"]
Blk --> Net["attach net devices"]
Net --> Etc["attach vsock / balloon / rng (as configured)"]
Etc --> Reg["MMIODeviceManager: allocate window+IRQ, insert on bus, irqfd+ioeventfd"]
Reg --> Adv["advertise: cmdline virtio_mmio.device / FDT node / ACPI"]
For each configured device, the builder calls an attach_* helper that constructs the device object,
hands it to the MMIODeviceManager for window/IRQ allocation and bus insertion, wires the eventfds to KVM,
and records the advertisement data. The serial console and i8042 (legacy) are attached through their own
paths (serial-console-and-legacy-devices.md). After all devices
are registered and advertised, the boot params/cmdline/FDT are finalized and the vCPUs start.
Reading exercise
# 1. The Bus.
rg -n "struct Bus|BusDevice|fn insert|get_device|BusRange|fn read\b|fn write\b" src/vmm/src/devices/ src/vmm/src/device_manager/
# 2. The device managers.
rg -n "struct DeviceManager|MMIODeviceManager|PortIODeviceManager|ACPIDeviceManager" src/vmm/src/device_manager/
# 3. Window + IRQ allocation and KVM wiring.
rg -n "allocate|mmio_base|MMIO_LEN|register_irqfd|register_ioevent|register_mmio" src/vmm/src/device_manager/
# 4. Advertisement to the guest.
rg -n "virtio_mmio.device|add_virtio_mmio|fdt.*virtio|cmdline" src/vmm/src/ src/vmm/src/arch/
# 5. The attach_* calls in the builder.
rg -n "attach_block_devices|attach_net_devices|attach_vsock|attach_balloon|attach_entropy|attach_legacy" src/vmm/src/builder.rs
# 6. In a booted guest, see the advertised devices:
# cat /proc/cmdline (the virtio_mmio.device= tokens)
# ls /sys/devices/platform/*.virtio_mmio
Answer:
- How does a
VcpuExit::MmioWrite(addr, data)become a call on a specific device? Name each step. - What three things does the
MMIODeviceManagerdo when it registers a virtio device? - Why must the guest be told where devices are, and what are the two pieces of information per device?
- What goes wrong if the advertised base/IRQ disagree with the bus registration?
- Name the three managers under
DeviceManagerand what each owns. - Where in the boot sequence does device registration sit, and why there?
Common bugs and symptoms
| Symptom | Root cause | Where to look |
|---|---|---|
Guest never finds a device (no /dev/vdX, no NIC) | Advertised base/IRQ ≠ bus registration | window/IRQ allocation vs cmdline/FDT advertisement |
| MMIO access hits no device (ignored/error) | Device not inserted on the bus, or wrong window | MMIODeviceManager insert/register |
| Two devices overlap / second device clobbers first | Window allocation reused an address | the MMIO window allocator |
| Device registers but never interrupts | irqfd registered on a different line than advertised | interrupts-and-irqchip.md |
| Kicks cause expensive MMIO exits instead of fast path | ioeventfd for QueueNotify not registered | register_ioevent in the manager |
| aarch64 guest sees no virtio devices | FDT virtio_mmio node missing/malformed | FDT generation in arch/aarch64/ |
Validation: prove you understand this
- Draw the path from an MMIO exit address to a device
write, including the bus lookup and offset. - Explain the
Busdata structure and howget_devicefinds the owner of an address. - List the three managers under
DeviceManagerand the bus each drives. - Explain placement vs advertisement and why they must agree, with the x86 cmdline token format.
- Describe what
MMIODeviceManagerdoes for one virtio device, including the two KVM eventfd registrations. - Place device registration in the boot sequence and justify its position relative to memory, irqchip, and vCPU start.
Next: virtio Transport (MMIO) — what actually lives inside each device's MMIO register block, and the virtio status/feature state machine.