「Linuxカーネル2.6解読室」(以降、旧版)出版後、Linuxには多くの機能が追加され、エンタープライズ領域をはじめとする様々な場所で使われるようになりました。
それに伴いコードが肥大かつ複雑化し、多くのエンジニアにとって解読不能なブラックボックスとなっています。
世界中のトップエンジニア達の傑作であるLinuxカーネルにメスを入れ、ブラックボックスをこじ開けて、時に好奇心の赴くままにカーネルの世界を解読する「新Linuxカーネル解読室」プロジェクト。
本稿では、KVM機能実装の概要について説明します。
執筆者 : 高橋 浩和
※「新Linuxカーネル解読室」連載記事一覧はこちら
はじめに
Linuxカーネルはハイパーバイザーとして動作するための機能も備えており、KVMと呼ばれています。 KVM上では、ゲストOS(Windowsや別のLinuxなど)を動かすことができます。 「RISC-V OSを作ろう (12) ~ KVM上で動かそう」の記事では、LinuxカーネルのKVM上で自作OS(Sophia OS)を動かしました。
最近のメジャーなCPU(x86,ARM64,RISC-Vなど)はハイパーバイザー実装を助ける仮想化支援機能を備えており、KVMはこれらの機能を活用し、効率良く実装されています。 ハイパーバイザー(KVMを提供するLinuxカーネル)は物理的なハードウェア(CPU、メモリ、ストレージ、ネットワークなど)を制御し、ゲストOSに対して仮想的なハードウェア(仮想CPU、仮想メモリ、仮想ストレージ、仮想ネットワークなど)を提供します。
今回は、このKVMがどのように実装されているか見ていきます。KVMは様々なCPUアーキテクチャ上で動きますが、ここではなるべく各種CPUやコントローラが提供する仮想化支援機能の詳細には触れず、アーキテクチャ共通となる部分を中心に見ていきます。
1. KVMの構成
Linuxの仮想マシン環境は、VMM*1、KVM、ゲストOS の3階層で構成されます。本稿では、VMMとしてQemuを利用する場合を前提に解説します。*2 KVMはLinuxカーネルが持つハイパーバイザー機能です。 QemuとKVMが連携して仮想マシンを用意し、その仮想マシン内でゲストOS(LinuxやWindowsなど)を動かします。
Qemuプロセス1つが仮想マシン1台に対応し、Qemuスレッド(Qemuプロセス内のスレッド)それぞれが仮想CPUに対応します。Qemuプロセスが仮想マシンのメモリ空間を用意し、Qemuスレッドは仮想マシンのメモリ空間内でゲストOSのコードを実行します。 ゲストOSが許可されていない操作(制御レジスタ操作やハードウェアへの直接アクセスなど)を行おうとすると、KVMとQemuがその操作を検出し代行する仕組みです。
ゲストOSをハイパーバイザー(KVM)より一段低い実行モードで動作させることにより、ゲストOSによる許可されていない操作を検出します。

2. 仮想CPU
KVMでは、各Qemuスレッドが仮想CPUをエミュレートする実装となっています。
仮想CPUの起動
Linux KVM環境では、ゲストOSの起動にQemuを使います。 *3 Qemu起動時に、バックエンドとしてKVMを使うことをオプション(-accel kvm、-enable-kvmなど)で指定すると、Qemuは/dev/kvmを介して仮想マシンの生成、仮想CPUの生成を行います。 Qemuが生成した各スレッドは、仮想CPUのエミュレートを始めます。*4
起動した各仮想CPU(Qemuスレッド)は、実行モードをハイパーバイザー動作モードからゲストOS動作モードに遷移し、仮想マシンのリセットエントリから実行を開始します。 *5
ゲストOS実行の中断
ゲストOS動作モード時は、特権モードであっても実行できない命令や操作があります。ゲストOSがそれらの命令を実行しようとするとKVM(ハイパーバイザー)が介入し、その命令をエミュレートします。 一種の例外が発生して実行モードがゲストOS動作モードからハイパーバイザー動作モードに切り替わり、KVMが起動します。KVMは例外の要因となった命令のエミュレートをした後、実行モードをゲストOS動作モードに切り戻し、例外を発生させた命令の次から実行を再開します。 *6
KVMで命令をエミュレートできない時は、Qemuにまでフォールバックします。 KVMはQemuスレッドが発行したioctlシステムコールの延長で動作していますが、このioctlを一旦終了させます。Qemuスレッドは命令のエミュレーションを完了した後、再度ioctlシステムコールを発行し、仮想CPUのエミュレーションを再開します。
ゲストOS動作時にホストOS(Linux)への割り込みが発生した時や、ゲストOSによる明示的なKVM機能の呼び出し(ハイパーバイザーコール)があった場合も、ゲストOS動作モードからハイパーバイザー動作モードに切り替わり、KVMが呼び出されます。

仮想CPUの切り替え
KVMは仮想CPUのスケジュールには、Linuxカーネルのプロセススケジューラを使っています。Linuxカーネルから見ると仮想CPUは単なるQemuスレッドであり、他のプロセスやスレッドと区別することなくスケジュールできます。 Qemuスレッド(仮想CPU)の切り替えに関しては少々考慮が必要です。仮想CPUは通常のLinuxプロセス・スレッドよりも大きなコンテキストを持っており、それらのコンテキストも切り替える必要があります。汎用レジスタ以外に仮想CPUが持つ制御レジスタなども切り替える必要があります。
この切り替えは、「新Linuxカーネル解読室 - プロセスディスパッチャ(前編) 4.3 notifier」の機能を利用して実現しています。 仮想CPUをエミュレートするQemuスレッドは、このフックに制御レジスタ保存・復帰を行う関数を登録しています。
省電力と応答性
最近のCPUでは、仮想CPUがアイドル状態になった時に実行する命令(例:WFI命令やHLT命令)をゲストOS実行モードではトラップ(KVMが検出)するように設定できます。 この機能を使うことにより、KVMは仮想CPUのアイドル状態を即座に検出できるようになります。KVMはアイドル状態になった仮想CPUの実行権を他の仮想CPUやLinuxプロセスに明け渡します。
物理CPUで動作可能な仮想CPUやLinuxプロセスが存在しなくなれば、物理CPU上でアイドルプロセスが動作し、省電力モードに遷移することもできます。
3. メモリ仮想化
ゲストOSが物理メモリとしているメモリは本当の物理メモリではなく、ハイパーバイザー(KVM)が用意した仮想化された物理メモリです。
本記事では、次の用語を使うことにします。
| 用語 | 説明 |
|---|---|
| ホスト物理メモリ | 本当の物理メモリ。KVMが管理する。 |
| ゲスト物理メモリ | KVMにより仮想化された物理メモリ。ゲスト物理メモリとホスト物理メモリの対応はKVMが管理する。ゲストOSからは、ゲスト物理メモリは本当の物理メモリに見える。 |
| KVMメモリスロット | Qemuプロセス空間内に確保した仮想マシン用のメモリ |
ゲストOSによるゲスト物理メモリアクセスは、仮想化機能によりホスト物理メモリへのアクセスに変換されます。最近の仮想化機能を持つCPUは、このアドレス変換機能を持つMMUを備えています。 CPUアーキテクチャやCPUベンダごとに異なる名称で呼んでいますが、機能的にはほぼ同じです。
| アーキテクチャ | アドレス変換機能 |
|---|---|
| Intel x86 | EPT (Extended Page Tables) |
| AMD64 | NPT (Nested Page Tables) |
| ARM64 | Stage 2 MMU |
| RISC-V | 2 stage address translation |
このMMU機能の登場前のハイパーバイザーでは、シャドウページテーブルという技術が使われていました。 これは、ゲストOSによるページテーブルの更新をハイパーバイザーが捕捉し、ゲストOSの仮想アドレスをホストの物理アドレスに直接マップする「シャドウページテーブル」を裏側で更新・維持する技術です。 実は、現在でもNested Virtualization(ハイパーバイザーの上でハイパーバイザーを動かす)を実現する時に利用されています。
Qemuプロセスメモリとゲスト物理メモリ
仮想マシンに割り当てているメモリはどこから確保しているのでしょうか? 実は、Qemuが仮想マシンを作る際にQemuプロセス空間内に確保しています。 KVM上で起動した仮想マシンは、このQemuプロセス空間に確保したメモリ(KVMメモリスロット)を、仮想マシンの空間にマップしています。仮想マシンのメモリは、Qemuプロセスからも参照できます。
仮想マシンを起動する時に、ゲスト物理メモリとホスト物理メモリを対応付けるページテーブルを確保します。しかし、確保直後のページテーブルにはホスト物理メモリのページは登録されていません。 ゲストOSが特定のゲスト物理メモリを初めて参照した時、対応するページテーブルエントリには、まだホスト物理メモリのアドレスが登録されていないため、CPUはメモリフォルト例外を発生させます。 KVMのメモリフォルトハンドラは、次の処理を行います。
- ゲスト物理メモリ空間の例外発生アドレスに対応するKVMメモリスロット内の仮想アドレスを求める。
- KVMメモリスロット内の仮想アドレスからホスト物理メモリアドレスを求める。
- 求めたホスト物理メモリアドレスをページテーブル(ゲスト物理アドレス/ホスト物理アドレス変換ページテーブル)に登録する。
ホスト物理メモリアドレスは、Qemuプロセス空間内に確保されたKVMメモリスロットとホスト物理メモリとのマップ情報から求めます。

4. 仮想割り込み
ゲストOSへの割り込みはKVM(ハイパーバイザー)が制御します。KVMは仮想CPUに対して割り込みを要求し、仮想CPUのスケジューリング時に割り込みを発生させます。 KVMは仮想CPUごとに仮想割り込みコントローラを生成し、仮想CPUに対応付けます。仮想CPUは、普通の割り込みコントローラのように仮想割り込みコントローラを利用できます。
仮想割り込み生成と配送
各仮想CPUはコンテキストとして、割り込み制御レジスタおよび割り込みコントローラの状態を持ちます。
仮想CPUに割り込みを発生させるとき、ハイパーバイザーはこの割り込み制御用のコンテキストを更新します。仮想割り込みコントローラに、指定した割り込み番号の割り込み要求を書き込み、仮想CPUの割り込み制御レジスタに割り込み要求フラグを立てます。 *7
これらの割り込み制御用のコンテキストは、仮想CPU切り替え時に保存・復帰が行われます。仮想CPUがスケジュールされ、ハイパーバイザー実行モードからゲストOS実行モードに遷移するタイミングで、割り込み制御用のコンテキストに割り込み要求がある場合に仮想割り込みが発生します。
*8

割り込みコントローラの仮想化支援
従来の実装ではゲストOSが仮想割り込みコントローラを操作する都度ハイパーバイザーが介入し、割り込みコントローラの動作をエミュレートする必要がありました。 この方式はオーバーヘッドが大きいため、最近の割り込みコントローラは仮想割り込みコントローラ機能をハードウェアで支援する機能を搭載するようになってきました。 仮想割り込みコントローラのレジスタを、ゲストOSから直接制御することを許可します。ハイパーバイザー介入によるエミュレーションが不要になり、オーバーヘッドが削減されます。
最新の割り込みコントローラでは、割り込みパススルーという機能も実装されています。 この機能により、デバイスパススルーを利用する時のオーバーヘッドを削減できます。(この機能は後述する「デバイスパススルー」で重要になります)
従来の割り込みコントローラでは、物理割り込みが発生するとKVMが一旦受け付け、KVMがそれを中継してゲストOSに仮想割り込みを配送していました。 割り込み発生毎にKVMが動作するオーバーヘッドが発生していました。 割り込みパススルー機能を持つ割り込みコントローラでは、物理割り込み発生時に、自動的に仮想割り込みコントローラのコンテキストを更新します。(もちろん、物理割り込みと仮想割り込みの対応は登録しておく必要があります)
| CPUアーキテクチャ | 割り込みコントローラ機能 |
|---|---|
| ARM | GIC(Generic Interrupt Controller) v4 direct injection |
| Intel x86 | PI(Posted Interrupts) |
| AMD | AVIC(Advanced Virtual Interrupt Controller) |
| RISC-V | AIA(Advanced Interrupt Architecture) |
5. 仮想デバイス
KVMは、ゲストOSのデバイスアクセス(ネットワークアクセス、ストレージアクセスなど)を、実際にデバイス操作を行うバックエンドドライバに中継します。
デバイスのフルエミュレーション
現実世界のデバイスを完全にエミュレートさせることができます。 この機能により、実ハードウェア用に実装されたOSを変更せずそのまま仮想マシン環境で動かすことができます。 ただし、エミュレーションのオーバーヘッドは非常に大きいものとなります。
仮想マシン環境で動作するOSがI/Oコントローラ*9のレジスタ*10にアクセスするたびにトラップが発生してKVMが起動し、更にQemuまでフォールバックし、Qemu内でI/Oコントローラのエミュレーションを行います。Qemuはエミュレーションの結果に従って、I/Oコントローラの状態更新や仮想マシンのメモリを参照・更新します。 仮想マシンのメモリ領域はQemuプロセス空間のメモリ(KVMメモリスロット)をマップしたものであるため、Qemuからの仮想マシンのメモリ操作は容易です。

準仮想化デバイス
準仮想化デバイスは、PV(Paravirtualized)デバイスとも呼ばれ、仮想マシン専用に最適化された仮想デバイスです。 KVM環境のPVデバイスはVirtIOが標準となっています。VirtIOは仮想マシン環境での利用を前提に設計されており、最小限のオーバーヘッドで利用できます。
VirtIOドライバは、フロントエンドドライバとバックエンドドライバから構成されます。フロントエンドドライバは、ゲストOSに組み込まれるドライバです。バックエンドドライバは実際の物理デバイスのアクセスを行うドライバで、フロントエンドドライバからの要求を物理デバイスへのアクセスに変換します。 VirtIOはPCIデバイスとして見えるように設計されています。 仮想マシン(ゲストOS)とKVM(ハイパーバイザー)が共有するリングバッファ(Virtqueue)を用意し、このバッファを通してフロントエンドドライバとバックエンドドライバがI/O要求をやり取りします。 バックエンドドライバは、Linuxカーネル内のデバイスドライバとして用意されたものと、Qemuに組み込まれたドライバ*11の2種類があります。
Qemu組み込みのドライバをバックエンドとした場合、デバイスへの書き込み要求(または送信要求)は次のように動作します。
- ゲストOSが、リングバッファにI/O要求を書き込む。
- ゲストOSは、ハイパーバイザーをキックする。*12
- キックによりKVMが起動する。
- KVMはQemuでエミュレートすべきI/O要求であると判断し、Qemuにフォールバックする。
- Qemuはioctlが終了した原因がPVデバイスへのI/O要求であると判断し、リングバッファからI/O要求を取り出し、物理デバイスへのアクセスに変換する。*13
- QemuはKVMを介して、ゲストOSにI/O完了割り込み(仮想割り込み)を送る。
バックエンドドライバとして、Linuxカーネルのvhostドライバ*14を指定することもできます。Qemuへのフォールバックのオーバーヘッドが無いため、高速に動作します。
デバイスパススルー
KVMは、主に性能向上を目的として、ゲストOSが物理デバイスを直接制御することを許可する機能も実装しています。この機能はデバイスパススルーと呼ばれています。 デバイス単位、またはデバイスが複数のファンクションを持つ場合は、そのファンクション単位で特定の仮想マシンに貸し出すことができます。*15 パススルーされたデバイスは、貸し出しを受けた仮想マシン(ゲストOS)からのみアクセスを許可されます。
IOMMU
ゲストOSのデバイスドライバがDMAを使う際に、必ず解決しなければならない課題があります。
- ゲストOSは、ゲスト物理アドレスしか知らない。
- ゲストOSは、DMAコントローラにゲスト物理アドレスしか渡せない。
- 本当にDMAしたいメモリのアドレスは、ホスト物理メモリのアドレスである。
この課題を解決するために、仮想化支援機能をもつIOMMU(入出力メモリ管理ユニット)が用意されています。 ゲスト物理アドレスをホスト物理アドレスにマッピングするアドレス変換テーブル(IOMMUページテーブル)を登録できます。I/Oコントローラごと*16に異なるアドレス変換テーブルを登録します。
| CPUアーキテクチャ | IOMMU |
|---|---|
| Intel x86 | VT-d |
| AMD | AMD-Vi |
| ARM | SMMU*17 |
| RISC-V | RISC-V IOMMU |

割り込みパススルー
割り込みパススルーは、物理割り込みをハイパーバイザー(KVM)を経由せずゲストOSに仮想割り込みとして通知する機能です。 この機能は、割り込み発生時のハイパーバイザー(KVM)介入のオーバーヘッドを避けるため、最近のハードウェアで実装されつつあります。
割り込みパススルー機能を持った割り込みコントローラは、物理割り込み発生時に仮想割り込みコントローラのコンテキストを自動更新します。 あらかじめ、物理割り込み番号と、仮想マシンとマッピング先の仮想割り込み番号の対応を登録しておく必要があります。 *18
最後に
KVMはLinuxをインストールすると直ぐに利用できるお手軽な仮想化基盤です。Linuxカーネルの他機能(cgroup、AppArmorなど)と組み合わせて利用することもできます。 ハードウェアによる仮想化支援機能の拡充が進み、KVM上の仮想マシンもベアメタルに近い性能が出せるようになっています。
本稿ではKVM実装の全体像を俯瞰してきました。 Intel x86やARM64向けのKVMがどのような実装になっているかもっと知りたいという方もおられると思います。今後機会をみて、KVMの各機能の詳細を説明できたらと考えています。
*1:Virtual Machine Monitor
*2:VMMとして軽量なFirecrackerなどを使うこともできます。
*3:libvirtなどを使っている場合でも、最終的にQemuが呼び出されます。
*4:生成された仮想マシンや仮想CPUにはファイルディスクリプタが割り当てられ、仮想CPUのファイルディスクリプタに対して、仮想CPUを実行するというioctl(仮想CPUの実行を指示)を発行すると、そのシステムコールを発行したスレッド自身が仮想CPUのエミュレーションを開始します。
*5:x86環境ではハイパーバイザー動作モードにVMX rootモードを使い、ゲストOS動作モードにVMX non-rootモードを使います。 ARM64環境ではハイパーバイザー動作モードにEL2モードを使い、ゲストOS動作モードにEL1を使います。 RISC-V環境ではハイパーバイザー動作モードにHSモードを使い、ゲストOS動作モードにVSモードを使います。
*6:x86環境では、これら動作モード遷移をそれぞれVM Exit・VM Entryと呼んでいます。ARM64やRISC-V環境ではこれらは単に例外の一種として扱っており、特別な名前は付けられていません。
*7:具体的な操作はアーキテクチャ毎に異なります
*8:RISC-Vのタイマ割り込みのように、割り込みコントローラを通さない割り込みもあります。その場合は、仮想CPUの割り込み制御レジスタのみ更新します。
*9:Qemuが用意する仮想I/Oコントローラ
*10:またはコントローラとCPUで共有するMMIO領域
*11:正式名称はVirtIO バックエンドデバイスモデル (VirtIO Backend Device Model)と言います。virtio-net-pci、virtio-blk-pciなどがあります。
*12:キックを行うためのレジスタの位置は、VirtIOデバイスのPCIコンフィギュレーション空間から求めます。
*13:仮想マシンのメモリ域はQemuプロセス空間のメモリ(KVMメモリスロット)をマップしたものであるため、QemuはリングバッファやゲストOSのメモリにアクセスできる。
*14:vhost-net、vhost-blkなど
*15:ハードウェアが仮想化支援機能をサポートしていることが前提です。
*16:正確にはファンクションごと
*17:CPUのMMUとIOMMUでページテーブルを共有することもできる。
*18:Intel x86環境ではVT-dにこの割り込みリマッピング機能があります。AMD環境では、AVICと呼ばれる割り込みコントローラがAMD-Viと連携してこの機能を実現しています。ARM64環境ではSMMUとGICが連携してこの機能を実現します。