kk Blog —— 通用基础

date [-d @int|str] [+%s|"+%F %T"]

KVM源代码分析2:虚拟机的创建与运行

http://www.oenhan.com/kvm-src-2-vm-run

基本原理里面提到kvm虚拟化由用户态程序Qemu和内核态驱动kvm配合完成,qemu负责HOST用户态层面进程管理,IO处理等,KVM负责把qemu的部分指令在硬件上直接实现,从虚拟机的创建和运行上看,qemu的代码占了流程上的主要部分。下面的代码主要主要针对与qemu。 而Qemu和kvm的配合流程如下:

接下来参考上图分析qemu代码流程: 从vl.c代码的main函数开始。 atexit(qemu_run_exit_notifiers)注册了qemu的退出处理函数,后面在具体看qemu_run_exit_notifiers函数。 module_call_init则开始初始化qemu的各个模块,陆陆续续的有以下参数:

1
2
3
4
5
6
7
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,是在machine_init(pc_machine_init)函数注册的,pc_machine_init则是针对PC(即是X86)的qemu虚拟化方案,至于它被谁调用的,把machine_init这个宏展开,看到它前面的修饰是__attribute__((constructor)),这个导致machine_init或者type_init等会在main()之前就被执行。module_call_init针对X86则是调用machine_init,即pc_machine_init,完成了虚拟的机器类型注册。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void pc_machine_init(void)
{
	qemu_register_machine(&pc_machine_v1_3);
	qemu_register_machine(&pc_machine_v1_2);
	qemu_register_machine(&pc_machine_v1_1);
	qemu_register_machine(&pc_machine_v1_0);
	qemu_register_machine(&pc_machine_v1_0_qemu_kvm);
	qemu_register_machine(&pc_machine_v0_15);
	qemu_register_machine(&pc_machine_v0_14);
	qemu_register_machine(&pc_machine_v0_13);
	qemu_register_machine(&pc_machine_v0_12);
	qemu_register_machine(&pc_machine_v0_11);
	qemu_register_machine(&pc_machine_v0_10);
}
machine_init(pc_machine_init);

下面涉及对OPT入参的解析过程略过不提。 qemu准备模拟的机器的类型从下面语句获得:

1
2
current_machine = MACHINE(object_new(object_class_get_name(
					  OBJECT_CLASS(machine_class))));

machine_class则是通过入参传入的

1
2
3
4
5
6
7
8
9
10
11
case QEMU_OPTION_machine:
	olist = qemu_find_opts("machine");
	opts = qemu_opts_parse(olist, optarg, 1);
	if (!opts) {
		exit(1);
	}
	optarg = qemu_opt_get(opts, "type");
	if (optarg) {
		machine_class = machine_parse(optarg);
	}
	break;

man qemu

1
2
3
-machine [type=]name[,prop=value[,...]]
	Select the emulated machine by name.
	Use "-machine help" to list available machines

cpu_exec_init_all中记录了CPU执行前的一些初始化工作。

qemu_set_log设置日志输出,kvm对外的日志是从这里配置的。

中间的代码忽略过,直接到configure_accelerator函数,进行虚拟机模拟器的配置, 这是一个重点关注的函数,它调用了accel_list[i].init()函数,而accel_list初始化如下:

1
2
3
4
5
6
7
8
9
10
11
12
static struct {
	const char *opt_name;
	const char *name;
	int (*available)(void);
	int (*init)(QEMUMachine *);
	bool *allowed;
} accel_list[] = {
	{ "tcg", "tcg", tcg_available, tcg_init, &tcg_allowed },
	{ "xen", "Xen", xen_available, xen_init, &xen_allowed },
	{ "kvm", "KVM", kvm_available, kvm_init, &kvm_allowed },
	{ "qtest", "QTest", qtest_available, qtest_init_accel, &qtest_allowed },
};

kvm_available很简单,重点在kvm_init上,实际调用kvm_init函数,kvm_init通过qemu_open(“/dev/kvm”)检查内核驱动插入情况,通过kvm_ioctl(s, KVM_GET_API_VERSION, 0)获取API接口版本,最重点是调用了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)实现,具体内核态的工作内容后面分析。到此模拟器init的工作就完成了,最主要的工作就是创建的虚拟机。

下面是guest启动的内核配置,qemu线程的初始化等,涉及虚拟机CPU,内存初始化在下面:

1
2
3
4
5
6
7
8
9
10
QEMUMachineInitArgs args = { .machine = machine,
				.ram_size = ram_size,
				.boot_order = boot_order,
				.kernel_filename = kernel_filename,
				.kernel_cmdline = kernel_cmdline,
				.initrd_filename = initrd_filename,
				.cpu_model = cpu_model };

current_machine->init_args = args;
machine->init(¤t_machine->init_args);

前面提到了pc_machine_init注册虚拟机器类型,我们直接看pc_machine_v1_0_qemu_kvm即可,QEMUMachine对应的结构如下:

1
2
3
4
5
6
7
8
9
10
11
static QEMUMachine pc_machine_v1_0_qemu_kvm = {
	PC_I440FX_1_2_MACHINE_OPTIONS,
	.name = "pc-1.0-qemu-kvm",
	.alias = "pc-1.0-precise",
	.init = pc_init_pci_1_2_qemu_kvm,
	.compat_props = (GlobalProperty[]) {
		PC_COMPAT_1_0_QEMU_KVM,
		{ /* end of list */ }
	},
	.hw_version = "1.0",
};

init函数是pc_init_pci_1_2_qemu_kvm,去除中间的一些兼容性代码工作,流程就是pc_init_pci->pc_init1。

在pc_init1中重点看两个函数,pc_cpus_init和pc_memory_init,顾名思义,CPU和内存的初始化,中断等初始化先忽略掉,先看这两个。

pc_cpus_init首先for循环中针对每个CPU初始化,即pc_new_cpu,里面有cpu_x86_init函数,主要就是把CPUX86State填充了一下,涉及到CPUID和其他的feature。下面是x86_cpu_realize,即唤醒CPU,重点是qemu_init_vcpu,MCE忽略掉,走到qemu_kvm_start_vcpu,qemu创建VCPU,如下:

1
2
3
4
5
6
7
//创建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对应的注册关系。根据下句完成工作

1
#define type_init(function) module_init(function, MODULE_INIT_QOM)

从上面看,pc_cpus_init函数过程已经理顺了,下面看一下,vcpu所在的线程对应的qemu_kvm_cpu_thread_fn中:

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种情况,如代码:

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函数中等待运行:

1
2
3
4
5
6
7
8
9
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函数决定的

1
2
3
4
5
6
7
8
9
10
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给内核。