前回は、x86プロセッサの仮想化支援機能(Intel VT、AMD-V)について、その仕組みを紹介しました。
今回は、オープンソースの仮想マシンソフトウェアであるLinux KVMのソースコードを読み、仮想マシンソフトウェアがどのようにIntel VTやAMD-Vを利用してプロセッサを仮想化しているか、具体的に追っていきたいと思います。
Linux KVMのソースコード構成
Linux KVMは、Linux向けのカーネルモジュールとして、Linuxカーネルにマージされています。最新の開発版について興味があれば、Linux KVMの開発サイトから入手することをお勧めします。今回はLinux 2.6.38.2のソースツリーに含まれるLinux KVMのソースコードを基に解説します。
Linux KVMのソースコード ディレクトリ
Linux KVMのカーネルモジュールは、Linuxカーネルのソースコードツリーのうち、以下のディレクトリに収録されています。
virt/kvm/ ――アーキテクチャ非依存コード
Linux KVMの実装部分のうち、APIなど、ユーザモードから見える部分については、このディレクトリ以下に収録されています。たとえば、/dev/kvmデバイスのインターフェース実装などが含まれます。
arch/*/kvm/ ――各アーキテクチャ向けコード
仮想マシン機能を実現するために、x86などの各アーキテクチャによって実装が異なるハードウェア固有部分は、こちらのディレクトリ以下に、各アーキテクチャごとに収録されています。たとえばx86プロセッサ向けのコードはarch/x86/kvm/に見つかります。
Linux KVMはおもにx86で利用できますが、実際にはその他のアーキテクチャにも対応しています。現時点でLinux KVMがポートされているアーキテクチャについて整理すると、下記のとおりとなっています。
x86(x86アーキテクチャ 32ビット, 64ビット)
ia64(IA-64アーキテクチャ)
s390(S/390アーキテクチャ)
ppc(Power PCアーキテクチャ)
x86プロセッサ向け仮想化支援機能の抽象化
x86プロセッサに限って、ハードウェア固有部分には、さらにもうひとつのレイヤが存在します。これまで紹介してきたように、Linux KVMはIntel VT、およびAMD-Vの両方に対応しています。しかし両者には実装上の互換性がないため、別々に対応しなければなりません。
このため、Intel VT搭載機向けのコード、AMD-V搭載機向けのコードは別々のソースファイルに分離されています。そして、コンパイル時には、それぞれのソースファイルからkvm-intel.koおよびkvm-amd.koと2つカーネルモジュールが作られます。Linux KVMのロード時には、Linux KVM本体(kvm.ko)に加え、プロセッサの種別からkvm-intel.koもしくはkvm-amd.koのどちらかが併せてロードされる仕組みとなっています。
もっとも、Intel VTとAMD-Vの間で互換性がないといっても、これらのコードの目的はどちらも“ x86プロセッサの仮想化” です。このため、実際には“ どちらにも通用するコード” が存在し、この部分が切り出されたのがx86.cです。各仮想化支援機能は、x86.cはvmx.cやsvm.cのアダプタとしての役割を持っています。もし、将来、x86プロセッサ向けの新しいプロセッサ仮想化手法が登場した暁には、このインターフェースを踏襲する形で、ソースファイルの追加によっての対応も可能でしょう。
表1 おもなx86プロセッサの仮想化支援対応コード
ソースコード 説明
x86.c x86アーキテクチャサポート(Intel VT/AMD-V対応)
vmx.c Intel VTサポート
svm.c AMD-Vサポート
Linux KVMからのIntel VTの利用
では、Linux KVMカーネルモジュールが具体的にどのような処理を行っているのか、vmx.cの内容を読んでいきましょう。
VMCSの生成
Intel VT-xでは、論理プロセッサを実行するためにVMCSが必要です。Linux KVMは、仮想マシンに対してvcpu(仮想CPU)が作成されたタイミングで、システムメモリ上にVMCS領域を生成します。仮想マシンが2つの vcpuを持つ場合、vcpuごとに1つ、合計2つのVMCSが生成されます。プロセス、仮想マシン、プロセッサの数を図で表すと図1のような関係になります。
図1 Linux KVMにおけるインスタンス、VMCSの数量関係
VMCSは、システムメモリ上のページから割り当てて使います。ページとは、プロセッサが管理するメモリ空間の単位で、x86プロセッサの場合は4KBである場合が殆どです。Linux KVMは、Linuxカーネル本体のメモリ管理ルーチンよりシステムメモリの割り当てを受け、VMCSとして内容を初期化します。
arch/x86/kvm/vmx.c
static struct vmcs *alloc_vmcs_cpu(int cpu)
{
int node = cpu_to_node(cpu);
struct page *pages;
struct vmcs *vmcs;
pages = alloc_pages_exact_node(node, GFP_KERNEL, vmcs_config.order);
if (!pages)
return NULL;
vmcs = page_address(pages);
memset(vmcs, 0, vmcs_config.size);
vmcs->revision_id = vmcs_config.revision_id; /* vmcs revision id */
return vmcs;
}
さらに、作成されたVMCSは必要に応じてレジスタの初期値などが設定されます。また、ファイルとして保存されていた仮想マシンを復元する場合、マイグレーション機能により別の環境から移ってきた仮想マシンの場合は、その論理プロセッサの状態を復元する必要もあるでしょう。
論理プロセッサの実行
プロセッサの初期化が終わったとして、Linux KVMがIntel VT-xの論理プロセッサを用いて仮想マシンを実行する部分について読み進めていきましょう。
論理プロセッサへのモード遷移を行うにあたっての最初の処理は、Linux KVMを含む“ Linux自体” のステートをシステムメモリ上に保存することです。保存されたステートは、仮想マシンから復帰した後に、プロセッサの状態を元に戻してLinux自体の実行を再開すために使われます。
セグメントレジスタなど一部のレジスタはIntel VT-xの機能を用いてステート保存しますが、それで全てではありません。Intel VT-xが保存しないものの、論理プロセッサから戻った後に必要なレジスタ値は、仮想マシンソフトウェア自身で保存しなければなりません。
論理プロセッサのステートは、VMCS内のほか、Linux KVMがvcpu->arch.regs[]で管理しています。論理プロセッサにモード遷移する前には、ここから論理プロセッサ上のレジスタをロードます。また、論理プロセッサから戻る際には、その時のレジスタ値で構造体を更新し、次の論理プロセッサへのモード遷移に備えます。
Intel VT-xを使った論理プロセッサへのモード遷移処理はvmx_vcpu_run()関数にあります。モード遷移の前後は、プロセッサのレジスタ値のレベルで細かくコントロール必要がありC言語でのプログラム記述が難しいことから、モード遷移前後の部分はインラインアセンブラにて記述されています。
arch/x86/kvm/vmx.c
static void vmx_vcpu_run(struct kvm_vcpu *vcpu)
{
struct vcpu_vmx *vmx = to_vmx(vcpu);
asm(
/* Store host registers */
"push %%"R"dx; push %%"R"bp;"
"push %%"R"cx \n\t"
"cmp %%"R"sp, %c[host_rsp](%0) \n\t"
"je 1f \n\t"
"mov %%"R"sp, %c[host_rsp](%0) \n\t"
__ex(ASM_VMX_VMWRITE_RSP_RDX) "\n\t"
"1: \n\t"
/* Reload cr2 if changed */
"mov %c[cr2](%0), %%"R"ax \n\t"
"mov %%cr2, %%"R"dx \n\t"
"cmp %%"R"ax, %%"R"dx \n\t"
"je 2f \n\t"
"mov %%"R"ax, %%cr2 \n\t"
"2: \n\t"
/* Check if vmlaunch of vmresume is needed */
"cmpl $0, %c[launched](%0) \n\t"
/* Load guest registers. Don't clobber flags. */
"mov %c[rax](%0), %%"R"ax \n\t"
"mov %c[rbx](%0), %%"R"bx \n\t"
"mov %c[rdx](%0), %%"R"dx \n\t"
"mov %c[rsi](%0), %%"R"si \n\t"
"mov %c[rdi](%0), %%"R"di \n\t"
"mov %c[rbp](%0), %%"R"bp \n\t"
#ifdef CONFIG_X86_64
"mov %c[r8](%0), %%r8 \n\t"
"mov %c[r9](%0), %%r9 \n\t"
"mov %c[r10](%0), %%r10 \n\t"
"mov %c[r11](%0), %%r11 \n\t"
"mov %c[r12](%0), %%r12 \n\t"
"mov %c[r13](%0), %%r13 \n\t"
"mov %c[r14](%0), %%r14 \n\t"
"mov %c[r15](%0), %%r15 \n\t"
#endif
"mov %c[rcx](%0), %%"R"cx \n\t" /* kills %0 (ecx) */
準備ができたらVMRESUME命令(初回のみVMLAUNCH命令)を実行し、論理プロセッサにモード遷移します。
/* Enter guest mode */
"jne .Llaunched \n\t"
__ex(ASM_VMX_VMLAUNCH) "\n\t"
"jmp .Lkvm_vmx_return \n\t"
".Llaunched: " __ex(ASM_VMX_VMRESUME) "\n\t"
何らかの理由により論理プロセッサの実行が終了すると、VMRESUME命令(VMLAUNCH命令)の次の命令から実行が再開されます。論理プロセッサから戻った後に行う最初の処理は、論理プロセッサのステート保存です。
".Lkvm_vmx_return: "
/* Save guest registers, load host registers, keep flags */
"xchg %0, (%%"R"sp) \n\t"
"mov %%"R"ax, %c[rax](%0) \n\t"
"mov %%"R"bx, %c[rbx](%0) \n\t"
"push"Q" (%%"R"sp); pop"Q" %c[rcx](%0) \n\t"
"mov %%"R"dx, %c[rdx](%0) \n\t"
"mov %%"R"si, %c[rsi](%0) \n\t"
"mov %%"R"di, %c[rdi](%0) \n\t"
"mov %%"R"bp, %c[rbp](%0) \n\t"
#ifdef CONFIG_X86_64
"mov %%r8, %c[r8](%0) \n\t"
"mov %%r9, %c[r9](%0) \n\t"
"mov %%r10, %c[r10](%0) \n\t"
"mov %%r11, %c[r11](%0) \n\t"
"mov %%r12, %c[r12](%0) \n\t"
"mov %%r13, %c[r13](%0) \n\t"
"mov %%r14, %c[r14](%0) \n\t"
"mov %%r15, %c[r15](%0) \n\t"
#endif
"mov %%cr2, %%"R"ax \n\t"
"mov %%"R"ax, %c[cr2](%0) \n\t"
"pop %%"R"bp; pop %%"R"bp; pop %%"R"dx \n\t"
"setbe %c[fail](%0) \n\t"
: : "c"(vmx), "d"((unsigned long)HOST_RSP),
[launched]"i"(offsetof(struct vcpu_vmx, launched)),
[fail]"i"(offsetof(struct vcpu_vmx, fail)),
[host_rsp]"i"(offsetof(struct vcpu_vmx, host_rsp)),
[rax]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_RAX])),
[rbx]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_RBX])),
[rcx]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_RCX])),
[rdx]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_RDX])),
[rsi]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_RSI])),
[rdi]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_RDI])),
[rbp]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_RBP])),
#ifdef CONFIG_X86_64
[r8]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_R8])),
[r9]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_R9])),
[r10]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_R10])),
[r11]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_R11])),
[r12]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_R12])),
[r13]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_R13])),
[r14]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_R14])),
[r15]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_R15])),
#endif
[cr2]"i"(offsetof(struct vcpu_vmx, vcpu.arch.cr2))
: "cc", "memory"
, R"ax", R"bx", R"di", R"si"
#ifdef CONFIG_X86_64
, "r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15"
#endif
);
論理プロセッサの実行を終えた後の処理として注目したいのが、プロセッサがVMCSに設定したVMEXITの原因コードをVMCSから読み取っている点です。この値は、後にLinux KVM内部、およびユーザモードプロセスであるqemu-kvmにより参照され、VMEXITの理由を判断して仮想マシンソフトウェアとしての処理(メモリの割り当てやデバイスI/Oのエミュレーション等)を進める際に使われます。
vmx->exit_reason = vmcs_read32(VM_EXIT_REASON);
vmx->exit_intr_info = vmcs_read32(VM_EXIT_INTR_INFO);
}
VMEXIT後のイベントディスパッチ
次に、vmx_handle_exit()関数を見て見ましょう。ここには、論理プロセッサから戻った後に、論理プロセッサが停止した理由をチェックし必要な処理を行うこと――すなわち、trap-and-emulate処理のうち、センシティブ命令をトラップし、論理プロセッサの停止理由に従って適切なイベントハンドラを呼び出す機能があります。
arch/x86/kvm/vmx.c
static int vmx_handle_exit(struct kvm_vcpu *vcpu)
{
struct vcpu_vmx *vmx = to_vmx(vcpu);
u32 exit_reason = vmx->exit_reason;
u32 vectoring_info = vmx->idt_vectoring_info;
if (exit_reason < kvm_vmx_max_exit_handlers
&& kvm_vmx_exit_handlers[exit_reason])
return kvm_vmx_exit_handlers[exit_reason](vcpu);
else {
vcpu->run->exit_reason = KVM_EXIT_UNKNOWN;
vcpu->run->hw.hardware_exit_reason = exit_reason;
}
return 0;
}
実際のイベントハンドラはkvm_vmx_exit_handlers[]テーブルに登録されており、VMCSから読み出したVMEXITの理由番号を使って、このテーブルからルックアップします。たとえば、EXIT_REASON_EXCEPTION_NMIが発生した場合には、本表に従って handle_exception(vcpu)が呼び出されることになります。
arch/x86/kvm/vmx.c
static int (*kvm_vmx_exit_handlers[])(struct kvm_vcpu *vcpu) = {
[EXIT_REASON_EXCEPTION_NMI] = handle_exception,
[EXIT_REASON_EXTERNAL_INTERRUPT] = handle_external_interrupt,
[EXIT_REASON_TRIPLE_FAULT] = handle_triple_fault,
[EXIT_REASON_NMI_WINDOW] = handle_nmi_window,
[EXIT_REASON_IO_INSTRUCTION] = handle_io,
[EXIT_REASON_CR_ACCESS] = handle_cr,
[EXIT_REASON_DR_ACCESS] = handle_dr,
[EXIT_REASON_CPUID] = handle_cpuid,
[EXIT_REASON_MSR_READ] = handle_rdmsr,
[EXIT_REASON_MSR_WRITE] = handle_wrmsr,
[EXIT_REASON_PENDING_INTERRUPT] = handle_interrupt_window,
[EXIT_REASON_HLT] = handle_halt,
[EXIT_REASON_INVD] = handle_invd,
[EXIT_REASON_INVLPG] = handle_invlpg,
};
VMEXITハンドラの例 ――RDMSR/WRMSRのエミュレーション
Intel VT-xでは、論理プロセッサの実行中にセンシティブ命令に遭遇するとVMEXITを起こします。仮想マシンソフトウェアは、VMEXITの理由を確認し、必要なエミュレーションをしなければなりません。
ここでは、センシティブ命令の一例として、仮想マシンソフトウェアでエミュレーションが必要となるRDMSR命令/WRMSR命令に遭遇した際、Linux KVMがどのように命令をエミュレーションしているかを見ておきましょう。
MSRの読み取り(handle_rdmsr)
RDMSR命令は、Machine Specific Register (MSR)の設定値を汎用レジスタにロードするためのリング0の命令です。あらかじめECXレジスタを読み取りたいMSRの番号を設定しておきRDMSR 命令を実行すると、MSRの上位32ビット分がEDX、下位32ビット分がEAXにコピーされる、というものです。
Intel VT-xの論理プロセッサ上でこの命令が実行された場合には、handle_rdmsr()関数が呼び出されます。この関数では、論理プロセッサのECX の値が示すMSRレジスタの値を、KVMのMSR管理ルーチンから読み出しまて、取得されたMSRレジスタの値を、論理プロセッサのEAX, EDXに書き戻し、「 次に実行すべき命令のアドレス」を指すインストラクションポインタ(IP)を、RDMSRの命令の長さだけ進める、という、命令エミュレーション処理を行っています。このため、次に論理プロセッサがが実行される際には、まるでRDMSRが実行された直後のような状態になっており、次の命令から実行されることになります。
arch/x86/kvm/vmx.c
static int handle_rdmsr(struct kvm_vcpu *vcpu)
{
u32 ecx = vcpu->arch.regs[VCPU_REGS_RCX];
u64 data;
if (vmx_get_msr(vcpu, ecx, &data)) {
trace_kvm_msr_read_ex(ecx);
kvm_inject_gp(vcpu, 0);
return 1;
}
trace_kvm_msr_read(ecx, data);
/* FIXME: handling of bits 32:63 of rax, rdx */
vcpu->arch.regs[VCPU_REGS_RAX] = data & -1u;
vcpu->arch.regs[VCPU_REGS_RDX] = (data >> 32) & -1u;
skip_emulated_instruction(vcpu);
return 1;
}
MSRの書き込み(handle_wrmsr)
handle_rdmsr()の仕組みを理解した後であれば、handle_wrmsr()関数もすぐに理解できるでしょう。こちらはECXの値が指すMSRに対してEDX:EAXの64ビット値を書き込み、次の命令から論理プロセッサの実行を再開するという仕組みです。
arch/x86/kvm/vmx.c
static int handle_wrmsr(struct kvm_vcpu *vcpu)
{
u32 ecx = vcpu->arch.regs[VCPU_REGS_RCX];
u64 data = (vcpu->arch.regs[VCPU_REGS_RAX] & -1u)
| ((u64)(vcpu->arch.regs[VCPU_REGS_RDX] & -1u) << 32);
if (vmx_set_msr(vcpu, ecx, data) != 0) {
trace_kvm_msr_write_ex(ecx, data);
kvm_inject_gp(vcpu, 0);
return 1;
}
trace_kvm_msr_write(ecx, data);
skip_emulated_instruction(vcpu);
return 1;
}
ここまではLinux KVMのソースコードを読んできましたが、次回はLinux KVMの理解を深めるために、Linux KVMの呼び出し元であるqemu-kvmのソースコードも読んでみることにします。お楽しみに。