QEMU 源码vl.c中的main()函数调用流程分析,基于1.2.0版本

目录

1. main() 函数的关键点

KVM 虚拟化由用户空间的 QEMU 和内核中的 KVM 模块配合完成,QEMU 通过ioctl()/dev/kvm发送指令字,对虚拟机进行操作。配合流程如下:

QEMU 的入口main()函数位于vl.c中,重点关注以下几点:

  1. 何处创建 KVM 虚拟机并获取 fd?
  2. 虚拟机 CPU、内存在何处进行初始化?
  3. vCPU 子线程在何处创建,如何运行?
  4. 热迁移的 handlers 在何处注册?
  5. 主线程如何监听事件?

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 ()