Qemu-KVM虛擬機器初始化及建立過程原始碼簡要分析(二)
阿新 • • 發佈:2019-01-05
前面我們講了KVM核心層建立及初始化虛擬機器的一些工作過程,現在講一下Qemu層的流程以及與KVM核心層的配合過程。
Qemu層是從vl.c中的main()函式開始的,這裡通過在程式碼中新增一些註釋的方式來進行講解,中間省略了很多不重要或者我也沒有搞清楚的程式碼。 囧
前面提到過e->init()函式,並沒有展開細說,現在來說一下這個函式。實際上e->init()函式是在machine_init(pc_machine_init)函式註冊時註冊到ModuleTypeList的ModuleEntry上的,module_call_init()針對X86架構時呼叫amchine_init(),隨即呼叫pc_machine_init()函式,程式碼如下:int main(int argc, char **argv, char **envp) { ...... atexit(qemu_run_exit_notifiers);//註冊了Qemu的退出函式 ...... module_call_init(MODULE_INIT_QOM);//初始化Qemu的各個模組,具體見下面註釋 /* module_call_init實際上是設計了一個函式連結串列ModuleTypeList,引數作為一個Type,相關的函式註冊到這個函式連結串列上, 然後內部通過呼叫e->init()函式完成所有Type相關的裝置的初始化。關於e->init()的具體內容後面再細說。 Type總計有這些型別: typedef enum { MODULE_INIT_BLOCK, MODULE_INIT_MACHINE, MODULE_INIT_QAPI, MODULE_INIT_QOM, MODULE_INIT_MAX } module_init_type; */ qemu_add_opts(&qemu_drive_opts);//將各種函式指標(也就是操作)集合新增到連結串列中 qemu_add_opts(&qemu_chardev_opts); qemu_add_opts(&qemu_device_opts); qemu_add_opts(&qemu_netdev_opts); qemu_add_opts(&qemu_net_opts); qemu_add_opts(&qemu_rtc_opts); qemu_add_opts(&qemu_global_opts); qemu_add_opts(&qemu_mon_opts); qemu_add_opts(&qemu_trace_opts); qemu_add_opts(&qemu_option_rom_opts); qemu_add_opts(&qemu_machine_opts); qemu_add_opts(&qemu_boot_opts); qemu_add_opts(&qemu_sandbox_opts); qemu_add_opts(&qemu_add_fd_opts); qemu_add_opts(&qemu_object_opts); qemu_add_opts(&qemu_tpmdev_opts); qemu_add_opts(&qemu_realtime_opts); ...... init_clocks();//時鐘初始化相關 rtc_clock = host_clock; ...... module_call_init(MODULE_INIT_MACHINE); machine = find_default_machine(); ...... ...... cpudef_init();//初始化CPU def相關 ...... if (log_mask) {//日誌相關的設定,KVM對外的日誌在這裡配置 int mask; if (log_file) { qemu_set_log_filename(log_file); } mask = qemu_str_to_log_mask(log_mask); if (!mask) { qemu_print_log_usage(stdout); exit(1); } qemu_set_log(mask); } ...... configure_accelerator();//進行虛擬機器模擬器的配置,這裡重點注意,它內部呼叫了accel_list[i].init()函式 /* for (i = 0; i < ARRAY_SIZE(accel_list); i++) { if (strcmp(accel_list[i].opt_name, buf) == 0) { if (!accel_list[i].available()) { printf("%s not supported for this target\n",accel_list[i].name); continue; } *(accel_list[i].allowed) = true; ret = accel_list[i].init(); if (ret < 0) { init_failed = true; fprintf(stderr, "failed to initialize %s: %s\n",accel_list[i].name,strerror(-ret)); *(accel_list[i].allowed) = false; } else { accel_initialised = true; } break; } } //accel_list定義如下,實際上在kvm平臺,我們就關注kvm_init即可。 static struct { const char *opt_name; const char *name; int (*available)(void); int (*init)(void); 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, &qtest_allowed }, }; //kvm_init函式內首先開啟用於使用者層以及核心層互動的字元裝置檔案/dev/kvm,然後通過kvm_ioctl()與核心進行互動,比如 KVM_GET_API_VERSION,KVM_CREATE_VM等命令,其中KVM_CREATE_VM命令建立虛擬機器並獲得虛擬機器控制代碼,後續kvm_arch_init()、 kvm_irqchip_create()等函式就可以通過kvm_vm_ioctl系統呼叫進行更進一步的一些配置。這些系統呼叫實際上是傳遞到核心層,由核心來完成相應的操作並返回到使用者層,核心層的相關函式很多就是前一篇文章註冊過的函式指標。 */ ...... ...... cpu_exec_init_all();//記錄CPU執行前的一些初始化工作 ...... ...... return 0; }
static void pc_machine_init(void) { qemu_register_machine(&pc_i440fx_machine_v1_5); qemu_register_machine(&pc_i440fx_machine_v1_4); 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_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); qemu_register_machine(&isapc_machine); #ifdef CONFIG_XEN qemu_register_machine(&xenfv_machine); #endif } machine_init(pc_machine_init);
注意這裡註冊的第一個為pc_i440fx_machine_v1_5,這個結構體定義為下:
static QEMUMachine pc_i440fx_machine_v1_5 = { .name = "pc-i440fx-1.5", .alias = "pc", .desc = "Standard PC (i440FX + PIIX, 1996)", .init = pc_init_pci, .hot_add_cpu = pc_hot_add_cpu, .max_cpus = 255, .is_default = 1, DEFAULT_MACHINE_OPTIONS, };
.init=pc_init_pci, pc_init_pci()即為初始化時候呼叫的函式,一路跟下去,其實最終調到pc_init1()這個函式。再看pc_init1()這個函式,這裡面進行了記憶體(pc_memory_init)、cpu(pc_cpus_init)、中斷等等多種的初始化,這裡不細說,重點看cpu的初始化。
void pc_cpus_init(const char *cpu_model, DeviceState *icc_bridge)
{
int i;
X86CPU *cpu = NULL;
Error *error = NULL;
/* init CPUs */
if (cpu_model == NULL) {
#ifdef TARGET_X86_64
cpu_model = "qemu64";
#else
cpu_model = "qemu32";
#endif
}
current_cpu_model = cpu_model;
for (i = 0; i < smp_cpus; i++) {
cpu = pc_new_cpu(cpu_model, x86_cpu_apic_id_from_index(i),//對每一個CPU進行初始化及建立
icc_bridge, &error);
if (error) {
fprintf(stderr, "%s\n", error_get_pretty(error));
error_free(error);
exit(1);
}
}
/* map APIC MMIO area if CPU has APIC */
if (cpu && cpu->env.apic_state) {
/* XXX: what if the base changes? */
sysbus_mmio_map_overlap(SYS_BUS_DEVICE(icc_bridge), 0,
APIC_DEFAULT_ADDRESS, 0x1000);
}
}
該函式內呼叫pc_new_cpu()函式對每一個CPU進行初始化,其後依次呼叫關係為:
int kvm_init_vcpu(CPUState *cpu)
{
KVMState *s = kvm_state;
long mmap_size;
int ret;
DPRINTF("kvm_init_vcpu\n");
ret = kvm_vm_ioctl(s, KVM_CREATE_VCPU, (void *)kvm_arch_vcpu_id(cpu));//通過ioctl呼叫向核心發起建立CPU請求,核心完成相關工作。
if (ret < 0) {
DPRINTF("kvm_create_vcpu failed\n");
goto err;
}
cpu->kvm_fd = ret;
cpu->kvm_state = s;
cpu->kvm_vcpu_dirty = true;
mmap_size = kvm_ioctl(s, KVM_GET_VCPU_MMAP_SIZE, 0);//通過ioctl呼叫獲取核心與使用者層共享的記憶體大小,這部分記憶體以記憶體對映的方式進行共享。
if (mmap_size < 0) {
ret = mmap_size;
DPRINTF("KVM_GET_VCPU_MMAP_SIZE failed\n");
goto err;
}
cpu->kvm_run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED,//獲取記憶體大小後,使用者層進行共享記憶體對映。
cpu->kvm_fd, 0);
if (cpu->kvm_run == MAP_FAILED) {
ret = -errno;
DPRINTF("mmap'ing vcpu state failed\n");
goto err;
}
if (s->coalesced_mmio && !s->coalesced_mmio_ring) { //mmio相關的一部分共享記憶體設定
s->coalesced_mmio_ring =
(void *)cpu->kvm_run + s->coalesced_mmio * PAGE_SIZE;
}
ret = kvm_arch_init_vcpu(cpu);//相關初始化
if (ret == 0) {
qemu_register_reset(kvm_reset_vcpu, cpu);
kvm_arch_reset_vcpu(cpu);
}
err:
return ret;
}
回到上面一些列呼叫中的qemu_kvm_cpu_thread_fn()函式中,在呼叫了kvm_init_vcpu()函式完成cpu的初始化之後,又呼叫kvm_cpu_exec()函式執行cpu,也就是運行了整個虛擬機器。
我們來看kvm_cpu_exec()這個函式
int kvm_cpu_exec(CPUArchState *env)
{
.......
do{
run_ret = kvm_vcpu_ioctl(cpu, KVM_RUN, 0);
.......
trace_kvm_run_exit(cpu->cpu_index, run->exit_reason);
switch (run->exit_reason) {
case KVM_EXIT_IO:
......
break;
case KVM_EXIT_MMIO:
......
break;
case KVM_EXIT_IRQ_WINDOW_OPEN:
......
break;
case KVM_EXIT_SHUTDOWN:
......
break;
case KVM_EXIT_UNKNOWN:
......
break;
case KVM_EXIT_INTERNAL_ERROR:
......
break;
default:
......
break;
}
} while (ret == 0);
.......
return ret;
}
首先一個kvm_vcpu_ioctl系統呼叫,向核心請求執行虛擬機器,然後核心執行虛擬機器,kvm_cpu_exec()內是有一個while迴圈,只要是沒有錯誤就會不斷執行不會終止,後面的switch語句實際上是接收核心傳來的退出原因,因為I/O等是需要Qemu即使用者層來完成的,這樣虛擬機器執行時核心層遇到I/O等就需要退出到Qemu層並記錄退出原因,Qemu根據退出原因執行相關操作,完成後再次執行ioctl操作轉到核心層繼續執行虛擬機器。關於異常退出的具體流程下篇文章再詳細講解。關於記憶體初始化相關工作,在之前提到過的kvm初始化主函式kvm_init()函式裡,依次呼叫:
memory_listener_register(&kvm_memory_listener, &address_space_memory);
listener_add_address_space(listener, as);
listener->region_add(listener, §ion);
.region_add = kvm_region_add,
kvm_set_phys_mem(section, true);
err = kvm_set_user_memory_region(s, mem);
return kvm_vm_ioctl(s, KVM_SET_USER_MEMORY_REGION, &mem);
這些函式依次呼叫,基本完成記憶體初始化過程,這裡最後的ioctl呼叫是設定影子頁表資訊以及設定頁面訪問許可權等。
最終,在核心與使用者層的配合下完成整個虛擬機器的建立和初始化工作,並執行虛擬機器。