Three illusions, not one
By now you know the big idea: a hypervisor presents each guest with a software illusion of a whole machine, a virtual machine, and the previous guide showed the heart of how the processor is shared — trap-and-emulate. But a real computer is not just a CPU. A program also expects memory it can address and devices it can read and write. So a convincing virtual machine has to fake three different things at once, and each one is faked by a quite different trick. This guide takes them in turn: CPU, then memory, then I/O. Get all three right and the guest OS — itself a building manager that thinks it runs the whole building — never notices it is a tenant.
Keep one principle in your pocket as we go. Virtualization works by interposition: the hypervisor inserts itself between the guest and the real hardware, lets the harmless things run directly on the metal at full speed, and only steps in for the dangerous or shared parts. The whole art is making that intervention rare and cheap. If the hypervisor had to interpret every single instruction, a VM would crawl. The goal is the opposite — most of the time the guest runs on the bare CPU, and the hypervisor is the silent landlord you only meet when you try to touch something that is not really yours.
Virtualizing the CPU: run direct, trap the rest
Recall the rule that makes the whole thing tractable: a guest's ordinary arithmetic, its loops, its memory reads — all the harmless instructions — run directly on the real CPU at native speed. Only the privileged instructions, the ones that would reconfigure the actual machine (set the timer, talk to a device, change the page-table register), must be caught. The hypervisor runs the guest OS in user mode even though the guest believes it is in kernel mode. When the guest tries a privileged instruction from user mode, the hardware refuses and raises a trap into the hypervisor, which then quietly does the right thing on the guest's behalf and resumes it. That is trap-and-emulate in one sentence: privileged actions trap; the hypervisor emulates.
The previous guide was honest about why this was historically painful on x86: a handful of x86 instructions were non-virtualizable — they behaved differently in kernel and user mode but did NOT trap when run unprivileged, they just silently misbehaved. So a guest could peek at the real machine state and notice it was lying to itself. Early VMware solved this with binary translation, scanning the guest's kernel code and rewriting the troublesome instructions on the fly. Then chip makers fixed the architecture itself: hardware-assisted virtualization (Intel VT-x, AMD-V) added a genuine new privilege layer beneath the kernel. The guest OS gets to sit in real kernel mode in its own world, while the hypervisor sits one level deeper in 'root mode'; the dangerous instructions now trap cleanly to it. No rewriting needed.
Virtualizing memory: a page table on top of a page table
Now the deepest puzzle, and the one with the most beautiful solution. The guest OS runs its own page tables, translating its processes' virtual addresses into what it thinks are physical addresses. But those are not real physical addresses — they are 'guest-physical' addresses, themselves just an illusion the hypervisor hands out. The real RAM is divided among several guests, so the same guest-physical address 'frame 100' must point to entirely different real frames for VM A and VM B. We therefore have not one but two layers of translation: guest-virtual to guest-physical (the guest's own page table), then guest-physical to host-physical (the hypervisor's mapping). The hardware MMU only walks one table — so something has to collapse two layers into one.
The first solution was the shadow page table, done purely in software. The hypervisor secretly builds a third table that maps guest-virtual addresses straight to real host-physical frames — composing the two layers by hand. This shadow table is what the real hardware MMU actually uses. The guest never sees it; it edits its own page table believing that drives translation, but the hypervisor keeps the guest's table marked read-only, traps every change the guest makes, and patches the shadow accordingly. It works, but the cost is brutal: every guest page-table update is now a trap into the hypervisor, and on a busy guest those are constant. Faithful, but expensive.
Then hardware fixed this too, and elegantly: the nested page table (Intel calls it EPT, AMD calls it RVI/NPT). Now the MMU itself understands two levels. The guest keeps and freely edits its own page table — guest-virtual to guest-physical — with NO traps at all. The hypervisor keeps a second table — guest-physical to host-physical — and the hardware automatically walks both, one after the other, on every miss. The cost moved from software (constant traps) to hardware (a longer page-table walk on a TLB miss). Which is the better deal? It depends on the workload: nested paging wins when the guest changes its mappings a lot; shadow paging could win when the guest is page-table-stable but TLB misses are frequent. Honesty point — there is no free lunch, only a different bill.
Without VM (one translation):
process-virtual --[page table]--> physical frame
With VM (two translations the MMU must do):
guest-virtual --[guest page table]--> guest-physical
guest-physical --[nested page table]--> host-physical frame
Shadow page table = precompute the whole chain in software:
guest-virtual ----------[shadow table]----------> host-physicalVirtualizing I/O: emulate, or just be honest
The guest also wants disks, network cards, and a clock. The first approach is full device emulation: the hypervisor pretends to be a specific, real, well-known piece of hardware — say an old Intel network card — bit for bit. Every time the guest pokes one of that card's memory-mapped I/O registers, it traps to the hypervisor, which decodes the access and acts it out. The huge upside is that the guest needs no special driver at all; its existing driver for that famous old card just works. The downside is speed: a single network packet can cause many register pokes, and every poke is a trap. Emulating a device faithfully is correct but slow, for the same reason interpreting every CPU instruction would be slow.
The clever fix is to stop pretending. Instead of emulating real hardware the guest already has a driver for, define a brand-new, deliberately simple, virtualization-aware device — and ship a driver for it inside the guest. This is exactly the paravirtualization idea applied to devices, and the common standard for it is called virtio. A virtio device and its guest driver share a ring buffer in memory: the guest queues up many requests at once and then signals the hypervisor just once ('there is work in the queue'), instead of trapping per register poke. Far fewer trips across the boundary, far higher throughput. The trade-off is honesty about being virtual — the guest must run a virtio driver, so it knows it is in a VM. Speed bought with a small loss of the perfect illusion.
Live migration: moving a running machine without stopping it
Once all of a machine's state — its CPU registers, its memory, its virtual devices — lives in the hypervisor's data structures rather than soldered to one physical board, an astonishing thing becomes possible: you can pick up a running VM and move it to a different physical host with barely a hiccup. This is live migration, and it is the quiet superpower behind the cloud. It is how a provider drains a machine for maintenance, or rebalances load, without your server ever appearing to go down. The hard part is that the VM keeps changing its own memory while you are busy copying that memory away. The trick is to chase the changes until almost nothing is left.
- Pre-copy, round one: with the VM still running on the source host, copy all of its memory pages across the network to the destination. This takes a while, and the guest keeps writing during it.
- Track the dirty pages: any page the guest writes to after it was copied is marked 'dirty'. Now copy only those dirty pages again — a much smaller set than the whole memory.
- Repeat, converging: each round re-copies only what changed since the last round. Because a busy VM dirties a shrinking set of pages, each pass moves less, until the remaining dirty set is tiny.
- The brief stop-and-switch: pause the VM for a few milliseconds, copy the last handful of dirty pages plus the CPU registers, then resume the VM on the destination host. The pause is so short that network connections survive and users rarely notice.
Be honest about the limits. Live migration is not magic teleportation — it leans on a fast network between the two hosts, on shared storage so the disk does not also have to be copied, and on the dirty set actually shrinking. A VM that frantically rewrites its entire memory faster than the network can ship it will never converge; in that case the hypervisor eventually just forces the brief pause and accepts a slightly longer one. And nothing here crosses CPU architectures: you cannot live-migrate from an x86 host to an Arm host, because the guest's saved registers and instructions only make sense on the chip they came from.
Where this is heading: a lighter illusion
Step back and notice how much machinery we just built to fool a guest OS into thinking it owns a whole computer. We faked a CPU with traps and a hidden privilege level, faked memory with a second layer of page tables, and faked devices with emulation or virtio. All of that exists because a VM runs a complete, separate guest operating system — its own kernel, its own everything — on top of the hypervisor. That gives strong isolation: the guest kernel is walled off in its own machine. But it is heavy. Booting a VM means booting an entire OS; running ten of them means ten kernels' worth of memory.
This sets up the pivot for the rest of the rung. What if many isolated worlds could share ONE kernel instead of each carrying its own? That is the container idea, the subject of the next guide. A container does not run a guest OS and does not need a hypervisor faking hardware at all. Instead the host kernel hands each container its own private view of the system — its own process IDs, its own filesystem mounts, its own network — using two Linux mechanisms called namespaces (what a container can see) and cgroups (how much it can use), layered over a stackable overlay filesystem image. The result is far lighter than a VM and starts in a blink. The honest cost, which the final guide of this rung will weigh carefully, is weaker isolation: because containers share the host kernel, a flaw in that one shared kernel can endanger them all, whereas a VM's full guest kernel is a thicker wall.