AArch 64の仮想化支援機構

以上が一般的な現代のハードウェア仮想化支援機構についての話になるんですけど、ここからはArmというか、AArch 64の仮想化支援機構についての解説を始めます。

対象アーキテクチャについてです。Virtualization ExtensionというのがArm v7のときの名前で、AArch 64ではどうもただの仮想化機構としか呼ばれていないみたいなんですけど、とりあえずAArch 64だけを対象に発表します。基本的には特権レベルの名前とかが変わるだけでだいたい同じです。

今回僕はもともとArmに詳しいわけではないのでいろいろ資料を調べて作ったんですけど、この資料ポイントの順に読むとかなり理解が深まります。

1個目が公式ドキュメントで、ざっとした外観がわかります。2個目にKVMがArmの実装をしたときの詳細な論文があるんですけど、これはけっこうよく書かれている。Virtualization Host Extensionの論文というのがあって、それもいいです。最後に1、2、3の資料がかなりまとまったクレタ大の講義資料がありまして、これはあとで読むと概念がよく整理されます。

センシティブ命令をすべてトラップできるEL2

ではArmのVirtualization Extensionの、というかArmの仮想化支援機構全般についての解説を始めます。

Armでこのようなセンシティブ命令をトラップするハードウェア仮想化支援機構をどうやって実現したかというと、OSのセッションであったように、ArmにはEL0、EL1というものがあって、EL0がユーザーアプリケーション用のExceptionレベルで、EL1がカーネル用のExceptionレベルなんですね。

ここにさらにもっと下のレイヤーのものとして、ハイパーバイザー用のレイヤーEL2を導入しました。でもEL2はちょっと状況が特殊でして、EL0、EL1とかと(比べて)ただ偉いというよりは、EL0、EL1で発行されたすべてのセンシティブ命令をトラップできるようなレイヤーがEL2という実装になっています。

これはどういうことかと言うと、Armもx86と同じく仮想化要件を満たしていないアーキテクチャなので、例えばEL0で発行したすべてのセンシティブ命令がEL1でトラップできるわけではないんですね。でもそのようなセンシティブ命令をすべてトラップできることを保証したEL2というレイヤーを新しく作ることで、ここならハイパーバイザーが作れることを保証しています。

一方ですべてのセンシティブ命令をトラップしたいわけではないこともあります。例えばシステムコールは、EL0でシステムコールが発行されたものが一旦EL2にきてEL1に返すよりは、直接EL0からEL1に行ってほしいので、そういうものはトラップしないようにも設定できます。

EL2によるゲストOSの仮想化

EL2によるゲストOSの仮想化の方法なんですが、ゲストOSやそのユーザーアプリケーションがEL1、EL0でセンシティブ命令を発行すると、それらは例外としてトラップされてEL2にスイッチします。

例はたくさんあります。次のスライドで説明するWait For Interrupt InstructionとかメモリマップIO。ちょっとこれはフォルトを使う必要もあるので仕組みは若干違うのですが。

そういうものもトラップして、それからメモリマップの変更とかもトラップできます。ただしメモリマップの変更に関してはCPUの中でメモリマップのエミュレーションを完結させる機能があるのでもっと効率的な方法が存在します。

そしてトラップしたセンシティブ命令に対してハイパーバイザーが今仮想化されているCPUの状態とつじつまが合うように結果を処理してCPUの仮想化を実現します。

WFI命令

TWI……これWFIの間違いですね。すみません。EL2による仮想化でWFI命令を例に動作を説明します。WFI命令というのはWait For Interruptの略で、CPUを待機状態にするように使われます。実際はInterruptを待つための命令なんですけど。

ゲストOSが勝手にこの命令を発行できてしまうと勝手にCPUが待機状態に入ってしまうので、本当はそんなことさせたくない。トラップして別のゲストOSにCPUを明け渡したいわけですね。これによってvCPU間のスイッチを実現したいわけです。

このときに何が起きるかと言うと、あるOSがEL1でWFIを発行しました。すると仮想化支援機構によって例外としてそれがEL2にトラップされます。EL2でハイパーバイザーがレジスタなどEL1の状態をメモリ上に退避します。

ここでちょっとx86を知っている方は違いがあるのかなと疑問に感じると思うんですけど、Armはセンシティブ命令をトラップしたときにCPUの状態を自動でメモリに退避することはしません。なので保存したい状態を自分でメモリに退避するのはハイパーバイザーの仕事です。

この状態でハイパーバイザーはほかのゲストOSやほかのvCPUに切り替えたいので、メモリから別のゲストOSの状態をEL0やEL1の戻りたいExceptionレベルに対して復元します。

復元した状態に対してそのELに切り替えることで、WFIのセンシティブ命令をトラップして別のゲストOSに切り替えることでCPUを効率的に使うということが実現できます。

Intel VT-xとの違い

ここで今Armの概要をざっと説明したんですが、Intel VT-xとの違いを説明します。Intel VT-xというのはArmの仮想化支援機構みたいな感じのやつがx86にもあって、その名前ですね。

x86はRing0とRing3がそれぞれカーネルとユーザーランドの権限レベルなんですけど、仮想化の際にArmのようにRing-1みたいなのを生やすわけじゃなくて、root modeとnon-root modeというものを実現しました。root modeがふだんのハイパーバイザーが動く普通のOSが動く場所で、non-root modeはゲストOSが動く場所なんですね。

ゲストOSのセンシティブ命令がトラップされると、VMExitでnon-root modeのCPUの状態がごそっとroot modeに切り替える。逆に仮想CPUで動かすときはごそっとまたゲストモードに切り替えるという仕組みになっています。

一方ArmはOSより上の権限レベルとしてEL2を導入した造りになっています。これが実は本質的な違いとしてハイパーバイザーの作りやすさに影響してしまうという問題があります。

具体的にはType-2 Hypervisorが実装しづらいんですね。ハイパーバイザーはType-1とType-2というのに分類されまして。Type-1というのはXenとかWindowsのHyper-Vみたいに、まずハードウェア上でハイパーバイザーが動いて、その上でホストOSもゲストOSもすべて実行するようなもの。

一方、KVMとかみたいにホストOSの中にハイパーバイザーの機能が入っていて、ホストOSはハードウェアの上で直接動く。ホストOSのアプリケーションはその上で動くんだけれど、ゲストOSはさらにそのホストOSの上でVMとして動くという造りなのがType-2 Hypervisorです。(注:「ホストOSのアプリケーションとしてゲストOSのVMが動く」というのが、本来のType-2の分類で、KVMのようにホストOSにハイパーバイザー機能が組み込まれた作りのものは、Type-2のType-1の二つの中間だとされたり、まったく反対にType-1だと分類されることもあります)

実はEL2はまったく新しいExceptionレベルなので、EL1用にコンパイルされたカーネルはそのままでは動きません。なぜなら触るべきレジスタがそもそも違うというのと、EL1ではユーザー空間とカーネル空間に2つのページテーブルレジスタがあるんですけど……これはOSのセッションで説明していたやつですね。このページテーブルレジスタがEL2には1つしかないとかいう細かい違いがたくさんあります。

なのでホストOSがハイパーバイザーを兼ねるかたちのType-2 Hypervisorの実装が、Armの仮想化支援機構、旧Virtualization Extensionではこれが大変だったという話があります。

右の図を見ていただくとわかるんですけど、EL2でホストOS機能をハイパーバイザーで動かしたいんだけど、ホストOSはEL1でしか動かないのでこれが実現できないわけです。

そこでKVM on Armは何をしたかというと、KVM自身をEL2で動くLowvisorとEL1で動くHighvisorというコンポーネントに分割しました。これによってなんとかKVMがArmで動くんですけど、LowvisorとHighvisorのスイッチにコストがかかるうえに、メモリ空間が別になりがちなのでちょっと実装が複雑になってしまうという問題がありました。

そこでこの問題を解決するために、Virtualization Host ExtensionsというものがArmv8.1から入っています。これはEL2でEL1のレジスタをエミュレートしたりしてEL2用のOSを小さな修正で……あ、逆ですね。これ。すみません。EL1用のOSを小さな修正でEl2で動かせるようにしたものです。

これによってType-2 Hypervisorが作りやすくなります。クレタ大の講義資料によると、KVMもArmv8.1以降ではx86と同じ構造のまま素直に動くということです。この状態だとEL2でホストカーネルとハイパーバイザーというか、KVMなのでLinux KernelとKVMがこのままEL2で動くという作りになっています。

これ自分では元ネタが見つけられなかったんですけど、たしかにVirtualization Host Extensionの論文を書いているのもKVM Armの開発者たちだったので、このVHEを利用した造りは、たぶんKVMに載っているんだろうと思います。

もっと進んだ仮想化支援機構

一旦ここでArmの仮想化支援の概要の説明は終わりまして、次にいくつかもっと進んだ仮想化支援機構について説明します。

より便利な仮想化支援化というものがあって、原理的にはあらゆるものはセンシティブ命令をトラップしてソフトウェアでつじつまが合うようにエミュレートすればなんでも仮想化できるんですね。 ところがパフォーマンスや利便性のためにハードウェアだけで仮想化を完結してもらいたいものがいくつかあります。例えばメモリ空間の仮想化、VMの物理アドレスを実際の物理アドレスに変換するやつとか。あとは割り込みとかタイマーの仮想化はハードウェアでできればやってほしいわけです。

なぜかと言うと、割り込みとかだったら、割り込みってレジスタを書き換えて読んでみたいなプロトコルがいっぱいあるんですけど、そのプロトコルのたびにVMExitが発生していてはなかなか高コストなので、なんとかCPU側だけで仮想化を完結したいみたいな要求があるわけですね。

メモリ空間の仮想化についてはStage 2 Translationと呼ばれる仕組みがArmの仮想化支援機構に載っていまして、CPUによってVMのメモリアドレスから物理メモリアドレスへの変換機構があります。ちょっとややこしいんですけど、ここで言っているVMというのはバーチャルマシンなので仮想メモリの話じゃないです。

仮想マシンの中で、仮想マシンの仮想アドレスから仮想マシンの中での物理アドレスの変換がゲストOSによって行われて……これは当然ですね。その外でさらにゲストOSが言っている物理アドレスは、これはまた仮想化されたメモリアドレス空間のものなので、ホストOSのほうで本当の物理アドレスへの変換をしてあげたいわけです。

この2段階の変換を自動でやってくれるような仮想化支援機構がArmのCPUに入っていて、2段階あるからたぶんStage2と呼ばれているんだと思います。

もし仮にこれがなければ、VMの中でメモリマッピングの読み書きをするセンシティブ命令をすべてトラップして、例えば仮想アドレスの1000番を物理の2000番にするようなものがあって、本当はホストOSがこれを2000じゃなくて4000にしたいんだったら、2000と書いてある場所を4000に書き換えて実際はアドレス変換をするとか。

今度はそれに対して読み込み側から、本当は4000に変換されているんだけど、仮想マシンが思い込んでいる2000に変換して返してあげる必要があるので4000を2000に書き換えて返すみたいなメモリマッピングの読み書きをいちいち書き換える必要があるんですね。これはシャドーページングと呼ばれる手法でけっこう前から存在していてx86にやっていたことがあるんですけど、けっこう大変だという話があります。

これはハードウェア仮想化支援が載っているので大丈夫っていう話ですね。DMAの変換をするSMMUと呼ばれる機構もArmには載っています。

割り込み・タイマーの仮想化ですが、ArmにはそもそもGeneric Interrupt Controller、Generic Timerという統一された割り込みコントローラーとタイマーのインターフェースがあるらしくて、それらのインターフェースも仮想化に対応しているので、がんばってソフトウェアでプロトコルをエミュレーションする必要はありません。x86のvirtuial APICとかに対応しているものになります。ただしArmのほうが登場は先らしい、とのことです。

そして最後の発展仮想化支援機構として、Nested Virtualizationについてです。Armv8.3から、EL1でEL2のレジスタにアクセスしたり、仮想化周りの命令を発行した場合、それをEL2でトラップできるようになりました。

これはどういうことかと言うと、もともとのEL2用の仮想化支援命令たちをEL1で動かしたら、それがセンシティブ命令になるという変更が入るんですね。それによってEL1でゲストハイパーバイザーのバイナリをそのまま動かすと、EL2系の命令はセンシティブ命令としてEL2にトラップされるので、効率的なNested Virtualizationをきれいに実現できます。

ちなみにNested Virtualizationというのは、仮想化されたVMの中でさらに仮想化されたVMを動かしたいときに必要なやつです。IaaSとかだとよく発生する状況だと思います。

Arm Virtualizationのまとめ

以上のArm Virtualizationのまとめです。

AArch 64ではEL0、EL1のセンシティブ命令をトラップできるEL2というExceptionレベルによって仮想化支援機構を実現しました。

EL2はEL1と異なる点があるのですが、Host Extensionsを有効にすると普通のEL1用のカーネルも動くようになります。KVMではVHEがない環境ではEL2において動くLowvisor、EL1で動くHighvisorに分割されるんですが、VHEがある環境ではLinuxカーネルごとEL2で動くらしいです。

メモリアドレス、割り込みコントローラー、タイマーのハードウェア仮想化がサポートされていて、Nested Virtualizationも視野に入って効率的にサポートされています。

以上がArmの仮想化支援機構の概要です。

Apple Hypervisor FrameworkとArmの話

最後に、最初にお話した本発表のゴール3つではないんですけど、おまけの話。Apple Hypervisor FrameworkとArmの話をしたいと思います。

そもそもApple Hypervisor Frameworkってマイナーで、ご存知ではない方がけっこう多いと思うんですが、Apple謹製のmacOS組み込みのVMMを使うためのAPIです。

VMMとはVirtual Machine Monitorの略なんですけど、CPUの仮想化支援機構を抽象化したモジュールと言えます。これはざっくりmacOS版のKVMにあたるものですね。VMwareやParallelsが仮想マシンを作るために利用しています。

Hypervisor Frameworkどうなる問題っていうのが僕はずっと思っていて。 どうもAppleの発表によるとArmにも対応しているらしい……のですが、問題はApple Hypervisor FrameworkはIntel上ではほぼIntel VT-xの薄いラッパーだったんですね。

例えばhv_vmx_vcpu_read_vmcsとかいう命令が生えています。vmcsというのは完全にx86専用の仮想マシンの仮想CPUの状態を入れた構造体なんですけど、こういう命令が生えている状態でいったいArmの対応ってどうするんだ? 抽象化大丈夫なのか? っていう話が気になっていて、軽く調べてみました。

結論から言うと、ぜんぜん大丈夫じゃない。APIがぜんぜん違うので、これは終わりですっていう感じなんですね。名前と基礎概念だけApple Hypervisor Frameworkのもので統一されていて。基礎概念っていうのはCPUを作って、待って、エミュレーションして、exitするみたいな、ライフサイクルだけ共通っていう意味です。

ただしコードはぜんぜん共有できていないので、Apple Hypervisor Frameworkが作るソフトウェアはArmとx86ではぜんぜん違うコードを書く必要があって、あんまり抽象化はされていません。

ちょっとわかりづらいんですけど、左がArm用のもので右がx86用のものです。hv_vcpu_runとかは共通しているんですけど、hv_vcpu_get_pending_interruptとか、hv_vcpu_ set_pending_interruptとか、右側にぜんぜんないものがある。右側は右側でhv_vcpu_invalidate_tlbとか左側にないものがあるっていう。

ぜんぜんAPIが共有していないので、同じコードを再コンパイルしたら動くということがApple Hypervisor Frameworkではないです。残念ながら。という感じですね。

仮想化セッションのまとめ

以下、仮想化セッションのまとめです。

まず最近の仮想マシンはハードウェア仮想化支援機構を利用しています。CPUのハードウェア仮想化支援機構とは、センシティブ命令をトラップする方法を提供するものだと抽象化して表現できます。メモリ空間の仮想化など、単なるセンシティブ命令のトラップよりも効率的な仮想化の仕組みも提供されています。

そしてAArch64のVirtualization Extension……すみません、これは先ほど指摘があったんですけど、AArch64ではVirtualization Extensionという呼び方は正しくないらしいです。ただの仮想化支援機構ですね。

AArch64の仮想化支援機構はType-1 Hypervisorを実現するEL2を導入します。さらにType-2 Hypervisorを実現しやすくするVirtualization Host Extensionというものも最近のArmには載っています。

そしてApple Hypervisor FrameworkはどうもArmに対応するらしいんですが、APIはx86のものとはぜんぜん別物だという話でした。

以上で仮想化セッションの発表を終わります。ありがとうございます。