KVMを理解りたい
KVMってなに
Linux Kernelに組み込まれた、Virtualizationの仕組み。
Linux KernelがHypervisorの一部として動作することになる。
(今の理解では、)KVMはDevice emulationはしないので、その辺りはQEMUやFirecrackerといったものにお願いすることになる。したがって、Host OSと同じCPU ArchなGuest OSを実行することができる。
kvmtool
KVMを利用した、軽量Hypervisorの実装。
つまり、QEMUやFirecrackerと同じことをしている。
まずは動かしてみる
使ってみるためには、以下のものが必要になる
- kvm tools
- Linux Kernel
- Rootfs
また、ここでは、x86_64 な環境を前提とする。
準備
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.136.tar.xz
tar xf linux-5.15.136.tar.xz
cd linux-5.15.136.tar.xz
make defconfig
make -j `nproc`
cd ../
git clone git://git.kernel.org/pub/scm/linux/kernel/git/will/kvmtool.git
cd kvmtool
make -j `nproc`
ARCH="$(uname -m)"
wget https://s3.amazonaws.com/spec.ccfc.min/firecracker-ci/v1.6/${ARCH}/ubuntu-22.04.ext4
実行
sudo ./lkvm run --disk ubuntu-22.04.ext4 --kernel ~/linux-6.1.59/arch/x86_64/boot/bzImage
Info: # lkvm run -k /home/isato/linux-6.1.59/arch/x86_64/boot/bzImage -m 448 -c 4 --name guest-143039
No EFI environment detected.
early console in extract_kernel
input_data: 0x0000000002b5740d
input_len: 0x0000000000ada20a
output: 0x0000000001000000
output_len: 0x00000000025ea420
kernel_total_size: 0x0000000002430000
needed_size: 0x0000000002600000
trampoline_32bit: 0x000000000009d000
Physical KASLR using RDRAND RDTSC...
Virtual KASLR using RDRAND RDTSC...
Decompressing Linux... Parsing ELF... Performing relocations... done.
Booting the kernel.
[ 0.000000] Linux version 6.1.59 (isato@archdev) (gcc (GCC) 13.2.1 20230801, GNU ld (GNU Binutils) 2.41.0) #1 SMP PREEMPT_DYNAMIC Sat Oct 21 01:37:03 JST 2023
[ 0.000000] Command line: noapic noacpi pci=conf1 reboot=k panic=1 i8042.direct=1 i8042.dumbkbd=1 i8042.nopnp=1 earlyprintk=serial i8042.noaux=1 console=ttyS0 root=/dev/vda rw
[ 0.000000] BIOS-provided physical RAM map:
[ 0.000000] BIOS-e820: [mem 0x0000000000000000-0x000000000009fbff] usable
[ 0.000000] BIOS-e820: [mem 0x000000000009fc00-0x000000000009ffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000000f0000-0x00000000000ffffe] reserved
[ 0.000000] BIOS-e820: [mem 0x0000000000100000-0x000000001bffffff] usable
[ 0.000000] printk: bootconsole [earlyser0] enabled
[ 0.000000] ERROR: earlyprintk= earlyser already used
[ 0.000000] NX (Execute Disable) protection: active
[ 0.000000] DMI not present or invalid.
[ 0.000000] Hypervisor detected: KVM
[ 0.000000] kvm-clock: Using msrs 4b564d01 and 4b564d00
[ 0.000001] kvm-clock: using sched offset of 310923831 cycles
[ 0.001866] clocksource: kvm-clock: mask: 0xffffffffffffffff max_cycles: 0x1cd42e4dffb, max_idle_ns: 881590591483 ns
[ 0.007791] tsc: Detected 3892.684 MHz processor
[ 0.009468] last_pfn = 0x1c000 max_arch_pfn = 0x400000000
[ 0.011565] Disabled
[ 0.012342] x86/PAT: MTRRs disabled, skipping PAT initialization too.
...
参考にしたもの
読んでみる
RUNを追う
run を引数に受け取ると、これを実行する
int kvm_cmd_run(int argc, const char **argv, const char *prefix)
{
int ret = -EFAULT;
struct kvm *kvm;
kvm = kvm_cmd_run_init(argc, argv);
if (IS_ERR(kvm))
return PTR_ERR(kvm);
ret = kvm_cmd_run_work(kvm);
kvm_cmd_run_exit(kvm, ret);
return ret;
}
init は大体 struct kvmを作ってる
static struct kvm *kvm_cmd_run_init(int argc, const char **argv)
{
static char default_name[20];
unsigned int nr_online_cpus;
struct kvm *kvm = kvm__new();
if (IS_ERR(kvm))
return kvm;
struct kvm {
struct kvm_arch arch;
struct kvm_config cfg;
int sys_fd; /* For system ioctls(), i.e. /dev/kvm */
int vm_fd; /* For VM ioctls() */
timer_t timerid; /* Posix timer for interrupts */
int nrcpus; /* Number of cpus to run */
struct kvm_cpu **cpus;
u32 mem_slots; /* for KVM_SET_USER_MEMORY_REGION */
u64 ram_size; /* Guest memory size, in bytes */
void *ram_start;
u64 ram_pagesize;
struct mutex mem_banks_lock;
struct list_head mem_banks;
bool nmi_disabled;
bool msix_needs_devid;
const char *vmlinux;
struct disk_image **disks;
int nr_disks;
int vm_state;
#ifdef KVM_BRLOCK_DEBUG
pthread_rwlock_t brlock_sem;
#endif
};
以下でkvm構造体で定義されているcpu数分、pthreadを作ってVMをbootさせる
static int kvm_cmd_run_work(struct kvm *kvm)
{
int i;
for (i = 0; i < kvm->nrcpus; i++) {
if (pthread_create(&kvm->cpus[i]->thread, NULL, kvm_cpu_thread, kvm->cpus[i]) != 0)
die("unable to create KVM VCPU thread");
}
/* Only VCPU #0 is going to exit by itself when shutting down */
if (pthread_join(kvm->cpus[0]->thread, NULL) != 0)
die("unable to join with vcpu 0");
return kvm_cpu__exit(kvm);
}
スレッドが実行される
argには自身のCPU番号の struct kvm_cpu が渡されている
static void *kvm_cpu_thread(void *arg)
{
char name[16];
current_kvm_cpu = arg;
sprintf(name, "kvm-vcpu-%lu", current_kvm_cpu->cpu_id);
kvm__set_thread_name(name);
if (kvm_cpu__start(current_kvm_cpu))
goto panic_kvm;
return (void *) (intptr_t) 0;
struct kvm_cpu {
pthread_t thread; /* VCPU thread */
unsigned long cpu_id;
struct kvm *kvm; /* parent KVM */
int vcpu_fd; /* For VCPU ioctls() */
struct kvm_run *kvm_run;
struct kvm_cpu_task *task;
struct kvm_regs regs;
struct kvm_sregs sregs;
struct kvm_fpu fpu;
struct kvm_msrs *msrs; /* dynamically allocated */
u8 is_running;
u8 paused;
u8 needs_nmi;
struct kvm_coalesced_mmio_ring *ring;
};
cpuが仕事をし始める
signalを設定し、while loopが始まる。
int kvm_cpu__start(struct kvm_cpu *cpu)
{
sigset_t sigset;
sigemptyset(&sigset);
sigaddset(&sigset, SIGALRM);
pthread_sigmask(SIG_BLOCK, &sigset, NULL);
signal(SIGKVMEXIT, kvm_cpu_signal_handler);
signal(SIGKVMPAUSE, kvm_cpu_signal_handler);
signal(SIGKVMTASK, kvm_cpu_signal_handler);
kvm_cpu__reset_vcpu(cpu);
if (cpu->kvm->cfg.single_step)
kvm_cpu__enable_singlestep(cpu);
while (cpu->is_running) {
...
}
while loopの大部分では、VM EXITが発生した時のケアをしている。
exitした時のreasonによって分岐し、それぞれ処理する。
どういった分岐があるかをCopilotに出してもらった。
- KVM_EXIT_UNKNOWN: 何もしない
- KVM_EXIT_DEBUG: レジスタとコードを表示する
- KVM_EXIT_IO: 入出力をエミュレートする
- KVM_EXIT_MMIO: メモリマップドI/Oをエミュレートする
- KVM_EXIT_INTR: CPUが実行中の場合は何もしない。そうでない場合は、KVMから出る
- KVM_EXIT_SHUTDOWN: KVMから出る
- KVM_EXIT_SYSTEM_EVENT: システムイベントのタイプを表示し、すべてのシステムイベントをシャットダウン要求として扱う。
割と単純ではある。
いつVM Enterしたか
よくあるIntel VT-x の解説だとVM実行前にVM Entryをすることで、Guest OSで動いているモードにCPUがなる。
ここからは、CPU命令的にどこでVMが動き始めるかを探してみる。
まずは、KernelのソースからVM Entry(vmlaunch命令)をしている部分を探して、kvmtoolまで浮上してみる。
Kernelは実際に動かしたlinux-5.15.136を見る。
vmlaunch命令
vmlaunch命令はアセンブリで実際に書かれている。
これは __vmx_vcpu_run 関数の中で実行される。
arch/x86/kvm/vmx/vmenter.S
YM_FUNC_START(__vmx_vcpu_run)
push %_ASM_BP
mov %_ASM_SP, %_ASM_BP
...
jz .Lvmlaunch
...
.Lvmlaunch:
vmlaunch
jmp .Lvmfail
この関数は
arch/x86/kvm/vmx/vmx.c
で呼ばれている。
static noinstr void vmx_vcpu_enter_exit(struct kvm_vcpu *vcpu,
struct vcpu_vmx *vmx,
unsigned long flags)
{
kvm_guest_enter_irqoff();
/* L1D Flush includes CPU buffer clear to mitigate MDS */
if (static_branch_unlikely(&vmx_l1d_should_flush))
vmx_l1d_flush(vcpu);
else if (static_branch_unlikely(&mds_user_clear))
mds_clear_cpu_buffers();
else if (static_branch_unlikely(&mmio_stale_data_clear) &&
kvm_arch_has_assigned_device(vcpu->kvm))
mds_clear_cpu_buffers();
vmx_disable_fb_clear(vmx);
if (vcpu->arch.cr2 != native_read_cr2())
native_write_cr2(vcpu->arch.cr2);
vmx->fail = __vmx_vcpu_run(vmx, (unsigned long *)&vcpu->arch.regs,
flags);
vcpu->arch.cr2 = native_read_cr2();
vmx_enable_fb_clear(vmx);
kvm_guest_exit_irqoff();
}
そしてこれは同じファイルの以下関数で呼ばれている。
static fastpath_t vmx_vcpu_run(struct kvm_vcpu *vcpu)
{
struct vcpu_vmx *vmx = to_vmx(vcpu);
unsigned long cr3, cr4;
...
/* The actual VMENTER/EXIT is in the .noinstr.text section. */
vmx_vcpu_enter_exit(vcpu, vmx, __vmx_vcpu_run_flags(vmx));
さて、ここで vmx_vcpu_run
関数がどこで呼ばれているかだが、これは少しややこしい。
直接は呼ばれておらず、この関数は構造体のメンバにされている。
これは、struct kvm_x86_ops という構造体が、vmx(Intelの仮想化支援)とsmv (AMDの仮想化支援)を抽象化しているためである。
static struct kvm_x86_ops vmx_x86_ops __initdata = {
...
.run = vmx_vcpu_run,
したがって、vmx_vcpu_run
は直接ではなく構造体を通して呼ばれることになる。
また、抽象化されたため、ここからはCPU依存ではない実装になってくる。