QEMU 源码
vl.c
中的main()
函数调用流程分析,基于1.2.0
版本
目录
1. main() 函数的关键点
KVM 虚拟化由用户空间的 QEMU 和内核中的 KVM 模块配合完成,QEMU 通过ioctl()
向/dev/kvm
发送指令字,对虚拟机进行操作。配合流程如下:
QEMU 的入口main()
函数位于vl.c
中,重点关注以下几点:
- 何处创建 KVM 虚拟机并获取 fd?
- 虚拟机 CPU、内存在何处进行初始化?
- vCPU 子线程在何处创建,如何运行?
- 热迁移的 handlers 在何处注册?
- 主线程如何监听事件?
2. main() 函数调用流程图
在以上几点做了详细的调用流程展开,其他函数不再深入,部分函数省略,整个main()
函数的处理逻辑如下图所示:
int main()
├─ atexit(qemu_run_exit_notifiers) // 注册 QEMU 的退出处理函数
├─ module_call_init(MODULE_INIT_QOM) // 初始化 QOM
├─ runstate_init()
├─ init_clocks() // 初始化时钟源
├─ module_call_init(MODULE_INIT_MACHINE)
├─ switch(popt->index) case QEMU_OPTION_XXX // 解析 QEMU 参数
├─ socket_init()
├─ os_daemonize()
├─ configure_accelerator() // 启用 KVM 加速支持
| └─ kvm_init() // 【1】创建 KVM 虚拟机并获取对应的 fd
| ├─ kvm_ioctl(KVM_GET_API_VERSION) // 检查 KVM API 版本
| ├─ kvm_ioctl(KVM_CREATE_VM) // 创建虚拟机,并获取 vmfd
| ├─ kvm_arch_init()
| └─ memory_listener_register(&kvm_memory_listener) // 注册 kvm_memory_listener
|
├─ qemu_init_cpu_loop() // 初始化 vCPU 线程竞争的锁
├─ qemu_init_main_loop()
| └─ main_loop_init()
|
├─ qemu_spice_init() // 初始化 SPICE
├─ cpu_exec_init_all() // 【2】初始化虚拟机的地址空间,主要是 QEMU 侧的内存布局
| ├─ memory_map_init() // 初始化 MemoryRegion 及其对应的 FlatView
| | ├─ memory_region_init() // 初始化 system_memory/io 这两个全局 MemoryRegion
| | ├─ set_system_memory_map() // address_space_memory->root = system_memory
| | | └─ memory_region_update_topology() // 为 MemoryRegion 生成 FlatView
| | | └─ address_space_update_topology() // as->current_map = new_view
| | | ├─ generate_memory_topology() // 将 MemoryRegion 的拓扑结构渲染为 FlatRange 数组
| | | | ├─ flatview_init(&view)
| | | | ├─ render_memory_region(&view, mr, ...) // 根据 mr 生成 view
| | | | └─ flatview_simplify(&view) // 合并相邻的 FlatRange
| | | |
| | | ├─ address_space_update_topology_pass()
| | | | └─ kvm_region_add() // region_add 对应的回调实现
| | | | └─ kvm_set_phys_mem() // 根据传入的 section 填充 KVMSlot
| | | | └─ kvm_set_user_memory_region() // 将 QEMU 侧的内存布局注册到 KVM 中
| | | | └─ kvm_ioctl(KVM_SET_USER_MEMORY_REGION)
| | | |
| | | └─ address_space_update_ioeventfds()
| | |
| | └─ memory_listener_register() // 注册 core_memory_listener、io_memory_listener
| | └─ listener_add_address_space()
| |
| └─ io_mem_init() // 初始化 I/O MemoryRegion
| └─ memory_region_init_io() // ram/rom/unassigned/notdirty/subpage-ram/watch
| └─ memory_region_init()
|
├─ bdrv_init_with_whitelist()
├─ blk_mig_init()
| └─ register_savevm_live("block", &savevm_block_handlers, ...) // 注册块设备热迁移的处理函数
|
├─ register_savevm_live("ram", &savevm_ram_handlers, ...) // 注册内存热迁移的处理函数
├─ select_vgahw(vga_model) // 选择 VGA 显卡设备,std/cirrus/vmware/xenfb/qxl/none
├─ select_watchdog(watchdog)
├─ qdev_machine_init()
├─ machine->init() // QEMU 1.2.0 默认的 QEMUMachine 为 pc_machine_v1_2
| └─ pc_init_pci()
| └─ pc_init1()
| ├─ pc_cpus_init(cpu_model) // 【3】CPU 初始化,根据 smp_cpus 参数创建对应数量的 vCPU 子线程
| | └─ pc_new_cpu(cpu_model)
| | └─ cpu_x86_init(cpu_model)
| | └─ x86_cpu_realize()
| | └─ qemu_init_vcpu()
| | └─ qemu_kvm_start_vcpu()
| | └─ qemu_thread_create() // 顺序创建 vCPU 子线程,失败会阻塞
| | └─ qemu_kvm_cpu_thread_fn()
| | ├─ kvm_init_vcpu()
| | | ├─ kvm_ioctl(KVM_CREATE_VCPU) // 获取 vCPU 对应的 fd
| | | └─ kvm_arch_init_vcpu()
| | |
| | ├─ qemu_kvm_init_cpu_signals()
| | ├─ kvm_cpu_exec()
| | | └─ kvm_vcpu_ioctl(KVM_RUN) // 运行 vCPU
| | | └─ kvm_arch_vcpu_ioctl_run() // 进入内核,由 KVM 处理
| | | └─ __vcpu_run()
| | | └─ vcpu_enter_guest()
| | | └─ kvm_mmu_reload()
| | | └─ kvm_mmu_load() // spin_lock(&vcpu->kvm->mmu_lock)
| | |
| | └─ qemu_kvm_wait_io_event()
| |
| ├─ kvmclock_create()
| ├─ pc_memory_init() // 【4】内存初始化,从 QEMU 进程的地址空间中进行实际的内存分配
| | ├─ memory_region_init_ram() // 创建 pc.ram, pc.rom 并分配内存
| | | ├─ memory_region_init()
| | | └─ qemu_ram_alloc()
| | | └─ qemu_ram_alloc_from_ptr()
| | |
| | ├─ vmstate_register_ram_global() // 将 MR 的 name 写入 RAMBlock 的 idstr
| | | └─ vmstate_register_ram()
| | | └─ qemu_ram_set_idstr()
| | |
| | ├─ memory_region_init_alias() // 初始化 ram_below_4g, ram_above_4g
| | └─ memory_region_add_subregion() // 在 system_memory 中添加 subregions
| | └─ memory_region_add_subregion_common()
| | └─ memory_region_update_topology() // 为 MemoryRegion 生成 FlatView
| | └─ address_space_update_topology() // as->current_map = new_view
| | ├─ generate_memory_topology() // 将 MemoryRegion 的拓扑结构渲染为 FlatRange 数组
| | | ├─ flatview_init(&view)
| | | ├─ render_memory_region(&view, mr, ...) // 根据 mr 生成 view
| | | └─ flatview_simplify(&view) // 合并相邻的 FlatRange
| | |
| | ├─ address_space_update_topology_pass()
| | | └─ kvm_region_add() // region_add 对应的回调实现
| | | └─ kvm_set_phys_mem() // 根据传入的 section 填充 KVMSlot
| | | └─ kvm_set_user_memory_region() // 将 QEMU 侧的内存布局注册到 KVM 中
| | | └─ kvm_ioctl(KVM_SET_USER_MEMORY_REGION)
| | |
| | └─ address_space_update_ioeventfds()
| |
| ├─ i440fx_init()
| ├─ ioapic_init(gsi_state)
| ├─ pc_vga_init()
| ├─ pc_basic_device_init()
| ├─ pci_piix3_ide_init()
| ├─ audio_init()
| ├─ pc_cmos_init()
| └─ pc_pci_device_init()
|
├─ cpu_synchronize_all_post_init()
├─ set_numa_modes() // 设置 NUMA
├─ vnc_display_init() // 初始化 VNC
├─ qemu_spice_display_init()
├─ qemu_run_machine_init_done_notifiers()
├─ os_setup_post()
├─ resume_all_vcpus()
├─ main_loop() // 【5】主线程开启循环,监听事件
| └─ main_loop_wait()
| └─ os_host_main_loop_wait()
| └─ select()
|
├─ bdrv_close_all()
├─ pause_all_vcpus()
├─ net_cleanup()
└─ res_free()
大致流程如下图所示(图源自网络),对应 VMX 模式下 root 和 non-root 模式的概念:
- 左边蓝色部分即为根模式下的 Ring 3,即为用户空间中的 QEMU,通过循环调用
ioctl(KVM_RUN)
进入内核运行 vCPU,并处理 I/O 请求 - 中间橙色部分即为根模式下的 Ring 0,即为内核空间中的 KVM,通过
VM-Entry
进入非根模式,运行 Guest OS,并处理VM-Exit
。如果能在内核处理,则处理后再次通过VM-Entry
进入 Guest OS;如果不能处理(例如 I/O 请求),则退出到用户空间,由 QEMU 进行处理 - 右边紫色部分即为非根模式,Guest OS 运行在非根模式下的 Ring 0,所有的敏感指令都被重新定义,以便产生相应的 EXIT_REASON 交给 KVM 处理。Guest OS 中的进程则运行在非根模式下的 Ring 3
3. pstack 打印堆栈
通过virsh
启动一台 32 核 CPU 的虚拟机,使用pstack
打印堆栈验证一下:
> pstack $(pidof qemu-system-x86_64)
...(省略重复的堆栈)
Thread 6 (Thread 0x7fdcd4dfa700 (LWP 37340)):
#0 0x00007fdd46002307 in ioctl () from /lib64/libc.so.6
#1 0x00000000005e4bcb in kvm_vcpu_ioctl ()
#2 0x00000000005e57d8 in kvm_cpu_exec ()
#3 0x00000000005a2601 in qemu_kvm_cpu_thread_fn ()
#4 0x00007fdd462d8893 in start_thread () from /lib64/libpthread.so.0
#5 0x00007fdd46009bfd in clone () from /lib64/libc.so.6
Thread 5 (Thread 0x7fdcbbfff700 (LWP 37341)):
#0 0x00007fdd46002307 in ioctl () from /lib64/libc.so.6
#1 0x00000000005e4bcb in kvm_vcpu_ioctl ()
#2 0x00000000005e57d8 in kvm_cpu_exec ()
#3 0x00000000005a2601 in qemu_kvm_cpu_thread_fn ()
#4 0x00007fdd462d8893 in start_thread () from /lib64/libpthread.so.0
#5 0x00007fdd46009bfd in clone () from /lib64/libc.so.6
Thread 4 (Thread 0x7fdcbb5fe700 (LWP 37342)):
#0 0x00007fdd46002307 in ioctl () from /lib64/libc.so.6
#1 0x00000000005e4bcb in kvm_vcpu_ioctl ()
#2 0x00000000005e57d8 in kvm_cpu_exec ()
#3 0x00000000005a2601 in qemu_kvm_cpu_thread_fn ()
#4 0x00007fdd462d8893 in start_thread () from /lib64/libpthread.so.0
#5 0x00007fdd46009bfd in clone () from /lib64/libc.so.6
Thread 3 (Thread 0x7fdcbabfd700 (LWP 37343)):
#0 0x00007fdd46002307 in ioctl () from /lib64/libc.so.6
#1 0x00000000005e4bcb in kvm_vcpu_ioctl ()
#2 0x00000000005e57d8 in kvm_cpu_exec ()
#3 0x00000000005a2601 in qemu_kvm_cpu_thread_fn ()
#4 0x00007fdd462d8893 in start_thread () from /lib64/libpthread.so.0
#5 0x00007fdd46009bfd in clone () from /lib64/libc.so.6
Thread 2 (Thread 0x7fc4a73fd700 (LWP 37451)):
#0 0x00007fdd462dc115 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
#1 0x0000000000568781 in qemu_cond_wait ()
#2 0x00000000005952c3 in vnc_worker_thread_loop ()
#3 0x0000000000595778 in vnc_worker_thread ()
#4 0x00007fdd462d8893 in start_thread () from /lib64/libpthread.so.0
#5 0x00007fdd46009bfd in clone () from /lib64/libc.so.6
Thread 1 (Thread 0x7fdd47cce700 (LWP 37247)):
#0 0x00007fdd460029f3 in select () from /lib64/libc.so.6
#1 0x000000000053b325 in main_loop_wait ()
#2 0x0000000000536ef4 in main ()