RustでマイクロKVM実装 - 用語集

これまでの記事に登場した専門用語を、カテゴリ別に整理した用語集です。記事が増えるのにあわせて随時更新します。

仮想化の基礎

  • 特権レベル(Ring 0〜3):x86 CPUが持つ4段階の実行権限です。OSのカーネルはRing 0で動作し、割り込み制御やページテーブルの書き換えといった特権命令を実行します。1つの物理CPU上でRing 0を名乗れる主体は1つしかないため、ゲストOSにそのままRing 0を渡すことはできません
  • トラップ・アンド・エミュレート:ゲストOSを低い特権レベルで動かし、特権命令を実行しようとするたびにCPUに例外を発生させてハイパーバイザに処理を委ねる、仮想化の古典的な実現方式です。x86には特権命令のはずなのに例外を発生させず黙って失敗する命令があり、この方式だけでは正しく仮想化できないことが知られていました
  • VT-x(Intel)/ AMD-V(AMD):2000年代半ば以降のCPUに搭載された、ハードウェア仮想化支援機能です。トラップ・アンド・エミュレートを成立させるために必要な「特権命令を確実に捕捉する仕組み」を、ソフトウェアの工夫ではなくCPUの機能として提供します
  • VMX root mode / VMX non-root mode:VT-xが既存のRing 0〜3とは別に追加する2つの動作モードです。ハイパーバイザはroot modeで、ゲストOSはnon-root modeの中でRing 0〜3をそのまま使って動作します
  • VM Exit:ゲストが特権命令やI/Oアクセスなど、ハイパーバイザの管理下に置くべき操作を行おうとしたときに、CPUがハードウェアレベルでVMX root modeへ制御を戻すイベントです。詳しくはKVMとは何かを参照してください
  • KVM(Kernel-based Virtual Machine):VT-x/AMD-Vをアプリケーションから使えるようにするLinuxカーネルモジュールです。CPUとメモリの仮想化に必要な最小限の機能だけを提供し、デバイスのエミュレーションは範囲外です
  • VMM(Virtual Machine Monitor):KVMが公開するAPIを使ってVMを組み立て、デバイスをエミュレートし、ゲストのライフサイクルを管理するユーザー空間側のプログラムです。本プロジェクトで自作しているプログラム自体がこれにあたります
  • vCPU(仮想CPU):VMMが1つ以上作成する、ゲストから見たCPUコアです。1つのvCPUは1つのKVM_RUNループに対応します
  • ゲスト / ホスト:仮想マシンの中で動くOS・プロセス側を「ゲスト」、それを実行している側の物理マシン・OS・プロセスを「ホスト」と呼びます

KVM API

  • ioctl:デバイスファイルに対して汎用的なコマンドを発行するLinuxのシステムコールです。KVMのAPIは/dev/kvm(およびそこから派生するVM fd・vCPU fd)に対するioctl呼び出しとして提供されます
  • _IO/_IOW/_IOR:「マジックナンバー・コマンド番号・データの向き・データサイズ」を1つの整数にビットパックする、Linux全般のioctlで使われる規約です。KVM固有ではありません。詳しくは生ioctlでKVMを叩くを参照してください
  • KVM_CREATE_VM:KVM fdに対して発行し、VMという「入れ物」をカーネル内に作ってVM fdを取得するioctlです
  • メモリスロット(KVM_SET_USER_MEMORY_REGION:「ホストのどのメモリ範囲を、ゲストのどの物理アドレスとして見せるか」という対応関係を1件登録するためのAPIです。userspace_addr(ホスト側の仮想アドレス)とguest_phys_addr(ゲストCPUから見た物理アドレス)が同じメモリを指しながら別々の番地体系である点が重要です。詳しくはゲストメモリを複数リージョンに分割するを参照してください
  • EPT(Extended Page Table):ゲスト物理アドレスとホスト物理アドレスの対応関係を、CPUがゲストのメモリアクセスのたびに参照して変換するハードウェア機構です。この変換はVM Exitを起こさずに完結します
  • KVM_CREATE_VCPU / vCPU fd:VM fdに対して発行し、vCPUを1つ作成してvCPU fdを取得するioctlです。1つのvCPUにつき1回呼び出します
  • KVM_RUN:vCPU fdに対して発行すると、実際にゲストコードの実行が始まるioctlです。ゲストの実行が何らかの理由(hlt命令、I/Oポートアクセスなど)で中断される(VM Exitが起きる)までブロックします
  • kvm_run構造体 / exit_reason:vCPU fdをmmapした共有メモリ領域に配置される構造体です。VM Exitが起きるたびに、その理由(exit_reason)と関連情報がここに書き込まれます
  • VcpuExitkvm-ioctlsクレートが提供する列挙型で、kvm_runexit_reasonと関連データを1つの型付きの値にまとめたものです。VcpuExit::HltVcpuExit::IoOut(port, data)のように、exitの種類ごとに必要なデータがバリアントとして直接渡ってきます
  • repr(C) / repr(C, packed):Rustコンパイラに対して「フィールド順を並び替えず、Cと同じメモリレイアウトを使う」ことを指定するアトリビュートです。KVMのC構造体をRustへそのまま移植する際や、MP仕様のようにパディングを一切許さないバイナリ形式を扱う際に使います。packedを付けるとフィールド間の自動パディングも取り除かれます
  • unsafe:Rustコンパイラによる安全性検証の対象外になる操作であることを示すキーワードです。set_user_memory_regionのように、渡した値が実際に有効なメモリを指しているかをクレート側では検証できない呼び出しに付きます

rust-vmmクレート

  • rust-vmm:単一のクレート名ではなく、KVM操作に必要な機能ごとに分割された複数のクレートを公開しているGitHub上のプロジェクトです。Firecrackerを含む複数のVMM実装が、これらのクレートを部品として使っています
  • kvm-ioctls:rust-vmmが提供する、KVMのioctl呼び出しをラップしたクレートです。Kvm/VmFd/VcpuFdといった型と、VcpuExit列挙型を提供します
  • kvm-bindings:rust-vmmが提供する、KVMのC構造体(kvm_regskvm_sregskvm_userspace_memory_regionなど)をRustへ機械的に移植したクレートです
  • vm-memory:rust-vmmが提供する、ゲストメモリを扱うためのクレートです。GuestMemoryMmap(複数リージョンのmmapをまとめて管理する型)、GuestAddress(ゲスト物理アドレスを表す型)、Bytesトレイト(write_slice/read_objなどゲストメモリへの読み書きメソッド一式)を提供します
  • ByteValuedvm-memoryが定義するマーカートレイトです。unsafe impl ByteValued for MyStruct {}と書くことで、その構造体をバイト列としてそのままゲストメモリへ読み書き(write_obj/read_obj)できるようになります
  • linux-loader:rust-vmmが提供する、Linuxカーネルイメージ(bzImage/ELF)のロードとboot_paramsの組み立てを行うクレートです。詳しくはlinux-loaderでLinuxカーネルをロードするを参照してください
  • vm-superio:rust-vmmが提供する、UART 16550やRTCなど典型的な周辺デバイスのレジスタ挙動を正確にエミュレートするクレートです。割り込みの発生方法はTriggerトレイトとして外部から注入する設計になっています

CPU/x86アーキテクチャ

  • リアルモード / ロングモード:x86 CPUの動作モードです。リアルモードは16bit時代からの互換モードで、CPUは電源投入直後この状態から動作を始めます。ロングモードはx86-64のネイティブな64bit動作モードで、ページングが常に有効な前提で動きます
  • セグメントレジスタ(CS/DS/SS など)とセグメンテーション:メモリアドレスの基準点(base)や範囲(limit)、権限を保持するレジスタです。ロングモードではbase/limitの大半がCPUに無視されますが、コードセグメントのLビット(64bitコードか否か)のような権限まわりのビットは引き続き有効です
  • CR0/CR3/CR4/EFER:CPUの動作モードを制御するレジスタ群です。CR0はページング有効化(PGビット)など、CR3はページテーブルの物理アドレス、CR4はPAE(物理アドレス拡張)有効化など、EFERはロングモード有効化(LME/LMAビット)を制御します
  • PML4/PDPT/PD/PT とページテーブル:x86-64の4段階ページングを構成するテーブルです。CR3がPML4の物理アドレスを指し、そこから各段のエントリを辿って最終的な物理アドレスを得ます。今回のプロジェクトではPDの段階で2MiBページとして扱うことで最下層のPTを省略した、簡略化した2段のページテーブルを使っています
  • 恒等マッピング(identity mapping):仮想アドレスと物理アドレスが常に一致するように構成されたページテーブルの状態です。カーネルの64bitエントリポイントに直接ジャンプする際、事前にこの状態のページテーブルを用意しておく必要があります
  • TSS(Task State Segment)/ identity map領域(Intel VMX向け設定)KVM_SET_TSS_ADDR/KVM_SET_IDENTITY_MAP_ADDRで予約する、KVMがVMX特有のリアルモード/16bitコードのエミュレーションを行う際に内部的に使う、ゲスト物理アドレス上の小さな作業領域です。Intel CPU環境では必須で、省略するとKVM_RUNが失敗します。AMD-V(SVM)では不要です
  • CPUID:CPUに「あなたはどんな機能を持っているか」を尋ねるx86命令です。EAXレジスタに質問番号(leaf)を入れてから実行し、答えがEAX/EBX/ECX/EDXの4レジスタに返ってきます
  • leaf(CPUIDのleaf):CPUIDで「何番の質問をするか」を表す番号です。leaf 1は基本的な機能フラグやAPIC ID、leaf 4はキャッシュの階層構造や実コア数など、leafごとに聞ける項目が決まっています
  • APIC(ローカルAPIC)/ APIC ID:CPUコアに内蔵されている、割り込みを受け取って処理する専用回路です。コアごとに1個ずつ付いており、それぞれに識別番号(APIC ID)が割り振られています。CPUID leaf 1のEBX[31:24]には、CPU自身が「自分の初期APIC IDは何番か」を答える値が入ります
  • BSP(Bootstrap Processor)/ AP(Application Processor):複数コア環境で、電源投入直後から動作しOSのブート処理を一手に引き受けるコアがBSP(通常0番)、BSPが後から起こすまで指示待ち状態のコアがAPです
  • INIT-SIPI-SIPI:BSPがAPを起動させるために送る、Intel仕様で定められた一連の割り込みです。INITはAPを電源投入直後と同じ状態にリセットし、SIPI(Startup IPI)は実行を開始するアドレス(ベクタ)を指定します。SIPIは仕様上2回連続で送ります
  • HTTビット(Hyper-Threading Technology):CPUID leaf 1のEDX[28]にあるフラグです。1コアが複数の論理CPUに見える(ハイパースレッディング対応)ことを示します。leaf 1(論理プロセッサ数)とleaf 4(実コア数)の情報が矛盾していると、ゲストカーネル側のsmp_num_siblings(1コアあたりの論理CPU数)計算が破綻することがあります
  • undefined behavior(未定義動作):プログラミング言語の仕様上「これをやったら何が起きるか決められていない」操作です。C言語では、32bit整数を32bit以上シフトする、負の数だけシフトするといった操作がこれにあたります。エラーになるのではなく、何が起きるか誰も保証しないという点でバグの温床になりやすい状態です
  • UBSAN(Undefined Behavior Sanitizer):カーネル自身が未定義動作を実行時に検知して警告を出す仕組みです。shift-out-of-bounds(シフト量が範囲外)のような警告としてdmesgに現れます
  • MPテーブル(Intel MP仕様) / MPF(MP Floating Pointer):ACPIを実装していないVMMで、複数CPUの存在をゲストカーネルに伝えるための古典的な仕組みです。ゲストメモリ上の決まった領域(ベースメモリ最後の1KBなど)に_MP_というシグネチャを持つMPFを置き、そこからCPU一覧・バス・IOAPICなどのエントリが並ぶ本体(MPCテーブル)を指し示します

割り込み・タイマー

  • PIC(Programmable Interrupt Controller)/ IOAPIC:ハードウェアからの割り込み要求をCPUに伝える役割を持つコントローラです。KVM_CREATE_IRQCHIPでこれらをKVMのカーネル内部(in-kernel)に用意すると、HLT後の割り込み待ちをVMMを介さずKVM内部で完結できます
  • PIT(Programmable Interval Timer):一定間隔でタイマー割り込みを発生させるチップです。KVM_CREATE_PIT2でin-kernel実装を用意します。これが無いと、ゲストカーネルはスケジューラの初期化付近で実質的に停止します
  • KVM_IRQ_LINEVmFd::set_irq_line:in-kernel irqchipに対して、指定した番号の割り込み線を上げ下げするAPIです。trueで線を上げ直後にfalseで下げることで、1回分の割り込みとしてゲストCPUへ配送します
  • UART 16550 / LSR・THR:シリアルポートの実装で広く使われるチップの型番です。LSR(Line Status Register)は送受信の状態、THR(Transmitter Holding Register)は送信するバイトを書き込むレジスタです
  • CMOS/RTC と UIPビット:実時刻を保持するチップです。UIP(Update In Progress)ビットは「時刻を更新中かどうか」を示し、カーネルはこれを見て読み取りタイミングを判断します。固定で0を返すだけでも、実時刻を提供せずにブート時の待ちループを回避できます
  • i8042(キーボードコントローラ)とリセット要求:ポート0x64への0xFE書き込みはCPUリセット要求を意味します。カーネルコマンドラインにreboot=kを指定すると、リブート処理はこの経路を使うため、VMM側でこれを検知してVMの終了処理に使えます

ブートプロセス

  • Linux x86 boot protocol:ブートローダー(VMM)がLinuxカーネルに制御を渡す際に従うべき取り決めです。カーネルのロードアドレス、boot_paramsの構造、64bitエントリ時のレジスタの意味(rsiにboot_paramsのアドレスを入れるなど)を定めています
  • bzImage:圧縮されたLinuxカーネルの標準的な配布形式です。ヘッダ部分(setup_header)と圧縮された本体で構成されます
  • boot_params(zero page)/ setup_header:カーネル起動時にレジスタ経由で渡す設定一式をまとめた構造体です。通称「zero page」と呼ばれます。ブートローダー側が書き換える必要のあるフィールド(コマンドラインの位置など)はこの中のsetup_headerにまとまっています
  • e820マップ:物理メモリのどこが使用可能なRAMかを伝えるメモリマップです。名前は実機のBIOSが提供していたINT 0x15(AX=0xE820)割り込みに由来し、仮想マシンでもこの形式がそのまま使われています。エントリ数が1個以下だと丸ごと無効とみなされ古いフォールバック経路に切り替わる仕様があるため、意図的に2エントリ以上に分割する必要があります
  • initramfs:カーネルが起動の最後にユーザースペースへ制御を渡す先を用意するための、メモリ上に展開される最小限のファイルシステムです
  • カーネルコマンドライン(cmdline)console=ttyS0reboot=kのように、起動時にカーネルへ渡す設定文字列です。ブートローダーがゲストメモリへ書き込み、setup_headercmd_line_ptrでその場所を指します

VIRTIO

  • VIRTIO:実ハードウェアの模倣ではなく、仮想化専用に設計されたデバイスの標準規格です。「ゲストとホストの間でメモリ上のバッファをやり取りする」という要求に特化した、virtqueueという共通の仕組みを持ちます
  • virtio-mmio:VIRTIOのtransport(デバイスとやり取りする手段)の1つです。固定サイズのレジスタ群をゲスト物理メモリ上の1箇所に並べただけの、シンプルな仕組みです。PCIのような自動列挙の仕組みを持たないため、カーネルコマンドラインで場所を明示する必要があります
  • virtqueue(split virtqueue):descriptor table・avail ring・used ringの3つのリング状領域で構成される、VIRTIOのデータ受け渡し機構です。デバイスの種類に関係なく使い回せます
  • descriptor table / avail ring / used ring:virtqueueを構成する3つの領域です。descriptor tableはデータの物理アドレスと長さを持つエントリの配列、avail ringはドライバが「このdescriptorを使ってください」とホストに伝えるリング、used ringはホストが「処理が終わりました」とドライバに伝えるリングです
  • TX / RX:ネットワーク機器の世界でよく使われる略語で、Transmit(送信)・Receive(受信)を表します。いずれもそのデバイス自身から見た向きを指すため、ゲストのvirtio-net(仮想NIC)を主語にすると、TXは「ゲストが外へ送るデータ」、RXは「ゲストが外から受け取るデータ」です
  • config space(VIRTIOのconfig space):virtio-mmioのオフセット0x100以降にある、デバイス固有の情報をゲストへ渡すための領域です。virtio-netはMACアドレス、virtio-blockはディスク容量をここで公開します
  • virtio_net_hdr / virtio_blk_outhdr:virtqueueでやり取りする実データの前に付く、デバイスごとに決まった固定長ヘッダです。virtio_net_hdrはチェックサムオフロードなどの制御情報(今回は無効化しているため実質素通し)、virtio_blk_outhdrは読み書きの種別とセクタ番号を持ちます
  • TAPデバイス:Linuxがソフトウェアだけで提供する、イーサネットケーブルの片側のようなインターフェースです。/dev/net/tunを開いてread()/write()するだけで、反対側の端子に何をつなぐかをアプリケーション側が自由に決められます
  • feature bit:VIRTIOデバイスが対応する機能を、ホストとゲストの間で64bitのビット列としてやり取りする仕組みです。デバイス種別ごとにビット番号の意味が仕様で定められており、たとえばvirtio-netのVIRTIO_NET_F_MACはbit 5、VIRTIO_NET_F_CSUM(送信パケットのチェックサム計算をデバイス側が肩代わりする機能)はbit 0です。意図しないビットを立てると、ゲストのドライバがデバイス側にない機能を前提とした動作をしてしまいます

開発環境・ツール

  • /dev/kvmとkvmグループ:KVMのAPIを公開するデバイスファイルです。読み書き権限にはkvmグループへの参加が必要です
  • CAP_NET_ADMIN:TAPデバイスの新規作成(TUNSETIFF)に必要な権限です。一度rootでpersistentなTAPデバイスを作って所有者を自分のユーザーに設定しておけば、以降はVMMプロセスを非rootのまま起動しても名前指定でattachできます
  • 静的リンク(crt-static:glibcなどのライブラリを動的リンクではなく実行ファイルへ埋め込む形でリンクする指定です。initramfsの中には動的リンカを置かないため、ゲスト内で実行するバイナリはこの指定でビルドする必要があります
  • Arc<Mutex<T>>:Rustで複数スレッドから安全に共有・変更するための定番の組み合わせです。Arcが所有権の共有、Mutexが排他制御を担います。TAP受信スレッドとvCPU実行スレッドの両方から触るデバイス状態などに使います
  • busybox:多数のUnixコマンドを1つの実行ファイルに詰め込んだツールです。コマンド名と同じ名前(多くはシンボリックリンク)から起動すると、その名前に応じたコマンドとして振る舞います

参考リンク