MMIO and Port IO Emulation

A guest operating system talks to its “hardware” by reading and writing device registers. On real silicon those registers live behind a memory-mapped I/O (MMIO) address or a legacy x86 I/O port. In a virtual machine there is no real device behind those addresses, so every such access must be trapped and emulated: the CPU faults out of the guest with a VM exit, KVM decodes what the guest was trying to do, and either an in-kernel device model answers it or KVM hands the access up to the userspace virtual-machine monitor (VMM) through the kvm_run shared page as a KVM_EXIT_MMIO or KVM_EXIT_IO. This single trap-decode-dispatch path is the most-trodden hot path in the whole hypervisor, and the entire performance story of paravirtualized I/O — coalesced MMIO and ioeventfd — is about not taking it. This note traces the mechanism against the Linux 6.12 LTS sources (arch/x86/kvm/x86.c, virt/kvm/coalesced_mmio.c, virt/kvm/eventfd.c).

Mental Model

Think of a device register access as a question the guest asks the hardware. On bare metal the memory controller or the I/O bus answers instantly. In a VM, the address the guest pokes is deliberately left unbacked — there is no host RAM and no EPT mapping for it — so the access cannot complete and the CPU is forced to VM-exit into the hypervisor. KVM then plays the role of the bus: it figures out the address, length, direction, and (for writes) the data, and routes the question to whoever owns that address. The router checks three tiers in order: (1) is this a write that can be silently batched into a ring (coalesced MMIO)? (2) does an in-kernel device model own this address (the ioeventfd / in-kernel APIC / PIT path)? (3) otherwise, fill in kvm_run and return to userspace so the VMM can emulate it.

flowchart TD
  G["Guest executes MOV to MMIO addr<br/>or IN/OUT to I/O port"] --> EX{"VM exit"}
  EX -->|"unbacked GPA"| EPT["EPT violation / EPT misconfig<br/>(MMIO)"]
  EX -->|"IN / OUT instr"| PIO["I/O instruction exit (PIO)"]
  EPT --> DEC["KVM decodes faulting instruction<br/>(x86 emulator): addr, len, dir, data"]
  PIO --> BUS
  DEC --> BUS["kvm_io_bus_write / kvm_io_bus_read"]
  BUS -->|"coalesced write zone hit"| RING["Append to coalesced MMIO ring<br/>(NO userspace exit)"]
  BUS -->|"ioeventfd addr match"| EFD["eventfd_signal()<br/>(NO userspace exit)"]
  BUS -->|"in-kernel device (APIC/PIT)"| KDEV["Handle in kernel"]
  BUS -->|"no in-kernel handler"| US["Fill kvm_run.mmio / kvm_run.io<br/>exit_reason = KVM_EXIT_MMIO / KVM_EXIT_IO"]
  US --> VMM["ioctl(KVM_RUN) returns;<br/>VMM emulates, re-enters"]
  RING --> REENTER["Re-enter guest immediately"]
  EFD --> REENTER
  KDEV --> REENTER

The MMIO/PIO emulation decision tree. What it shows: every device-register access leaves the guest via a VM exit; KVM’s job is to resolve it as cheaply as possible. The three left branches (coalesced ring, ioeventfd, in-kernel device) all avoid the round-trip to userspace; only the rightmost branch pays the full KVM_EXIT_MMIO/KVM_EXIT_IO cost. The insight to take: “emulating a device” is mostly an exercise in eliminating the userspace exit — the slow KVM_EXIT_* path is the fallback, not the norm, for any device that has been optimized.

Mechanical Walk-through — MMIO

How an MMIO address becomes a trap

A guest-physical address (GPA) used for MMIO is never registered as guest RAM. In the KVM API, userspace registers RAM with KVM_SET_USER_MEMORY_REGION; an MMIO region is simply an address range the VMM declines to back with host memory (QEMU builds it with memory_region_init_io(), which has no ram flag, so no memory slot is created — per the terenceli KVM MMIO writeup). When the guest’s CPU walks its own page tables and then the second-dimension Extended Page Table (EPT) walk to resolve that GPA to a host-physical page, the EPT has no valid entry, so the hardware raises an EPT violation VM exit (Intel VT-x; the AMD equivalent is a nested-page-fault).

KVM’s memory-management-unit fault handler (kvm_mmu_page_fault()) recognizes that the faulting GPA has no struct page behind it — there is no memory slot — and concludes this is an MMIO access rather than a demand-fault to map in real memory (kernel-internals.org KVM exit handling). At that point KVM does not map a page; it emulates the instruction that caused the fault.

The EPT-misconfiguration fast trap (the MMIO SPTE trick)

There is a clever optimization in the first-touch path. Rather than leave the MMIO GPA permanently un-mapped (so that every access re-runs the relatively heavy EPT-violation path), KVM installs a deliberately invalid EPT entry — a “MMIO shadow page-table entry (SPTE)” — that encodes reserved bits the hardware can never satisfy (historically a pattern marking “write/execute but not readable,” using reserved physical-address bits). On the next access to that page, the CPU does not raise an EPT violation but an EPT misconfiguration exit, which handle_ept_misconfig() routes directly into the MMIO-emulation path without re-deriving that the page is MMIO (terenceli writeup). The MMIO GPA is also cached in the SPTE so the emulator can skip the guest-virtual-to-guest-physical translation. The reader’s takeaway: “EPT violation” and “EPT misconfiguration” are both MMIO triggers — the first on cold access, the second on the optimized warm path.

Decoding the instruction

KVM cannot just read the exit reason and know what to do — an MMIO access is encoded in an ordinary MOV (or MOVS, REP MOVS, etc.), and KVM must learn the address, the access size, the direction, and (for a write) the value. So it runs the x86 instruction emulator (kvm_emulate_instruction()emulator_read_write() in arch/x86/kvm/x86.c). The emulator fetches the guest instruction bytes, decodes the operands, and calls back into emulator_read_write_onepage(). That function (6.12, x86.c ~line 7921) first tries to resolve the access in-kernel:

/* Is this MMIO handled locally? */
handled = ops->read_write_mmio(vcpu, gpa, bytes, val);   /* -> kvm_io_bus_{read,write} */
if (handled == bytes)
    return X86EMUL_CONTINUE;       /* fully served in kernel, no userspace exit */

kvm_io_bus_write()/kvm_io_bus_read() consult the registered in-kernel I/O devices (the in-kernel local APIC, PIT, the coalesced-MMIO device, and any ioeventfd). If a device claims the access and serves it fully, emulation continues in-kernel and the guest re-enters with no userspace round-trip.

Falling through to userspace: KVM_EXIT_MMIO

If no in-kernel device serves the access, KVM records a “fragment” and fills the kvm_run page (6.12, x86.c ~line 8013):

vcpu->mmio_needed = 1;
vcpu->run->mmio.len      = min(8u, vcpu->mmio_fragments[0].len);
vcpu->run->mmio.is_write = ops->write;
vcpu->run->exit_reason   = KVM_EXIT_MMIO;
vcpu->run->mmio.phys_addr = gpa;

The matching struct kvm_run union member (6.12, include/uapi/linux/kvm.h) is:

/* KVM_EXIT_MMIO */
struct {
    __u64 phys_addr;   /* guest-physical address of the access */
    __u8  data[8];     /* write data out; read data in */
    __u32 len;         /* 1..8 bytes */
    __u8  is_write;    /* 1 = write, 0 = read */
} mmio;

ioctl(vcpu_fd, KVM_RUN) now returns to the VMM with exit_reason == KVM_EXIT_MMIO. For a write, KVM has already copied the value into mmio.data; the VMM emulates the side-effect and re-enters. For a read, the VMM computes the value, writes it into mmio.data, and on the next KVM_RUN KVM resumes the emulator (complete_emulated_mmio), splices the value into the guest register, and advances the instruction pointer. A read therefore costs a full exit-and-re-enter; a write costs one exit (the value is delivered eagerly).

Mechanical Walk-through — Port I/O

x86 has a second, older device interface: the I/O port address space (separate from physical memory), accessed with the IN and OUT instructions. These do not fault on a missing page — instead VT-x/SVM is configured to make IN/OUT cause a dedicated I/O instruction VM exit. KVM decodes the port number, size, and direction in emulator_pio_in_out() (6.12, x86.c ~line 8132). It first tries the in-kernel KVM_PIO_BUS:

for (i = 0; i < count; i++) {
    if (in)  r = kvm_io_bus_read (vcpu, KVM_PIO_BUS, port, size, data);
    else     r = kvm_io_bus_write(vcpu, KVM_PIO_BUS, port, size, data);
    if (r) { if (i == 0) goto userspace_io; ... }
    data += size;
}

If no in-kernel device claims port 0 of the access it falls through to userspace_io: and fills kvm_run.io:

vcpu->run->exit_reason   = KVM_EXIT_IO;
vcpu->run->io.direction  = in ? KVM_EXIT_IO_IN : KVM_EXIT_IO_OUT;
vcpu->run->io.size       = size;
vcpu->run->io.data_offset = KVM_PIO_PAGE_OFFSET * PAGE_SIZE; /* where data lives in the mmap'd region */
vcpu->run->io.count      = count;
vcpu->run->io.port       = port;

Note the difference from MMIO: PIO data does not live inline in the union. Instead io.data_offset points the VMM at a separate page in the mmap’d KVM_RUN region (the “PIO data page”), which can hold many values for a string INS/OUTS (io.count of them). The api.rst for 6.12 explicitly notes “KVM_EXIT_IO is significantly faster than KVM_EXIT_MMIO” and recommends PIO for performance-sensitive paravirtual signalling — because PIO does not require running the heavyweight memory-access instruction emulator, only a trivial port decode.

There is also a kvm_fast_pio() path (x86.c ~line 9384) for the common single-value IN/OUT al, dx form that avoids the full emulator and is used by some paravirtual fast paths.

Coalesced MMIO — batching write-only registers

Many device registers are written repeatedly with no intervening read whose result depends on the writes (think of pushing a stream of bytes into a serial transmit register, or programming a sequence of framebuffer pixels). Taking a userspace exit on every such write is ruinous. Coalesced MMIO turns those writes into appends to a shared ring buffer with no userspace exit; userspace drains the ring lazily, the next time it takes an exit for some other reason on the same device.

Userspace registers a coalesced zone with ioctl(vm_fd, KVM_REGISTER_COALESCED_MMIO, &zone) (the zone is a {addr, size, pio} triple — coalesced PIO exists too, gated by KVM_CAP_COALESCED_PIO). In the kernel, kvm_vm_ioctl_register_coalesced_mmio() (6.12, virt/kvm/coalesced_mmio.c) creates a kvm_coalesced_mmio_dev and registers it on the KVM_MMIO_BUS (or KVM_PIO_BUS). When the emulator’s kvm_io_bus_write() finds that this device’s coalesced_mmio_write() claims the address, the write is not sent to userspace — it is appended to the ring:

/* virt/kvm/coalesced_mmio.c, coalesced_mmio_write() */
insert = READ_ONCE(ring->last);
if (insert >= KVM_COALESCED_MMIO_MAX ||
    (insert + 1) % KVM_COALESCED_MMIO_MAX == READ_ONCE(ring->first)) {
    spin_unlock(&dev->kvm->ring_lock);
    return -EOPNOTSUPP;          /* ring full -> fall back to a real exit */
}
ring->coalesced_mmio[insert].phys_addr = addr;
ring->coalesced_mmio[insert].len       = len;
memcpy(ring->coalesced_mmio[insert].data, val, len);
ring->coalesced_mmio[insert].pio       = dev->zone.pio;
smp_wmb();
ring->last = (insert + 1) % KVM_COALESCED_MMIO_MAX;

The ring is one page (KVM_COALESCED_MMIO_MAX = (PAGE_SIZE - sizeof(struct kvm_coalesced_mmio_ring)) / sizeof(struct kvm_coalesced_mmio), per include/uapi/linux/kvm.h, 6.12). The first/last indices form a classic single-producer (vCPU thread, under ring_lock)/single-consumer (VMM) circular buffer; one slot is intentionally left empty so an empty ring (first == last) is distinguishable from a full one. Crucially, when the ring is full the write returns -EOPNOTSUPP, the emulator does not find it “handled,” and the access degrades gracefully into a normal KVM_EXIT_MMIO. The api.rst (6.12 §4.116) frames the contract: deferred writes are flushed by userspace the next time any access to a register on the same device causes a real vmexit — “this last access will cause a vmexit and userspace will process accesses from the ring buffer before emulating it.”

ioeventfd — turning a doorbell write into a signal

Coalesced MMIO still requires someone eventually to take an exit and drain the ring. The virtio data path needs something better: a guest “kick” (a write to a notification/doorbell register meaning “I’ve queued descriptors, go look”) should wake a backend without disturbing the vCPU at all. That is ioeventfd: KVM is told to translate a write to a specific address (optionally only when the written value matches a datamatch) into an eventfd_signal() on a file descriptor the backend is polling. See irqfd and ioeventfd for the eventfd plumbing and the symmetric interrupt-injection side (irqfd).

Userspace registers it with ioctl(vm_fd, KVM_IOEVENTFD, &args) where struct kvm_ioeventfd (6.12, include/uapi/linux/kvm.h) is {datamatch, addr, len, fd, flags}. The flags select MMIO vs KVM_IOEVENTFD_FLAG_PIO, optional KVM_IOEVENTFD_FLAG_DATAMATCH, and deassign. In the kernel kvm_assign_ioeventfd_idx() (6.12, virt/kvm/eventfd.c) registers a _ioeventfd device on the appropriate bus; on a matching write its ioeventfd_write() does exactly one thing:

static int ioeventfd_write(struct kvm_vcpu *vcpu, struct kvm_io_device *this,
                           gpa_t addr, int len, const void *val)
{
    struct _ioeventfd *p = to_ioeventfd(this);
    if (!ioeventfd_in_range(p, addr, len, val))
        return -EOPNOTSUPP;
    eventfd_signal(p->eventfd);   /* wake the backend; NO userspace round-trip */
    return 0;
}

ioeventfd_in_range() requires an exact address match; if len == 0 (“wildcard length”) it matches any size, and with datamatch set it compares the written value (1/2/4/8 bytes) against p->datamatch before signalling. This is what lets one virtio device multiplex many queues onto distinct doorbell values.

There is a further optimization for the zero-length case: kvm_assign_ioeventfd() (6.12, virt/kvm/eventfd.c ~line 980) registers a length-ignoring MMIO ioeventfd on a second, dedicated KVM_FAST_MMIO_BUS for faster lookup:

if (!args->len && bus_idx == KVM_MMIO_BUS) {
    ret = kvm_assign_ioeventfd_idx(kvm, KVM_FAST_MMIO_BUS, args);
    ...
}

This is the “fast MMIO” path (gated by KVM_CAP_IOEVENTFD_ANY_LENGTH). Historically it skipped instruction decoding entirely by using the hardware-provided VM-exit instruction length to advance past the kick. Paolo Bonzini’s 2017 fix (patchwork 1502890494) corrected this: “neither EPT violations nor misconfigurations are listed in the manual among the VM exits that set the VM-exit instruction length field.” So the fast path now uses EMULTYPE_SKIP to safely decode-and-skip — still signalling the eventfd before full emulation, just not relying on an architecturally-undefined field.

Failure Modes and Common Misunderstandings

  • “MMIO and PIO are the same exit.” They are not. MMIO arrives via an EPT violation/misconfiguration and requires the full memory-access instruction emulator; PIO arrives via a dedicated I/O-instruction exit and needs only a trivial port decode. The 6.12 api.rst explicitly notes PIO is “significantly faster,” and PIO data lives at io.data_offset in a separate page, not inline in the union — a frequent VMM bug is reading mmio.data for a PIO exit.
  • Coalesced ring overflow silently degrades. If userspace stops draining (e.g. a stuck device thread), the ring fills, coalesced_mmio_write() returns -EOPNOTSUPP, and every subsequent write becomes a real KVM_EXIT_MMIO. Symptom: a sudden collapse in throughput and a spike in MMIO exits with no apparent functional change — the optimization just quietly stopped working.
  • Read-after-coalesced ordering. Because coalesced writes are buffered, a register read (or a write to a non-coalesced register) on the same device must flush the ring first, or the device sees stale ordering. The kernel does not enforce this across devices — it is the VMM’s responsibility to drain the ring before servicing a real exit on the same device. Getting this wrong manifests as dropped or reordered writes (e.g. corrupt serial output, glitched framebuffer).
  • ioeventfd datamatch mismatch never fires. If the guest driver and the registered datamatch disagree (wrong endianness, wrong width), ioeventfd_in_range() returns false, no signal is sent, and the write also isn’t handled by the fast path — it falls back to a slow KVM_EXIT_MMIO, which a generic VMM may not even recognize as a doorbell. Symptom: virtio “stalls” — the guest queues work but the backend never wakes.
  • MMIO emulator can’t decode the instruction. Exotic or malformed guest instructions touching MMIO can defeat the x86 emulator, producing an emulation failure that KVM surfaces as KVM_EXIT_INTERNAL_ERROR (with KVM_INTERNAL_ERROR_EMULATION). This is the classic “emulation failure” seen with buggy guests or device models touching MMIO in unusual ways.

Alternatives and When to Choose Them

The four mechanisms form a cost ladder, cheapest last-resort to fastest:

  1. Full userspace emulation (KVM_EXIT_MMIO/KVM_EXIT_IO) — maximum flexibility, the VMM models arbitrary device behaviour, but each access is a full exit + emulator + userspace round-trip. Correct default for cold, rarely-touched registers (PCI config space, BAR setup).
  2. In-kernel device models — the local APIC, PIT, and IOAPIC are emulated inside KVM so their hot registers never reach userspace. Not extensible by the VMM; reserved for a fixed set of latency-critical legacy devices.
  3. Coalesced MMIO/PIO — for write-only register streams where the value matters but the timing of servicing does not. Cuts exits to roughly one per drain. Wrong for registers whose write has an immediate observable side-effect that a later read depends on.
  4. ioeventfd / fast MMIO — for pure doorbell writes (virtio kicks) where the write itself carries no data the device must store — only “go.” Zero userspace round-trip; the backend (often vhost in-kernel or a polling vhost-user process) is woken via eventfd. The foundation of the virtio fast path.

The deeper alternative is to remove emulation entirely: VFIO device passthrough gives the guest a real device whose registers are mapped straight through, so there is no exit at all — at the cost of losing live migration and overcommit.

Production Notes

In real virtio-net/virtio-blk deployments, the kick register is an ioeventfd, the completion interrupt is an irqfd, and the descriptor processing is offloaded to vhost — so a busy I/O path can run with essentially zero MMIO exits to userspace, which is the entire reason virtio scales. Legacy emulated devices (an emulated e1000 NIC, an IDE controller) lack these and exhibit far higher exit rates; this is visible in perf kvm stat or the per-VM kvm_stat counters, where mmio_exits and io_exits dominate for badly-virtualized guests. A known pathological case is older Windows guests hammering the emulated VGA framebuffer and the PM timer, generating floods of ept_violation and MMIO exits — the standard remedy is enabling virtio/QXL drivers so the hot registers move to coalesced MMIO or ioeventfd (kvm.vger.kernel.narkive discussion on Windows MMIO exit floods).

Uncertain

Verify: the precise reserved-bit pattern of the MMIO SPTE in 6.12 (older sources cite (0x3ull << 62) | 0x6ull, i.e. write/execute-but-not-readable using reserved physical-address bits). Reason: the exact mask is generated dynamically (kvm_mmu_set_mmio_spte_mask() / per-CPU shadow_mmio_value) and has changed across releases; I verified the behaviour (EPT-misconfig fast trap for warm MMIO) against the terenceli writeup (dated 2018) but did not re-read the 6.12 arch/x86/kvm/mmu/spte.c to confirm the literal bit pattern. To resolve: read spte.c/mmu.c at tag v6.12 and pin the current mask. uncertain

See Also