1.KVM模型结构
为什么有OS虚拟化?随着CPU计算能力的提高,单独的OS已不能充分利用CPU的计算能力,1.很多应用的执行需要单独占用一个OS环境,如安全测试等;2.而IAAS云计算厂商也是以OS为范围销售计算能力。那么在所有虚拟化方案中,都是由hypervisor取代原生的OS去控制具体硬件资源,而同时hypervisor将资源分配具体的VM,VM中运行的是没有修改过的OS,如果让VM中的OS能正常运行,hypervisor的任务就是模拟具体的硬件资源,让OS不能识别出是真是假。
当然上面的模型是Xen示例,OS对应用而言是硬件资源管理中心,那么hypervisor就是具体VM的OS了,KVM是就利用了这一点,利用现有的kernel代码,构建了一个hypervisor,这个样子内存分配,进程调度等就无需重写代码,如此hypervisor就是所谓的host,VM中的OS就是guest。
guest OS保证具体运行场景中的程序正常执行,而KVM的代码则部署在HOST上,Userspace对应的是QEMU,Kernel对应的是KVM Driver,KVM Driver负责模拟虚拟机的CPU运行,内存管理,设备管理等;QEMU则模拟虚拟机的IO设备接口以及用户态控制接口。QEMU通过KVM等fd进行IOCTL控制KVM驱动的运行过程。
如上图所示,guest自身有自己的用户模式和内核模式;guest是在host中是作为一个用户态进程存在的,这个进程就是qemu,qemu本身就是一个虚拟化程序,只是纯软件虚拟化效率很低,它被KVM进行改造后,作为KVM的前端存在,用来进行创建进程或者IO交互等;而KVM Driver则是Linux内核模式,它提供KVM fd给qemu调用,用来进行cpu虚拟化,内存虚拟化等。QEMU通KVM提供的fd接口,通过ioctl系统调用创建和运行虚拟机。KVM Driver使得整个Linux成为一个虚拟机监控器,负责接收qemu模拟效率很低的命令。
2.KVM工作原理
上图是一个执行过程图,首先启动一个虚拟化管理软件qemu,开始启动一个虚拟机,通过ioctl等系统调用向内核中申请指定的资源,搭建好虚拟环境,启动虚拟机内的OS,执行 VMLAUCH 指令,即进入了guest代码执行过程。如果 Guest OS 发生外部中断或者影子页表缺页之类的事件,暂停 Guest OS 的执行,退出QEMU即guest VM-exit,进行一些必要的处理,然后重新进入客户模式,执行guest代码;这个时候如果是io请求,则提交给用户态下的qemu处理,qemu处理后再次通过IOCTL反馈给KVM驱动。
3.CPU虚拟化
X86体系结构CPU虚拟化技术的称为 Intel VT-x 技术,引入了VMX,提供了两种处理器的工作环境。 VMCS 结构实现两种环境之间的切换。 VM Entry 使虚拟机进去guest模式,VM Exit 使虚拟机退出guest模式。
VMM调度guest执行时,qemu 通过 ioctl 系统调用进入内核模式,在 KVM Driver中获得当前物理 CPU的引用。之后将guest状态从VMCS中读出, 并装入物理CPU中。执行 VMLAUCH 指令使得物理处理器进入非根操作环境,运行guest OS代码。
当 guest OS 执行一些特权指令或者外部事件时, 比如I/O访问,对控制寄存器的操作,MSR的读写等, 都会导致物理CPU发生 VMExit, 停止运行 Guest OS,将 Guest OS保存到VMCS中, Host 状态装入物理处理器中, 处理器进入根操作环境,KVM取得控制权,通过读取 VMCS 中 VM_EXIT_REASON 字段得到引起 VM Exit 的原因。 从而调用kvm_exit_handler 处理函数。 如果由于 I/O 获得信号到达,则退出到userspace模式的 Qemu 处理。处理完毕后,重新进入guest模式运行虚拟 CPU。
4.Mem虚拟化
OS对于物理内存主要有两点认识:1.物理地址从0开始;2.内存地址是连续的。VMM接管了所有内存,但guest OS的对内存的使用就存在这两点冲突了,除此之外,一个guest对内存的操作很有可能影响到另外一个guest乃至host的运行。VMM的内存虚拟化就要解决这些问题。
在OS代码中,应用也是占用所有的逻辑地址,同时不影响其他应用的关键点在于有线性地址这个中间层;解决方法则是添加了一个中间层:guest物理地址空间;guest看到是从0开始的guest物理地址空间(类比从0开始的线性地址),而且是连续的,虽然有些地址没有映射;同时guest物理地址映射到不同的host逻辑地址,如此保证了VM之间的安全性要求。
这样MEM虚拟化就是GVA->GPA->HPA的寻址过程,传统软件方法有影子页表,硬件虚拟化提供了EPT支持。
总体描述到此,后面代码里面见真相。
前段时间挖了一个坑,KVM源代码分析1:基本工作原理,准备写一下kvm的代码机制,结果一直没时间填土,现在还一下旧账,争取能温故而知新。 基本原理里面提到kvm虚拟化由用户态程序Qemu和内核态驱动kvm配合完成,qemu负责HOST用户态层面进程管理,IO处理等,KVM负责把qemu的部分指令在硬件上直接实现,从虚拟机的创建和运行上看,qemu的代码占了流程上的主要部分。下面的代码主要主要针对与qemu,KVM部分另外开篇再说。
代码:
QEMU:git://git.qemu.org/qemu.git v2.4.0
KVM:https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git v4.2
QEMU和KVM是通过IOCTL进行配合的,直接抓住这个线看有kvm_ioctl、kvm_vm_ioctl、kvm_vcpu_ioctl、kvm_device_ioctl等,他们还都在一个C文件里面。
使用kvm_ioctl很少了,直接看调用的代码,有KVM_GET_VCPU_MMAP_SIZE,KVM_CHECK_EXTENSION,KVM_GET_API_VERSION,KVM_CREATE_VM,KVM_GET_SUPPORTED_CPUID等等,需要记住只有KVM_CREATE_VM。
而调用kvm_vm_ioctl的函数真是海了去了,需要看的是KVM_SET_USER_MEMORY_REGION,KVM_CREATE_VCPU,KVM_CREATE_DEVICE。
所有寄存器的交换信息都是通过kvm_vcpu_ioctl,需要记住的操作只有,KVM_RUN。
所有看QEMU和KVM的配合流程如下:
接下来参考上图分析qemu代码流程: 从vl.c代码的main函数开始。 atexit(qemu_run_exit_notifiers)注册了qemu的退出处理函数,后面在具体看qemu_run_exit_notifiers函数。 module_call_init则开始初始化qemu的各个模块,陆陆续续的有以下参数:
typedef enum { MODULE_INIT_BLOCK, MODULE_INIT_MACHINE, MODULE_INIT_QAPI, MODULE_INIT_QOM, MODULE_INIT_MAX } module_init_type;
|
typedef
enum
{
MODULE_INIT_BLOCK
,
MODULE_INIT_MACHINE
,
MODULE_INIT_QAPI
,
MODULE_INIT_QOM
,
MODULE_INIT
_MAX
}
module_init_type
;
|
最开始初始化的MODULE_INIT_QOM,QOM是qemu实现的一种模拟设备,具体可以参考http://wiki.qemu.org/Features/QOM,代码下面的不远处就MODULE_INIT_MACHINE的初始化,这两条语句放到一起看,直接说一下module_call_init的机制。 module_call_init实际设计的一个函数链表,ModuleTypeList ,链表关系如下图
它把相关的函数注册到对应的数组链表上,通过执行init项目完成所有设备的初始化。module_call_init就是执行e->init()完成功能的,而e->init是什么时候通过register_module_init注册到ModuleTypeList上的ModuleEntry,是module_init注册的,而调用module_init的有
#define block_init(function) module_init(function, MODULE_INIT_BLOCK) #define machine_init(function) module_init(function, MODULE_INIT_MACHINE) #define qapi_init(function) module_init(function, MODULE_INIT_QAPI) #define type_init(function) module_init(function, MODULE_INIT_QOM)
|
#define block_init(function) module_init(function, MODULE_INIT_BLOCK)
#define machine_init(function) module_init(function, MODULE_INIT_MACHINE)
#define qapi_init(function) module_init(function, MODULE_INIT_QAPI)
#define type_init(function) module_init(function, MODULE_INIT_QOM)
|
那么执行machine_init则是挂到了MODULE_INIT_MACHINE,type_init则将函数挂载了MODULE_INIT_QOM。那么排查一下是,我们只关注PC的注册,那么就是machine_init(pc_machine_init_##suffix),源自DEFINE_PC_MACHINE(suffix, namestr, initfn, optsfn)宏,而DEFINE_I440FX_MACHINE有
#define DEFINE_I440FX_MACHINE(suffix, name, compatfn, optionfn) static void pc_init_##suffix(MachineState *machine) { void (*compat)(MachineState *m) = (compatfn); if (compat) { compat(machine); } pc_init1(machine); } DEFINE_PC_MACHINE(suffix, name, pc_init_##suffix, optionfn) #define DEFINE_PC_MACHINE(suffix, namestr, initfn, optsfn) static void pc_machine_##suffix##_class_init(ObjectClass *oc, void *data) { MachineClass *mc = MACHINE_CLASS(oc); optsfn(mc); mc->name = namestr; mc->init = initfn; } static const TypeInfo pc_machine_type_##suffix = { .name = namestr TYPE_MACHINE_SUFFIX, .parent = TYPE_PC_MACHINE, .class_init = pc_machine_##suffix##_class_init, }; static void pc_machine_init_##suffix(void) { type_register(&pc_machine_type_##suffix); } machine_init(pc_machine_init_##suffix)
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
|
#define DEFINE_I440FX_MACHINE(suffix, name, compatfn, optionfn)
static
void
pc_init_
##suffix(MachineState *machine)
{
void
(
*
compat
)
(
MachineState
*
m
)
=
(
compatfn
)
;
if
(
compat
)
{
compat
(
machine
)
;
}
pc_init1
(
machine
)
;
}
DEFINE_PC_MACHINE
(
suffix
,
name
,
pc_init_
##suffix, optionfn)
#define DEFINE_PC_MACHINE(suffix, namestr, initfn, optsfn)
static
void
pc_machine_
##suffix##_class_init(ObjectClass *oc, void *data)
{
MachineClass
*
mc
=
MACHINE_CLASS
(
oc
)
;
optsfn
(
mc
)
;
mc
->
name
=
namestr
;
mc
->
init
=
initfn
;
}
static
const
TypeInfo
pc_machine_type_
##suffix = {
.
name
=
namestr
TYPE_MACHINE_SUFFIX
,
.
parent
=
TYPE_PC_MACHINE
,
.
class_init
=
pc_machine_
##suffix##_class_init,
}
;
static
void
pc_machine_init_
##suffix(void)
{
type_register
(
&
pc_machine_type_
##suffix);
}
machine_init
(
pc_machine_init_
##suffix)
|
DEFINE_PC_MACHINE注册的函数pc_init_##suffix在DEFINE_I440FX_MACHINE中定义,怎么组合都无关,pc_init1(machine)函数一定要执行,本质就是pc_init1赋值给了mc->init,其他爱看不看吧。
而module_init的宏是
#define module_init(function, type) static void __attribute__((constructor)) do_qemu_init_ ## function(void) { register_dso_module_init(function, type); } #else /* This should not be used directly. Use block_init etc. instead. */ #define module_init(function, type) static void __attribute__((constructor)) do_qemu_init_ ## function(void) { register_module_init(function, type); }
1
2
3
4
5
6
7
8
9
10
11
12
|
#define module_init(function, type)
static
void
__attribute__
(
(
constructor
)
)
do_qemu_init_
## function(void)
{
register_dso_module_init
(
function
,
type
)
;
}
#else
/* This should not be used directly. Use block_init etc. instead. */
#define module_init(function, type)
static
void
__attribute__
(
(
constructor
)
)
do_qemu_init_
## function(void)
{
register_module_init
(
function
,
type
)
;
}
|
它前面的修饰是__attribute__((constructor)),这个导致machine_init或者type_init等会在main()之前就被执行。所有type_init(kvm_type_init)-> kvm_accel_type -> kvm_accel_class_init -> kvm_init依次完成了函数注册,所有说module_call_init(MODULE_INIT_QOM)函数已经完成了kvm_init的执行,所有这样就清楚KVM调用关系了。
如此就先去看kvm_init函数,前面主要干了一件事,填充KVMState *s结构体,然后通过kvm_ioctl(s, KVM_GET_API_VERSION, 0)判断内核KVM驱动和当前QEMU版本是否兼容,下面则是执行kvm_ioctl(s, KVM_CREATE_VM, type)进行虚拟机的创建活动,创建了KVM虚拟机,获取虚拟机句柄。具体KVM_CREATE_VM在内核态做了什么,ioctl的工作等另外再说,现在假定KVM_CREATE_VM所代表的虚拟机创建成功,下面通过检查kvm_check_extension结果填充KVMState,kvm_arch_init初始化KVMState,其中有IDENTITY_MAP_ADDR,TSS_ADDR,NR_MMU_PAGES等,cpu_register_phys_memory_client注册qemu对内存管理的函数集,kvm_create_irqchip创建kvm中断管理内容,通过kvm_vm_ioctl(s, KVM_CREATE_IRQCHIP)实现,具体内核态的工作内容后面分析。到此kvm_init的工作就完成了,最主要的工作就是创建的虚拟机。
这样绕了这么大圈,重新回到vl.c上面来,前面刚说了module_call_init(MODULE_INIT_MACHINE)本质就是把pc_init1赋值给了mc->init,然后machine_class = find_default_machine(),如此可以看到machine_class的init函数一定会执行pc_init1。
下面涉及对OPT入参的解析过程略过不提。 qemu准备模拟的机器的类型从下面语句获得:
current_machine = MACHINE(object_new(object_class_get_name( OBJECT_CLASS(machine_class))));
|
current_machine
=
MACHINE
(
object_new
(
object_class_get_name
(
OBJECT_CLASS
(
machine_class
)
)
)
)
;
|
machine_class则是通过入参传入的
case QEMU_OPTION_machine: olist = qemu_find_opts("machine"); opts = qemu_opts_parse_noisily(olist, optarg, true); if (!opts) { exit(1); } break;
|
case
QEMU_OPTION_machine
:
olist
=
qemu_find_opts
(
"machine"
)
;
opts
=
qemu_opts_parse_noisily
(
olist
,
optarg
,
true
)
;
if
(
!
opts
)
{
exit
(
1
)
;
}
break
;
|
man qemu
-machine [type=]name[,prop=value[,...]] Select the emulated machine by name. Use "-machine help" to list available machines
|
-
machine
[
type
=
]
name
[
,
prop
=
value
[
,
.
.
.
]
]
Select
the
emulated
machine
by
name
.
Use
"-machine help"
to
list
available
machines
|
下面有cpu_exec_init_all就是执行了qemu的内存结构体的初始化而已,cpudef_init则提供了VCPU的不同型号的模拟,qemu_set_log设置日志输出,kvm对外的日志是从这里配置的。中间的乱七八糟的就忽略掉即可,然后直接到了machine_class->init(current_machine)函数,其实就是执行了pc_init1。暂且记下来,先看下面的,cpu_synchronize_all_post_init就是内核和qemu数据不一致同步一下。下面的函数没有重要的了,只有vm_start()函数需要记一下,后面会用到。
现在进入pc_init1函数:
在pc_init1中重点看两个函数,pc_cpus_init和pc_memory_init,顾名思义,CPU和内存的初始化,中断,vga等函数的初始化先忽略掉,先看这两个。
pc_cpus_init入参是cpu_model,前面说过这是具体的CPU模型,所有X86的CPU模型都在builtin_x86_defs中定义,取其中一个看看
{ .name = "SandyBridge", .level = 0xd, .vendor = CPUID_VENDOR_INTEL, .family = 6, .model = 42, .stepping = 1, .features[FEAT_1_EDX] = CPUID_VME | CPUID_SSE2 | CPUID_SSE | CPUID_FXSR | CPUID_MMX | CPUID_CLFLUSH | CPUID_PSE36 | CPUID_PAT | CPUID_CMOV | CPUID_MCA | CPUID_PGE | CPUID_MTRR | CPUID_SEP | CPUID_APIC | CPUID_CX8 | CPUID_MCE | CPUID_PAE | CPUID_MSR | CPUID_TSC | CPUID_PSE | CPUID_DE | CPUID_FP87, .features[FEAT_1_ECX] = CPUID_EXT_AVX | CPUID_EXT_XSAVE | CPUID_EXT_AES | CPUID_EXT_TSC_DEADLINE_TIMER | CPUID_EXT_POPCNT | CPUID_EXT_X2APIC | CPUID_EXT_SSE42 | CPUID_EXT_SSE41 | CPUID_EXT_CX16 | CPUID_EXT_SSSE3 | CPUID_EXT_PCLMULQDQ | CPUID_EXT_SSE3, .features[FEAT_8000_0001_EDX] = CPUID_EXT2_LM | CPUID_EXT2_RDTSCP | CPUID_EXT2_NX | CPUID_EXT2_SYSCALL, .features[FEAT_8000_0001_ECX] = CPUID_EXT3_LAHF_LM, .features[FEAT_XSAVE] = CPUID_XSAVE_XSAVEOPT, .features[FEAT_6_EAX] = CPUID_6_EAX_ARAT, .xlevel = 0x80000008, .model_id = "Intel Xeon E312xx (Sandy Bridge)", },
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
|
{
.
name
=
"SandyBridge"
,
.
level
=
0xd
,
.
vendor
=
CPUID_VENDOR_INTEL
,
.
family
=
6
,
.
model
=
42
,
.
stepping
=
1
,
.
features
[
FEAT_1_EDX
]
=
CPUID_VME
|
CPUID_SSE2
|
CPUID_SSE
|
CPUID_FXSR
|
CPUID_MMX
|
CPUID_CLFLUSH
|
CPUID_PSE36
|
CPUID_PAT
|
CPUID_CMOV
|
CPUID_MCA
|
CPUID_PGE
|
CPUID_MTRR
|
CPUID_SEP
|
CPUID_APIC
|
CPUID_CX8
|
CPUID_MCE
|
CPUID_PAE
|
CPUID_MSR
|
CPUID_TSC
|
CPUID_PSE
|
CPUID_DE
|
CPUID_FP87
,
.
features
[
FEAT_1_ECX
]
=
CPUID_EXT_AVX
|
CPUID_EXT_XSAVE
|
CPUID_EXT_AES
|
CPUID_EXT_TSC_DEADLINE_TIMER
|
CPUID_EXT_POPCNT
|
CPUID_EXT_X2APIC
|
CPUID_EXT_SSE42
|
CPUID_EXT_SSE41
|
CPUID_EXT_CX16
|
CPUID_EXT_SSSE3
|
CPUID_EXT_PCLMULQDQ
|
CPUID_EXT_SSE3
,
.
features
[
FEAT_8000_0001_EDX
]
=
CPUID_EXT2_LM
|
CPUID_EXT2_RDTSCP
|
CPUID_EXT2_NX
|
CPUID_EXT2_SYSCALL
,
.
features
[
FEAT_8000_0001_ECX
]
=
CPUID_EXT3_LAHF_LM
,
.
features
[
FEAT_XSAVE
]
=
CPUID_XSAVE_XSAVEOPT
,
.
features
[
FEAT_6_EAX
]
=
CPUID_6_EAX_ARAT
,
.
xlevel
=
0x80000008
,
.
model_id
=
"Intel Xeon E312xx (Sandy Bridge)"
,
}
,
|
你可以cat一个本地的/proc/cpuinfo,builtin_x86_defs定义的就是这些参数。
然后是for循环中针对每个CPU初始化,即pc_new_cpu,直接进入cpu_x86_create函数,
主要就是把CPUX86State填充了一下,涉及到CPUID和其他的feature。下面是x86_cpu_realize,即唤醒CPU,重点是qemu_init_vcpu,MCE忽略掉,走到qemu_kvm_start_vcpu,qemu创建VCPU,如下:
//创建VPU对于的qemu线程,线程函数是qemu_kvm_cpu_thread_fn qemu_thread_create(cpu->thread, thread_name, qemu_kvm_cpu_thread_fn, cpu, QEMU_THREAD_JOINABLE); //如果线程没有创建成功,则一直在此处循环阻塞。说明多核vcpu的创建是顺序的 while (!cpu->created) { qemu_cond_wait(&qemu_cpu_cond, &qemu_global_mutex); }
|
//创建VPU对于的qemu线程,线程函数是qemu_kvm_cpu_thread_fn
qemu_thread_create
(
cpu
->
thread
,
thread_name
,
qemu_kvm_cpu_thread_fn
,
cpu
,
QEMU_THREAD_JOINABLE
)
;
//如果线程没有创建成功,则一直在此处循环阻塞。说明多核vcpu的创建是顺序的
while
(
!
cpu
->
created
)
{
qemu_cond_wait
(
&
qemu_cpu_cond
,
&
qemu_global_mutex
)
;
}
|
线程创建完成,具体任务支线提,回到主流程上,qemu_init_vcpu执行完成后,下面就是cpu_reset,此处的作用是什么呢?答案是无用,本质是一个空函数,它的主要功能就是CPUClass的reset函数,reset在cpu_class_init里面注册的,注册的是cpu_common_reset,这是一个空函数,没有任何作用。cpu_class_init则是被cpu_type_info即TYPE_CPU使用,而cpu_type_info则由type_init(cpu_register_types)完成,type_init则是前面提到的和machine_init对应的注册关系。根据下句完成工作
#define type_init(function) module_init(function, MODULE_INIT_QOM)
|
#define type_init(function) module_init(function, MODULE_INIT_QOM)
|
从上面看,pc_cpus_init函数过程已经理顺了,下面看一下,vcpu所在的线程对应的qemu_kvm_cpu_thread_fn中:
//初始化VCPU r = kvm_init_vcpu(env); //初始化KVM中断 qemu_kvm_init_cpu_signals(env); //标志VCPU创建完成,和上面判断是对应的 cpu->created = true; qemu_cond_signal(&qemu_cpu_cond); while (1) { if (cpu_can_run(env)) { //CPU进入执行状态 r = kvm_cpu_exec(env); if (r == EXCP_DEBUG) { cpu_handle_guest_debug(env); } } qemu_kvm_wait_io_event(env); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
//初始化VCPU
r
=
kvm_init_vcpu
(
env
)
;
//初始化KVM中断
qemu_kvm_init_cpu_signals
(
env
)
;
//标志VCPU创建完成,和上面判断是对应的
cpu
->
created
=
true
;
qemu_cond_signal
(
&
qemu_cpu_cond
)
;
while
(
1
)
{
if
(
cpu_can_run
(
env
)
)
{
//CPU进入执行状态
r
=
kvm_cpu_exec
(
env
)
;
if
(
r
==
EXCP_DEBUG
)
{
cpu_handle_guest_debug
(
env
)
;
}
}
qemu_kvm_wait_io_event
(
env
)
;
}
|
CPU进入执行状态的时候我们看到其他的VCPU包括内存可能还没有初始化,关键是此处有一个开关,qemu_cpu_cond,打开这个开关才能进入到CPU执行状态,谁来打开这个开关,后面再说。先看kvm_init_vcpu,通过kvm_vm_ioctl,KVM_CREATE_VCPU创建VCPU,用KVM_GET_VCPU_MMAP_SIZE获取env->kvm_run对应的内存映射,kvm_arch_init_vcpu则填充对应的kvm_arch内容,具体内核部分,后面单独写。kvm_init_vcpu就是获取了vcpu,将相关内容填充了env。
qemu_kvm_init_cpu_signals则是将中断组合掩码传递给kvm_set_signal_mask,最终给内核KVM_SET_SIGNAL_MASK。kvm_cpu_exec此时还在阻塞过程中,先挂起来,看内存的初始化。
内存初始化函数是pc_memory_init,memory_region_init_ram传入了高端内存和低端内存的值,memory_region_init负责填充mr,重点在qemu_ram_alloc,即qemu_ram_alloc_from_ptr,首先有RAMBlock,ram_list,那就直接借助find_ram_offset函数一起看一下qemu的内存分布模型。
qemu模拟了普通内存分布模型,内存的线性也是分块被使用的,每个块称为RAMBlock,由ram_list统领,RAMBlock.offset则是区块的线性地址,即相对于开始的偏移位,RAMBlock.length(size)则是区块的大小,find_ram_offset则是在线性区间内找到没有使用的一段空间,可以完全容纳新申请的ramblock length大小,代码就是进行了所有区块的遍历,找到满足新申请length的最小区间,把ramblock安插进去即可,返回的offset即是新分配区间的开始地址。
而RAMBlock的物理则是在RAMBlock.host,由kvm_vmalloc(size)分配真正物理内存,内部qemu_vmalloc使用qemu_memalign页对齐分配内存。后续的都是对RAMBlock的插入等处理。
从上面看,memory_region_init_ram已经将qemu内存模型和实际的物理内存初始化了。
vmstate_register_ram_global这个函数则是负责将前面提到的ramlist中的ramblock和memory region的初始地址对应一下,将mr->name填充到ramblock的idstr里面,就是让二者有确定的对应关系,如此mr就有了物理内存使用。
后面则是subregion的处理,memory_region_init_alias初始化,其中将ram传递给mr->owner确定了隶属关系,memory_region_add_subregion则是大头,memory_region_add_subregion_common前面的判断忽略,QTAILQ_INSERT_TAIL(&mr->subregions, subregion, subregions_link)就是插入了链表而已,主要内容在memory_region_transaction_commit。
memory_region_transaction_commit中引入了新的结构address_spaces(AS),注释里面提到“AddressSpace: describes a mapping of addresses to #MemoryRegion objects”,就是内存地址的映射关系,因为内存有不同的应用类型,address_spaces以链表形式存在,commit函数则是对所有AS执行address_space_update_topology,先看AS在哪里注册的,就是前面提到的kvm_init里面,执行memory_listener_register,注册了address_space_memory和address_space_io两个,涉及的另外一个结构体则是MemoryListener,有kvm_memory_listener和kvm_io_listener,就是用于监控内存映射关系发生变化之后执行回调函数。
下面进入到address_space_update_topology函数,FlatView则是“Flattened global view of current active memory hierarchy”,address_space_get_flatview直接获取当前的,generate_memory_topology则根据前面已经变化的mr重新生成FlatView,然后通过address_space_update_topology_pass比较,简单说address_space_update_topology_pass就是两个FlatView逐条的FlatRange进行对比,以后一个FlatView为准,如果前面FlatView的FlatRange和后面的不一样,则对前面的FlatView的这条FlatRange进行处理,差别就是3种情况,如代码:
while (iold < old_view->nr || inew < new_view->nr) { if (iold < old_view->nr) { frold = &old_view->ranges[iold]; } else { frold = NULL; } if (inew < new_view->nr) { frnew = &new_view->ranges[inew]; } else { frnew = NULL; } if (frold && (!frnew || int128_lt(frold->addr.start, frnew->addr.start) || (int128_eq(frold->addr.start, frnew->addr.start) && !flatrange_equal(frold, frnew)))) { /* In old but not in new, or in both but attributes changed. */ if (!adding) { //这个判断代码添加的无用,可以直接删除, //address_space_update_topology里面的两个pass也可以删除一个 MEMORY_LISTENER_UPDATE_REGION(frold, as, Reverse, region_del); } ++iold; } else if (frold && frnew && flatrange_equal(frold, frnew)) { /* In both and unchanged (except logging may have changed) */ if (adding) { MEMORY_LISTENER_UPDATE_REGION(frnew, as, Forward, region_nop); if (frold->dirty_log_mask && !frnew->dirty_log_mask) { MEMORY_LISTENER_UPDATE_REGION(frnew, as, Reverse, log_stop); } else if (frnew->dirty_log_mask && !frold->dirty_log_mask) { MEMORY_LISTENER_UPDATE_REGION(frnew, as, Forward, log_start); } } ++iold; ++inew; } else { /* In new */ if (adding) { MEMORY_LISTENER_UPDATE_REGION(frnew, as, Forward, region_add); } ++inew; } }
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
|
while
(
iold
<
old_view
->
nr
||
inew
<
new_view
->
nr
)
{
if
(
iold
<
old_view
->
nr
)
{
frold
=
&
old_view
->
ranges
[
iold
]
;
}
else
{
frold
=
NULL
;
}
if
(
inew
<
new_view
->
nr
)
{
frnew
=
&
new_view
->
ranges
[
inew
]
;
}
else
{
frnew
=
NULL
;
}
if
(
frold
&&
(
!
frnew
||
int128_lt
(
frold
->
addr
.
start
,
frnew
->
addr
.
start
)
||
(
int128_eq
(
frold
->
addr
.
start
,
frnew
->
addr
.
start
)
&&
!
flatrange_equal
(
frold
,
frnew
)
)
)
)
{
/* In old but not in new, or in both but attributes changed. */
if
(
!
adding
)
{
//这个判断代码添加的无用,可以直接删除,
//address_space_update_topology里面的两个pass也可以删除一个
MEMORY_LISTENER_UPDATE_REGION
(
frold
,
as
,
Reverse
,
region_del
)
;
}
++
iold
;
}
else
if
(
frold
&&
frnew
&&
flatrange_equal
(
frold
,
frnew
)
)
{
/* In both and unchanged (except logging may have changed) */
if
(
adding
)
{
MEMORY_LISTENER_UPDATE_REGION
(
frnew
,
as
,
Forward
,
region_nop
)
;
if
(
frold
->
dirty_log_mask
&&
!
frnew
->
dirty_log_mask
)
{
MEMORY_LISTENER_UPDATE_REGION
(
frnew
,
as
,
Reverse
,
log_stop
)
;
}
else
if
(
frnew
->
dirty_log_mask
&&
!
frold
->
dirty_log_mask
)
{
MEMORY_LISTENER_UPDATE_REGION
(
frnew
,
as
,
Forward
,
log_start
)
;
}
}
++
iold
;
++
inew
;
}
else
{
/* In new */
if
(
adding
)
{
MEMORY_LISTENER_UPDATE_REGION
(
frnew
,
as
,
Forward
,
region_add
)
;
}
++
inew
;
}
}
|
重点在MEMORY_LISTENER_UPDATE_REGION函数上,将变化的FlatRange构造一个MemoryRegionSection,然后遍历所有的memory_listeners,如果memory_listeners监控的内存区域和MemoryRegionSection一样,则执行第四个入参函数,如region_del函数,即kvm_region_del函数,这个是在kvm_init中初始化的。kvm_region_del主要是kvm_set_phys_mem函数,主要是将MemoryRegionSection有效值转换成KVMSlot形式,在kvm_set_user_memory_region中使用kvm_vm_ioctl(s, KVM_SET_USER_MEMORY_REGION, &mem)传递给kernel。
我们看内存初始化真正需要做的是什么?就是qemu申请内存,把申请物理地址传递给kernel进行映射,那我们直接就可以KVMSlot申请内存,然后传递给kvm_vm_ioctl,这样也是OK的,之所以有这么多代码,因为qemu本身是一个软件虚拟机,mr涉及的地址已经是vm的地址,对于KVM是多余的,只是方便函数复用而已。
内存初始化之后还是pci等处理先跳过,如此pc_init就完成了,但是前面VM线程已经初始化成功,在qemu_kvm_cpu_thread_fn函数中等待运行:
while (1) { if (cpu_can_run(cpu)) { r = kvm_cpu_exec(cpu); if (r == EXCP_DEBUG) { cpu_handle_guest_debug(cpu); } } qemu_kvm_wait_io_event(cpu); }
|
while
(
1
)
{
if
(
cpu_can_run
(
cpu
)
)
{
r
=
kvm_cpu_exec
(
cpu
)
;
if
(
r
==
EXCP_DEBUG
)
{
cpu_handle_guest_debug
(
cpu
)
;
}
}
qemu_kvm_wait_io_event
(
cpu
)
;
}
|
判断条件就是cpu_can_run函数,即cpu->stop && cpu->stopped && current_run_state != running 都是false,而这几个参数都是由vm_start函数决定的
void vm_start(void) { if (!runstate_is_running()) { cpu_enable_ticks(); runstate_set(RUN_STATE_RUNNING); vm_state_notify(1, RUN_STATE_RUNNING); resume_all_vcpus(); monitor_protocol_event(QEVENT_RESUME, NULL); } }
|
void
vm_start
(
void
)
{
if
(
!
runstate_is_running
(
)
)
{
cpu_enable_ticks
(
)
;
runstate_set
(
RUN_STATE_RUNNING
)
;
vm_state_notify
(
1
,
RUN_STATE_RUNNING
)
;
resume_all_vcpus
(
)
;
monitor_protocol_event
(
QEVENT_RESUME
,
NULL
)
;
}
}
|
如此kvm_cpu_exec就真正进入执行阶段,即通过kvm_vcpu_ioctl传递KVM_RUN给内核。
在虚拟机的创建与运行章节里面笼统的介绍了KVM在qemu中的创建和运行,基本的qemu代码流程已经梳理清楚,后续主要写一些硬件虚拟化的原理和代码流程,主要写原理和qemu控制KVM运行的的ioctl接口,后续对内核代码的梳理也从这些接口下手。
QEMU:git://git.qemu.org/qemu.git v2.4.0
KVM:https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git v4.2
1.VT-x 技术
Intel处理器支持的虚拟化技术即是VT-x,之所以CPU支持硬件虚拟化是因为软件虚拟化的效率太低。
处理器虚拟化的本质是分时共享,主要体现在状态恢复和资源隔离,实际上每个VM对于VMM看就是一个task么,之前Intel处理器在虚拟化上没有提供默认的硬件支持,传统 x86 处理器有4个特权级,Linux使用了0,3级别,0即内核,3即用户态,(更多参考CPU的运行环、特权级与保护)而在虚拟化架构上,虚拟机监控器的运行级别需要内核态特权级,而CPU特权级被传统OS占用,所以Intel设计了VT-x,提出了VMX模式,即VMX root operation 和 VMX non-root operation,虚拟机监控器运行在VMX root operation,虚拟机运行在VMX non-root operation。每个模式下都有相对应的0~3特权级。
为什么引入这两种特殊模式,在传统x86的系统中,CPU有不同的特权级,是为了划分不同的权限指令,某些指令只能由系统软件操作,称为特权指令,这些指令只能在最高特权级上才能正确执行,反之则会触发异常,处理器会陷入到最高特权级,由系统软件处理。还有一种需要操作特权资源(如访问中断寄存器)的指令,称为敏感指令。OS运行在特权级上,屏蔽掉用户态直接执行的特权指令,达到控制所有的硬件资源目的;而在虚拟化环境中,VMM控制所有所有硬件资源,VM中的OS只能占用一部分资源,OS执行的很多特权指令是不能真正对硬件生效的,所以原特权级下有了root模式,OS指令不需要修改就可以正常执行在特权级上,但这个特权级的所有敏感指令都会传递到root模式处理,这样达到了VMM的目的。
在KVM源代码分析1:基本工作原理章节中也说了kvm分3个模式,对应到VT-x 中即是客户模式对应vmx非root模式,内核模式对应VMX root模式下的0特权级,用户模式对应vmx root模式下的3特权级。
如下图
在非根模式下敏感指令引发的陷入称为VM-Exit,VM-Exit发生后,CPU从非根模式切换到根模式;对应的,VM-Entry则是从根模式到非根模式,通常意味着调用VM进入运行态。VMLAUCH/VMRESUME命令则是用来发起VM-Entry。
2.VMCS寄存器
VMCS保存虚拟机的相关CPU状态,每个VCPU都有一个VMCS(内存的),每个物理CPU都有VMCS对应的寄存器(物理的),当CPU发生VM-Entry时,CPU则从VCPU指定的内存中读取VMCS加载到物理CPU上执行,当发生VM-Exit时,CPU则将当前的CPU状态保存到VCPU指定的内存中,即VMCS,以备下次VMRESUME。
VMLAUCH指VM的第一次VM-Entry,VMRESUME则是VMLAUCH之后后续的VM-Entry。VMCS下有一些控制域:
VM-execution controls |
Determines what operations cause VM exits |
CR0, CR3, CR4, Exceptions, IO Ports, Interrupts, Pin Events, etc |
Guest-state area |
Saved on VM exits,Reloaded on VM entry |
EIP, ESP, EFLAGS, IDTR, Segment Regs, Exit info, etc |
Host-state area |
Loaded on VM exits |
CR3, EIP set to monitor entry point, EFLAGS hardcoded, etc |
VM-exit controls |
Determines which state to save, load, how to transition |
Example: MSR save-load list |
VM-entry controls |
Determines which state to load, how to transition |
Including injecting events (interrupts, exceptions) on entry |
关于具体控制域的细节,还是翻Intel手册吧。
3.VM-Entry/VM-Exit
VM-Entry是从根模式切换到非根模式,即VMM切换到guest上,这个状态由VMM发起,发起之前先保存VMM中的关键寄存器内容到VMCS中,然后进入到VM-Entry,VM-Entry附带参数主要有3个:1.guest是否处于64bit模式,2.MSR VM-Entry控制,3.注入事件。1应该只在VMLAUCH有意义,3更多是在VMRESUME,而VMM发起VM-Entry更多是因为3,2主要用来每次更新MSR。
VM-Exit是CPU从非根模式切换到根模式,从guest切换到VMM的操作,VM-Exit触发的原因就很多了,执行敏感指令,发生中断,模拟特权资源等。
运行在非根模式下的敏感指令一般分为3个方面:
1.行为没有变化的,也就是说该指令能够正确执行。
2.行为有变化的,直接产生VM-Exit。
3.行为有变化的,但是是否产生VM-Exit受到VM-Execution控制域控制。
主要说一下”受到VM-Execution控制域控制”的敏感指令,这个就是针对性的硬件优化了,一般是1.产生VM-Exit;2.不产生VM-Exit,同时调用优化函数完成功能。典型的有“RDTSC指令”。除了大部分是优化性能的,还有一小部分是直接VM-Exit执行指令结果是异常的,或者说在虚拟化场景下是不适用的,典型的就是TSC offset了。
VM-Exit发生时退出的相关信息,如退出原因、触发中断等,这些内容保存在VM-Exit信息域中。
4.KVM_CREATE_VM
创建VM就写这里吧,kvm_dev_ioctl_create_vm函数是主干,在kvm_create_vm中,主要有两个函数,kvm_arch_init_vm和hardware_enable_all,需要注意,但是更先一步的是KVM结构体,下面的struct是精简后的版本。
struct kvm { struct mm_struct *mm; /* userspace tied to this vm */ struct kvm_memslots *memslots; /*qemu模拟的内存条模型*/ struct kvm_vcpu *vcpus[KVM_MAX_VCPUS]; /* 模拟的CPU */ atomic_t online_vcpus; int last_boosted_vcpu; struct list_head vm_list; //HOST上VM管理链表, struct kvm_io_bus *buses[KVM_NR_BUSES]; struct kvm_vm_stat stat; struct kvm_arch arch; //这个是host的arch的一些参数 atomic_t users_count; long tlbs_dirty; struct list_head devices; };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
struct
kvm
{
struct
mm_struct
*
mm
;
/* userspace tied to this vm */
struct
kvm_memslots
*
memslots
;
/*qemu模拟的内存条模型*/
struct
kvm_vcpu
*
vcpus
[
KVM_MAX_VCPUS
]
;
/* 模拟的CPU */
atomic_t
online_vcpus
;
int
last_boosted_vcpu
;
struct
list_head
vm_list
;
//HOST上VM管理链表,
struct
kvm_io_bus
*
buses
[
KVM_NR_BUSES
]
;
struct
kvm_vm_stat
stat
;
struct
kvm_arch
arch
;
//这个是host的arch的一些参数
atomic_t
users_count
;
long
tlbs_dirty
;
struct
list_head
devices
;
}
;
|
kvm_arch_init_vm基本没有特别动作,初始化了KVM->arch,以及更新了kvmclock函数,这个另外再说。
而hardware_enable_all,针对于每个CPU执行“on_each_cpu(hardware_enable_nolock, NULL, 1)”,在hardware_enable_nolock中先把cpus_hardware_enabled置位,进入到kvm_arch_hardware_enable中,有hardware_enable和TSC初始化规则,主要看hardware_enable,crash_enable_local_vmclear清理位图,判断MSR_IA32_FEATURE_CONTROL寄存器是否满足虚拟环境,不满足则将条件写入到寄存器内,CR4将X86_CR4_VMXE置位,另外还有kvm_cpu_vmxon打开VMX操作模式,外层包了vmm_exclusive的判断,它是kvm_intel.ko的外置参数,默认唯一,可以让用户强制不使用VMM硬件支持。
5.KVM_CREATE_VCPU
kvm_vm_ioctl_create_vcpu主要有三部分,kvm_arch_vcpu_create,kvm_arch_vcpu_setup和kvm_arch_vcpu_postcreate,重点自然是kvm_arch_vcpu_create。老样子,在这之前先看一下VCPU的结构体。
struct kvm_vcpu { struct kvm *kvm; //归属的KVM #ifdef CONFIG_PREEMPT_NOTIFIERS struct preempt_notifier preempt_notifier; #endif int cpu; int vcpu_id; int srcu_idx; int mode; unsigned long requests; unsigned long guest_debug; struct mutex mutex; struct kvm_run *run; //运行时的状态 int fpu_active; int guest_fpu_loaded, guest_xcr0_loaded; wait_queue_head_t wq; //队列 struct pid *pid; int sigset_active; sigset_t sigset; struct kvm_vcpu_stat stat; //一些数据 #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; struct kvm_vcpu_arch arch; //当前VCPU虚拟的架构,默认介绍X86 };
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
|
struct
kvm_vcpu
{
struct
kvm
*
kvm
;
//归属的KVM
#ifdef CONFIG_PREEMPT_NOTIFIERS
struct
preempt_notifier
preempt_notifier
;
#endif
int
cpu
;
int
vcpu_id
;
int
srcu_idx
;
int
mode
;
unsigned
long
requests
;
unsigned
long
guest_debug
;
struct
mutex
mutex
;
struct
kvm_run
*
run
;
//运行时的状态
int
fpu_active
;
int
guest_fpu_loaded
,
guest_xcr0_loaded
;
wait_queue_head_t
wq
;
//队列
struct
pid
*
pid
;
int
sigset_active
;
sigset_t
sigset
;
struct
kvm_vcpu_stat
stat
;
//一些数据
#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
;
struct
kvm_vcpu_arch
arch
;
//当前VCPU虚拟的架构,默认介绍X86
}
;
|
借着看kvm_arch_vcpu_create,它借助kvm_x86_ops->vcpu_create即vmx_create_vcpu完成任务,vmx是X86硬件虚拟化层,从代码看,qemu用户态是一层,kernel 中KVM通用代码是一层,类似kvm_x86_ops是一层,针对各个不同硬件架构,而vcpu_vmx则是具体架构的虚拟化方案一层。首先是kvm_vcpu_init初始化,主要是填充结构体,可以注意的是vcpu->run分派了一页内存,下面有kvm_arch_vcpu_init负责填充x86 CPU结构体,下面就是kvm_vcpu_arch:
struct kvm_vcpu_arch { /* * rip and regs accesses must go through * kvm_{register,rip}_{read,write} functions. */ unsigned long regs[NR_VCPU_REGS]; u32 regs_avail; u32 regs_dirty; //类似这些寄存器就是就是用来缓存真正的CPU值的 unsigned long cr0; unsigned long cr0_guest_owned_bits; unsigned long cr2; unsigned long cr3; unsigned long cr4; unsigned long cr4_guest_owned_bits; unsigned long cr8; u32 hflags; u64 efer; u64 apic_base; struct kvm_lapic *apic; /* kernel irqchip context */ unsigned long apic_attention; int32_t apic_arb_prio; int mp_state; u64 ia32_misc_enable_msr; bool tpr_access_reporting; u64 ia32_xss; /* * Paging state of the vcpu * * If the vcpu runs in guest mode with two level paging this still saves * the paging mode of the l1 guest. This context is always used to * handle faults. */ struct kvm_mmu mmu; //内存管理,更多的是附带了直接操作函数 /* * Paging state of an L2 guest (used for nested npt) * * This context will save all necessary information to walk page tables * of the an L2 guest. This context is only initialized for page table * walking and not for faulting since we never handle l2 page faults on * the host. */ struct kvm_mmu nested_mmu; /* * Pointer to the mmu context currently used for * gva_to_gpa translations. */ struct kvm_mmu *walk_mmu; struct kvm_mmu_memory_cache mmu_pte_list_desc_cache; struct kvm_mmu_memory_cache mmu_page_cache; struct kvm_mmu_memory_cache mmu_page_header_cache; struct fpu guest_fpu; u64 xcr0; u64 guest_supported_xcr0; u32 guest_xstate_size; struct kvm_pio_request pio; void *pio_data; u8 event_exit_inst_len; struct kvm_queued_exception { bool pending; bool has_error_code; bool reinject; u8 nr; u32 error_code; } exception; struct kvm_queued_interrupt { bool pending; bool soft; u8 nr; } interrupt; int halt_request; /* real mode on Intel only */ int cpuid_nent; struct kvm_cpuid_entry2 cpuid_entries[KVM_MAX_CPUID_ENTRIES]; int maxphyaddr; /* emulate context */ //下面是KVM的软件模拟模式,也就是没有vmx的情况,估计也没人用这一套 struct x86_emulate_ctxt emulate_ctxt; bool emulate_regs_need_sync_to_vcpu; bool emulate_regs_need_sync_from_vcpu; int (*complete_userspace_io)(struct kvm_vcpu *vcpu); gpa_t time; struct pvclock_vcpu_time_info hv_clock; unsigned int hw_tsc_khz; struct gfn_to_hva_cache pv_time; bool pv_time_enabled; /* set guest stopped flag in pvclock flags field */ bool pvclock_set_guest_stopped_request; struct { u64 msr_val; u64 last_steal; u64 accum_steal; struct gfn_to_hva_cache stime; struct kvm_steal_time steal; } st; u64 last_guest_tsc; u64 last_host_tsc; u64 tsc_offset_adjustment; u64 this_tsc_nsec; u64 this_tsc_write; u64 this_tsc_generation; bool tsc_catchup; bool tsc_always_catchup; s8 virtual_tsc_shift; u32 virtual_tsc_mult; u32 virtual_tsc_khz; s64 ia32_tsc_adjust_msr; atomic_t nmi_queued; /* unprocessed asynchronous NMIs */ unsigned nmi_pending; /* NMI queued after currently running handler */ bool nmi_injected; /* Trying to inject an NMI this entry */ struct mtrr_state_type mtrr_state; u64 pat; unsigned switch_db_regs; unsigned long db[KVM_NR_DB_REGS]; unsigned long dr6; unsigned long dr7; unsigned long eff_db[KVM_NR_DB_REGS]; unsigned long guest_debug_dr7; u64 mcg_cap; u64 mcg_status; u64 mcg_ctl; u64 *mce_banks; /* Cache MMIO info */ u64 mmio_gva; unsigned access; gfn_t mmio_gfn; u64 mmio_gen; struct kvm_pmu pmu; /* used for guest single stepping over the given code position */ unsigned long singlestep_rip; /* fields used by HYPER-V emulation */ u64 hv_vapic; cpumask_var_t wbinvd_dirty_mask; unsigned long last_retry_eip; unsigned long last_retry_addr; struct { bool halted; gfn_t gfns[roundup_pow_of_two(ASYNC_PF_PER_VCPU)]; struct gfn_to_hva_cache data; u64 msr_val; u32 id; bool send_user_only; } apf; /* OSVW MSRs (AMD only) */ struct { u64 length; u64 status; } osvw; struct { u64 msr_val; struct gfn_to_hva_cache data; } pv_eoi; /* * Indicate whether the access faults on its page table in guest * which is set when fix page fault and used to detect unhandeable * instruction. */ bool write_fault_to_shadow_pgtable; /* set at EPT violation at this point */ unsigned long exit_qualification; /* pv related host specific info */ struct { bool pv_unhalted; } pv; };
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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
|
struct
kvm_vcpu_arch
{
/*
* rip and regs accesses must go through
* kvm_{register,rip}_{read,write} functions.
*/
unsigned
long
regs
[
NR_VCPU_REGS
]
;
u32
regs_avail
;
u32
regs_dirty
;
//类似这些寄存器就是就是用来缓存真正的CPU值的
unsigned
long
cr0
;
unsigned
long
cr0_guest_owned_bits
;
unsigned
long
cr2
;
unsigned
long
cr3
;
unsigned
long
cr4
;
unsigned
long
cr4_guest_owned_bits
;
unsigned
long
cr8
;
u32
hflags
;
u64
efer
;
u64
apic_base
;
struct
kvm_lapic
*
apic
;
/* kernel irqchip context */
unsigned
long
apic_attention
;
int32_t
apic_arb_prio
;
int
mp_state
;
u64
ia32_misc_enable_msr
;
bool
tpr_access_reporting
;
u64
ia32_xss
;
/*
* Paging state of the vcpu
*
* If the vcpu runs in guest mode with two level paging this still saves
* the paging mode of the l1 guest. This context is always used to
* handle faults.
*/
struct
kvm_mmu
mmu
;
//内存管理,更多的是附带了直接操作函数
/*
* Paging state of an L2 guest (used for nested npt)
*
* This context will save all necessary information to walk page tables
* of the an L2 guest. This context is only initialized for page table
* walking and not for faulting since we never handle l2 page faults on
* the host.
*/
struct
kvm_mmu
nested_mmu
;
/*
* Pointer to the mmu context currently used for
* gva_to_gpa translations.
*/
struct
kvm_mmu
*
walk_mmu
;
struct
kvm_mmu_memory_cache
mmu_pte_list_desc_cache
;
struct
kvm_mmu_memory_cache
mmu_page_cache
;
struct
kvm_mmu_memory_cache
mmu_page_header_cache
;
struct
fpu
guest_fpu
;
u64
xcr0
;
u64
guest_supported_xcr0
;
u32
guest_xstate_size
;
struct
kvm_pio_request
pio
;
void
*
pio_data
;
u8
event_exit_inst_len
;
struct
kvm_queued_exception
{
bool
pending
;
bool
has_error_code
;
bool
reinject
;
u8
nr
;
u32
error_code
;
}
exception
;
struct
kvm_queued_interrupt
{
bool
pending
;
bool
soft
;
u8
nr
;
}
interrupt
;
int
halt_request
;
/* real mode on Intel only */
int
cpuid_nent
;
struct
kvm_cpuid_entry2
cpuid_entries
[
KVM_MAX_CPUID_ENTRIES
]
;
int
maxphyaddr
;
/* emulate context */
//下面是KVM的软件模拟模式,也就是没有vmx的情况,估计也没人用这一套
struct
x86_emulate_ctxt
emulate_ctxt
;
bool
emulate_regs_need_sync_to_vcpu
;
bool
emulate_regs_need_sync_from_vcpu
;
int
(
*
complete_userspace_io
)
(
struct
kvm_vcpu
*
vcpu
)
;
gpa_t
time
;
struct
pvclock_vcpu_time_info
hv_clock
;
unsigned
int
hw_tsc_khz
;
struct
gfn_to_hva_cache
pv_time
;
bool
pv_time_enabled
;
/* set guest stopped flag in pvclock flags field */
bool
pvclock_set_guest_stopped_request
;
struct
{
u64
msr_val
;
u64
last_steal
;
u64
accum_steal
;
struct
gfn_to_hva_cache
stime
;
struct
kvm_steal_time
steal
;
}
st
;
u64
last_guest_tsc
;
u64
last_host_tsc
;
u64
tsc_offset_adjustment
;
u64
this_tsc_nsec
;
u64
this_tsc_write
;
u64
this_tsc_generation
;
bool
tsc_catchup
;
bool
tsc_always_catchup
;
s8
virtual_tsc_shift
;
u32
virtual_tsc_mult
;
u32
virtual_tsc_khz
;
s64
ia32_tsc_adjust_msr
;
atomic_t
nmi_queued
;
/* unprocessed asynchronous NMIs */
unsigned
nmi_pending
;
/* NMI queued after currently running handler */
bool
nmi_injected
;
/* Trying to inject an NMI this entry */
struct
mtrr_state_type
mtrr_state
;
u64
pat
;
unsigned
switch_db_regs
;
unsigned
long
db
[
KVM_NR_DB_REGS
]
;
unsigned
long
dr6
;
unsigned
long
dr7
;
unsigned
long
eff_db
[
KVM_NR_DB_REGS
]
;
unsigned
long
guest_debug_dr7
;
u64
mcg_cap
;
u64
mcg_status
;
u64
mcg_ctl
;
u64
*
mce_banks
;
/* Cache MMIO info */
u64
mmio_gva
;
unsigned
access
;
gfn_t
mmio_gfn
;
u64
mmio_gen
;
struct
kvm_pmu
pmu
;
/* used for guest single stepping over the given code position */
unsigned
long
singlestep_rip
;
/* fields used by HYPER-V emulation */
u64
hv_vapic
;
cpumask_var_t
wbinvd_dirty_mask
;
unsigned
long
last_retry_eip
;
unsigned
long
last_retry_addr
;
struct
{
bool
halted
;
gfn_t
gfns
[
roundup_pow_of_two
(
ASYNC_PF_PER_VCPU
)
]
;
struct
gfn_to_hva_cache
data
;
u64
msr_val
;
u32
id
;
bool
send_user_only
;
}
apf
;
/* OSVW MSRs (AMD only) */
struct
{
u64
length
;
u64
status
;
}
osvw
;
struct
{
u64
msr_val
;
struct
gfn_to_hva_cache
data
;
}
pv_eoi
;
/*
* Indicate whether the access faults on its page table in guest
* which is set when fix page fault and used to detect unhandeable
* instruction.
*/
bool
write_fault_to_shadow_pgtable
;
/* set at EPT violation at this point */
unsigned
long
exit_qualification
;
/* pv related host specific info */
struct
{
bool
pv_unhalted
;
}
pv
;
}
;
|
整个arch结构真是长,很适合凑篇幅,很多结构其他过程涉及到的再提吧,反正我也不知道。
kvm_arch_vcpu_init初始化了x86在虚拟化底层的实现函数,首先是pv和emulate_ctxt,这些不支持VMX下的模拟虚拟化,尤其是vcpu->arch.emulate_ctxt.ops = &emulate_ops,emulate_ops初始化虚拟化模拟的对象函数。
static struct x86_emulate_ops emulate_ops = { .read_std = kvm_read_guest_virt_system, .write_std = kvm_write_guest_virt_system, .fetch = kvm_fetch_guest_virt, .read_emulated = emulator_read_emulated, .write_emulated = emulator_write_emulated, .cmpxchg_emulated = emulator_cmpxchg_emulated, .invlpg = emulator_invlpg, .pio_in_emulated = emulator_pio_in_emulated, .pio_out_emulated = emulator_pio_out_emulated, .get_segment = emulator_get_segment, .set_segment = emulator_set_segment, .get_cached_segment_base = emulator_get_cached_segment_base, .get_gdt = emulator_get_gdt, .get_idt = emulator_get_idt, .set_gdt = emulator_set_gdt, .set_idt = emulator_set_idt, .get_cr = emulator_get_cr, .set_cr = emulator_set_cr, .cpl = emulator_get_cpl, .get_dr = emulator_get_dr, .set_dr = emulator_set_dr, .set_msr = emulator_set_msr, .get_msr = emulator_get_msr, .halt = emulator_halt, .wbinvd = emulator_wbinvd, .fix_hypercall = emulator_fix_hypercall, .get_fpu = emulator_get_fpu, .put_fpu = emulator_put_fpu, .intercept = emulator_intercept, .get_cpuid = emulator_get_cpuid, };
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
|
static
struct
x86_emulate_ops
emulate_ops
=
{
.
read_std
=
kvm_read_guest_virt_system
,
.
write_std
=
kvm_write_guest_virt_system
,
.
fetch
=
kvm_fetch_guest_virt
,
.
read_emulated
=
emulator_read_emulated
,
.
write_emulated
=
emulator_write_emulated
,
.
cmpxchg_emulated
=
emulator_cmpxchg_emulated
,
.
invlpg
=
emulator_invlpg
,
.
pio_in_emulated
=
emulator_pio_in_emulated
,
.
pio_out_emulated
=
emulator_pio_out_emulated
,
.
get_segment
=
emulator_get_segment
,
.
set_segment
=
emulator_set_segment
,
.
get_cached_segment_base
=
emulator_get_cached_segment_base
,
.
get_gdt
=
emulator_get_gdt
,
.
get_idt
=
emulator_get_idt
,
.
set_gdt
=
emulator_set_gdt
,
.
set_idt
=
emulator_set_idt
,
.
get_cr
=
emulator_get_cr
,
.
set_cr
=
emulator_set_cr
,
.
cpl
=
emulator_get_cpl
,
.
get_dr
=
emulator_get_dr
,
.
set_dr
=
emulator_set_dr
,
.
set_msr
=
emulator_set_msr
,
.
get_msr
=
emulator_get_msr
,
.
halt
=
emulator_halt
,
.
wbinvd
=
emulator_wbinvd
,
.
fix_hypercall
=
emulator_fix_hypercall
,
.
get_fpu
=
emulator_get_fpu
,
.
put_fpu
=
emulator_put_fpu
,
.
intercept
=
emulator_intercept
,
.
get_cpuid
=
emulator_get_cpuid
,
}
;
|
x86_emulate_ops函数看看就好,实际上也很少有人放弃vmx直接软件模拟。后面又有mp_state,给pio_data分配了一个page,kvm_set_tsc_khz设置TSC,kvm_mmu_create则是初始化MMU的函数,里面的函数都是地址转换的重点,在内存虚拟化重点提到。kvm_create_lapic初始化lapic,初始化mce_banks结构,还有pv_time,xcr0,xstat,pmu等,类似x86硬件结构上需要存在的,OS底层需要看到的硬件名称都要有对应的软件结构。
回到vmx_create_vcpu,vmx的guest_msrs分配得到一个page,后面是vmcs的分配,vmx->loaded_vmcs->vmcs = alloc_vmcs(),alloc_vmcs为当前cpu执行alloc_vmcs_cpu,alloc_vmcs_cpu中alloc_pages_exact_node分配给vmcs,alloc_pages_exact_node调用__alloc_pages实现,原来以为vmcs占用了一个page,但此处从伙伴系统申请了2^vmcs_config.order页,此处vmcs_config在setup_vmcs_config中初始化,vmcs_conf->order = get_order(vmcs_config.size),而vmcs_conf->size = vmx_msr_high & 0x1fff,又rdmsr(MSR_IA32_VMX_BASIC, vmx_msr_low, vmx_msr_high),此处size由于与0x1fff与运算,大小必然小于4k,order则为0,然来绕去还是一个page大小。这么做估计是为了兼容vmcs_config中的size计算。
下面根据vmm_exclusive进行kvm_cpu_vmxon,进入vmx模式,初始化loaded_vmcs,然后用kvm_cpu_vmxoff退出vmx模式。
vmx_vcpu_load加载VCPU的信息,切换到指定cpu,进入到vmx模式,将loaded_vmcs的vmcs和当前cpu的vmcs绑定到一起。vmx_vcpu_setup则是初始化vmcs内容,主要是赋值计算,下面的vmx_vcpu_put则是vmx_vcpu_load的反运算。下面还有一些apic,nested,pml就不说了。
vmx_create_vcpu结束就直接回到kvm_vm_ioctl_create_vcpu函数,下面是kvm_arch_vcpu_setup,整个就一条线到kvm_arch_vcpu_load函数,主要有kvm_x86_ops->vcpu_load(vcpu, cpu)和tsc处理,vcpu_load就是vmx_vcpu_load,刚说了,就是进入vcpu模式下准备工作。
kvm_arch_vcpu_setup后面是create_vcpu_fd为proc创建控制fd,让qemu使用。kvm_arch_vcpu_postcreate则是马后炮般,重新vcpu_load,写msr,tsc。
如此整个vcpu就创建完成了。
6.KVM_RUN
KVM run涉及内容也不少,先写完内存虚拟化之后再开篇专门写RUN流程。
下一篇:
KVM源代码分析4:内存虚拟化
———-完———-
——————–下面未编辑的留存————————————-
给vmcs分配空间并初始化,在alloc_vmcs_cpu分配一个页大小内存,用来保存vm和vmm信息。
vmx->vmcs = alloc_vmcs(); if (!vmx->vmcs) goto free_msrs; vmcs_init(vmx->vmcs);
|
vmx
-
&
gt
;
vmcs
=
alloc_vmcs
(
)
;
if
(
!
vmx
-
&
gt
;
vmcs
)
goto
free_msrs
;
vmcs_init
(
vmx
-
&
gt
;
vmcs
)
;
|
执行vm entry的时候将vmm状态保存到vmcs的host area,并加载对应vm的vmcs guest area信息到CPU中,vm exit的时候则反之,vmcs具体结构分配由硬件实现,程序员只需要通过VMWRITE和VMREAD指令去访问。
vmx执行完后,回到kvm_vm_ioctl_create_vcpu函数。kvm_arch_vcpu_reset对vcpu的结构进行初始化,后面一些就是检查vcpu的合法性,最后和kvm串接到一起。
vcpu的创建到此结束,下面说一下vcpu的运行。
VCPU一旦创建成功,后续的控制基本上从kvm_vcpu_ioctl开始,控制开关有KVM_RUN,KVM_GET_REGS,KVM_SET_REGS,KVM_GET_SREGS,KVM_SET_SREGS,KVM_GET_MP_STATE,KVM_SET_MP_STATE,KVM_TRANSLATE,KVM_SET_GUEST_DEBUG,KVM_SET_SIGNAL_MASK等,如果不清楚具体开关作用,可以直接到qemu搜索对应开关代码,一目了然。
KVM_RUN的实现函数是kvm_arch_vcpu_ioctl_run,进行安全检查之后进入__vcpu_run中,在while循环里面调用vcpu_enter_guest进入guest模式,首先处理vcpu->requests,对应的request做处理,kvm_mmu_reload加载mmu,通过kvm_x86_ops->prepare_guest_switch(vcpu)准备陷入到guest,prepare_guest_switch实现是vmx_save_host_state,顾名思义,就是保存host的当前状态。
kvm_x86_ops->prepare_guest_switch(vcpu); if (vcpu->fpu_active) kvm_load_guest_fpu(vcpu); kvm_load_guest_xcr0(vcpu); vcpu->mode = IN_GUEST_MODE; /* We should set ->mode before check ->requests, * see the comment in make_all_cpus_request. */ smp_mb(); local_irq_disable();
1
2
3
4
5
6
7
8
9
10
11
12
13
|
kvm_x86_ops
-
&
gt
;
prepare_guest_switch
(
vcpu
)
;
if
(
vcpu
-
&
gt
;
fpu_active
)
kvm_load_guest_fpu
(
vcpu
)
;
kvm_load_guest_xcr0
(
vcpu
)
;
vcpu
-
&
gt
;
mode
=
IN_GUEST_MODE
;
/* We should set ->mode before check ->requests,
* see the comment in make_all_cpus_request.
*/
smp_mb
(
)
;
local_irq_disable
(
)
;
|
然后加载guest的寄存器等信息,fpu,xcr0,将vcpu模式设置为guest状态,屏蔽中断响应,准备进入guest。但仍进行一次检查,vcpu->mode和vcpu->requests等,如果有问题,则恢复host状态。
kvm_guest_enter做了两件事:account_system_vtime计算虚拟机系统时间;rcu_virt_note_context_switch对rcu锁数据进行保护,完成上下文切换。
准备工作搞定,kvm_x86_ops->run(vcpu),开始运行guest,由vmx_vcpu_run实现。
if (vmx->emulation_required && emulate_invalid_guest_state) return; if (test_bit(VCPU_REGS_RSP, (unsigned long *)&vcpu->arch.regs_dirty)) vmcs_writel(GUEST_RSP, vcpu->arch.regs[VCPU_REGS_RSP]); if (test_bit(VCPU_REGS_RIP, (unsigned long *)&vcpu->arch.regs_dirty)) vmcs_writel(GUEST_RIP, vcpu->arch.regs[VCPU_REGS_RIP]);
|
if
(
vmx
-
&
gt
;
emulation_required
&
amp
;
&
amp
;
emulate_invalid_guest_state
)
return
;
if
(
test_bit
(
VCPU_REGS_RSP
,
(
unsigned
long
*
)
&
amp
;
vcpu
-
&
gt
;
arch
.
regs_dirty
)
)
vmcs_writel
(
GUEST_RSP
,
vcpu
-
&
gt
;
arch
.
regs
[
VCPU_REGS_RSP
]
)
;
if
(
test_bit
(
VCPU_REGS_RIP
,
(
unsigned
long
*
)
&
amp
;
vcpu
-
&
gt
;
arch
.
regs_dirty
)
)
vmcs_writel
(
GUEST_RIP
,
vcpu
-
&
gt
;
arch
.
regs
[
VCPU_REGS_RIP
]
)
;
|
判断模拟器,RSP,RIP寄存器值。
主要功能在这段内联汇编上
asm( /* Store host registers */ "push %%"R"dx; push %%"R"bp;" "push %%"R"cx nt" /* placeholder for guest rcx */ "push %%"R"cx nt" //如果vcpu host rsp和环境不等,则将其拷贝到vpu上 "cmp %%"R"sp, %c[host_rsp](%0) nt" "je 1f nt" "mov %%"R"sp, %c[host_rsp](%0) nt" __ex(ASM_VMX_VMWRITE_RSP_RDX) "nt"//__kvm_handle_fault_on_reboot write host rsp "1: nt" /* Reload cr2 if changed */ "mov %c[cr2](%0), %%"R"ax nt" "mov %%cr2, %%"R"dx nt" //环境上cr2值和vpu上的值不同,则将vpu上值拷贝到环境上 "cmp %%"R"ax, %%"R"dx nt" "je 2f nt" "mov %%"R"ax, %%cr2 nt" "2: nt" /* Check if vmlaunch of vmresume is needed */ "cmpl $0, %c[launched](%0) nt" /* Load guest registers. Don't clobber flags. */ "mov %c[rax](%0), %%"R"ax nt" "mov %c[rbx](%0), %%"R"bx nt" "mov %c[rdx](%0), %%"R"dx nt" "mov %c[rsi](%0), %%"R"si nt" "mov %c[rdi](%0), %%"R"di nt" "mov %c[rbp](%0), %%"R"bp nt" #ifdef CONFIG_X86_64 "mov %c[r8](%0), %%r8 nt" "mov %c[r9](%0), %%r9 nt" "mov %c[r10](%0), %%r10 nt" "mov %c[r11](%0), %%r11 nt" "mov %c[r12](%0), %%r12 nt" "mov %c[r13](%0), %%r13 nt" "mov %c[r14](%0), %%r14 nt" "mov %c[r15](%0), %%r15 nt" #endif "mov %c[rcx](%0), %%"R"cx nt" /* kills %0 (ecx) */ /* Enter guest mode */ //此处和cmpl $0, %c[launched](%0)是对应的,此处选择进入guest的两种模式 //RESUME和LAUNCH,通过__ex __kvm_handle_fault_on_reboot执行 "jne .Llaunched nt" __ex(ASM_VMX_VMLAUNCH) "nt" "jmp .Lkvm_vmx_return nt" ".Llaunched: " __ex(ASM_VMX_VMRESUME) "nt" //退出vmx,保存guest信息,加载host信息 ".Lkvm_vmx_return: " /* Save guest registers, load host registers, keep flags */ "mov %0, %c[wordsize](%%"R"sp) nt" "pop %0 nt" "mov %%"R"ax, %c[rax](%0) nt" "mov %%"R"bx, %c[rbx](%0) nt" "pop"Q" %c[rcx](%0) nt" "mov %%"R"dx, %c[rdx](%0) nt" "mov %%"R"si, %c[rsi](%0) nt" "mov %%"R"di, %c[rdi](%0) nt" "mov %%"R"bp, %c[rbp](%0) nt" #ifdef CONFIG_X86_64 "mov %%r8, %c[r8](%0) nt" "mov %%r9, %c[r9](%0) nt" "mov %%r10, %c[r10](%0) nt" "mov %%r11, %c[r11](%0) nt" "mov %%r12, %c[r12](%0) nt" "mov %%r13, %c[r13](%0) nt" "mov %%r14, %c[r14](%0) nt" "mov %%r15, %c[r15](%0) nt" #endif "mov %%cr2, %%"R"ax nt" "mov %%"R"ax, %c[cr2](%0) nt" "pop %%"R"bp; pop %%"R"dx nt" "setbe %c[fail](%0) nt" : : "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)), [wordsize]"i"(sizeof(ulong)) : "cc", "memory" , R"ax", R"bx", R"di", R"si" #ifdef CONFIG_X86_64 , "r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15" #endif
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
|
asm
(
/* Store host registers */
&
quot
;
push
%
%
&
quot
;
R
&
quot
;
dx
;
push
%
%
&
quot
;
R
&
quot
;
bp
;
&
quot
;
&
quot
;
push
%
%
&
quot
;
R
&
quot
;
cx
nt
&
quot
;
/* placeholder for guest rcx */
&
quot
;
push
%
%
&
quot
;
R
&
quot
;
cx
nt
&
quot
;
//如果vcpu host rsp和环境不等,则将其拷贝到vpu上
&
quot
;
cmp
%
%
&
quot
;
R
&
quot
;
sp
,
%
c
[
host_rsp
]
(
%
0
)
nt
&
quot
;
&
quot
;
je
1f
nt
&
quot
;
&
quot
;
mov
%
%
&
quot
;
R
&
quot
;
sp
,
%
c
[
host_rsp
]
(
%
0
)
nt
&
quot
;
__ex
(
ASM_VMX_VMWRITE_RSP_RDX
)
&
quot
;
nt
&
quot
;
//__kvm_handle_fault_on_reboot write host rsp
&
quot
;
1
:
nt
&
quot
;
/* Reload cr2 if changed */
&
quot
;
mov
%
c
[
cr2
]
(
%
0
)
,
%
%
&
quot
;
R
&
quot
;
ax
nt
&
quot
;
&
quot
;
mov
%
%
cr2
,
%
%
&
quot
;
R
&
quot
;
dx
nt
&
quot
;
//环境上cr2值和vpu上的值不同,则将vpu上值拷贝到环境上
&
quot
;
cmp
%
%
&
quot
;
R
&
quot
;
ax
,
%
%
&
quot
;
R
&
quot
;
dx
nt
&
quot
;
&
quot
;
je
2f
nt
&
quot
;
&
quot
;
mov
%
%
&
quot
;
R
&
quot
;
ax
,
%
%
cr2
nt
&
quot
;
&
quot
;
2
:
nt
&
quot
;
/* Check if vmlaunch of vmresume is needed */
&
quot
;
cmpl
$
0
,
%
c
[
launched
]
(
%
0
)
nt
&
quot
;
/* Load guest registers. Don't clobber flags. */
&
quot
;
mov
%
c
[
rax
]
(
%
0
)
,
%
%
&
quot
;
R
&
quot
;
ax
nt
&
quot
;
&
quot
;
mov
%
c
[
rbx
]
(
%
0
)
,
%
%
&
quot
;
R
&
quot
;
bx
nt
&
quot
;
&
quot
;
mov
%
c
[
rdx
]
(
%
0
)
,
%
%
&
quot
;
R
&
quot
;
dx
nt
&
quot
;
&
quot
;
mov
%
c
[
rsi
]
(
%
0
)
,
%
%
&
quot
;
R
&
quot
;
si
nt
&
quot
;
&
quot
;
mov
%
c
[
rdi
]
(
%
0
)
,
%
%
&
quot
;
R
&
quot
;
di
nt
&
quot
;
&
quot
;
mov
%
c
[
rbp
]
(
%
0
)
,
%
%
&
quot
;
R
&
quot
;
bp
nt
&
quot
;
#ifdef CONFIG_X86_64
&
quot
;
mov
%
c
[
r8
]
(
%
0
)
,
%
%
r8
nt
&
quot
;
&
quot
;
mov
%
c
[
r9
]
(
%
0
)
,
%
%
r9
nt
&
quot
;
&
quot
;
mov
%
c
[
r10
]
(
%
0
)
,
%
%
r10
nt
&
quot
;
&
quot
;
mov
%
c
[
r11
]
(
%
0
)
,
%
%
r11
nt
&
quot
;
&
quot
;
mov
%
c
[
r12
]
(
%
0
)
,
%
%
r12
nt
&
quot
;
&
quot
;
mov
%
c
[
r13
]
(
%
0
)
,
%
%
r13
nt
&
quot
;
&
quot
;
mov
%
c
[
r14
]
(
%
0
)
,
%
%
r14
nt
&
quot
;
&
quot
;
mov
%
c
[
r15
]
(
%
0
)
,
%
%
r15
nt
&
quot
;
#endif
&
quot
;
mov
%
c
[
rcx
]
(
%
0
)
,
%
%
&
quot
;
R
&
quot
;
cx
nt
&
quot
;
/* kills %0 (ecx) */
/* Enter guest mode */
//此处和cmpl $0, %c[launched](%0)是对应的,此处选择进入guest的两种模式
//RESUME和LAUNCH,通过__ex __kvm_handle_fault_on_reboot执行
&
quot
;
jne
.
Llaunched
nt
&
quot
;
__ex
(
ASM_VMX_VMLAUNCH
)
&
quot
;
nt
&
quot
;
&
quot
;
jmp
.
Lkvm_vmx_return
nt
&
quot
;
&
quot
;
.
Llaunched
:
&
quot
;
__ex
(
ASM_VMX_VMRESUME
)
&
quot
;
nt
&
quot
;
//退出vmx,保存guest信息,加载host信息
&
quot
;
.
Lkvm_vmx_return
:
&
quot
;
/* Save guest registers, load host registers, keep flags */
&
quot
;
mov
%
0
,
%
c
[
wordsize
]
(
%
%
&
quot
;
R
&
quot
;
sp
)
nt
&
quot
;
&
quot
;
pop
%
0
nt
&
quot
;
&
quot
;
mov
%
%
&
quot
;
R
&
quot
;
ax
,
%
c
[
rax
]
(
%
0
)
nt
&
quot
;
&
quot
;
mov
%
%
&
quot
;
R
&
quot
;
bx
,
%
c
[
rbx
]
(
%
0
)
nt
&
quot
;
&
quot
;
pop
&
quot
;
Q
&
quot
;
%
c
[
rcx
]
(
%
0
)
nt
&
quot
;
&
quot
;
mov
%
%
&
quot
;
R
&
quot
;
dx
,
%
c
[
rdx
]
(
%
0
)
nt
&
quot
;
&
quot
;
mov
%
%
&
quot
;
R
&
quot
;
si
,
%
c
[
rsi
]
(
%
0
)
nt
&
quot
;
&
quot
;
mov
%
%
&
quot
;
R
&
quot
;
di
,
%
c
[
rdi
]
(
%
0
)
nt
&
quot
;
&
quot
;
mov
%
%
&
quot
;
R
&
quot
;
bp
,
%
c
[
rbp
]
(
%
0
)
nt
&
quot
;
#ifdef CONFIG_X86_64
&
quot
;
mov
%
%
r8
,
%
c
[
r8
]
(
%
0
)
nt
&
quot
;
&
quot
;
mov
%
%
r9
,
%
c
[
r9
]
(
%
0
)
nt
&
quot
;
&
quot
;
mov
%
%
r10
,
%
c
[
r10
]
(
%
0
)
nt
&
quot
;
&
quot
;
mov
%
%
r11
,
%
c
[
r11
]
(
%
0
)
nt
&
quot
;
&
quot
;
mov
%
%
r12
,
%
c
[
r12
]
(
%
0
)
nt
&
quot
;
&
quot
;
mov
%
%
r13
,
%
c
[
r13
]
(
%
0
)
nt
&
quot
;
&
quot
;
mov
%
%
r14
,
%
c
[
r14
]
(
%
0
)
nt
&
quot
;
&
quot
;
mov
%
%
r15
,
%
c
[
r15
]
(
%
0
)
nt
&
quot
;
#endif
&
quot
;
mov
%
%
cr2
,
%
%
&
quot
;
R
&
quot
;
ax
nt
&
quot
;
&
quot
;
mov
%
%
&
quot
;
R
&
quot
;
ax
,
%
c
[
cr2
]
(
%
0
)
nt
&
quot
;
&
quot
;
pop
%
%
&
quot
;
R
&
quot
;
bp
;
pop
%
%
&
quot
;
R
&
quot
;
dx
nt
&
quot
;
&
quot
;
setbe
%
c
[
fail
]
(
%
0
)
nt
&
quot
;
:
:
&
quot
;
c
&
quot
;
(
vmx
)
,
&
quot
;
d
&
quot
;
(
(
unsigned
long
)
HOST_RSP
)
,
//下面加了前面寄存器的指针值,对应具体结构的值
[
launched
]
&
quot
;
i
&
quot
;
(
offsetof
(
struct
vcpu_vmx
,
launched
)
)
,
[
fail
]
&
quot
;
i
&
quot
;
(
offsetof
(
struct
vcpu_vmx
,
fail
)
)
,
[
host_rsp
]
&
quot
;
i
&
quot
;
(
offsetof
(
struct
vcpu_vmx
,
host_rsp
)
)
,
[
rax
]
&
quot
;
i
&
quot
;
(
offsetof
(
struct
vcpu_vmx
,
vcpu
.
arch
.
regs
[
VCPU_REGS_RAX
]
)
)
,
[
rbx
]
&
quot
;
i
&
quot
;
(
offsetof
(
struct
vcpu_vmx
,
vcpu
.
arch
.
regs
[
VCPU_REGS_RBX
]
)
)
,
[
rcx
]
&
quot
;
i
&
quot
;
(
offsetof
(
struct
vcpu_vmx
,
vcpu
.
arch
.
regs
[
VCPU_REGS_RCX
]
)
)
,
[
rdx
]
&
quot
;
i
&
quot
;
(
offsetof
(
struct
vcpu_vmx
,
vcpu
.
arch
.
regs
[
VCPU_REGS_RDX
]
)
)
,
[
rsi
]
&
quot
;
i
&
quot
;
(
offsetof
(
struct
vcpu_vmx
,
vcpu
.
arch
.
regs
[
VCPU_REGS_RSI
]
)
)
,
[
rdi
]
&
quot
;
i
&
quot
;
(
offsetof
(
struct
vcpu_vmx
,
vcpu
.
arch
.
regs
[
VCPU_REGS_RDI
]
)
)
,
[
rbp
]
&
quot
;
i
&
quot
;
(
offsetof
(
struct
vcpu_vmx
,
vcpu
.
arch
.
regs
[
VCPU_REGS_RBP
]
)
)
,
#ifdef CONFIG_X86_64
[
r8
]
&
quot
;
i
&
quot
;
(
offsetof
(
struct
vcpu_vmx
,
vcpu
.
arch
.
regs
[
VCPU_REGS_R8
]
)
)
,
[
r9
]
&
quot
;
i
&
quot
;
(
offsetof
(
struct
vcpu_vmx
,
vcpu
.
arch
.
regs
[
VCPU_REGS_R9
]
)
)
,
[
r10
]
&
quot
;
i
&
quot
;
(
offsetof
(
struct
vcpu_vmx
,
vcpu
.
arch
.
regs
[
VCPU_REGS_R10
]
)
)
,
[
r11
]
&
quot
;
i
&
quot
;
(
offsetof
(
struct
vcpu_vmx
,
vcpu
.
arch
.
regs
[
VCPU_REGS_R11
]
)
)
,
[
r12
]
&
quot
;
i
&
quot
;
(
offsetof
(
struct
vcpu_vmx
,
vcpu
.
arch
.
regs
[
VCPU_REGS_R12
]
)
)
,
[
r13
]
&
quot
;
i
&
quot
;
(
offsetof
(
struct
vcpu_vmx
,
vcpu
.
arch
.
regs
[
VCPU_REGS_R13
]
)
)
,
[
r14
]
&
quot
;
i
&
quot
;
(
offsetof
(
struct
vcpu_vmx
,
vcpu
.
arch
.
regs
[
VCPU_REGS_R14
]
)
)
,
[
r15
]
&
quot
;
i
&
quot
;
(
offsetof
(
struct
vcpu_vmx
,
vcpu
.
arch
.
regs
[
VCPU_REGS_R15
]
)
)
,
#endif
[
cr2
]
&
quot
;
i
&
quot
;
(
offsetof
(
struct
vcpu_vmx
,
vcpu
.
arch
.
cr2
)
)
,
[
wordsize
]
&
quot
;
i
&
quot
;
(
sizeof
(
ulong
)
)
:
&
quot
;
cc
&
quot
;
,
&
quot
;
memory
&
quot
;
,
R
&
quot
;
ax
&
quot
;
,
R
&
quot
;
bx
&
quot
;
,
R
&
quot
;
di
&
quot
;
,
R
&
quot
;
si
&
quot
;
#ifdef CONFIG_X86_64
,
&
quot
;
r8
&
quot
;
,
&
quot
;
r9
&
quot
;
,
&
quot
;
r10
&
quot
;
,
&
quot
;
r11
&
quot
;
,
&
quot
;
r12
&
quot
;
,
&
quot
;
r13
&
quot
;
,
&
quot
;
r14
&
quot
;
,
&
quot
;
r15
&
quot
;
#endif
|
以上代码相对容易理解的,根据注释大致清楚了具体作用。
然后就是恢复系统NMI等中断:
vmx_complete_atomic_exit(vmx); vmx_recover_nmi_blocking(vmx); vmx_complete_interrupts(vmx);
|
vmx_complete_atomic_exit
(
vmx
)
;
vmx_recover_nmi_blocking
(
vmx
)
;
vmx_complete_interrupts
(
vmx
)
;
|
回到vcpu_enter_guest,通过hw_breakpoint_restore恢复硬件断点。
if (hw_breakpoint_active()) hw_breakpoint_restore(); kvm_get_msr(vcpu, MSR_IA32_TSC, &vcpu->arch.last_guest_tsc); //设置vcpu模式,恢复host相关内容 vcpu->mode = OUTSIDE_GUEST_MODE; smp_wmb(); local_irq_enable(); ++vcpu->stat.exits; /* * We must have an instruction between local_irq_enable() and * kvm_guest_exit(), so the timer interrupt isn't delayed by * the interrupt shadow. The stat.exits increment will do nicely. * But we need to prevent reordering, hence this barrier(): */ barrier(); //刷新系统时间 kvm_guest_exit(); preempt_enable(); vcpu->srcu_idx = srcu_read_lock(&vcpu->kvm->srcu); /* * Profile KVM exit RIPs: */ if (unlikely(prof_on == KVM_PROFILING)) { unsigned long rip = kvm_rip_read(vcpu); profile_hit(KVM_PROFILING, (void *)rip); } kvm_lapic_sync_from_vapic(vcpu); //处理vmx退出 r = kvm_x86_ops->handle_exit(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
|
if
(
hw_breakpoint_active
(
)
)
hw_breakpoint_restore
(
)
;
kvm_get_msr
(
vcpu
,
MSR_IA32_TSC
,
&
amp
;
vcpu
-
&
gt
;
arch
.
last_guest_tsc
)
;
//设置vcpu模式,恢复host相关内容
vcpu
-
&
gt
;
mode
=
OUTSIDE_GUEST_MODE
;
smp_wmb
(
)
;
local_irq_enable
(
)
;
++
vcpu
-
&
gt
;
stat
.
exits
;
/*
* We must have an instruction between local_irq_enable() and
* kvm_guest_exit(), so the timer interrupt isn't delayed by
* the interrupt shadow. The stat.exits increment will do nicely.
* But we need to prevent reordering, hence this barrier():
*/
barrier
(
)
;
//刷新系统时间
kvm_guest_exit
(
)
;
preempt_enable
(
)
;
vcpu
-
&
gt
;
srcu_idx
=
srcu_read_lock
(
&
amp
;
vcpu
-
&
gt
;
kvm
-
&
gt
;
srcu
)
;
/*
* Profile KVM exit RIPs:
*/
if
(
unlikely
(
prof_on
==
KVM_PROFILING
)
)
{
unsigned
long
rip
=
kvm_rip_read
(
vcpu
)
;
profile_hit
(
KVM_PROFILING
,
(
void
*
)
rip
)
;
}
kvm_lapic_sync_from_vapic
(
vcpu
)
;
//处理vmx退出
r
=
kvm_x86_ops
-
&
gt
;
handle_exit
(
vcpu
)
;
|
handle_exit退出函数由vmx_handle_exit实现,主要设置vcpu->run->exit_reason,让外部感知退出原因,并对应处理。对于vpu而言,handle_exit只是意味着一个传统linux一个时间片的结束,后续的工作都是由handle完成的,handle_exit对应的函数集如下:
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, [EXIT_REASON_VMCALL] = handle_vmcall, [EXIT_REASON_VMCLEAR] = handle_vmx_insn, [EXIT_REASON_VMLAUNCH] = handle_vmx_insn, [EXIT_REASON_VMPTRLD] = handle_vmx_insn, [EXIT_REASON_VMPTRST] = handle_vmx_insn, [EXIT_REASON_VMREAD] = handle_vmx_insn, [EXIT_REASON_VMRESUME] = handle_vmx_insn, [EXIT_REASON_VMWRITE] = handle_vmx_insn, [EXIT_REASON_VMOFF] = handle_vmx_insn, [EXIT_REASON_VMON] = handle_vmx_insn, [EXIT_REASON_TPR_BELOW_THRESHOLD] = handle_tpr_below_threshold, [EXIT_REASON_APIC_ACCESS] = handle_apic_access, [EXIT_REASON_WBINVD] = handle_wbinvd, [EXIT_REASON_XSETBV] = handle_xsetbv, [EXIT_REASON_TASK_SWITCH] = handle_task_switch, [EXIT_REASON_MCE_DURING_VMENTRY] = handle_machine_check, [EXIT_REASON_EPT_VIOLATION] = handle_ept_violation, [EXIT_REASON_EPT_MISCONFIG] = handle_ept_misconfig, [EXIT_REASON_PAUSE_INSTRUCTION] = handle_pause, [EXIT_REASON_MWAIT_INSTRUCTION] = handle_invalid_op, [EXIT_REASON_MONITOR_INSTRUCTION] = handle_invalid_op, };
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
|
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
,
[
EXIT_REASON_VMCALL
]
=
handle_vmcall
,
[
EXIT_REASON_VMCLEAR
]
=
handle_vmx_insn
,
[
EXIT_REASON_VMLAUNCH
]
=
handle_vmx_insn
,
[
EXIT_REASON_VMPTRLD
]
=
handle_vmx_insn
,
[
EXIT_REASON_VMPTRST
]
=
handle_vmx_insn
,
[
EXIT_REASON_VMREAD
]
=
handle_vmx_insn
,
[
EXIT_REASON_VMRESUME
]
=
handle_vmx_insn
,
[
EXIT_REASON_VMWRITE
]
=
handle_vmx_insn
,
[
EXIT_REASON_VMOFF
]
=
handle_vmx_insn
,
[
EXIT_REASON_VMON
]
=
handle_vmx_insn
,
[
EXIT_REASON_TPR_BELOW_THRESHOLD
]
=
handle_tpr_below_threshold
,
[
EXIT_REASON_APIC_ACCESS
]
=
handle_apic_access
,
[
EXIT_REASON_WBINVD
]
=
handle_wbinvd
,
[
EXIT_REASON_XSETBV
]
=
handle_xsetbv
,
[
EXIT_REASON_TASK_SWITCH
]
=
handle_task_switch
,
[
EXIT_REASON_MCE_DURING_VMENTRY
]
=
handle_machine_check
,
[
EXIT_REASON_EPT_VIOLATION
]
=
handle_ept_violation
,
[
EXIT_REASON_EPT_MISCONFIG
]
=
handle_ept_misconfig
,
[
EXIT_REASON_PAUSE_INSTRUCTION
]
=
handle_pause
,
[
EXIT_REASON_MWAIT_INSTRUCTION
]
=
handle_invalid_op
,
[
EXIT_REASON_MONITOR_INSTRUCTION
]
=
handle_invalid_op
,
}
;
|
有handle_task_switch进行任务切换,handle_io处理qemu的外部模拟IO等,具体处理内容后面在写。
再次退回到__vcpu_run函数,在while (r > 0)中,循环受vcpu_enter_guest返回值控制,只有运行异常的时候才退出循环,否则通过kvm_resched一直运行下去。
if (need_resched()) { srcu_read_unlock(&kvm->srcu, vcpu->srcu_idx); kvm_resched(vcpu); vcpu->srcu_idx = srcu_read_lock(&kvm->srcu); }
|
if
(
need_resched
(
)
)
{
srcu_read_unlock
(
&
amp
;
kvm
-
&
gt
;
srcu
,
vcpu
-
&
gt
;
srcu_idx
)
;
kvm_resched
(
vcpu
)
;
vcpu
-
&
gt
;
srcu_idx
=
srcu_read_lock
(
&
amp
;
kvm
-
&
gt
;
srcu
)
;
}
|
再退就到了kvm_arch_vcpu_ioctl_run函数,此时kvm run的执行也结束。
KVM cpu虚拟化的理解基本如上,涉及到的具体细节有时间后开篇另说。
KVM源代码分析未完待续
终于把KVM源代码分析3:CPU虚拟化写完了,虽然还有run的部分另外在写,还是先看一下内存虚拟化部分。
代码版本:https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git v4.5
在虚拟机的创建与运行中pc_init_pci负责在qemu中初始化虚拟机,内存初始化也是在这里完成的,还是一步步从qemu说起,在vl.c的main函数中有ram_size参数,由qemu入参标识QEMU_OPTION_m设定,顾名思义就是虚拟机内存的大小,通过machine->init一步步传递给pc_init1函数。在这里分出了above_4g_mem_size和below_4g_mem_size,即高低端内存(也不一定是32bit机器..),然后开始初始化内存,即pc_memory_init,内存通过memory_region_init_ram下面的qemu_ram_alloc分配,使用qemu_ram_alloc_from_ptr。
插播qemu对内存条的模拟管理,是通过RAMBlock和ram_list管理的,RAMBlock就是每次申请的内存池,ram_list则是RAMBlock的链表,他们结构如下:
typedef struct RAMBlock { //对应宿主的内存地址 uint8_t *host; //block在ramlist中的偏移 ram_addr_t offset; //block长度 ram_addr_t length; uint32_t flags; //block名字 char idstr[256]; QLIST_ENTRY(RAMBlock) next; #if defined(__linux__) && !defined(TARGET_S390X) int fd; #endif } RAMBlock; typedef struct RAMList { //看代码理解就是list的head,但是不知道为啥叫dirty... uint8_t *phys_dirty; QLIST_HEAD(ram, RAMBlock) blocks; } RAMList;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
typedef
struct
RAMBlock
{
//对应宿主的内存地址
uint8_t
*
host
;
//block在ramlist中的偏移
ram_addr_t
offset
;
//block长度
ram_addr_t
length
;
uint32_t
flags
;
//block名字
char
idstr
[
256
]
;
QLIST_ENTRY
(
RAMBlock
)
next
;
#if defined(__linux__) && !defined(TARGET_S390X)
int
fd
;
#endif
}
RAMBlock
;
typedef
struct
RAMList
{
//看代码理解就是list的head,但是不知道为啥叫dirty...
uint8_t
*
phys_dirty
;
QLIST_HEAD
(
ram
,
RAMBlock
)
blocks
;
}
RAMList
;
|
下面再回到qemu_ram_alloc_from_ptr函数,使用find_ram_offset赋值给new block的offset,find_ram_offset具体工作模型已经在KVM源代码分析2:虚拟机的创建与运行中提到了,不赘述。然后是一串判断,在kvm_enabled的情况下使用new_block->host = kvm_vmalloc(size),最终内存是qemu_vmalloc分配的,使用qemu_memalign干活。
void *qemu_memalign(size_t alignment, size_t size) { void *ptr; //使用posix进行内存针对页大小对齐 #if defined(_POSIX_C_SOURCE) && !defined(__sun__) int ret; ret = posix_memalign(&ptr, alignment, size); if (ret != 0) { fprintf(stderr, "Failed to allocate %zu B: %sn", size, strerror(ret)); abort(); } #elif defined(CONFIG_BSD) ptr = qemu_oom_check(valloc(size)); #else //所谓检查oom就是看memalign对应malloc申请内存是否成功 ptr = qemu_oom_check(memalign(alignment, size)); #endif trace_qemu_memalign(alignment, size, ptr); return ptr; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
void
*
qemu_memalign
(
size_t
alignment
,
size_t
size
)
{
void
*
ptr
;
//使用posix进行内存针对页大小对齐
#if defined(_POSIX_C_SOURCE) && !defined(__sun__)
int
ret
;
ret
=
posix_memalign
(
&
ptr
,
alignment
,
size
)
;
if
(
ret
!=
0
)
{
fprintf
(
stderr
,
"Failed to allocate %zu B: %sn"
,
size
,
strerror
(
ret
)
)
;
abort
(
)
;
}
#elif defined(CONFIG_BSD)
ptr
=
qemu_oom_check
(
valloc
(
size
)
)
;
#else
//所谓检查oom就是看memalign对应malloc申请内存是否成功
ptr
=
qemu_oom_check
(
memalign
(
alignment
,
size
)
)
;
#endif
trace_qemu_memalign
(
alignment
,
size
,
ptr
)
;
return
ptr
;
}
|
以上qemu_vmalloc进行内存申请就结束了。在qemu_ram_alloc_from_ptr函数末尾则是将block添加到链表,realloc整个ramlist,用memset初始化整个ramblock,madvise对内存使用限定。
然后一层层的退回到pc_memory_init函数。
此时pc.ram已经分配完成,ram_addr已经拿到了分配的内存地址,MemoryRegion ram初始化完成。下面则是对已有的ram进行分段,即ram-below-4g和ram-above-4g,也就是高端内存和低端内存。用memory_region_init_alias初始化子MemoryRegion,然后将memory_region_add_subregion添加关联起来,memory_region_add_subregion具体细节“KVM源码分析2”中已经说了,参考对照着看吧,中间很多映射代码过程也只是qemu遗留的软件实现,没看到具体存在的意义,直接看到kvm_set_user_memory_region函数,内核真正需要kvm_vm_ioctl传递过去的参数是什么, struct kvm_userspace_memory_region mem而已,也就是
struct kvm_userspace_memory_region { __u32 slot; __u32 flags; __u64 guest_phys_addr; __u64 memory_size; /* bytes */ __u64 userspace_addr; /* start of the userspace allocated memory */ };
|
struct
kvm_userspace_memory_region
{
__u32
slot
;
__u32
flags
;
__u64
guest_phys_addr
;
__u64
memory_size
;
/* bytes */
__u64
userspace_addr
;
/* start of the userspace allocated memory */
}
;
|
kvm_vm_ioctl进入到内核是在KVM_SET_USER_MEMORY_REGION参数中,即执行kvm_vm_ioctl_set_memory_region,然后一直向下,到__kvm_set_memory_region函数,check_memory_region_flags检查mem->flags是否合法,而当前flag也就使用了两位,KVM_MEM_LOG_DIRTY_PAGES和KVM_MEM_READONLY,从qemu传递过来只能是KVM_MEM_LOG_DIRTY_PAGES,下面是对mem中各参数的合规检查,(mem->memory_size & (PAGE_SIZE – 1))要求以页为单位,(mem->guest_phys_addr & (PAGE_SIZE – 1))要求guest_phys_addr页对齐,而((mem->userspace_addr & (PAGE_SIZE – 1)) || !access_ok(VERIFY_WRITE,(void __user *)(unsigned long)mem->userspace_addr,mem->memory_size))则保证host的线性地址页对齐而且该地址域有写权限。
id_to_memslot则是根据qemu的内存槽号得到kvm结构下的内存槽号,转换关系来自id_to_index数组,那映射关系怎么来的,映射关系是一一对应的,在kvm_create_vm虚拟机创建过程中,kvm_init_memslots_id初始化对应关系,即slots->id_to_index[i] = slots->memslots[i].id = i,当前映射是没有意义的,估计是为了后续扩展而存在的。
扩充了new的kvm_memory_slot,下面直接在代码中注释更方便:
//映射内存有大小,不是删除内存条 if (npages) { //内存槽号没有虚拟内存条,意味内存新创建 if (!old.npages) change = KVM_MR_CREATE; else { /* Modify an existing slot. */ //修改已存在的内存修改标志或者平移映射地址 //下面是不能处理的状态(内存条大小不能变,物理地址不能变,不能修改只读) if ((mem->userspace_addr != old.userspace_addr) || (npages != old.npages) || ((new.flags ^ old.flags) & KVM_MEM_READONLY)) goto out; //guest地址不同,内存条平移 if (base_gfn != old.base_gfn) change = KVM_MR_MOVE; else if (new.flags != old.flags) //修改属性 change = KVM_MR_FLAGS_ONLY; else { /* Nothing to change. */ r = 0; goto out; } } } else if (old.npages) { //申请插入的内存为0,而内存槽上有内存,意味删除 change = KVM_MR_DELETE; } else /* Modify a non-existent slot: disallowed. */ goto out;
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
|
//映射内存有大小,不是删除内存条
if
(
npages
)
{
//内存槽号没有虚拟内存条,意味内存新创建
if
(
!
old
.
npages
)
change
=
KVM_MR_CREATE
;
else
{
/* Modify an existing slot. */
//修改已存在的内存修改标志或者平移映射地址
//下面是不能处理的状态(内存条大小不能变,物理地址不能变,不能修改只读)
if
(
(
mem
->
userspace_addr
!=
old
.
userspace_addr
)
||
(
npages
!=
old
.
npages
)
||
(
(
new
.
flags
^
old
.
flags
)
&
KVM_MEM_READONLY
)
)
goto
out
;
//guest地址不同,内存条平移
if
(
base_gfn
!=
old
.
base_gfn
)
change
=
KVM_MR_MOVE
;
else
if
(
new
.
flags
!=
old
.
flags
)
//修改属性
change
=
KVM_MR_FLAGS_ONLY
;
else
{
/* Nothing to change. */
r
=
0
;
goto
out
;
}
}
}
else
if
(
old
.
npages
)
{
//申请插入的内存为0,而内存槽上有内存,意味删除
change
=
KVM_MR_DELETE
;
}
else
/* Modify a non-existent slot: disallowed. */
goto
out
;
|
另外看kvm_mr_change就知道memslot的变动值了:
enum kvm_mr_change { KVM_MR_CREATE, KVM_MR_DELETE, KVM_MR_MOVE, KVM_MR_FLAGS_ONLY, };
|
enum
kvm_mr_change
{
KVM_MR_CREATE
,
KVM_MR_DELETE
,
KVM_MR_MOVE
,
KVM_MR_FLAGS_ONLY
,
}
;
|
在往下是一段检查
if ((change == KVM_MR_CREATE) || (change == KVM_MR_MOVE)) { /* Check for overlaps */ r = -EEXIST; kvm_for_each_memslot(slot, kvm->memslots) { if ((slot->id >= KVM_USER_MEM_SLOTS) || //下面排除掉准备操作的内存条,在KVM_MR_MOVE中是有交集的 (slot->id == mem->slot)) continue; //下面就是当前已有的slot与new在guest线性区间上有交集 if (!((base_gfn + npages <= slot->base_gfn) || (base_gfn >= slot->base_gfn + slot->npages))) goto out; //out错误码就是EEXIST } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
if
(
(
change
==
KVM_MR_CREATE
)
||
(
change
==
KVM_MR_MOVE
)
)
{
/* Check for overlaps */
r
=
-
EEXIST
;
kvm_for_each_memslot
(
slot
,
kvm
->
memslots
)
{
if
(
(
slot
->
id
>=
KVM_USER_MEM_SLOTS
)
||
//下面排除掉准备操作的内存条,在KVM_MR_MOVE中是有交集的
(
slot
->
id
==
mem
->
slot
)
)
continue
;
//下面就是当前已有的slot与new在guest线性区间上有交集
if
(
!
(
(
base_gfn
+
npages
<=
slot
->
base_gfn
)
||
(
base_gfn
>=
slot
->
base_gfn
+
slot
->
npages
)
)
)
goto
out
;
//out错误码就是EEXIST
}
}
|
如果是新插入内存条,代码则走入kvm_arch_create_memslot函数,里面主要是一个循环,KVM_NR_PAGE_SIZES是分页的级数,此处是3,第一次循环,lpages = gfn_to_index(slot->base_gfn + npages – 1,slot->base_gfn, level) + 1,lpages就是一级页表所需要的page数,大致是npages>>0*9,然后为slot->arch.rmap[i]申请了内存空间,此处可以猜想,rmap就是一级页表了,继续看,lpages约为npages>>1*9,此处又多为lpage_info申请了同等空间,然后对lpage_info初始化赋值,现在看不到lpage_info的具体作用,看到后再补上。整体上看kvm_arch_create_memslot做了一个3级的软件页表。
如果有脏页,并且脏页位图为空,则分配脏页位图, kvm_create_dirty_bitmap实际就是”页数/8″.
if ((new.flags & KVM_MEM_LOG_DIRTY_PAGES) && !new.dirty_bitmap) { if (kvm_create_dirty_bitmap(&new) < 0) goto out_free; }
|
if
(
(
new
.
flags
&
KVM_MEM_LOG_DIRTY_PAGES
)
&&
!
new
.
dirty_bitmap
)
{
if
(
kvm_create_dirty_bitmap
(
&
new
)
<
0
)
goto
out_free
;
}
|
当内存条的改变是KVM_MR_DELETE或者KVM_MR_MOVE,先申请一个slots,把kvm->memslots暂存到这里,首先通过id_to_memslot获取准备插入的内存条对应到kvm的插槽是slot,无论删除还是移动,将其先标记为KVM_MEMSLOT_INVALID,然后是install_new_memslots,其实就是更新了一下slots->generation的值,
——–待编辑———–