0%

【虚拟化】KVM

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

img

img

img

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

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

img

img

VM-exit

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

img

img

img

内存虚拟化

为了实现内存虚拟化,让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)。

img

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)物理页页帧号)

img

img

img

​ __direct_map 主要负责检查EPT表完整性,如果有页表项为初始化,则新建页表页并链接到页表项中。

img

​ 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

img

I/O虚拟化

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

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

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

Libvirt

在虚拟化中,libvirt充当着重要的角色,它提供了一个统一、稳定的Hypervisor应用框架,提供了对虚拟化各种对象(Hypervisor、主机、存储池、卷等)的管理,以及对多种不同的 Hypervisor 的支持,包括 Xen 驱动、QEMU 驱动、VMware 驱动等。∂

它的三大组成部分是:

  • API(应用程序接口)使得其他人可以开发出基于 libvirt 的虚拟机管理工具,例如virt-manager
  • libvirtd(守护进程)守护进程,接收并处理 API 请求
  • virsh(CLI,命令行工具,通常在linux直接敲virsh命令即可方便的操作虚拟机)

如下图所示,libvirt 通过libvird实现核心交互,它既支持本地操作,又支持远程操作(跨虚拟机的操作),它负责与QEMU通信,然后由 QEMU 驱动 KVM,从而实现libvirt 对于虚拟化层的调用。

  • 常见的virsh命令

  • libvirt的信息存储形式

    在host中,libvirt通过xml记录虚拟机信息

    如磁盘设备相关

img

网络设备相关

参考资料