Виртуализация: основы и обзор стека¶
Виртуализация — это создание иллюзии, что ОС работает на собственном железе, хотя в реальности под ней находится другая ОС или специальный software, который перехватывает все привилегированные операции и решает, что с ними делать. Гостевая ОС думает, что владеет CPU, памятью и устройствами целиком; на самом деле всем этим распоряжается hypervisor (он же VMM — Virtual Machine Monitor), который мультиплексирует один физический ресурс между десятками гостей.
Главная сложность виртуализации не в «эмуляции инструкций» — её в том, что классическая x86 архитектура изначально не была рассчитана на это, и десятилетия инженерных решений шли вокруг одной задачи: как заставить guest OS работать быстро, не модифицируя её исходный код и не теряя при этом контроль над железом.
Что значит «виртуализировать»¶
Когда обычное приложение в user mode выполняет привилегированную инструкцию (например, MOV CR3 или HLT), CPU
выбрасывает #GP — general protection fault — и управление переходит в kernel. Kernel либо эмулирует операцию, либо
убивает процесс. На этом построена защита между user space и kernel space.
Виртуализация переносит ту же идею на уровень выше: гостевое ядро становится для hypervisor таким же ненадёжным исполнителем, как user-программа для kernel. Когда guest пытается выполнить инструкцию, которая может затронуть общие ресурсы (записать в CR3, прочитать MSR, остановить CPU через HLT), управление перехватывается hypervisor, который либо выполняет операцию «по-настоящему» на виртуальных ресурсах гостя, либо эмулирует её эффект.
graph TB
subgraph BezVirt["Без виртуализации"]
UA1["user application"] --> K1["OS kernel<br/>(Ring 0, владеет CR3)"]
K1 --> HW1["CPU + RAM + devices"]
end
subgraph SVirt["С виртуализацией"]
UA2["user application"] --> GK["guest OS kernel<br/>(думает, что Ring 0)"]
GK --> HV["hypervisor / VMM<br/>(настоящий Ring 0, владеет CR3, EPT)"]
HV --> HW2["CPU + RAM + devices"]
end
Hypervisor поддерживает для каждого гостя:
- vCPU — виртуальный процессор: набор регистров, контекст, поток исполнения. На каждый vCPU обычно приходится один поток на host'е.
- vRAM — виртуальная физическая память: участок host-памяти, который гость видит как «настоящую» RAM, начиная с адреса 0.
- virtual devices — virtual NIC, virtual disk, virtual interrupt controller. Часть из них эмулируется в software (QEMU), часть — пробрасывается с реального железа через IOMMU.
Гость не знает, что под ним hypervisor (за исключением случаев paravirtualization — там знает и сотрудничает).
Краткая история виртуализации¶
timeline
title История виртуализации
1972 : IBM CP-67 / VM/370 (mainframe virtualization)
1998 : VMware Workstation (x86 без HW, binary translation)
2003 : Xen 1.0 (paravirt, modified guest)
2005 : Intel VT-x / AMD-V (HW-assisted, Ring -1)
2007 : KVM merged into Linux mainline (2.6.20)
2008 : Intel EPT / AMD NPT (nested page tables)
1972, IBM CP-67 / VM/370. Первая полноценная коммерческая виртуализация, на mainframe System/370. IBM сразу заложила в архитектуру правильное разделение privileged/non-privileged инструкций — все sensitive операции честно trap'ились в supervisor, что делало классический подход «trap-and-emulate» сразу применимым.
1998, VMware Workstation 1.0. Первая виртуализация x86 без поддержки железа. x86 спроектирован не для
виртуализации: 17 инструкций (POPF, PUSHF, SIDT, SGDT, SLDT, LAR, LSL, SMSW, STR, MOV from CS/SS,
и др.) ведут себя по-разному в Ring 0 и Ring 3, но не trap'ятся в Ring 3 — просто молча возвращают неправильный
результат. VMware решила эту проблему через binary translation: на лету переписывала гостевой код, заменяя
проблемные инструкции на callbacks в hypervisor.
2003, Xen 1.0 (Cambridge). Альтернативный подход — paravirtualization: вместо перехвата инструкций изменить гостевую ОС так, чтобы она сама вызывала hypervisor через hypercalls вместо проблемных инструкций. Дешевле в runtime, но требует модификации kernel — работало только с Linux и FreeBSD, Windows не поддерживался.
2005, Intel VT-x (Vanderpool) и AMD-V (Pacifica). Аппаратная поддержка. CPU получил новый режим работы — VMX root mode (для hypervisor) и VMX non-root mode (для гостя). В non-root режиме все sensitive инструкции автоматически вызывают VM-exit — переключение в hypervisor с сохранением состояния гостя. Никакого binary translation, никакой модификации гостя.
2007, KVM (Kernel-based Virtual Machine) в Linux mainline (2.6.20). Avi Kivity из Qumranet (потом купленной Red
Hat) сделал нечто простое и гениальное: превратил Linux kernel в hypervisor через kernel module kvm.ko. Каждая VM —
обычный процесс, vCPU — обычный thread. Весь scheduling, NUMA, cgroups — бесплатно от Linux. QEMU использовался как
userspace часть для эмуляции устройств.
2008, Intel EPT (Extended Page Tables) и AMD NPT (Nested Page Tables). Аппаратная поддержка вложенных таблиц страниц. До этого hypervisor поддерживал shadow page tables — теневую копию гостевых таблиц, синхронизированную с оригиналом, что стоило десятков VM-exit'ов на каждое изменение CR3. EPT/NPT убрали этот overhead полностью: MMU самостоятельно делает два уровня трансляции — guest virtual → guest physical → host physical.
Type 1 vs Type 2 hypervisors¶
Классическая классификация Голдберга и Попека (1974) делит hypervisors на два типа:
graph TB
subgraph T1["Type 1 (bare-metal)"]
VM1A["VM 1"] --> HV1["hypervisor<br/>(ESXi, Xen, Hyper-V)"]
VM2A["VM 2"] --> HV1
VM3A["VM 3"] --> HV1
HV1 --> HW1["bare metal hardware"]
end
subgraph T2["Type 2 (hosted)"]
VM1B["VM 1"] --> HV2["hypervisor<br/>(VirtualBox, VMware WS)"]
VM2B["VM 2"] --> HV2
HV2 --> HOST["host OS (Linux, Win)"]
HOST --> HW2["bare metal"]
end
Type 1 (bare-metal) работает прямо на железе, без host OS под собой. Hypervisor сам управляет CPU scheduling, memory allocation, hardware drivers. Примеры: VMware ESXi, Xen, Microsoft Hyper-V, IBM PR/SM.
У Xen есть особенность: первый запущенный гость — dom0 (domain 0) — получает привилегии для управления другими гостями и доступ к hardware drivers. Технически dom0 — обычная Linux/BSD, но через специальный API делегирует hardware access другим VMs.
Type 2 (hosted) запускается как обычное приложение поверх host OS. Все hardware drivers — от host OS, hypervisor лишь предоставляет виртуальную машину как пользовательский процесс. Примеры: VirtualBox, VMware Workstation, Parallels Desktop.
| Аспект | Type 1 | Type 2 |
|---|---|---|
| Установка | вместо OS | в OS, как программа |
| Drivers | свои (узкий список) | от host OS (любые) |
| Boot time | секунды | минуты (через host OS) |
| Overhead | минимальный | дополнительный слой host OS |
| Use case | datacenter, production | desktop, development |
| Изоляция | очень высокая | зависит от host OS |
KVM — гибрид¶
KVM формально не вписывается в эту классификацию. С одной стороны, KVM работает внутри Linux kernel — выглядит как
Type 2. С другой стороны, kernel module kvm.ko сам по себе hypervisor, использующий VT-x/AMD-V и работающий в Ring
0 без посредников — это поведение Type 1.
Правильнее называть KVM hybrid: hypervisor functionality (VM-entry/exit, EPT management, vCPU scheduling) живёт в kernel mode рядом с обычным Linux scheduler'ом. Linux при этом продолжает работать как обычная ОС — KVM-гость для него просто набор процессов, один thread на vCPU.
graph TB
G1["Guest OS 1 (Linux)"] -->|VM-exit| QEMU["QEMU (userspace, per-VM process)<br/>emulates: disk, NIC, GPU, ACPI"]
G2["Guest OS 2 (Windows)"] -->|VM-exit| QEMU
QEMU -->|ioctl(KVM_RUN)| KVM["Linux kernel + KVM module<br/>- vCPU scheduling (CFS/EEVDF)<br/>- VM-entry/VM-exit handling<br/>- EPT management<br/>- VMCS / VMCB регистры"]
KVM --> HW["Hardware: CPU (VT-x), IOMMU, NIC"]
С хозяйственной точки зрения KVM ближе к Type 1: Linux кernel и hypervisor неотделимы, оба работают в Ring 0, hardware drivers общие. Тип 2 предполагает hypervisor поверх неприкосновенного OS — а здесь kernel и hypervisor изначально одно целое.
Подходы к виртуализации¶
С момента выхода x86 разные команды пробовали разные способы сделать его виртуализуемым. Каждый подход решал конкретные проблемы и нёс свой набор trade-off'ов.
Trap-and-emulate¶
Классический подход, заложенный ещё в IBM/370. Принцип такой: запустить guest kernel в Ring 3 (как обычный
user-процесс), и тогда все привилегированные инструкции автоматически вызовут #GP. Hypervisor перехватит fault и
эмулирует операцию — например, при попытке записи в CR3 обновит свою shadow-таблицу страниц гостя.
flowchart TB
A["guest kernel в Ring 3<br/>MOV CR3, rax"] --> B["CPU: privileged instruction in Ring 3"]
B --> C["#GP exception → vector 13"]
C --> D["hypervisor's #GP handler в Ring 0"]
D --> E["декодирует инструкцию по rip гостя"]
E --> F["эмулирует эффект (обновляет shadow PT)"]
F --> G["инкрементит guest's rip"]
G --> H["возвращается в гостя через iret"]
Подход элегантный, но на x86 не работает в чистом виде. Голдберг и Попек в своей классической статье 1974 года сформулировали критерий: для классической виртуализации нужно, чтобы все sensitive instructions были также privileged (вызывали fault в user mode). x86 этому критерию не удовлетворяет — на ней 17 инструкций (см. историю VMware) ведут себя по-разному в разных ring'ах, но не trap'ятся.
Например, POPF восстанавливает RFLAGS из стека. В Ring 0 это меняет IF (interrupt flag), в Ring 3 — нет. Никакого
fault'а не возникает; guest kernel думает, что отключил interrupts, а на самом деле они продолжают работать. Через
несколько инструкций — катастрофа.
Binary translation (BT)¶
Чтобы обойти проблему, VMware придумала следующий трюк: перед запуском кусок guest-кода сканируется и переписывается. Все sensitive инструкции заменяются на callout'ы в hypervisor:
оригинальный guest code: переписанный код (cache блок):
pushq %rax pushq %rax
popfq ───────▶ call vmm_emulate_popfq
movq %cr3, %rax call vmm_read_cr3
cli call vmm_disable_irq
Переписанные блоки кэшируются (translation cache), так что один и тот же горячий код переписывается один раз.
Безопасные инструкции (add, mov reg,reg, call, etc.) копируются без изменений — но управление потоком (call,
jmp, ret) тоже переписывается, чтобы оставаться внутри cache'а.
VMware Workstation 1.0 (1998) была первым продуктом, который этого добился. Производительность была на удивление хорошая: для CPU-bound нагрузок overhead был всего 5-15%, потому что большинство инструкций виртуализовалось «через себя» — без замены.
Минусы:
- сложная реализация (десятилетия работы над BT-движком в VMware),
- проблемы с self-modifying code в госте,
- BT работает с CPU-инструкциями, но не упрощает виртуализацию I/O и interrupts,
- BT-overhead для system-call-intensive workloads был большой (каждый syscall в госте — несколько вызовов в hypervisor).
QEMU использует похожую технологию — TCG (Tiny Code Generator): переводит гостевые инструкции в TCG IR, потом в host машинный код. Это позволяет QEMU эмулировать любую архитектуру (ARM, RISC-V, MIPS) на любом host'е, но без hardware acceleration работает в 10-100 раз медленнее нативного.
Paravirtualization¶
Если переписывать guest binary на лету сложно — давайте перепишем guest source code заранее. Это идея Xen: изменить guest kernel так, чтобы он сам вызывал hypervisor вместо привилегированных инструкций:
guest kernel (модифицированный):
// вместо записи в CR3:
HYPERVISOR_mmu_update(new_cr3);
// вместо записи в page table:
HYPERVISOR_update_va_mapping(va, new_pte);
// вместо HLT:
HYPERVISOR_sched_op(SCHEDOP_block);
Каждый такой вызов — hypercall — компилируется в специальную инструкцию (int 0x82 на x86, vmmcall/vmcall
если есть HW поддержка), которая переключает в hypervisor.
Преимущества paravirt:
- минимальный overhead — hypercall быстрее, чем trap-and-emulate (точно знает, что делать, не нужно декодировать),
- одна транзакция вместо нескольких (можно сделать batch — обновить много PTE одним hypercall),
- никакой эмуляции sensitive инструкций — гость их просто не использует.
Недостатки:
- guest kernel нужно модифицировать (Linux это поддерживает, Windows — нет),
- каждый hypervisor определяет свой ABI hypercall'ов, гости несовместимы между Xen, KVM, Hyper-V,
- сложнее поддерживать ядро — два кодовых пути (paravirt и native).
Сейчас классический paravirt в значительной мере отжил — HW-assisted виртуализация даёт сопоставимый perf без модификации гостя. Но частичный paravirtualization (PV-on-HVM) живёт и здравствует: guest kernel виртуализован аппаратно (полная VT-x), но устройства (disk, NIC, console, balloon) — paravirtual через virtio. Это lingua franca современной виртуализации.
Hardware-assisted virtualization¶
Intel VT-x (Vanderpool, 2005) и AMD-V (Pacifica, 2006) добавили в CPU новый режим работы. Если раньше было четыре рингa (0–3), то теперь есть «измерение» поверх них: root mode для hypervisor и non-root mode для guest. В non-root mode гость получает полный набор Ring 0–3, как на настоящем железе.
classic x86 с VT-x
┌───────────────────┐ root mode non-root mode
│ Ring 3 │ (hypervisor) (guest)
│ Ring 2 │
│ Ring 1 │ Ring 3 Ring 3 (guest user)
│ Ring 0 │ Ring 2 Ring 2
└───────────────────┘ Ring 1 Ring 1
Ring 0 Ring 0 (guest kernel)
▲ │
│ VM-exit │ VMRESUME
└─────────────┘
Hypervisor в root mode переключается в гостя командой VMLAUNCH / VMRESUME. Гость выполняется до тех пор, пока не
случится одно из событий, которые спровоцируют VM-exit:
- выполнение sensitive инструкции (CPUID, MOV CR3, RDMSR, и т.д.),
- внешний interrupt,
- nested page fault (EPT violation),
- вызов
VMCALL(hypercall).
При VM-exit CPU автоматически сохраняет состояние гостя (RIP, RSP, регистры, CR-регистры) в специальную структуру
VMCS (Virtual Machine Control Structure), переключается в root mode и прыгает в hypervisor по адресу,
заранее заданному в VMCS. Hypervisor обрабатывает причину выхода, потенциально модифицирует гостевое состояние и
возвращается через VMRESUME.
Главное достижение HW-assisted подхода — никакой модификации гостя. Windows, Linux, FreeBSD, OS/2, DOS работают одинаково. Никакого binary translation, никаких hypercalls в коде ядра. CPU сам разруливает все sensitive инструкции через trap-and-emulate, но на этот раз — честно, потому что все sensitive операции теперь действительно вызывают VM-exit.
Подробности VMX, VMCS, VM-exit'ов и стоимости перехода — в статье про CPU virtualization.
KVM + QEMU стек¶
KVM — самый распространённый open-source hypervisor сейчас. Его архитектура отражает философию Linux: «не строй hypervisor с нуля, преврати kernel в hypervisor».
graph TB
subgraph US["userspace"]
subgraph QEMU["QEMU process (one per VM)"]
VCPU["vCPU thread 0, vCPU thread 1, ..."]
DEV["Device models:<br/>virtio-net, virtio-blk,<br/>AHCI, e1000, VGA, ACPI, ..."]
MEM["Memory backend:<br/>guest RAM = mmap region"]
end
end
subgraph KS["kernel space"]
subgraph DEVKVM["/dev/kvm"]
CORE["KVM core: VMCS manipulation,<br/>EPT setup, vCPU run loop, IRQ injection"]
VENDOR["kvm_intel.ko / kvm_amd.ko<br/>vendor-specific: VT-x / SVM intrinsics"]
end
end
subgraph HW["hardware (non-root mode)"]
GOS["Guest OS (in non-root mode)<br/>Guest kernel + guest user processes"]
end
QEMU -->|"ioctl(KVM_RUN, KVM_SET_REGS, ...)"| DEVKVM
DEVKVM -->|"VMLAUNCH / VMRESUME"| GOS
Цикл работы vCPU — один из основных потоков понимания KVM:
sequenceDiagram
participant QEMU as vCPU thread (QEMU)
participant KVM as kernel (KVM)
participant Guest
loop vCPU run loop
QEMU->>KVM: ioctl(vcpufd, KVM_RUN)
Note over KVM: load guest state to VMCS
KVM->>Guest: VMRESUME
Note over Guest: executes CPUID, IN,<br/>EPT viol, ...
Guest-->>KVM: VM-exit
Note over KVM: read exit_reason from VMCS
alt fast path (MSR, EPT viol)
KVM->>Guest: handle in kernel + VMRESUME
else slow path
KVM-->>QEMU: return from ioctl (KVM_EXIT_MMIO/IO/...)
Note over QEMU: emulate device,<br/>handle MMIO/PIO,<br/>update virtio queue
end
end
Разделение «fast path в kernel, slow path в userspace» — фундаментальный design choice. Простые operations (MSR read/write, EPT page-fault) обрабатываются прямо в kernel module, без переключения в userspace. Сложная эмуляция устройств (virtio, MMIO) уходит в QEMU, где есть весь инфраструктурный код. Это компромисс между latency и сложностью kernel-кода.
QEMU при этом — далеко не единственный userspace для KVM. Альтернативы:
- crosvm (Google) — минималистичный VMM для ChromeOS и Android Cuttlefish, написан на Rust.
- firecracker (AWS) — оптимизированный для микро-VMs (Lambda, Fargate), boot time < 125 ms.
- cloud-hypervisor (Intel) — fork firecracker для cloud workloads.
Все они используют то же /dev/kvm API, но переписывают device models, чтобы избавиться от исторического кода QEMU.
Терминология¶
| Термин | Значение |
|---|---|
| Hypervisor | software, который управляет VMs; синоним VMM |
| VMM | Virtual Machine Monitor; технически hypervisor — тип VMM |
| Guest | ОС, работающая внутри VM |
| Host | физическая машина + ОС, на которой работает hypervisor |
| vCPU | виртуальный CPU гостя; на host'е — обычно thread |
| vRAM | виртуальная физическая память гостя; на host'е — mmap-регион |
| Ring deprivileging | запуск guest kernel в Ring > 0 (классический approach VMware) |
| Hypercall | явный вызов hypervisor из гостя (VMCALL/VMMCALL, по аналогии с syscall) |
| Trap-and-emulate | hypervisor перехватывает privileged инструкцию и эмулирует её |
| Binary translation | переписывание guest-кода на лету |
| Paravirtualization | гость знает про hypervisor и сотрудничает (hypercalls вместо traps) |
| HVM | Hardware Virtual Machine — гость, использующий HW-assisted virt |
| PV-on-HVM | гибрид: HW-virt CPU, paravirt устройства (стандарт сейчас) |
| VMCS | Virtual Machine Control Structure (Intel) — 4 KB структура состояния VM |
| VMCB | Virtual Machine Control Block (AMD) — аналог VMCS |
| VM-exit | переход из non-root в root mode (из гостя в hypervisor) |
| VM-entry | переход из root в non-root mode (из hypervisor в гостя) |
| EPT | Extended Page Tables (Intel) — двухуровневая трансляция в железе |
| NPT | Nested Page Tables (AMD) — аналог EPT |
| SLAT | Second-Level Address Translation — общий термин для EPT/NPT |
| dom0 | управляющий guest в Xen, имеет привилегированный API |
| domU | обычный (непривилегированный) guest в Xen |
| virtio | стандартизированный paravirt интерфейс для устройств |
| IOMMU | I/O MMU — трансляция адресов для DMA (VT-d, AMD-Vi) |
| Posted interrupt | механизм доставки interrupt'а напрямую в guest APIC без VM-exit |
Виртуализация vs контейнеры¶
Контейнеры (Docker, LXC, containerd) часто противопоставляют виртуальным машинам, хотя это технологии разного уровня. Главное архитектурное отличие:
graph TB
subgraph VMS["Virtual Machines"]
AppAVM["App A"] --> LibsAVM["libs A"]
AppBVM["App B"] --> LibsBVM["libs B"]
LibsAVM --> GOSA["guest OS A (own kernel)"]
LibsBVM --> GOSB["guest OS B (own kernel)"]
GOSA --> HV["hypervisor"]
GOSB --> HV
HV --> HWVM["bare metal hardware"]
end
subgraph CONT["Containers"]
AppAC["App A"] --> LibsAC["libs A"]
AppBC["App B"] --> LibsBC["libs B"]
LibsAC --> KERN["Linux kernel (shared)<br/>namespaces + cgroups"]
LibsBC --> KERN
KERN --> HWC["bare metal hardware"]
end
VM запускает полное гостевое ядро в изолированной среде с виртуальными устройствами. Container — обычный процесс на host kernel, изолированный через namespaces (PID, mount, network, IPC, UTS, user) и ограниченный через cgroups.
| Аспект | VM | Container |
|---|---|---|
| Kernel | свой у каждого guest | shared с host |
| Boot time | секунды-минуты | миллисекунды |
| Memory overhead | сотни МБ (guest kernel + drivers) | мегабайты (только processes) |
| Density | десятки на host | сотни-тысячи на host |
| Изоляция | hardware-level (VT-x, EPT) | namespace-level (kernel-driven) |
| Attack surface | hypervisor API (узкий) | весь kernel syscall surface |
| Heterogeneous OS | Linux + Windows + BSD | только same kernel |
| Live migration | да (vMotion, KVM live migration) | сложно (CRIU, partial) |
| Resource pinning | дешёво (vCPU = thread) | дешёво (cgroup limits) |
Виртуализация и контейнеры не взаимоисключающие, а complementary: типичная production-инфраструктура запускает hypervisor (KVM в AWS Nitro, ESXi в VMware) на железе, и уже внутри VMs крутит контейнеры (Kubernetes). Это даёт два уровня изоляции:
- VMs изолируют tenants (security boundary, hypervisor очень узкий attack surface),
- containers внутри одного tenant'а изолируют workloads (быстрый boot, dense packing).
Существуют гибриды: Kata Containers и gVisor запускают каждый container в своей мини-VM, получая kernel-level isolation при container-like API. AWS Firecracker появился именно для этого сценария — Lambda function запускается в свежей microVM за < 125 ms.
Связанные темы¶
- CPU virtualization — VT-x/SVM, VMCS, VM-exit'ы, paravirt KVM
- Виртуальная память — двухуровневая трансляция в EPT/NPT строится поверх этих базовых механизмов
- Context switch — vCPU как thread, sCheduler управляет vCPU так же, как обычными процессами
- Namespaces — основа container isolation, альтернатива hypervisor-based virtualization
- Cgroups — ресурсные лимиты для контейнеров, аналог vCPU pinning для VMs
- Containers internals — как Docker/containerd используют namespaces + cgroups
Источники¶
- Popek, Goldberg. Formal Requirements for Virtualizable Third Generation Architectures. Communications of the ACM, 1974.
- Adams, Agesen. A Comparison of Software and Hardware Techniques for x86 Virtualization. ASPLOS, 2006.
- Barham et al. Xen and the Art of Virtualization. SOSP, 2003.
- Kivity et al. KVM: the Linux Virtual Machine Monitor. Ottawa Linux Symposium, 2007.
- Intel® 64 and IA-32 Architectures Software Developer's Manual, Vol. 3C — System Programming Guide, Part 3 (VMX).
- AMD64 Architecture Programmer's Manual, Vol. 2 — System Programming, ch. 15 (Secure Virtual Machine).
- KVM documentation — https://www.linux-kvm.org/page/Documents.
- QEMU documentation — https://www.qemu.org/docs/master/.
- Firecracker — https://github.com/firecracker-microvm/firecracker.