

KVM源代码分析2:虚拟机的创建与运行
source link: http://abcdxyzk.github.io/blog/2015/07/29/kvm-src2/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

KVM源代码分析2:虚拟机的创建与运行
2015-07-29 14:42:00
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的各个模块,陆陆续续的有以下参数:
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,完成了虚拟的机器类型注册。
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准备模拟的机器的类型从下面语句获得:
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(olist, optarg, 1);
if (!opts) {
exit(1);
}
optarg = qemu_opt_get(opts, "type");
if (optarg) {
machine_class = machine_parse(optarg);
}
break;
man qemu
-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初始化如下:
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,内存初始化在下面:
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对应的结构如下:
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,如下:
//创建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)
从上面看,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);
}
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;
}
}
重点在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);
}
判断条件就是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);
}
}
如此kvm_cpu_exec就真正进入执行阶段,即通过kvm_vcpu_ioctl传递KVM_RUN给内核。
Recommend
-
14
KVM源代码分析4:内存虚拟化 2015-07-29 14:49:00 http://www.oenhan.com/kvm-src-4-mem 在虚拟机的创建与运行中pc_init_pci负责在qemu中初始化虚拟机,内...
-
12
KVM源代码分析3:CPU虚拟化 2015-07-29 14:48:00 http://www.oenhan.com/kvm-src-3-cpu 在虚拟机的创建与运行章节里面笼统的介绍了KVM在qemu中的创建和运...
-
7
KVM源代码分析1:基本工作原理 2015-07-29 14:38:00 http://www.oenhan.com/kvm-src-1 1.KVM模型结构 所有的虚拟化方案都是两个模块:guest和host。...
-
11
Date Categories (634) 2020[+](70)
-
13
ubuntu安装kvm虚拟机 2015-07-07 14:35:00 sudo apt-get install qemu-kvm libvirt-bin virt-manager用 virt-manager 参考
-
9
centos安装kvm虚拟机 2015-07-07 14:33:00 最好在centos6装 TODO 虚拟机网桥连接没试 ...
-
6
kvm虚拟机开机之后报错Failed to mount |坐而言不如起而行! 二丫讲梵对于注定会优秀的人来说,他所需要的,只是时间!手懒得,必受贫穷,手勤的,必得富足----《圣经》帮助别人,成就自己。愿君在本站能真正有所收获!如果你在...
-
8
Linux KVM 下安装 Windows 虚拟机 可能对于很多人来说很简单,但是我自己看起来还是有点东西,只能写一下. 首先需要Virtio的驱动包以及系统安装镜像,然后开始安装. virt-install --virt-type=kvm \ -...
-
7
在 Archlinux 中安装sudo pacman -Sy qemu libvirt ebtables dnsmasq bridge-utils virt-managerkvm 负责 CPU 和内存的虚拟化qemu 向 Guest OS 模拟硬件(例如,CPU,网卡,磁盘,等)ovmf 为虚拟机启用UEFI支持
-
6
[译] 100 行 C 代码创建一个 KVM 虚拟机(2019) Published at 2023-11-05 | Last Update 2023-11-05 本文核心内容来自 2019 年的一篇英文博客: KVM HOST IN A FEW LINES OF...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK