これまで、x86システム仮想マシンの概要、およびその実例としてDebian GNU/Linux 6.0を利用した仮想マシンの実行方法について紹介してきました。今回からは、より具体的にCPU、メモリ、I/Oデバイスの仮想化がどうやって実現されているかを解説したいと思います。
今回は、仮想マシンを実装するための古典的手法およびそれを従来のx86プロセッサに適用する際の問題点、そして仮想マシンソフトウェアがどのようにそれらの障壁を乗り越えてきたかについて解説します。
システム仮想マシンに求められる条件
これまで、システム仮想マシンはどのようなものかについて説明してきましたが、仮想化仮想化の生みの親とも言えるGoldbergらは、1974年に書いた論文にて、仮想マシンソフトウェアとはどのようなものであるべきかについて、コンピュータアーキテクチャの観点から論じています。
彼らは、その仮想マシンソフトウェアを「仮想マシンモニタ」と呼びました。そして、仮想マシンモニタがどのような物であるべきか、次のように説明しています。
- プログラムを実行した結果、物理マシンと等しい結果が得られること
- あるプログラムを仮想マシン上で実行したとき、それは物理マシンで動かしたと同じ動作をしなければなりません。これにより、仮想マシンは、物理マシンのために作成されたプログラムがそのまま実行できるものであることを求めています。
- 仮想マシン上の命令の実行効率が高いこと
- 仮想マシン上でソフトウェアが実行される場合も、プログラムが実プロセッサにより直接実行されなければならなりません。これは、仮想マシンを利用した場合に著しい性能劣化が生じてはいけない、という点からの要求です。
- リソースコントロールが可能であること
- 仮想マシンモニタが全てのハードウェア資源を掌握し、仮想マシンに必要な分量のリソースだけを割り当てできなくてはいけません。また、割り当てたリソースを仮想マシンモニタが回収できなくてはいけません。
仮想マシンのしくみ:trap-and-emulateによる仮想マシンの実現
このような仮想マシンを実現するために、40年近く前から用いられてきたのがtrap-and-emulate、すなわち、「仮想マシン内で何か都合が悪い事が起きた時だけ仮想マシンモニタが介入し、エミュレートする方式」です。
当時から、多くのプロセッサにメモリやハードウェアリソースの保護機能が実装されています。保護機能を利用して、ユーザモードのプログラムがシステム全体にとって都合が悪い(かもしれない)処理をしようとした時に、プロセッサがオペレーティングシステムに制御を移し、オペレーティングシステムがその状況にどう対応するか(プログラムを強制終了させるか、など)を決定できるようになっています。このような保護機能をオペレーティングシステムにも適用し、仮想マシンモニタでオペレーティングシステムを制御することでtrap-end-emulateを実現してきました。
ゲストのオペレーティングシステムを特権がない状態で実行すると、権限が不要な処理はそのまま実行できますが、システムの状態に変更を加えたり、プロセッサに接続されたデバイスへの入出力をしたりしようとしたときにトラップが発生します。トラップが発生すると仮想マシンモニタに制御が移りますので、仮想マシンモニタはプロセッサが生成したトラップの情報やトラップが発生した場所から、トラップの原因を推測します。そして、まるで原因となった命令が「成功した」かのような状況を造りだしてから、再度ゲストのオペレーティングシステムの実行を開始します。このため、ゲストのオペレーティングシステムはそのコンピュータが物理マシンなのか仮想マシンなのかを意識することなく動作します。
trap-and-emulate方式による仮想マシンの実装は、仮想マシンの考え方が生まれた頃から使われている古典的な方法です。それならば、同じ方法を使えばx86プロセッサのシステムも仮想化が可能ではないか――、しかし、x86プロセッサの場合はそれが簡単に実現できない罠がありました。
x86プロセッサとプロテクトモード
第1回で若干触れましたが、x86プロセッサには、元祖8086と互換性があるリアルモードと、その後に追加された高度な機能が利用できるプロテクトモードなど複数のモードが存在します。x86プロセッサの仮想化について具体的な話を進めていく前に、リアルモードとプロテクトモードについて簡単に紹介しましょう。
リアルモード
現在使われているほとんどのx86システムは、起動直後はプロセッサはリアルモードで立ち上がり、ハードディスクからブートローダと呼ばれる小さなプログラムがロードされます。リアルモードとは8086との互換性を持つモードであり、以下のような特徴を持ちます。
- 16ビット単位の演算操作(メモリアクセス、汎用レジスタ幅)
- セグメント方式のメモリ管理
- メモリ、ハードウェアアクセスの保護機能は利用できない
プロテクトモード
最近のオペレーティングシステムは、32ビットもしくは64ビットで動作し、高度なメモリ/ハードウェアアクセスの保護機能を提供していますが、リアルモードでそのような機能は実現できません。オペレーティングシステムは、システムブート後にCPUの動作モードをプロテクトモードに切り替え、プロセッサの保護機能を有効化することで、さまざまな機能を実現しています。プロテクトモードには下記のような特徴を持ちます。
- 32ビット、64ビット単位の演算操作(メモリアクセス、汎用レジスタ幅)
- ページ単位のメモリ管理
- リングプロテクションによるソフトウェア間の権限切り替え
- メモリアクセス、およびI/Oのアクセスの保護機能
リングプロテクション
x86プロセッサのプロテクトモードで提供される機能のひとつとして、リングプロテクションがあります。リングプロテクションとは、オペレーティングシステムやドライバ、その他のアプリケーションを権限分けして動作させるための仕組みで、x86プロセッサに限らず、さまざまなアーキテクチャに実装されています。
このしくみを利用し、オペレーティングシステム自体をリング0、アプリケーション他のリングで動かすことにより、以下のような場合にはアプリケーションからオペレーティングシステムに強制的に制御を移すことが可能です。
- 特権命令(Privileged Instructions)を実行しようとした時
- メモリやハードウェアへ不正にアクセスしようとした時
これにより、オペレーティングシステムがその上で動作するアプリケーションの挙動を支配することが可能となります。たとえば、アプリケーションがオペレーティングシステムのルールに違反するような挙動をとったときに、システム全体が道連れにならないようプロセスを停止させることができます。
x86プロセッサのリングプロテクションでは、権限が強いほうから順番にリング0、リング1、リング2、リング3の4層になっており、LinuxやWindowsの場合はカーネルがリング0(カーネルモード)、その他のプログラムがリング3(ユーザモード)で動作しています。リング1, 2についても存在するものの、これらのリングを利用するオペレーティングシステムはあまり有りません。
x86のリングプロテクションを利用した仮想マシンモニタの実装
従来のx86プロセッサのリングプロテクションを利用してtrap-and-emulationによる仮想マシンをで実現しようとすると、以下の形をとることになります。
- ゲストとなるオペレーティングシステムをリング0からリング1(もしくリング2)へ移す
- ゲストのオペレーティングシステムが特権命令を実行した場合には、リング0で動作する仮想マシンモニタでトラップし、対応する
しかしx86プロセッサの場合、これは実際にはうまく動きません。この状態でオペレーティングシステムを実行すると、特権命令は仮想マシンモニタでトラップできるのですが、それ以外にも、次の問題を考慮する必要があります。
- 問題1:動作モードにより動きが変化する命令の存在
1つめの問題は、トラップの対象とならない非特権命令で、リング0で動くべきソフトウェアを他のリングで動かすと、プロセッサの動作が変わり、ソフトウェアが正常に動作しないことです。
具体例として、CPUが持つレジスタのひとつであるコードセグメントレジスタ(CS)が示す値に、そのプログラムがどの特権レベルで動作しているかという情報が入っているという点があります。このため、同じゲストOSを実ハードウェアのリング0で動作させた場合と、リング3で動作させた場合でプログラムの動作に差が生じてしまいます。このような命令はトラップする必要があるのですが、残念ながらx86プロセッサの仕様では、トラップ対象ではありません。
- 問題2:非特権命令でありながら実ハードウェアの状態を変更しうる命令の存在
2つめの問題は、実ハードウェアの状態を変更するようなインパクトの大きな命令でありながら、特権命令ではない命令が存在することです。
具体例としては、POPF(pop flags)命令が挙げられます。この命令はスタック上に保存されたCPUフラグレジスタの内容を復元するためのものですが、その際、演算装置やCPUの割り込みに対する動作などが変更される可能性があります。trap-and-emulateの考え方では、このような命令が仮想マシン内で実行され場合にはトラップし代替処理を行う必要がありますが、x86プロセッサの場合、POPFをはじめとする”リングプロテクションの仕組みでトラップできない”命令が複数存在します。
- 解決策:バイナリトランスレーション
このように、x86プロセッサに対してtrap-and-emulateによる仮想マシンの実装が難しい理由は、CPUの特権機能ではトラップできない“センシティブ命令”(sensitive instructions)の存在にあります。センシティブ命令とは、そのシステムの状態を変更したり、もしくはそのシステムの状態によって挙動が変わったりする命令を指します。
x86システム仮想マシンを実現した幾つかの仮想化ソフトウェアでは、この問題を解決するため、仮想マシン上で実行されるプログラムを逐一変換しながら実行するといった手法が採られています。この処理を、バイナリトランスレーション(Binary Translation)と呼びます。これにより仮想マシン上で実行されるプログラムは同じ機能を持ったまま、センシティブ命令がほかの命令に置き換えられて実行されます。バイナリトランスレーションは透過的に行われるため、仮想マシンの中側のソフトウェアは、命令が置き換えられていることを認知できません。
このように、x86システム仮想マシンの実装をプロセッサの既存機能だけで実現する“ソフトウェア実装の仮想マシンモニタ”は難易度が高く、x86システム仮想マシンを実現できる仮想マシンソフトウェアの選択肢はあまり多くありませんでした。
しかし、昨今のx86システムの性能向上、およびVMwareをはじめとするx86システム向けの仮想化技術が注目を浴び、ハードウェアレベルでも仮想化をサポートする流れがやってきました。これが、Intel VTやAMD-Vと呼ばれるCPUの仮想化支援機能です。
次回予告
今回は、システム仮想マシンを実現するための条件とその古典的実装方法を紹介しました。また、それらのアプローチをx86システムに適用する場合、どのような障壁があるかについてご紹介しました。次回は、x86プロセッサに仮想化支援機能が搭載されるようになった今、x86システム仮想マシンの実装方式がどのように変わってきたのかについて解説したいと思います。