KVM 是 Linux 下基于Intel VTAMD-V 等 CPU 硬件虚拟化技术 实现的虚拟化解决方案,全称为 Kernel-based Virtual Machine。KVM 由处于内核态KVM 模块kvm.ko用户态QEMU 两部分构成。内核模块kvm.ko实现了 CPU 和内存虚拟化 等决定关键性能和核心安全的功能,并向用户空间提供了使用这些功能的接口,QEMU 利用 KVM 模块提供的接口来实现设备模拟、I/O 虚拟化网络虚拟化等。单个虚拟机是宿主机上的一个普通 QEMU 进程,虚拟机中的 vCPUQEMU 的一个线程VM 的物理地址空间QEMU 的虚拟地址空间

技术背景

虚拟化概览 中介绍了 虚拟化准则,提出 VMM 应当满足的三个条件,并提出了两种类型的 VMM:

  • Resource Control,控制程序必须能够管理所有的系统资源。
  • Equivalence,在控制程序管理下运行的程序(包括操作系统),除时序和资源可用性之外的行为应该与没有控制程序时的完全一致,且预先编写的特权指令可以自由地执行。
  • Efficiency,绝大多数的客户机指令应该由主机硬件直接执行而无需控制程序的参与。

QEMU-KVM

KVM 是必须依赖硬件虚拟化技术辅助(例如 Intel VT-x、AMD-V)的 Hypervisor:

  • CPU:有 VMX rootnon-root 模式的支持,其运行效率是比较高的
  • 内存:有 Intel EPT/AMD NPT 的支持,内存虚拟化的效率也比较高
  • I/O:KVM 客户机的 I/O 操作需要VM-Exit到用户态由 QEMU 进行模拟
    • 传统的方式是使用纯软件的方式来模拟 I/O 设备,效率并不高
    • 现在应用最为广泛的 I/O 虚拟化是 virtio,可以参考 半虚拟化 I/O 框架 Virtio

作为一个HypervisorKVM 本身只关注虚拟机调度和内存管理 这两个方面,I/O 外设 的任务就交给了 Linux 内核QEMU

KVM 将整个虚拟化应用抽象成三层:

  • KVM 包含了一个内核加载模块kvm.ko,它只会负责提供vCPU以及对虚拟内存进行管理和调度
  • /dev/kvmKVM 内核模块提供给用户空间的一个接口,这个接口被qemu-kvm调用,通过ioctl系统调用就可以给用户提供一个工具,用以创建、删除、管理虚拟机
  • VM 运行期间,QEMU 会通过kvm.ko模块提供的系统调用进入内核,由 KVM 负责将虚拟机置于特殊模式运行
  • QEMU-KVM 是 KVM 团队通过修改 QEMU 代码而得出的专门用来创建和管理虚拟机的管理工具,为的是 KVM 能更好的和内核打交道

qemu-kvm就是通过open()close()ioctl()等方法去打开、关闭和调用这个接口,从而实现与 KVM 的互动

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
open("/dev/kvm")
ioctl(KVM_CREATE_VM)
ioctl(KVM_CREATE_VCPU)
for (;;) {
     ioctl(KVM_RUN)
     switch (exit_reason) {
     case KVM_EXIT_IO:  /* ... */
     case KVM_EXIT_HLT: /* ... */
     }
}

Guest 特点

  1. Guest作为一个普通进程运行于宿主机
  2. GuestCPU(vCPU)作为进程的线程存在,并受到宿主机内核的调度
  3. Guest继承了宿主机内核的一些属性,例如Huge Pages
  4. Guest磁盘 I/O网络 I/O 会受到宿主机设置的影响
  5. Guest通过宿主机上的虚拟网桥与外部相连

每一个虚拟机GuestHost上都被模拟为一个 QEMU 进程,即emulation进程。创建虚拟机后,使用virsh命令即可查看:

1
2
3
4
5
6
7
> virsh list --all
 Id    Name                           State
----------------------------------------------------
 1     kvm-01                         running

> ps aux | grep qemu
libvirt+ 20308 15.1  7.5 5023928 595884 ?      Sl   17:29   0:10 /usr/bin/qemu-system-x86_64 -name kvm-01 -S -machine pc-i440fx-wily,accel=kvm,usb=off -m 2048 -realtime mlock=off -smp 2 qemu ....

可以看到,此虚拟机就是一个普通的 Linux 进程,有自己的PID,并且有四个线程。线程数量不是固定的,但是至少会有三个(vCPU、I/O、Signal)。其中有两个是vCPU线程,一个I/O线程,还有一个Signal信号处理线程

1
2
3
4
5
6
> pstree -p 20308

qemu-system-x86(20308)-+-{qemu-system-x86}(20353)
                       |-{qemu-system-x86}(20408)
                       |-{qemu-system-x86}(20409)
                       |-{qemu-system-x86}(20412)

KVM 整体架构

CPU 虚拟化

虚拟化技术概览 中介绍了 CPU 虚拟化的三种技术方案,这里总结如下:

全虚拟化 半虚拟化 硬件辅助虚拟化
实现 动态二进制翻译、优先级压缩 HyperCall VMX 模式切换
兼容性 不修改内核,兼容性好 需要定制 GuestOS 内核,不支持 Windows,兼容性差 修改内核,兼容性好
性能 步骤繁琐,性能差 性能好 较好,接近半虚拟化
厂商 VMware、QEMU Xen KVM、VMware ESXi、Xen 3.0 等

在 KVM 中,使用的是硬件辅助虚拟化技术。

创建 vCPU

前文有提及 IOCTL 命令控制虚拟机,这里我们以 kvm_vm_ioctl 作为入口函数分析,看虚拟机如何创建 vCPU。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
static long kvm_vm_ioctl(struct file *filp,
			   unsigned int ioctl, unsigned long arg)
{
	struct kvm *kvm = filp->private_data;
	void __user *argp = (void __user *)arg;
	int r;

	if (kvm->mm != current->mm)
		return -EIO;
	switch (ioctl) {
	case KVM_CREATE_VCPU:
		r = kvm_vm_ioctl_create_vcpu(kvm, arg);
	case KVM_ENABLE_CAP:
		r = kvm_vm_ioctl_enable_cap_generic(kvm, &cap);
	case KVM_SET_USER_MEMORY_REGION:
		r = kvm_vm_ioctl_set_memory_region(kvm, &kvm_userspace_mem);
	case KVM_GET_DIRTY_LOG:
		r = kvm_vm_ioctl_get_dirty_log(kvm, &log);
#ifdef CONFIG_KVM_MMIO
	case KVM_REGISTER_COALESCED_MMIO:
		r = kvm_vm_ioctl_register_coalesced_mmio(kvm, &zone);
	case KVM_UNREGISTER_COALESCED_MMIO: {
		r = kvm_vm_ioctl_unregister_coalesced_mmio(kvm, &zone);
#endif
	case KVM_IRQFD:
		r = kvm_irqfd(kvm, &data);
	case KVM_IOEVENTFD:
		r = kvm_ioeventfd(kvm, &data);
#ifdef CONFIG_HAVE_KVM_MSI
	case KVM_SIGNAL_MSI:
		r = kvm_send_userspace_msi(kvm, &msi);
#endif
#ifdef __KVM_HAVE_IRQ_LINE
	case KVM_IRQ_LINE_STATUS:
	case KVM_IRQ_LINE:
		r = kvm_vm_ioctl_irq_line(kvm, &irq_event,
					ioctl == KVM_IRQ_LINE_STATUS);
#endif
	case KVM_CREATE_DEVICE: {
		r = kvm_ioctl_create_device(kvm, &cd);
	case KVM_CHECK_EXTENSION:
		r = kvm_vm_ioctl_check_extension_generic(kvm, arg);
	case KVM_RESET_DIRTY_RINGS:
		r = kvm_vm_ioctl_reset_dirty_pages(kvm);
	default:
		r = kvm_arch_vm_ioctl(filp, ioctl, arg);
	}
out:
	return r;
}

通过接受 ioctl 的 CPU 创建指令,执行 kvm_vm_ioctl_create_vcpu,紧接着,会先后执行 kvm_arch_vcpu_create 和 kvm_arch_vcpu_setup

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
static int kvm_vm_ioctl_create_vcpu(struct kvm *kvm, u32 id)
{
	int r;
	struct kvm_vcpu *vcpu;
	struct page *page;

	if (id >= KVM_MAX_VCPU_ID)
		return -EINVAL;

	mutex_lock(&kvm->lock);
	if (kvm->created_vcpus == KVM_MAX_VCPUS) {
		mutex_unlock(&kvm->lock);
		return -EINVAL;
	}

	kvm->created_vcpus++;
	mutex_unlock(&kvm->lock);

	r = kvm_arch_vcpu_precreate(kvm, id);
	if (r)
		goto vcpu_decrement;

	vcpu = kmem_cache_zalloc(kvm_vcpu_cache, GFP_KERNEL);
	if (!vcpu) {
		r = -ENOMEM;
		goto vcpu_decrement;
	}

	BUILD_BUG_ON(sizeof(struct kvm_run) > PAGE_SIZE);
	page = alloc_page(GFP_KERNEL_ACCOUNT | __GFP_ZERO);
	if (!page) {
		r = -ENOMEM;
		goto vcpu_free;
	}
	vcpu->run = page_address(page);

	kvm_vcpu_init(vcpu, kvm, id);

	r = kvm_arch_vcpu_create(vcpu);
	if (r)
		goto vcpu_free_run_page;

	if (kvm->dirty_ring_size) {
		r = kvm_dirty_ring_alloc(&vcpu->dirty_ring,
					 id, kvm->dirty_ring_size);
		if (r)
			goto arch_vcpu_destroy;
	}

	mutex_lock(&kvm->lock);
	if (kvm_get_vcpu_by_id(kvm, id)) {
		r = -EEXIST;
		goto unlock_vcpu_destroy;
	}

	vcpu->vcpu_idx = atomic_read(&kvm->online_vcpus);
	BUG_ON(kvm->vcpus[vcpu->vcpu_idx]);

	/* Now it's all set up, let userspace reach it */
	kvm_get_kvm(kvm);
	r = create_vcpu_fd(vcpu);
	if (r < 0) {
		kvm_put_kvm_no_destroy(kvm);
		goto unlock_vcpu_destroy;
	}

	kvm->vcpus[vcpu->vcpu_idx] = vcpu;

	/*
	 * Pairs with smp_rmb() in kvm_get_vcpu.  Write kvm->vcpus
	 * before kvm->online_vcpu's incremented value.
	 */
	smp_wmb();
	atomic_inc(&kvm->online_vcpus);

	mutex_unlock(&kvm->lock);
	kvm_arch_vcpu_postcreate(vcpu);
	kvm_create_vcpu_debugfs(vcpu);
	return r;

unlock_vcpu_destroy:
	mutex_unlock(&kvm->lock);
	kvm_dirty_ring_free(&vcpu->dirty_ring);
arch_vcpu_destroy:
	kvm_arch_vcpu_destroy(vcpu);
vcpu_free_run_page:
	free_page((unsigned long)vcpu->run);
vcpu_free:
	kmem_cache_free(kvm_vcpu_cache, vcpu);
vcpu_decrement:
	mutex_lock(&kvm->lock);
	kvm->created_vcpus--;
	mutex_unlock(&kvm->lock);
	return r;
}

kvm_arch_vcpu_create 主要负责:

  • 为结构体 kvm_vcpu 分配内存空间(这个数据结构表示一个 VCPU 描述符,所有对 VCPU 的操作都是对该数据结构相应字段的设置)
  • 对 vmcs 结构初始化(Virtual-Machine Control Structure, 该结构记录了 cpu 切换的上下文环境,帮助 cpu 在做 VM-entry 和 VM-exit 时保存信息)
  • 其他寄存器初始化
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
struct kvm_vcpu {
	struct kvm *kvm;

  /* ... */
	int cpu; /* host cpu id */
	int vcpu_id; /* id given by userspace at creation */
	int vcpu_idx; /* index in kvm->vcpus array */
	int srcu_idx;
	int mode;
	u64 requests;
	unsigned long guest_debug;

	int pre_pcpu;
	struct list_head blocked_vcpu_list;

	struct mutex mutex;
	struct kvm_run *run;

	struct rcuwait wait;
	struct pid __rcu *pid;
	int sigset_active;
	sigset_t sigset;
	struct kvm_vcpu_stat stat; // vcpu 状态
	unsigned int halt_poll_ns;
	bool valid_wakeup;

#ifdef CONFIG_HAS_IOMEM
	int mmio_needed;
	int mmio_read_completed;
	int mmio_is_write;
	int mmio_cur_fragment;
	int mmio_nr_fragments;
	struct kvm_mmio_fragment mmio_fragments[KVM_MAX_MMIO_FRAGMENTS];
#endif

#ifdef CONFIG_KVM_ASYNC_PF
	struct {
		u32 queued;
		struct list_head queue;
		struct list_head done;
		spinlock_t lock;
	} async_pf;
#endif

#ifdef CONFIG_HAVE_KVM_CPU_RELAX_INTERCEPT
	/*
	 * Cpu relax intercept or pause loop exit optimization
	 * in_spin_loop: set when a vcpu does a pause loop exit
	 *  or cpu relax intercepted.
	 * dy_eligible: indicates whether vcpu is eligible for directed yield.
	 */
	struct {
		bool in_spin_loop;
		bool dy_eligible;
	} spin_loop;
#endif
	bool preempted;
	bool ready;
	struct kvm_vcpu_arch arch;  // 架构信息
	struct kvm_dirty_ring dirty_ring;
};

kvm_arch_vcpu_setup 主要负责:

  • 为 vcpu 对应的 vmcs 绑定当前 cpu
  • 初始化虚拟 MMU 部件

VM-entry

当接受 ioctl 入口函数收到的虚拟机运行的指令时,会通过 kvm_arch_vcpu_ioctl_run->__vcpu_run->vcpu_enter_guest 的路径进入到 vcpu_enter_guest 函数。

在一系列操作后会执行 kvm_x86_ops->run,这个函数在初始化时,映射成了另外一个函数 vmx_vcpu_run,该函数做如下操作:

  • 记录进入 Guest 时间
  • 保存 host 寄存器信息
  • 加载 Guest 寄存器信息
  • 通过 VMLAUNCH 或 VMRESUME 进入 Guest(汇编指令)

VM-exit

在 vmx_vcpu_run 的函数中会因为接受到中断、异常或主动调用 VMCall 导致退出,vmx_complete_interrupts 函数即通过中断退出。vmx_complete_interrupts 函数中会对中断类型做判断,如软中断、硬中断、软件异常、硬件异常等做相应处理。

内存虚拟化

为了实现内存虚拟化,让Guest使用一个隔离的、从零开始且连续的内存空间,KVM 引入了一层新的地址空间,即客户机物理地址空间(Guest Physical Address,GPA)。这个地址空间并不是真正的物理地址空间,它只是Host虚拟地址空间在Guest地址空间的一个映射。对客户机来说,客户机物理地址空间都是从零开始的连续地址空间。但对宿主机来说,客户机的物理地址空间并不一定是连续的,客户机物理地址空间有可能映射在若干个不连续的宿主机地址区间,如下图所示:

客户机物理地址到宿主机虚拟地址的转换
客户机物理地址到宿主机虚拟地址的转换

为了将客户机物理地址转换成宿主机虚拟地址(Host Virtual Address,HVA),KVM 用一个kvm_memory_slot数据结构来记录每一个地址区间的映射关系,此数据结构包含了对应此映射区间的起始客户机页帧号(Guest Frame Number,GFN)映射的内存页数目以及起始宿主机虚拟地址,从而实现 GPA 到 HPA 的转换

实现内存虚拟化,最主要的是实现客户机虚拟地址GVA到宿主机物理地址HPA之间的转换。如果通过之前提到的两步映射的方式,客户机的每次内存访问都需要 KVM 介入,并由软件进行多次地址转换,其效率是非常低的

因此,为了提高GVAHPA的转换效率,KVM 提供了两种实现方式来进行GVAHPA之间的直接转换:

  • 影子页表(Shadow Page Table)
  • 基于硬件对虚拟化的支持:EPT

虚拟化技术概览 中可以看到对这两种技术的详细介绍。

EPT 的入口处理函数是 handle_ept_violation,该函数会依次调用 kvm_mmu_page_fault-»vcpu->arch.mmu.page_fault(该函数实际是执行 tdp_page_fault)。

tdp_page_fault 函数主要做几件事情:

  • 为核心结构体 kvm_mmu_page 等申请内存(kvm_mmu_page 表示一个 EPT 中的页表页)
  • 如果没有 kvm_memory_slot 映射 pfn,则申请空闲 kvm_memory_slot(kvm_memory_slot,客户机可根据页表实现 GVA 到 GPA 转换,再通过 kvm_memory_slot 实现 GPA 转换成 HVA,可根据宿主机的页表实现 GPA 转 HPA,进而完成 GVA 到 HPA 转换)
  • 做 EPT 缺页处理,建立 gfn 和 pfn 映射关系(gfn (Guest Frame Number)起始客户机页帧号,pfn(Page Frame Number)物理页页帧号)

EPT 页表页结构如下:

  • EPT 的页表结构分为四层(PML4(level-4)、PDPT(level-3)、PD(level-2)、PT(level))
  • EPT Pointer (EPTP)指向 PML4 的首地址
  • gpa 到 hpa:gpa 通过四级页表的寻址,得到相应的 pfn,然后加上 gpa 最后 12 位的 offset,得到 hpa
  • 物理页(physical page)是真正存放数据的页,页表页(MMU page)是存放 EPT 页表的页
  • 每个页表页(MMU page)对应一个数据结构 kvm_mmu_page

I/O 虚拟化

I/O 虚拟化现在主流三种方案如下,在 虚拟化技术概览 中可以看到对这两种技术的详细介绍。

  • 全虚拟化:完全由 QEMU 软件模拟
  • 半虚拟化:借助 virtio 实现
  • PCI pass-through:直接通过 PCI 直连设备

Guest虽然作为一个进程存在,但其内核的所有驱动都依然存在。只是硬件设备由 QEMU 模拟。Guest的所有硬件操作都会由 QEMU 来接管QEMU 负责与真实的宿主机硬件打交道