QEMUがコンピューターの動作をソフトウェアで再現する方法
Fabrice Bellardは、異なるCPUの命令を実行時に変換し、OSを変更せずに動かすQEMUの設計を発表しました。OSの検証、状態の保存と復元、組み込み機器の開発にも使える構成でした。
一台のハードウェアを複数の環境で使う
異なるOSやバージョンを同じ物理コンピューターで動かし、OSごとに実行環境を分けたい。物理サーバーを用途ごとに増やさず、開発や検証に必要な環境を作りたい。仮想化は、こうした要求に合わせてCPU、メモリ、I/Oを独立した実行環境へ割り当てます。
用途ごとに物理サーバーを用意すると、使われないCPUやRAMが残り、台数に応じて設置、電力、更新の管理も増えます。開発や検証では、異なるOSやバージョンを短時間だけ動かしたい場合もあります。
そこで、物理コンピューターのCPU、RAM、デバイスを、OSごとに独立した環境として割り当てます。この環境を仮想マシン仮想マシン一台の物理コンピュータ上で動く、別のコンピュータのような環境。と呼びます。物理コンピューター側をホスト、仮想マシン内で動くOSをゲストと呼びます。
QEMUQEMU仮想マシンのメモリ、起動環境、デバイスを用意するユーザー空間のプログラム。は、CPU、メモリ、デバイスをソフトウェアで再現し、OSに変更を加えずに動かせるマシンエミュレータとして開発されました。ゲストOSを仮想環境向けに書き換えずに動かす構成を完全仮想化完全仮想化ゲストOSを修正せずに動かせるよう、CPUやデバイスの環境を用意する方式。と呼びます。QEMUは異なる種類のCPUにも対応でき、実行を止めて状態を調べられる一方、仮想マシン内のOSやアプリの命令をホストCPU向けに変換する処理には時間がかかります。
2000年代半ば、IntelとAMDは、ゲストOSの命令を物理CPUで実行しながら、ホストへ制御を戻せる機能をx86 CPUへ加えました。QumranetのAvi Kivityらは、この機能をLinuxから使うためにKVMKVMLinuxをハイパーバイザとして使うための、カーネル内の仮想化サブシステム。を開発しました。
KVMとCPUの仮想化支援を使うと、多くの通常命令はQEMUによる命令変換を通らず、物理CPUで直接実行されます。命令変換に使うCPU時間が減るため、QEMUだけでCPUをエミュレーションする構成よりも、仮想マシン内のプログラムを速く実行できます。
Fabrice Bellardは、異なるCPUの命令を実行時に変換し、OSを変更せずに動かすQEMUの設計を発表しました。OSの検証、状態の保存と復元、組み込み機器の開発にも使える構成でした。
Avi Kivityは、Intelの仮想化支援をユーザー空間へ公開する/dev/kvmをLKMLへ投稿しました。仮想マシンをプロセス、仮想CPUをスレッドとして扱う設計です。
KVMはLinux 2.6.20へ入りました。当初は変更を加えたQEMUを併用し、2008年には基本的なKVM対応がQEMU本体へ取り込まれました。
KVMは、Linuxをハイパーバイザハイパーバイザ仮想マシンへCPUやメモリを割り当てる仕組み。として使うためのカーネル内の仮想化サブシステムです。仮想マシンと仮想CPUの状態、ゲストメモリの対応、仮想マシン内の命令実行、割り込みコントローラやタイマーの一部を管理します。仮想CPUはホスト上のスレッドとして動くため、Linuxのスケジューラが物理CPUへ割り当てます。
QEMUは、ゲストRAMに使うメモリを確保し、起動用ファームウェアと仮想デバイスを用意します。KVM APIを使う構成では、メモリをKVMへ登録し、仮想CPUの状態を設定して実行を開始します。現在もQEMUはKVMを使わずにCPUをエミュレーションでき、KVMもQEMU以外のユーザー空間プログラムから利用できます。
構成を読み、ゲストRAMに使うメモリを確保し、起動用ファームウェアとユーザー空間のデバイスモデルを用意します。
/dev/kvmKVM_CREATE_VM · KVM_CREATE_VCPU · KVM_RUNVMと仮想CPUの状態、ゲストメモリの対応、仮想マシン内の命令実行、割り込みとタイマーの一部を管理します。
/dev/kvmを通してVM、仮想CPU、ゲストメモリ、一部の仮想デバイスを設定します。MMU仮想化、VM exit、割り込み、タイマーなどをカーネル内で処理します。
製品資料では、Linuxのスケジューラ、メモリ管理、ストレージ、ネットワーク、セキュリティと、QEMUやlibvirtを組み合わせた全体を広く「KVM」と呼ぶ場合があります。
KVM APIでは、ユーザー空間のプログラムが/dev/kvmを開きます。KVM_CREATE_VMで仮想マシンを作り、KVM_SET_USER_MEMORY_REGIONでゲストが使うメモリを登録し、KVM_CREATE_VCPUで仮想CPUを作ります。仮想CPUを担当するスレッドがKVM_RUNを呼ぶと、KVMは設定済みのCPU状態から仮想マシン内のOSやアプリの命令を実行します。ユーザー空間で処理すべきVM exitが起きると、KVM_RUNから呼び出し元へ制御が返ります。
OSのカーネルは、ページテーブルの変更や割り込み制御など、強い権限が必要な処理を行います。複数のゲストカーネルを安全に動かすには、ゲストへOSとしての実行環境を見せながら、物理コンピューター全体の制御はホストに残す必要があります。
x86 CPUには、この切り替えをハードウェアで支援する機能があります。IntelはVT-xを実現する命令体系をVMX、AMDはAMD-Vを実現する命令体系をSVMと呼びます。KVMはこれらを使い、ゲスト用のCPU状態を保ちながら命令を実行します。
仮想マシンと仮想CPUの作成が終わり、ゲストOSが動いている場面を扱います。図は仮想CPUごとの処理を追います。QEMUのスレッドがKVM_RUNを呼ぶと、物理CPUはその仮想CPUへ割り当てられたOSやアプリの命令を実行します。VM exitが起きた後は、KVMまたはQEMUが原因に応じた処理を行い、次の命令実行へ進みます。
KVM_RUNを呼びます。QEMUでは、通常、仮想CPUごとにホストスレッドを用意します。そのスレッドがKVM_RUNを呼び、カーネル空間のKVMへ実行を移します。
複数の仮想CPUがある場合:VM exitの間、該当する仮想CPUではOSやアプリの命令実行が進みません。ほかの仮想CPUは、別の物理CPUで動き続ける場合があります。
仮想CPUを物理CPUで実行し、ホストの処理が必要なイベントでKVMへ制御を戻します。
デバイスが読み書きできる物理メモリの範囲をIOMMUで制御します。デバイスを仮想マシンへ直接割り当てるときにも使います。
仮想マシンの中では、ゲストOSが自分専用のCPU、RAM、デバイスを使っているように動きます。実際のCPU、RAM、デバイスはホスト上のほかの処理と共有するため、ホストはゲストへ見せる状態と物理ハードウェアの状態を対応させます。
この対応には、仮想マシン内のOSやアプリの命令を物理CPUで実行する処理、ゲストOSが使うアドレスを物理RAM上の位置へ変換する処理、ゲストOSのI/O要求をホスト側へ渡す処理があります。以下の図では、それぞれに何が加わり、ハードウェア支援やvirtioがどの処理を減らすのかを順に示します。
ソフトウェアで命令を変換するか、物理CPUの仮想化支援を使います。
VT-x / AMD-Vゲスト内とホスト側のアドレス対応を順にたどります。
EPT / NPTゲストの要求を、ホスト側のデバイス処理へ渡します。
virtio仮想マシン内のプログラムが使う命令も、最後は物理CPUが実行します。CPUをソフトウェアで再現する構成では、QEMUが仮想マシン内の命令をホストCPU用の命令へ変換します。VT-xまたはAMD-VをKVMから使う構成では、多くの通常命令を物理CPUで直接実行できます。
変換結果はキャッシュして再利用できます。それでも、命令変換とソフトウェアによるアドレス変換の処理が加わります。
KVMは、どのイベントでVM exitするかをCPUへ設定します。VM exitには状態の保存と復元が伴うため、回数が性能へ影響します。
N_exitはVM exitの回数です。括弧内は、ゲスト用の実行状態を抜け、原因を処理し、再びVM entryを行うまでの時間を表します。VM exitの回数と、一回ごとの処理時間が増えるほど、仮想マシン内の命令実行以外に使うCPU時間も増えます。
プログラムが使う仮想アドレスは、物理RAM上の位置へ変換してから読み書きします。仮想マシンでは、ゲストOSが管理する対応に加えて、ゲストへ見せる物理アドレスをホストの物理アドレスへ対応させる必要があります。
初期のKVMは、ゲストが管理するpage tablesを監視し、二段階の対応を反映したshadow page tableを保ちました。ゲストがpage tablesを変更すると、KVM側の更新も必要です。
CPUはゲストのpage tablesとEPTまたはNPTをたどります。一度得た変換結果はTLBへ保存され、同じアドレスへのアクセスで再利用されます。
EPTとNPTは、shadow page tableを同期するためのVM exitを減らします。TLB missやEPT violationが起きた場合はpage table walkやVM exitが必要になるため、アドレス変換の負荷が消えるわけではありません。
ゲストOSは、ディスクやネットワーク機器へI/O要求を出します。ホストは、その要求をQEMUのデバイスモデル、カーネル内のバックエンド、または割り当てた物理デバイスへ届けます。ここでは、QEMUによるデバイスエミュレーションとvirtioを比べます。
既存のデバイス用ドライバを使える代わりに、ゲストのレジスタ操作や割り込みを再現します。操作のたびにVM exitやQEMUへの切り替えが起こり得ます。
ドライバは複数の要求をキューへ追加できます。virtioには通知を抑制する仕組みがあり、要求数より少ない通知でバックエンドへ処理を知らせられる場合があります。
virtioが主に減らすのは、物理デバイスの動作を再現する処理と通知に伴う切り替えです。実データのコピー回数は、virtio-net、virtio-blk、vhost、ホスト側バックエンドなどの構成で変わります。
ゲストがネットワークへ送信したり、仮想ディスクを読み書きしたりするときは、I/O要求を物理デバイスまで届けます。主な構成は、デバイスエミュレーションデバイスエミュレーションQEMUなどが、既存の物理デバイスと互換の動作を再現する方式。、virtioによる準仮想化I/O準仮想化ゲストOSが仮想環境向けのインターフェースを使う方式。、デバイスの直接割り当てです。既存デバイスとの互換性、ホスト側での共有と制御、処理経路の短さのどれを重視するかによって選びます。
ゲストが知っている物理デバイスの動作を、QEMUが再現します。
ゲストとホストがvirtio仕様に沿って、virtqueueで要求と完了を渡します。
物理デバイスまたはVirtual Functionを、IOMMUで保護しながら仮想マシンへ割り当てます。
ゲストOSが既存のデバイス用ドライバを使えるように、ホストがデバイスの動作を再現するのが完全仮想化のI/Oです。ゲストOSが仮想環境向けのドライバを使い、ホストと協調してI/Oを渡す方法を準仮想化I/O準仮想化ゲストOSが仮想環境向けのインターフェースを使う方式。と呼びます。
現在のKVM仮想マシンでは、CPUをVT-xやAMD-Vで仮想化し、I/Oにはvirtioを使う構成が広く使われています。仮想マシン全体を二者択一で分類するのではなく、CPUとI/Oで異なる仕組みを組み合わせています。
virtiovirtio仮想マシン向けに標準化されたI/Oデバイスの仕様。は、仮想マシン向けに標準化されたI/Oデバイスの仕様です。ゲストはvirtio用ドライバを使い、ホスト側のデバイス実装と対応機能を確認してから通信します。
virtqueuevirtqueuevirtioドライバとデバイス実装が、I/O要求と完了を受け渡すキュー。は、ゲスト側ドライバがI/Oに使うメモリ領域を登録し、ホスト側が処理済みの領域を返すためのキューです。virtio-netはネットワーク、virtio-blkはブロックデバイスというように、用途ごとのデバイス仕様が同じ基本構造を使います。
既存デバイスをエミュレーションする構成では、ゲストOSのドライバが行うレジスタ操作をホスト側で捕捉します。操作のたびにVM exitが起こる場合があり、デバイスモデルが状態を更新している間は、その仮想CPUで仮想マシン内のOSやアプリを動かせません。virtioは、複数のI/O要求をメモリ上のqueueへ記述し、まとめて通知できる形式を定めています。これにより、デバイス固有のレジスタ操作と、それに伴う切り替えを減らせます。
ゲストOSは、実在するデバイス用のドライバを使います。ドライバがデバイスのレジスタへ書き込むと、QEMUのデバイスモデルは書き込まれた値を解釈し、対象デバイスの状態を更新します。
ゲストOSのvirtioドライバとホスト側のバックエンドは、virtioが定めたdescriptorとqueueの形式でI/Oを受け渡します。実在するデバイスのレジスタ操作を再現する必要はありません。
ゲストは実在するデバイス用のドライバを使います。QEMUは、そのドライバが行うレジスタの読み書き、割り込み、データ転送を、対象デバイスの仕様に沿って処理します。
ゲスト側ドライバは、読み書きするメモリ領域をdescriptorで示し、利用可能な要求をvirtqueueへ登録します。ホスト側は完了した要求を同じキューへ記録します。仮想環境向けに単純化した形式なので、既存デバイスの細かな動作を再現する処理を減らせます。
ネットワークやストレージへ多数の要求が届く構成では、要求を渡す形式だけでなく、ホスト側で何本の処理を同時に進められるかも性能に影響します。virtio-netとvirtio-blkは複数のvirtqueueを使うmultiqueueに対応しています。
ホストでは、QEMUのスレッドが一つの仮想CPUを担当します。このスレッドがデバイス処理まで行う構成では、VM exitから処理が終わるまで、その仮想CPUで仮想マシン内のOSやアプリを動かせません。
IOThreadIOThreadQEMUで、I/Oのevent loopをmain loopとは別に動かすスレッド。は、QEMUでI/Oを処理するevent loop用のスレッドです。I/O要求をIOThreadへ渡せる構成では、仮想CPUを担当するスレッドが先に仮想マシン内の命令実行を再開し、デバイス処理を別のホストCPUで進められます。vhostを使う構成では、virtioのデータ処理をホストカーネル側へ移せます。
再生すると、ホスト上の各スレッドが処理を進める時間を縦線が移動します。
ゲストOSのドライバによるレジスタ操作を捕捉すると、物理CPUは仮想マシン内の命令実行を中断し、ホスト側の処理へ移ります。ホスト側でデバイスの状態更新やI/O処理まで行う構成では、その処理がVM entryより前に続きます。
virtqueueへ登録された要求をIOThreadなどへ通知した後、仮想CPUスレッドはVM entryへ進めます。物理I/Oを待つ処理は別のホストCPUで進められます。
複数の仮想CPUがI/Oを出しても、要求は同じvirtqueueと同じ処理スレッドを通ります。前の要求に時間がかかると、後の要求はそこで待ちます。
ゲスト側ドライバが要求を複数のvirtqueueへ分け、ホスト側でも別々のIOThreadやvhostの処理先へ割り当てると、複数の要求をそれぞれ別のCPUで進められます。
QEMUプロセスがvirtqueueの要求を読み、ホストの機能へ渡します。デバイスモデルと仮想マシンの管理を一つのプロセスにまとめられます。
vhostvhostvirtioのデータ処理をホストカーネルで行う仕組み。は、virtioのデータ処理をホストカーネルで行う仕組みです。要求のたびにQEMUへ戻る経路を減らします。
virtioのデータ処理をQEMUとは別のユーザー空間プロセスで行います。QEMUとはUnix domain socketなどを通して設定をやり取りします。
ここまでは、QEMUが再現するデバイスやvirtioを通して、ゲストOSのI/O要求をホスト側へ渡す構成を見てきました。
一方、ホストに接続されたネットワークカードやGPUそのものを仮想マシンへ割り当て、ゲストOSのドライバから使う構成もあります。この構成では、デバイスの操作を仮想マシンへ渡すだけでなく、デバイスがアクセスできるRAMも仮想マシンごとに制限する必要があります。
ここでは、ネットワークカード(Network Interface Controller、NIC)、NVMe(Non-Volatile Memory Express)対応SSD、GPU(Graphics Processing Unit)を例にします。これらのデバイスは、大量のデータを転送するたびにCPUへコピーさせるのではなく、デバイス自身でRAMを読み書きできます。この機能をDirect Memory Access(DMA)DMAデバイスがCPUを介さず、RAMへ直接データを転送する機能。と呼びます。DMAはCPUの処理を減らせますが、デバイスへ物理RAM全体の読み書きを許すと、ホストや別の仮想マシンのメモリまでアクセスできてしまいます。
CPUは、Memory Management Unit(MMU)MMUCPUが仮想アドレスを物理アドレスへ変換するための回路。を使って、プログラムが指定した仮想アドレスを物理RAM上の位置へ変換します。デバイスのDMAにも同様の変換と保護を加えるハードウェアが、Input-Output Memory Management Unit(IOMMU)IOMMUデバイスのDMAアドレスを変換し、アクセスできるRAMの範囲を制限するハードウェア。です。IOMMUは、デバイスが使うI/O仮想アドレスを変換し、デバイスごとに読み書きできるRAMの範囲を制限します。
Linuxは、PCI Expressデバイスの機能を、個別に設定できるPCI Functionとして認識します。ホスト上のPCI Functionを仮想マシンへ直接割り当てる構成をPCIパススルーPCIパススルーホストのPCIデバイスまたはPCI Functionを、仮想マシンへ直接割り当てる構成。と呼びます。Virtual Function I/O(VFIO)VFIOIOMMUで保護したデバイスを、ユーザー空間から安全に扱うためのLinuxカーネルの枠組み。は、IOMMUで保護したデバイスのレジスタ、割り込み、DMAを、QEMUなどのユーザー空間プログラムから扱うためのLinuxカーネルの枠組みです。
Single Root I/O Virtualization(SR-IOV)SR-IOV一つのPCI Expressデバイスを、複数の独立した機能として見せるハードウェアの機能。は、一つのPCI Expressデバイスを、管理を担うPhysical Function(PF)と、割り当て可能な複数のVirtual Function(VF)として見せるハードウェアの機能です。VFは独立したPCI Functionとして見えるため、VFIOを使って仮想マシンへパススルーできます。
PCI Functionは、PCI Express上で個別に認識されるデバイスの機能です。NIC、GPU、NVMeコントローラなどのFunctionをホスト用ドライバから外し、VFIOを通してQEMUへ渡します。
ゲストOSのドライバが、割り当てられたFunctionのレジスタ、割り込み、DMAを使います。
VFIOではIOMMU groupが安全に分離できる最小単位です。同じgroupのデバイスをまとめて扱う必要がある場合があります。
IOMMUは、デバイスが使うI/O仮想アドレスを物理RAMへ変換し、読み書きできる範囲を制限します。
SR-IOVに対応するNICは、管理を担うPhysical Function(PF)と、独立したPCI Functionとして見えるVirtual Function(VF)を提供します。各VFは通常のPCIデバイスと同じようにVFIOで割り当てられます。
virtioバックエンドを通さずに送受信できるため、ホストCPUで行うパケット処理を減らせます。
NICとドライバによってはrepresentorを作り、Linuxのbridge、tc、Open vSwitchからVFの通信を制御できます。
同じSR-IOVでも、ハードウェアとドライバが提供する分離、監視、live migrationの範囲は異なります。
学習、推論、可視化では、GPUの演算器、GPUメモリ、メモリ帯域をどの環境へ割り当てるかが性能に影響します。分割して共有すれば、一台のGPUで複数の環境を動かせます。使える構成はGPU、ドライバ、仮想化ソフトウェアの対応範囲で決まります。
一つの物理GPUを一台の仮想マシンへ割り当てます。ゲストOSのGPUドライバが直接使い、割り当て中はほかの仮想マシンと共有しません。
一つのGPUを複数の仮想マシンから使います。SR-IOVのVF、vGPU、Linuxのmdevなど、製品に応じた仕組みがあります。分けられるGPUメモリや演算資源、時間分割か空間分割か、live migrationの可否は製品ごとに異なります。
コンテナはホストのカーネルとGPUドライバを共有します。コンテナruntimeが必要なdeviceとlibraryを公開する構成で、PCIパススルーとは異なります。
パススルーでは、QEMUのデバイスモデルやvirtioバックエンドをデータ経路から外せます。ホストの仮想switch、フィルタ、監視を通したい場合は、virtio、SR-IOV representor、SmartNICなど、要件に合う経路を選びます。
live migrationlive migration動作中の仮想マシンの状態を別のホストへ転送し、実行先を切り替える処理。では、RAMと仮想CPUに加えてデバイスの状態も転送します。VFIOで割り当てたデバイスを移動できるのは、デバイスとドライバが状態の保存・復元に対応する場合です。対応しないデバイスを含む仮想マシンは、動かしたまま別ホストへ移せません。