Что происходит, когда гостевая система пытается выполнить запрещённую инструкцию
Вы арендовали VPS, зашли по SSH и увидели обычный Linux — своё ядро, свой /proc/cpuinfo, свои права root. Кажется, что система работает прямо на процессоре хост-сервера, будто чужих виртуалок рядом вообще нет. Отчасти это правда: подавляющее большинство инструкций гостевой ОС действительно выполняется на реальном CPU без каких-либо посредников. Но стоит гостю попытаться сделать что-то, что затрагивает чужую память, реальные устройства или сам гипервизор — и происходит нечто иное: процессор аппаратно перехватывает эту инструкцию и передаёт управление гипервизору. Разберём, как устроен этот перехват на уровне железа и почему он не может быть бесплатным.
Содержание
Иллюзия прямого доступа к железу
Гостевая ОС внутри VPS на KVM, Xen или VMware ведёт себя точно так же, как если бы она была установлена на голый сервер: инициализирует таблицы страниц, настраивает прерывания, обращается к своему виртуальному диску и сетевой карте как к реальным устройствам. Ядро гостя искренне «думает», что выполняется в самом привилегированном режиме процессора — как если бы это было ядро на bare metal.
Раньше это было главной проблемой x86-виртуализации. У процессора есть уровни привилегий (кольца защиты, ring 0–3): ядро ОС работает в ring 0, пользовательские процессы — в ring 3. Гипервизору тоже нужен ring 0, а два независимых ядра — хостовое и гостевое — одновременно в ring 0 существовать не могут. До аппаратной поддержки виртуализации это решали обходами: VMware переписывал код гостя на лету (бинарная трансляция), Xen требовал модифицированное ядро гостя, которое само знало, что оно не главное (паравиртуализация). Оба подхода работали, но были либо медленными, либо требовали особой ОС.
Аппаратная виртуализация (Intel VT-x, AMD-V, середина 2000-х) решила проблему иначе: добавила процессору не третье кольцо привилегий, а отдельное измерение — режимы VMX root и VMX non-root (у AMD — аналогично, host/guest mode). Гостевая ОС в VMX non-root operation по-прежнему думает, что она в ring 0, и большинство её кода выполняется прямо на CPU. Переключение в VMX root, где живёт гипервизор, происходит только тогда, когда это действительно необходимо.
Какие инструкции считаются привилегированными
Не любая инструкция требует вмешательства гипервизора — иначе виртуализация была бы бессмысленно медленной. Перехватываются только те операции, которые способны нарушить изоляцию между гостями или затронуть общее для всех виртуалок состояние процессора и памяти. На практике это несколько категорий:
- Работа с управляющими регистрами (CR0, CR3, CR4). CR3 хранит физический адрес корневой таблицы страниц — то есть буквально определяет, какая память видна процессу. Разрешить гостю писать в CR3 напрямую значит разрешить ему указывать на чужую физическую память.
- Обращения к MSR (Model-Specific Registers). Через них настраиваются вещи вроде счётчиков производительности, режимов энергосбережения, некоторых функций безопасности процессора — общего ресурса, которым управляет хост.
- Порт-ориентированный ввод-вывод (
IN/OUT). Классический способ x86-программ говорить с устройствами напрямую по портам. У гостя нет реального устройства на том конце — есть эмуляция или проброшенное устройство, и это должен решать гипервизор. - Инструкции работы с таблицами дескрипторов и прерываниями (
LGDT,LIDT,LTRи связанные) — они определяют, как процессор реагирует на прерывания и исключения, а это общий для системы механизм. HLT— «остановить процессор до следующего прерывания». Гостю нельзя буквально останавливать физическое ядро: на нём могут крутиться другие виртуалки.- Инвалидация TLB и записи в структуры трансляции адресов (
INVLPGи подобные) — если гость может сбрасывать кэш трансляции адресов процессора как ему вздумается, это бьёт по всем, кто на этом ядре выполняется.
Именно эта проблема — что часть «чувствительных» инструкций на классическом x86 не была привилегированной в строгом смысле и потому не всегда вызывала аппаратное исключение — десятилетиями считалась препятствием для честной аппаратной виртуализации x86 (это разбирали ещё в академической литературе по требованиям Попека и Голдберга к virtualizable-архитектурам). VT-x и AMD-V закрыли именно эту дыру: конфигурируемый набор условий, при которых процессор обязан выйти в гипервизор, стал частью самого железа.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверVT-x и AMD-V: перехват как встроенная функция CPU
Ядро описанного механизма — структура данных, которую процессор читает и обновляет при каждом переключении между гостем и гипервизором. У Intel она называется VMCS (Virtual Machine Control Structure), у AMD — VMCB (Virtual Machine Control Block). Для каждой виртуальной машины гипервизор держит свою такую структуру.
VMCS хранит три вещи, важные для понимания перехвата:
- Guest-state area — сохранённое состояние гостя: регистры, указатель инструкций, сегментные регистры, часть управляющих регистров.
- Host-state area — состояние, в которое нужно вернуться при выходе из гостя: адрес обработчика гипервизора, его страничные таблицы, стек.
- Execution control fields — как раз то, что определяет, какие события порождают перехват: битовые карты портов ввода-вывода (I/O bitmap), битовые карты MSR (MSR bitmap), маски для CR0/CR4 (какие биты этих регистров гость может менять сам, а какие — только через гипервизор), список исключений, которые нужно перехватывать (exception bitmap).
Это последнее особенно важно: гипервизор сам решает, насколько тонко настроить перехват. Если гостю разрешено писать в какой-то MSR напрямую — соответствующий бит в MSR bitmap снят, и обращение выполняется на голом железе без выхода в гипервизор. Если нет — любая попытка немедленно порождает выход. Такой гибкости не было в эпоху программных ухищрений: разработчик гипервизора может целенаправленно сокращать число перехватов там, где это безопасно.
Проверить на хосте, что аппаратная поддержка виртуализации вообще есть и активна, можно так:
# наличие флага в самом CPU
grep -Eo '(vmx|svm)' /proc/cpuinfo | sort -u
# короткая сводка от lscpu
lscpu | grep -i virtualization
# загружен ли модуль KVM для конкретного вендора
lsmod | grep kvm
На Intel-хосте вы увидите vmx и модуль kvm_intel, на AMD — svm и kvm_amd. Внутри гостя эти флаги, что логично, тоже могут отображаться (современные гипервизоры умеют прокидывать вложенную виртуализацию), но это отдельная и куда более редкая история — подробно про вложенную виртуализацию в VPS стоит говорить отдельно.
Что физически происходит в момент перехвата
Возьмём конкретный пример: гостевая ОС переключает процесс и обновляет CR3, чтобы указать на новую таблицу страниц. Вот последовательность событий:
- Гость выполняет
MOV CR3, RAX— с его точки зрения это обычная инструкция ядра, ничем не отличающаяся от миллионов таких же на голом железе. - Процессор, находясь в VMX non-root operation, проверяет управляющие поля VMCS для этой виртуальной машины и видит, что запись в CR3 сконфигурирована как вызывающая VM exit.
- Происходит аппаратный, а не программный, переход: процессор синхронно переключается из VMX non-root в VMX root operation. Это не обычное прерывание и не системный вызов — это отдельный механизм, встроенный в микроархитектуру именно для виртуализации.
- Перед переключением CPU сохраняет полное состояние гостя (регистры общего назначения, RIP, RSP, сегментные регистры, часть системных регистров) в guest-state area активной VMCS.
- Управление передаётся по адресу, записанному в host-state area — это заранее зарегистрированный обработчик выходов внутри гипервизора (в KVM это часть модуля
kvm_intel/kvm_amdи кода вkvm.ko). - Гипервизор читает поле «exit reason» в VMCS — оно однозначно говорит, что именно вызвало выход: обращение к CR-регистру, инструкция ввода-вывода, обращение к MSR, нарушение доступа к памяти через EPT и десятки других вариантов.
- Гипервизор эмулирует нужный эффект с точки зрения гостя. Для CR3 это может быть, например, проверка нового значения (если используются теневые таблицы страниц) или, при включённом EPT/NPT, просто разрешение записи с последующей настройкой второго уровня трансляции адресов.
- Гипервизор выполняет инструкцию
VMRESUME(илиVMLAUNCHпри первом запуске), которая восстанавливает сохранённое состояние гостя из VMCS и возвращает процессор в VMX non-root operation. Гость продолжает выполнение со следующей инструкции — как будто CR3 записался сам, без всякого вмешательства.
С точки зрения гостевой ОС ничего необычного не произошло: инструкция «выполнилась». Она просто заняла заметно больше времени, чем обычная арифметическая операция — и гость об этом даже не узнал, если только не измерял точные таймстемпы вокруг критичных операций.
Почему это стоит времени, а не только у обычных вычислений
Ключевой момент: VM exit — это не «ещё одна инструкция», а полный, синхронный, блокирующий переход между режимами процессора с сохранением и восстановлением состояния. Даже без учёта работы самого обработчика в гипервизоре, сам факт входа и выхода из VMX root operation стоит заметно больше тактов, чем выполнение обычной инструкции сложения или пересылки данных между регистрами — просто потому, что здесь задействован целый механизм записи/чтения VMCS, а не одна операция АЛУ. Точные цифры сильно зависят от поколения процессора и причины выхода, поэтому конкретных чисел тактов называть не буду — они разные на разном железе и быстро устаревают. Важен сам факт: это на порядки дороже, чем «обычная» инструкция, которую гость выполняет напрямую на CPU без всякого перехвата.
Отсюда вытекает практический вывод, который многие интуитивно чувствуют, но не всегда формулируют: накладные расходы виртуализации зависят не от того, сколько «вычислений» делает гостевая машина, а от того, как часто она порождает VM exit. Число сложений, циклов, обращений к уже отображённой в память гостя памяти — практически бесплатно, потому что не требует ни одного перехвата. А вот:
- частые системные прерывания от таймера,
- интенсивный сетевой трафик с большим числом пакетов в секунду (каждый может означать эмуляцию регистра сетевой карты через порт ввода-вывода или MMIO),
- активное создание и уничтожение процессов с постоянной сменой таблиц страниц,
- интенсивная работа с диском через устаревшие эмулируемые контроллеры,
— всё это генерирует поток VM exit'ов, и именно здесь виртуализация «съедает» заметную часть производительности по сравнению с bare metal. Подробный разбор конкретных узких мест — в статье где виртуализация теряет производительность.
Отдельно стоит сказать про управление памятью — исторически один из самых дорогих источников перехватов. До появления аппаратной поддержки второго уровня трансляции адресов (Intel EPT, AMD NPT/RVI) гипервизор был вынужден вести «теневые» таблицы страниц: каждая попытка гостя изменить свои таблицы страниц перехватывалась, гипервизор синхронизировал изменение с реальными физическими адресами и только потом пропускал операцию — то есть VM exit почти на каждое существенное изменение памяти процесса. EPT/NPT добавили второй, независимый уровень трансляции адресов прямо в MMU процессора: гость управляет своими таблицами страниц (гостевой физический адрес), а CPU аппаратно транслирует их дальше в реальный физический адрес хоста без участия гипервизора на каждый шаг. Перехваты это не убрало полностью, но частоту сократило кардинально.
Что выполняется гостем напрямую, без единого перехвата
Важно не создать неверное впечатление, будто виртуальная машина постоянно «дёргает» гипервизор. На самом деле подавляющее большинство инструкций — арифметика, логические операции, переходы, обычные чтения и записи в уже отображённую память процесса, вызовы функций внутри пространства гостя — выполняются гостем напрямую на реальном физическом CPU, на полной скорости, без единого VM exit. Именно в этом смысл аппаратной виртуализации: перехватывается только узкий, заранее определённый набор «чувствительных» операций, а не всё подряд, как это было бы при программной эмуляции целого процессора.
Поэтому CPU-интенсивные задачи — сжатие видео, обучение моделей на CPU, компиляция большого проекта, любые вычисления, не упирающиеся постоянно в ввод-вывод — на VPS с аппаратной виртуализацией (KVM поверх VT-x/AMD-V) показывают производительность, близкую к процессору хост-сервера, скорректированную разве что на честное распределение ядра между соседями. А вот нагрузки с высокой частотой прерываний и обращений к устройствам — свою долю накладных расходов ощущают сильнее, и это не брак конкретного хостинга, а прямое следствие описанного механизма. Если вы выбираете тип и привязку CPU для виртуалки под конкретную задачу, это стоит учитывать заранее — подробнее в статье про настройку CPU для виртуалки: типы и привязку.
Стоит также различать перехват инструкций гипервизором и ситуацию, когда сосед по физическому ядру просто забирает себе больше процессорного времени, чем должен, — это другой эффект, наблюдаемый изнутри гостя как steal time, и он не связан напрямую с VM exit, хотя тоже влияет на ощущаемую производительность виртуалки.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Это работает одинаково на Intel и AMD?
Концептуально — да: у обоих вендоров есть отдельный корневой/некорневой режим работы процессора, структура для хранения состояния гостя и хоста (VMCS у Intel, VMCB у AMD) и настраиваемые условия для выхода в гипервизор. Названия полей и деталей реализации различаются, но сам принцип trap-and-emulate через аппаратный переход общий для обеих архитектур.
Значит ли это, что VPS на KVM медленнее выделенного сервера?
Для большинства реальных задач разница на уровне «незаметно», особенно если гипервизор настроен разумно и нет перегруза по CPU у соседей. Заметнее всего накладные расходы проявляются на нагрузках с очень высокой частотой прерываний и операций ввода-вывода — там, где VM exit'ы происходят часто. Для чисто вычислительных задач разница минимальна.
Как это связано с контейнерами вроде Docker или LXC?
Никак напрямую — контейнеры вообще не используют этот механизм. Они делят одно ядро с хостом через namespaces и cgroups, без отдельного гостевого «процессорного режима» и без VM exit'ов на привилегированные инструкции. Именно поэтому контейнеры легче и быстрее стартуют, но изоляция у них принципиально слабее, чем у полноценной аппаратной виртуализации.
Можно ли увидеть VM exit'ы своими глазами?
Да, но только со стороны хоста, у которого есть доступ к гипервизору, а не изнутри арендованной виртуалки. На Linux-хосте с KVM для этого есть утилита perf kvm stat live или kvm_stat — они показывают частоту и причины выходов в реальном времени, сгруппированные по exit reason.
При чём здесь паравиртуализация, если аппаратная поддержка уже решает проблему?
VT-x/AMD-V решает вопрос возможности перехвата, но не убирает его стоимость. Паравиртуализованные драйверы устройств (например, virtio для сети и диска) используют не эмуляцию через порты ввода-вывода с постоянными VM exit'ами, а прямой канал взаимодействия гостя с гипервизором — гипервызов вместо перехваченной инструкции. Это способ сократить число дорогих переходов там, где эмулировать «настоящее» железо не нужно.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →