Virtualization
Virtualization gives software a convincing illusion of hardware it does not exclusively own. A hypervisor fakes a whole machine so a guest kernel can boot on it; a container instead partitions the one kernel already running.
What Virtualization Means
Virtualization in operating system design — often shortened to OS virtualization — means presenting a virtual version of a resource that behaves like the real thing while being multiplexed underneath. The idea is not new to this page — virtual memory already gives every process the illusion of a private address space, and time-sharing already gives it the illusion of a private CPU.
A virtual machine is a software-created computer: an illusion of complete hardware, extending virtualization from a single resource to an entire machine. The guest sees a CPU, memory, disks, and network cards, and can boot an unmodified operating system on them. The hypervisor (or virtual machine monitor) maintains the illusion and shares the real hardware among guests.
The motivations are consolidation (many lightly used servers on one physical machine), isolation (a compromised or crashed guest does not affect its neighbours), and portability (a VM is a file, so it can be snapshotted, copied, and migrated between hosts while running). This is what operating system virtualization in cloud computing sells: one physical fleet, many isolated tenants.
Note that the term covers two quite different things, which the diagram on this page separates. System level virtualization fakes hardware so a guest kernel can run. OS level virtualization, also called operating system level virtualization, fakes nothing — it partitions the running kernel so processes get isolated views of it. Almost all confusion about virtualization comes from mixing these up.
- Virtual memory and time-sharing are already virtualization
- A VM extends the illusion to a whole machine
- Consolidation, isolation, and portability are the motives
- System-level fakes hardware; OS-level partitions a kernel
Hypervisor Types and the x86 Problem
A Type 1 (bare-metal) hypervisor runs directly on hardware with no host OS beneath it. VMware ESXi, Xen, and Hyper-V work this way. With nothing between it and the hardware, performance is close to native and the attack surface is small, which is why this is what data centres run.
A Type 2 (hosted) hypervisor runs as an application on an ordinary operating system — VirtualBox, VMware Workstation, QEMU. Every privileged guest operation passes through the host kernel, so there is more overhead, but you can run it on your laptop without dedicating the machine. KVM blurs the line: it turns the Linux kernel itself into a Type 1 hypervisor.
Popek and Goldberg established the formal condition in 1974: an architecture is virtualisable if every sensitive instruction — one that changes or reveals privileged state — is also privileged, so it traps when executed by a guest. The hypervisor can then emulate the trapped instruction and keep the illusion intact.
x86 failed this test. Instructions like POPF silently behaved differently in user mode instead of trapping, so a guest kernel could not simply be run. The workarounds were binary translation (VMware rewriting guest kernel instructions on the fly) and paravirtualization (Xen modifying the guest kernel to call the hypervisor explicitly). Intel VT-x and AMD-V fixed it in hardware around 2005–2006 by adding a guest mode where sensitive instructions trap properly, which is why unmodified guests run fast today. EPT/NPT later did the same for memory, removing the shadow page tables that had been the remaining bottleneck.
| Type 1 (bare metal) | Type 2 (hosted) | |
|---|---|---|
| Runs on | Hardware directly | A host operating system |
| Privileged ops go to | The hypervisor | Host kernel, then hypervisor |
| Performance | Near native | Lower — an extra layer |
| Attack surface | Small | The whole host OS |
| Examples | ESXi, Xen, Hyper-V | VirtualBox, VMware Workstation, QEMU |
- Type 1 on bare metal, Type 2 on a host OS; KVM is both
- Popek and Goldberg: sensitive instructions must be privileged
- x86 violated this, forcing binary translation and paravirtualization
- VT-x, AMD-V, and EPT/NPT fixed it in hardware
Terms, operations, and practical uses
Hypervisor types
- Type 1Bare metal — ESXi, Xen, Hyper-V. Near-native, small surface.
- Type 2Hosted on an OS — VirtualBox, QEMU. Convenient, more overhead.
- KVMTurns the Linux kernel itself into a Type 1 hypervisor.
Making x86 work
- Popek and GoldbergSensitive instructions must be privileged, so they trap.
- Binary translationRewriting guest kernel instructions on the fly.
- ParavirtualizationModifying the guest to call the hypervisor explicitly.
- VT-x / EPTHardware guest mode and nested paging fixed it properly.
Containers
- NamespacesPartition what a process can see — mount, PID, network, user.
- cgroupsPartition what it can consume — CPU, memory, I/O.
- Shared kernelMilliseconds to start; one kernel bug escapes every container.
- FirecrackerMicroVM isolation booting in about 125 ms.
Launch three workloads two ways
# What actually differs between a VM and a container: how many kernels boot.
VM_BOOT_MS, CONTAINER_START_MS = 2400, 40
class Hypervisor:
"""Type 1: each guest boots its own kernel on virtual hardware."""
def __init__(self):
self.kernels = 0
def launch(self, n):
self.kernels += n # every guest brings a kernel
return n * VM_BOOT_MS
class Kernel:
"""OS-level: one kernel, partitioned by namespaces and cgroups."""
def __init__(self):
self.kernels = 1 # the one already running
def launch(self, n):
return n * CONTAINER_START_MS # nothing boots -- just restricted views
vm, host = Hypervisor(), Kernel()
vm.launch(3)
host.launch(3)
print(f"VM boot {VM_BOOT_MS}ms x3 = {vm.kernels} kernels "
f"| container {CONTAINER_START_MS}ms x3 = {host.kernels} kernel shared")#include <iostream>
using namespace std;
// What actually differs between a VM and a container: how many kernels boot.
const int VM_BOOT_MS = 2400, CONTAINER_START_MS = 40;
struct Hypervisor { // Type 1: each guest boots its own kernel
int kernels = 0;
int launch(int n) {
kernels += n;
return n * VM_BOOT_MS;
}
};
struct Kernel { // OS-level: one kernel, partitioned
int kernels = 1; // the one already running
int launch(int n) {
return n * CONTAINER_START_MS;
}
// nothing boots
};
int main() {
Hypervisor vm;
Kernel host;
vm.launch(3);
host.launch(3);
cout << "VM boot " << VM_BOOT_MS << "ms x3 = " << vm.kernels
<< " kernels | container " << CONTAINER_START_MS << "ms x3 = "
<< host.kernels << " kernel shared\n";
}class Main {
// What differs between a VM and a container: how many kernels boot.
static final int VM_BOOT_MS = 2400, CONTAINER_START_MS = 40;
static class Hypervisor { // Type 1: each guest boots its own kernel
int kernels = 0;
int launch(int n) {
kernels += n;
return n * VM_BOOT_MS;
}
}
static class Kernel { // OS-level: one kernel, partitioned
int kernels = 1; // the one already running
int launch(int n) {
return n * CONTAINER_START_MS;
}
// nothing boots
}
public static void main(String[] args) {
Hypervisor vm = new Hypervisor();
Kernel host = new Kernel();
vm.launch(3);
host.launch(3);
System.out.println("VM boot " + VM_BOOT_MS + "ms x3 = " + vm.kernels
+ " kernels | container " + CONTAINER_START_MS + "ms x3 = "
+ host.kernels + " kernel shared");
}
}three instances, as VMs and as containersVM boot 2400ms x3 = 3 kernels | container 40ms x3 = 1 kernel sharedRun the example step by step
OS-Level Virtualization and Containers
OS-level virtualization is the technique of partitioning one running kernel so each workload sees an isolated view of it. There is no virtual hardware and no guest kernel. One kernel runs and gives each container an isolated view of it, so a process inside sees its own filesystem, its own network interfaces, and its own process tree.
On Linux this is built from two existing kernel features rather than a new subsystem. Namespaces partition what a process can see — separate mount, PID, network, user, IPC, and UTS namespaces mean a containerised process sees PID 1 as its own entry point and cannot see the host's processes at all. cgroups limit what it can consume: CPU shares, memory ceilings, I/O bandwidth.
The consequences are dramatic. A container starts in milliseconds because nothing boots — it is just a process with restricted views. It adds essentially no runtime overhead, since instructions execute natively with no trapping. And images are small, because only the application and its libraries are shipped, not a kernel and a full OS install.
The trade is the shared kernel. A kernel vulnerability is a container escape, whereas a VM would still contain it. Containers also cannot run a different kernel — no Windows containers on a Linux host, which is why Docker on macOS and Windows quietly runs a Linux VM underneath.
| Virtual machine | Container | |
|---|---|---|
| Virtualises | Hardware | The operating system |
| Guest kernel | One per guest | None — shares the host's |
| Start time | Seconds — a full boot | Milliseconds — nothing boots |
| Image size | Gigabytes | Megabytes |
| Runtime overhead | Small, with VT-x and EPT | Essentially none |
| Isolation boundary | Virtual hardware | Kernel namespaces |
| A kernel bug means | One guest affected | Every container escapes |
| Different OS kernel | Yes | No |
- Namespaces partition visibility; cgroups partition consumption
- Milliseconds to start, near-zero overhead, small images
- One kernel bug is an escape from every container
- Docker on macOS runs a hidden Linux VM for this reason
Choosing, and Where It Is Going
The decision is mostly about the isolation boundary you need. Untrusted or multi-tenant code, different guest kernels, or a regulatory requirement for strong separation all point to virtual machines. Many instances of your own trusted services, fast scaling, and dense packing point to containers.
In practice production runs both, layered: containers orchestrated by Kubernetes, running inside VMs that provide the tenant boundary. Cloud providers do exactly this, which is why 'containers replaced VMs' was never accurate.
The gap is narrowing from both sides. Firecracker, which powers AWS Lambda, is a microVM stripped to a minimal device model that boots in around 125 ms — VM-grade isolation at close to container speed. gVisor goes the other way, running a user-space kernel that intercepts container system calls so the host kernel is barely exposed. Kata Containers presents a container interface backed by a real lightweight VM.
Underneath all of it, the mechanisms are the ones covered across this course: privilege levels making sensitive instructions trap, page tables giving each guest its own memory, and the scheduler sharing CPUs. Virtualization is not a separate subject so much as the same operating-system ideas applied one level further down.
- VMs for untrusted tenants and differing kernels
- Containers for density, speed, and your own services
- Production layers containers inside VMs, not one or the other
- Firecracker and gVisor are closing the gap from both directions