0%

Linux 虚拟化 KVM-Qemu 分析(4)之 CPU 虚拟化(2).md

背景

  • Read the fucking source code! –By 鲁迅
  • A picture is worth a thousand words. –By 高尔基

说明:

  1. KVM 版本:5.9.1
  2. QEMU 版本:5.0.0
  3. 工具:Source Insight 3.5, Visio
  4. 文章同步在博客园:https://www.cnblogs.com/LoyenWang/

1. 概述

  • 本文围绕 ARMv8 CPU 的虚拟化展开;
  • 本文会结合 Qemu + KVM 的代码分析,捋清楚上层到底层的脉络;
  • 本文会提供一个 Sample Code,用于类比 Qemu 和 KVM 的关系,总而言之,大同小异,大题小做,大道至简,大功告成,大恩不言谢;

先来两段前戏。

1.1 CPU 工作原理

AI 的世界,程序的执行不再冰冷,CPU 对a.out说,hello啊,world已经ok啦,下来return吧!

既然要说 CPU 的虚拟化,那就先简要介绍一下 CPU 的工作原理:

  • CPU 的根本任务是执行指令,我们常说的取指-译码-执行-访存-写回,就是典型的指令 Pipeline 操作;
  • 从 CPU 的功能出发,可以简要分成三个逻辑模块:
    1. Control Unit:CPU 的指挥中心,协调数据的移动;
    2. ALU:运算单元,执行 CPU 内部所有的计算;
    3. Register:寄存器和Cache,都算是 CPU 内部的存储单元,其中寄存器可用于存储需要被译码和执行的指令、数据、地址等;
  • CPU 从内存中读取指令进行译码并执行,执行的过程中需要去访问内存中的数据,CPU 内部的寄存器可以暂存中间的指令和数据等信息,通常说的 CPU 的context指的就是 CPU 寄存器值;

在硬件支持虚拟化之前,Qemu 纯软件虚拟化方案,是通过tcg(tiny code generator)的方式来进行指令翻译,翻译成 Host 处理器架构的指令来执行。硬件虚拟化技术,是让虚拟机能直接执行在 Host CPU 上,让 Host CPU 直接来执行虚拟机,结合 CPU 的实际工作原理,应该怎么来理解呢?来张图:

  • CPU 通过pc寄存器获取下一条执行指令,进行取指译码执行等操作,因此给定 CPU 一个 Context,自然就能控制其执行某些代码;
  • CPU 的虚拟化,最终目标让虚拟机执行在 CPU 上,无非也是要进行 CPU 的 Context 切换,控制 CPU 去执行对应的代码,下文会进一步阐述;

既然都讲 CPU 了,那就捎带介绍下 ARMv8 的寄存器吧:

  1. 通用寄存器:

  • 图中描述的是EL3以下,AArch32AArch64寄存器对应关系;
  • AArch64中,总共 31 个通用寄存器,64bit 的为 X0-X30,32bit 的为 W0-W30;
  1. 特殊用途寄存器:

  • 这些特殊用途的寄存器,主要分为三种:1)存放异常返回地址的ELR_ELx;2)各个 EL 的栈指针SP_ELx;3)CPU 的状态相关寄存器;
  1. CPU 的状态PSTATE

  • CPU 的状态在AArch32时是通过CPSR来获取,在AArch64中,使用PSTATEPSTATE不是一个寄存器,它表示的是保存当前 CPU 状态信息的一组寄存器或一些标志信息的统称;

好了,ARMv8 的介绍该打住了,否则要跑偏了。。。

1.2 guest 模式

  • Linux 系统有两种执行模式:kernel 模式与 user 模式,为了支持虚拟化功能的 CPU,KVM 向 Linux 内核提供了 guest 模式,用于执行虚拟机系统非 I/O 的代码;
  • user 模式,对应的是用户态执行,Qemu 程序就执行在 user 模式下,并循环监听是否有 I/O 需要模拟处理;
  • kernel 模式,运行 kvm 模块代码,负责将 CPU 切换到 VM 的执行,其中包含了上下文的 load/restore;
  • guest 模式,本地运行 VM 的非 I/O 代码,在某些异常情况下会退出该模式,Host OS 开始接管;

好了啦,前戏结束,开始直奔主题吧。

2. 流程分析

不管你说啥,我上来就是一句中国万岁,对不起,跑题了。我上来就是一张 Qemu 初始化流程图:

  • 看过 Qemu 源代码的人可能都有种感觉,一开始看好像摸不到门框,这图简要画了下关键模块的流程;
  • Qemu 的源代码,后续的文章会详细介绍,本文只 focus 在vcpu相关部分;

除了找到了qemu_init_vcpu的入口,这张图好像跟本文的 vcpu 的虚拟化关系不是很大,不管了,就算是给后续的 Qemu 分析打个广告吧。

2.1 vcpu 的创建

2.1.1 qemu 中 vcpu 创建

  • Qemu 初始化流程图中,找到了qemu_init_vcpu的入口,顺着这个qemu_init_vcpu就能找到与底层 KVM 模块交互的过程;
  • Qemu 中为每个 vcpu 创建了一个线程,操作设备节点来创建和初始化 vcpu;

所以,接力棒甩到了 KVM 内核模块。

2.1.2 kvm 中 vcpu 创建

来一张前文的图:

  • 前文中分析过,系统在初始化的时候会注册字符设备驱动,设置好了各类操作函数集,等待用户层的ioctl来进行控制;
  • Qemu中设置KVM_CREATE_VCPU,将触发kvm_vm_ioctl_create_vcpu的执行,完成 vcpu 的创建工作;

  • 在底层中进行 vcpu 的创建工作,主要是分配一个kvm_vcpu结构,并且对该结构中的字段进行初始化;
  • 其中有一个用于与应用层进行通信的数据结构struct kvm_run,分配一页内存,应用层会调用 mmap 来进行映射,并且会从该结构中获取到虚拟机的退出原因;
  • kvm_arch_vcpu_create主要完成体系架构相关的初始化,包括 timer,pmu,vgic 等;
  • create_hyp_mappingskvm_vcpu结构体建立映射,以便在Hypervisor模式下能访问该结构;
  • create_vcpu_fd注册了kvm_vcpu_fops操作函数集,针对 vcpu 进行操作,Qemu中设置KVM_ARM_VCPU_INIT,将触发kvm_arch_vcpu_ioctl_vcpu_init的执行,完成的工作主要是 vcpu 的核心寄存器,系统寄存器等的 reset 操作,此外还包含了上层设置下来的值,放置在struct kvm_vcpu_init中;

2.2 vcpu 的执行

2.2.1 qemu 中 vcpu 的执行

  • Qemu中为每一个 vcpu 创建一个用户线程,完成了 vcpu 的初始化后,便进入了 vcpu 的运行,而这是通过kvm_cpu_exec函数来完成的;
  • kvm_cpu_exec函数中,调用kvm_vcpu_ioctl(,KVM_RUN,)来让底层的物理 CPU 进行运行,并且监测 VM 的退出,而这个退出原因就是存在放在kvm_run->exit_reason中,也就是上文中提到过的应用层与底层交互的机制;

2.2.2 kvm 中 vcpu 的执行

用户层通过KVM_RUN命令,将触发 KVM 模块中kvm_arch_vcpu_ioctl_run函数的执行:

  • vcpu 最终是要放置在物理 CPU 上执行的,很显然,我们需要进行 context 的切换:保存好 Host 的 Context,并切换到 Guest 的 Context 去执行,最终在退出时再恢复回 Host 的 Context;
  • __guest_enter函数完成最终的 context 切换,进入 Guest 的执行,当 Guest 退出时,fixup_guest_exit将会处理exit_code,判断是否继续返回 Guest 执行;
  • 当最终 Guest 退出到 Host 时,Host 调用handle_exit来处理异常退出,根据kvm_get_exit_handler去查询异常处理函数表对应的处理函数,最终进行执行处理;

3. Sample Code

  • 上文已经将 Qemu+KVM 的 CPU 的虚拟化大概的轮廓已经介绍了,方方面面,问题不大;
  • 来一段 Sample Code 类比 Qemu 和 KVM 的关系,在 Ubuntu16.04 系统上进行测试;

简要介绍一下:

  1. tiny_kernel.S,相当于 Qemu 中运行的 Guest OS,完成的功能很简单,没错,就是Hello, world打印;
  2. tiny_qemu.c,相当于 Qemu,用于加载 Guest 到 vCPU 上运行,最终通过 kvm 放到物理 CPU 上运行;

鲁迅在 1921 年的时候,说过这么一句话:Talk is cheap, show me the code

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
- tiny_kernel.S:

start:
/* Hello */
mov $0x48, %al
outb %al, $0xf1
mov $0x65, %al
outb %al, $0xf1
mov $0x6c, %al
outb %al, $0xf1
mov $0x6c, %al
outb %al, $0xf1
mov $0x6f, %al
outb %al, $0xf1
mov $0x2c, %al
outb %al, $0xf1

/* world */
mov $0x77, %al
outb %al, $0xf1
mov $0x6f, %al
outb %al, $0xf1
mov $0x72, %al
outb %al, $0xf1
mov $0x6c, %al
outb %al, $0xf1
mov $0x64, %al
outb %al, $0xf1

mov $0x0a, %al
outb %al, $0xf1

hlt

- `tiny_qemu.c`:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/kvm.h>
#include <sys/mman.h>

#define KVM_DEV "/dev/kvm"
#define TINY_KERNEL_FILE "./tiny_kernel.bin"
#define PAGE_SIZE 0x1000

int main(void)
{
int kvm_fd;
int vm_fd;
int vcpu_fd;
int tiny_kernel_fd;
int ret;
int mmap_size;

struct kvm_sregs sregs;
struct kvm_regs regs;
struct kvm_userspace_memory_region mem;
struct kvm_run *kvm_run;
void *userspace_addr;

/* open kvm device */
kvm_fd = open(KVM_DEV, O_RDWR);
assert(kvm_fd > 0);

/* create VM */
vm_fd = ioctl(kvm_fd, KVM_CREATE_VM, 0);
assert(vm_fd >= 0);

/* create VCPU */
vcpu_fd = ioctl(vm_fd, KVM_CREATE_VCPU, 0);
assert(vcpu_fd >= 0);

/* open tiny_kernel binary file */
tiny_kernel_fd = open(TINY_KERNEL_FILE, O_RDONLY);
assert(tiny_kernel_fd > 0);
/* map 4K into memory */
userspace_addr = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
assert(userspace_addr > 0);
/* read tiny_kernel binary into the memory */
ret = read(tiny_kernel_fd, userspace_addr, PAGE_SIZE);
assert(ret >= 0);

/* set user memory region */
mem.slot = 0;
mem.flags = 0;
mem.guest_phys_addr = 0;
mem.memory_size = PAGE_SIZE;
mem.userspace_addr = (unsigned long)userspace_addr;
ret = ioctl(vm_fd, KVM_SET_USER_MEMORY_REGION, &mem);
assert(ret >= 0);

/* get kvm_run */
mmap_size = ioctl(kvm_fd, KVM_GET_VCPU_MMAP_SIZE, NULL);
assert(mmap_size >= 0);
kvm_run = (struct kvm_run *)mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpu_fd, 0);
assert(kvm_run >= 0);

/* set cpu registers */
ret = ioctl(vcpu_fd, KVM_GET_SREGS, &sregs);
assert(ret >= 0);
sregs.cs.base = 0;
sregs.cs.selector = 0;
ret = ioctl(vcpu_fd, KVM_SET_SREGS, &sregs);
memset(®s, 0, sizeof(struct kvm_regs));
regs.rip = 0;
ret = ioctl(vcpu_fd, KVM_SET_REGS, ®s);
assert(ret >= 0);

/* vcpu run */
while (1) {
ret = ioctl(vcpu_fd, KVM_RUN, NULL);
assert(ret >= 0);

switch(kvm_run->exit_reason) {
case KVM_EXIT_HLT:
printf("----KVM EXIT HLT----\n");
close(kvm_fd);
close(tiny_kernel_fd);
return 0;
case KVM_EXIT_IO:
putchar(*(((char *)kvm_run) + kvm_run->io.data_offset));
break;
default:
printf("Unknow exit reason: %d\n", kvm_run->exit_reason);
break;
}
}

return 0;
}

为了表明我没有骗人,上一张在 Ubuntu16.04 的虚拟机上运行的结果图吧: