I/O Virtualization

We have given each virtual machine a virtual CPU (via VT-x) and virtual memory (via nested page tables). But a computer is useless without devices — a network card, a disk, a GPU. A guest OS expects to see real hardware to drive, yet ten guests cannot each own the one physical network card. Giving every VM its illusion of private devices is the job of I/O virtualization, and it turns out to be where most of the real performance is won or lost, because real workloads are dominated by I/O.

There is a spectrum of three approaches, trading compatibility against speed: full emulation, paravirtualized devices, and direct hardware passthrough.

Full emulation — maximum compatibility, minimum speed

The most compatible trick is to have the VMM pretend to be a real device — usually an old, well-understood one like the Intel e1000 network card or an IDE disk. The guest loads its ordinary driver, writes to what it thinks are the card's registers, and every one of those accesses traps into the VMM, which emulates the effect in software (QEMU is the classic emulator).

This works with an unmodified guest — even a decades-old OS that has never heard of virtualization — which is its great virtue. But it is slow: a single network packet can cause many VM-exits, each costing thousands of cycles. Emulation is for compatibility and boot-time devices, not for the fast path.

Paravirtual devices (virtio) — a shared ring buffer

The insight of virtio is to stop pretending. Instead of emulating real silicon, define a virtual device designed for virtualization, and give the guest a driver that knows it is a guest. Guest and host then communicate through a shared-memory virtqueue: the guest places many requests into a ring buffer and rings a doorbell once; the host processes the batch and posts completions back. It is the same batching idea as io_uring — amortise the expensive world-crossing over many operations.

Because one notification moves a whole batch, virtio slashes the number of VM-exits and gets close to native throughput. The price is a small loss of transparency: the guest needs virtio drivers (universal in modern Linux/Windows). This is the default for almost every cloud VM today.

// Emulation vs virtio: count the expensive VM-exits to move N I/O requests. // Emulation traps on each device-register access (~4 per request); virtio batches, ~1 exit per batch. function emulationExits(requests: number): number { const ACCESSES_PER_REQ = 4; // program descriptor, kick, read status, ack IRQ return requests * ACCESSES_PER_REQ; } function virtioExits(requests: number, batch: number): number { return Math.ceil(requests / batch); // one doorbell per batch of `batch` requests } const N = 1000, BATCH = 64; console.log(`emulation VM-exits for ${N} requests: ${emulationExits(N)}`); console.log(`virtio VM-exits for ${N} requests: ${virtioExits(N, BATCH)} (batch=${BATCH})`); console.log(`reduction: ${(emulationExits(N) / virtioExits(N, BATCH)).toFixed(0)}x fewer costly exits`);

Passthrough and SR-IOV — near-native, at a cost

For the very fastest I/O, hand the guest a real device to drive directly, with no VMM in the data path at all. The danger is DMA: a device programmed by the guest could be told to read or write any physical memory, escaping its VM. The IOMMU (Intel VT-d, AMD-Vi) solves this — it is an MMU for devices, translating and bounding the physical addresses a device may touch to the guest's own memory, so passthrough is safe.

But one card can only be handed to one VM. SR-IOV (Single-Root I/O Virtualization) fixes that: a single physical NIC advertises many lightweight virtual functions, each assignable to a different VM, each appearing as its own device. Guests get near-native network speed. The cost is flexibility: the VM is now bound to that specific hardware, complicating live migration.

This is the whole game, and the answer is the IOMMU. Without it, a device does DMA using raw physical addresses — so a malicious or buggy guest that owns a real NIC could program it to scribble over the hypervisor or a neighbouring VM, and hardware isolation would be a lie. The IOMMU sits between devices and memory exactly as the MMU sits between the CPU and memory: it translates the addresses a device issues and refuses any that fall outside the memory that VM is allowed to touch. It is the reason passthrough can be both fast and safe, and it is why "just give the VM the hardware" only became practical once IOMMUs shipped.

It is tempting to reach for SR-IOV everywhere because it is fastest — but each step toward the metal trades away the very flexibility that made virtualization attractive. An emulated device can be live-migrated to any host and works with any guest. A virtio device migrates easily too. But a VM bound to a specific physical NIC's virtual function cannot simply be paused and resumed on another machine — its device state lives in hardware you're leaving behind. Cloud providers therefore default to virtio and reserve passthrough/SR-IOV for workloads (high-frequency trading, NFV, GPUs) that genuinely need the last few microseconds and can give up seamless migration to get them.