Технологии виртуализации вчера, сегодня, завтра

       

Паравиртуализация и бинарная трансляция


Итак, как мы уже сказали, все пользовательские приложения сегодня, фактически, работают на «виртуальных» компьютерах - им предоставляется некая «обобщенно-стандартная» среда исполнения с виртуальной оперативной памятью, и с этим «виртуальным компьютером» они свободно работают, не задумываясь о том, какие реальные физические ресурсы за этой виртуальностью стоят. Центральная задача операционной системы - это поддержание этой «виртуальной реальности» и своевременное распределение между этими виртуальностями реальных аппаратных ресурсов. Сама операционная система тоже живёт на одном из «виртуальных компьютеров», но, в отличие от всех остальных «обитателей» компьютера, обладает возможностью свою (и чужие) «реальности» изменять и соотносить с физическими ресурсами компьютера.

И уже сама по себе подобная возможность позволяет, на самом деле, реализовывать практически всё, что угодно, с пользовательскими приложениями. К примеру, потенциально можно взять, «сохранить» состояние приложения на флэшку, «скопировать» на другой компьютер и «продолжить» выполнение программы уже на другом компьютере. Можно (потенциально) запускать в одной и той же операционной системе как Windows, так и POSIX-приложения (Linux, Unix-системы) - достаточно уметь создавать два «типа» виртуальных компьютеров, чтобы каждое приложение получало ровно ту среду исполнения, в которой оно привыкло работать. Но, к сожалению, для пользователя, подобные «хитрости», требующие активной поддержки со стороны операционной системы, реализовать на практике далеко не так просто, как рассказать о них. И обеспечить, скажем, «родную» поддержку Windows-приложений в Linux, равно как и обратную поддержку Linux-приложений в Windows, по причине активного противодействия Microsoft, невозможно. А потому пользователь вынужден обходиться без некоторых интересных функций и довольствоваться Windows-приложениями на Windows-системах и Linux-приложениями на Linux-системах.


Идея здесь очень простая: используя виртуальную память, мы можем сымитировать виртуальный компьютер практически любой сложности: так что «гостевой» операционной системе попросту «подсовывается» виртуальная машина, очень напоминающая «физическую» x86-машину. «Гость» принимает «обманку» за настоящий компьютер - и вполне успешно начинает на этой виртуальной машине, имитируемой «родительской» ОС, работать. Обратите внимание, на то, что это не подход, аналогичный «виртуальной машине Java» или эмуляторам древнего Sinclair, когда приложение-эмулятор виртуальной машины «вручную» разбирает код приложения и «вручную» же исполняет каждую его инструкцию. Гостевая операционная система и все запущенные в её рамках приложения работают на физических ресурсах компьютера практически так же, как это делает обычное запущенное на нём приложение, а «виртуализирующее приложение» только обеспечивает контроль над ним - тонюсенькая прослойка кода, поддержанная стандартными аппаратными ресурсами компьютера. Давайте разберём немножко подробнее, как такое оказывается возможным.

У нас есть некие аппаратные ресурсы, которые нужно имитировать. В архитектуре x86 их, в общем-то, всего три:


  • Регистры процессора (включая регистры служебного назначения).
  • Порты ввода-вывода (использующиеся для обмена информацией с периферией).
  • Оперативная память.


С пунктом 3 всё понятно и так - память у нас виртуальная, так что сымитировать кусок физической памяти «родительской» операционной системе не составляет особенного труда. Порты ввода-вывода - орешек немножко потруднее, но поскольку современные процессоры позволяют попросту запретить их использование конкретному приложению, то удаётся обмануть гостевую операционную систему, запретив ей использовать порты ввода-вывода, перехватывая возникающие при попытках обращения к этим портам ошибки и имитируя «правильную» реакцию виртуального компьютера на соответствующую инструкцию.


Обработчику ошибки нетрудно выяснить, что эту ошибку вызвало, и в случае ошибки обращения к порту ввода-вывода - «вручную» проделать нужные операции. Проконтролировать изменения регистров невозможно, но, к счастью, обычно этого и не требуется.

Но есть несколько неприятных исключений. Вот, к примеру, уже упоминавшийся регистр CR3, управляющий таблицей трансляции оперативной памяти. Собственно, зная «виртуальное» значение CR3, «базовой» операционной системе нетрудно сымитировать собственно таблицу трансляции: достаточно относящиеся к этой таблице области виртуальной памяти пометить при помощи P-флага, получить таким образом перехват всех обращений к этой таблице, и синхронизировать реальную таблицу трансляции с виртуальной, которую гостевая операционная система принимает за реальную (техника «теневых таблиц трансляции», Shadow Page Table). Но при этом, к сожалению, нужно как-то обманывать гостевую операционную систему, «подсовывая» ей «виртуальный CR3» вместо реального, а средств соответствующего аппаратного контроля обычный x86-процессор не предоставляет.

Еще одна проблема из той же серии - внутренний регистр процессора, отвечающий за «уровень привилегий» текущего запущенного приложения. Процессор использует его, чтобы перехватывать попытки обращения «обычных» приложений к «опасным», «недозволенным» инструкциям и областям памяти; назначается этот уровень привилегий операционной системой. Таких уровней всего четыре; о приложениях с заданным уровнем привилегий говорят, что они работают в соответствующем кольце. Чем меньше численное значение данного параметра, тем больше дозволено соответствующим приложениям. В кольце 0 (Ring 0), к примеру, работает операционная система и (обычно) драйвера операционной системы; в кольце 3 (Ring 3) - «обычные» пользовательские приложения. Так вот: доверять «гостевой» операционной системе нулевое кольцо нельзя - иначе невозможно будет перехватывать некоторые её действия, поскольку в нулевом кольце «дозволено всё» и многие проверки безопасности попросту не работают.


Но поскольку гостевая операционная система, естественно, по умолчанию предполагает, что её нужно запускать именно в нулевом кольце, а проверить сей факт особенного труда не представляет, то вполне естественно, что при попытке её запуска в каком-либо другом кольце приложение-виртуализатор добьётся разве что сообщения об ошибке. Поэтому, строго говоря, полноценную имитацию «физического» компьютера с помощью аппаратных ресурсов виртуализации в x86 нельзя. Говорят, что не выполнен критерий самовиртуализируемости Попека и Голберга (Popek and Goldberg self-virtualization requirements).

Как же тогда работают «виртуализаторы» типа VMWare? Довольно нетривиальным образом. Виртуализатор слегка «подрезает крылья» коду выполняющейся под его управлением операционной системы, на лету дизассемблируя её код и заменяя «плохие» инструкции (вроде чтения-записи регистра CR3) нейтральными с её точки зрения (это называется динамической трансляцией; dynamic recompilation). Сделать это, мягко говоря, не так уж просто, а гарантировать работоспособность получающегося на выходе результата - еще сложнее. Приплюсуйте сюда задачку имитации софтом виртуального x86-компьютера (требующую реализации специального сложнейшего драйвера), и вы получите представление о том, почему «виртуализирующее ПО» для x86 до сих пор не отличалось ни особенной надёжностью, ни особенной производительностью. Увы, но в архитектуре IA-32 с её изначально неплохой виртуализационной функциональностью изначально была заложена здоровенная «дырка», которую возможно обойти только с большим трудом.

Интересно, кстати, что в пришедшей на смену IA32 технологии AMD64/Intel EM64T, исправившей большинство неудачных и тонких мест архитектуры, ведущей свою родословную аж с процессора Intel i80386, эту «виртуализационную дырку» ни Intel, ни AMD так и не закрыли! Вместо этого они совершенно независимо друг от друга выпустили две совершенно несовместимые друг с другом «заплатки» к AMD64 и EM64T соответственно, по-разному облегчающие жизнь разработчикам виртуализационного ПО.


Содержание раздела