基本逻辑
QEMU作为VMM启动基于KVM的虚机,QEMU的作用仅仅在于配置虚机,虚机的数据面基本上都由
硬件承载了。QEMU通过下发各种KVM ioctl配置虚机的vCPU、GIC、Timer、内存、SMMU以及
IO。当然QEMU是可以支持KVM和软件模拟混合的,比如可以vCPU、memory使用硬件加速,IO
使用软件模拟。
当前QEMU-KVM的ARM系统上,基本上所有部件用硬件加速,每个部件都会使用少量软件模拟
补齐完整功能。
基础数据结构
accel类的继承关系:
1
| TYPE_OBJECT <- TYPE_ACCEL <- TYPE_KVM_ACCEL
|
machine类的继承关系:
1
| TYPE_OBJECT <- TYPE_MACHINE <- TYPE_VIRT_MACHINE
|
ARM核的数据结构大概是如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| typedef struct CPUArchState { <--- 这个是env ... xregs[32] <--- v8的32个64bit寄存器 pc; pstate; ... 各种系统寄存器 ... <--- 和下面的list寄存器的区别? } CPUArchState;
struct ArchCPU { <--- ARMCPU,一般是cpu指针 CPUState parent_obj; CPUArchState env; GHashTable *cp_regs; <--- list寄存器,是一个hash表 uint64_t *cpreg_indexes; uint64_t *cpreg_values; ... struct ARMISARegisters { } isar; <--- cpu->isar midr; revidr; ctr; clidr; ... }
|
qemu里CPU的数据结构是用面向对象的方式组织起来,类的关系是:
1
| TYPE_DEVICE <- TYPE_CPU <- TYPE_ARM_CPU <- TYPE_AARCH64_CPU <- 各种aarch64的具体CPU类型
|
最后面的各种类定义在target/arm/cpu64.c里的ARMCPUInfo aarch64_cpus[], 比如有:cortex-a57,
host等。
CPU实例的初始化函数会调用如上ARMCPUInfo结构里的initfn函数,拿host看下:
1 2 3 4 5 6
| aarch64_host_initfn /* 通过ioctl拿到KVM里系统寄存器的缓存值,保存到cpu->isar里。*/ +-> kvm_arm_set_cpu_features_from_host +-> kvm_arm_get_host_cpu_features /* ioctl KVM_GET_ONE_REG */ +-> read_sys_reg64
|
可以看到这里host语义就是虚拟机和host的CPU feature一致,通过KVM_GET_ONE_REG ioctl
得到的值是kvm->kvm_arch->id_reg里的值,这些值在vCPU初始化的时候,被更新为host上对应
寄存器的值。
可以看到aarch64_a57_initfn里直接定义了a57需要支持的CPU特性。
虚机的数据结构为:
1 2 3 4 5
| struct VirtMachineState { MachineState parent; Notifier machine_done; ... }
|
初始化和启动流程
如下是qemu启动中,虚机和vCPU创建的基本逻辑:
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 50 51 52 53 54 55 56 57 58 59
| main +-> qemu_init ... +-> qemu_create_machine | +-> configure_accelerators | +-> do_configure_accelerator | +-> accel_init_machine | /* | * accel类的关系是:TYPE_ACCEL <- TYPE_KVM_ACCEL, 这里调用kvm accel | * class里的kvm_init。(accel/kvm/kvm-all.c) | * | * kvm_init里打开/dev/kvm,并通过KVM_CREATE_VM ioctl获得vmfd, | * 调用kvm_arch_init,初始化脏页跟踪的数据结构。 | * | * /dev/kvm的fd和vm的fd保存在KVMState中,分别是fd和vmfd。 | */ | +-> acc->init_machine | ... | +-> kvm_arch_init <--- VM相关的cap探测和使能在这里 | +-> kvm_irqchip_create | +-> kvm_memory_listener_register | +-> qmp_x_exit_preconfig | +-> qemu_init_board | ... | +-> create_default_memdev <--- 在这里实际分配host内存(A) | +-> memory_region_init_ram | ... | +-> machine_run_board_init | +-> machine_class->init(machine) <--- 函数指针,在hw/arm/virt.c virt_machine_class_init里 | +-> machvirt_init <--- 初始化虚机上的各个设备,包括如下调用arm_cpu_realizefn,拉起vCPU线程,虚机内存注册等。 | 注意,vCPU这里没有实际投入运行,实际运行在下面,具体又分为热迁移迁入端(A)和普通虚机启动(B)。 | | +-> qemu_machine_creation_done | +-> qdev_machine_creation_done | +-> cpu_synchronize_all_post_init | /* | * 调用accel/kvm/kvm-accel-ops.c里的kvm_cpu_synchronize_post_init | * 的回调把kvm_arch_put_registers放到vcpu的work_list里。 | */ | +-> cpu_synchronize_post_init | | +-> if (incoming) <--- 如果是热迁移的迁入端,做相应的准备工作(A) | +-> qmp_migrate_incoming | +-> todo: ... | +-> vm_start | | else <--- 如上流程中拉起的vCPU线程中vCPU并没有投入运行,这里 | +-> qmp_cont 才实际上促使vCPU运行起来。(B) | +-> vm_start | +-> resume_all_vcpus | +-> cpu_resume | +-> qemu_cpu_kick | +-> accel_setup_post ... +-> qemu_main +-> main_loop_wait
|
machvirt_init展开如下:
1 2 3 4 5 6
| /* hw/arm/virt.c */ machvirt_init /* 创建虚机上的各个设备 */ .... +-> object_new(possible_cpus->cpus[n].type) <--- hw/arm/virt.c, 创建vCPU QOM实例 +-> qdev_realize(DEVICE(cpuobj), ...) <--- hw/arm/virt.c, realize触发arm_cpu_realizefn
|
vCPU线程在vCPU的realize函数中创建,线程函数是accel/kvm/kvm-accel-ops.c里的
kvm_start_vcpu_thread。这个是vCPU实际开始运行的位置。
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
| /* target/arm/cpu.c */ arm_cpu_realizefn +-> qemu_init_vcpu +-> cpus_accel->create_vcpu_thread <-- kvm_vcpu_thread_fn in kvm-accel-ops.c +-> kvm_init_vcpu +-> kvm_create_vcpu +-> kvm_vm_ioctl(s, KVM_CREATE_VCPU, vcpu_id) +-> kvm_arch_init_vcpu +-> kvm_check_extension <-- vCPU相关的特性的init,vCPU CAP的检测也可以放这里。 +-> kvm_arm_vcpu_init <-- KVM_ARM_VCPU_INIT ioctl /* 拿到kvm里的寄存器信息,并更新到cpreg里 */ +-> kvm_arm_init_cpreg_list
/* vCPU执行的核心循环 */ do { +-> kvm_cpu_exec +-> kvm_arch_put_registers(cpu, KVM_PUT_RUNTIME_STATE) +-> kvm_vcpu_ioctl(cpu, KVM_RUN, 0) /* 处理vCPU退出 */ +-> switch (run->exit_reason) case ... case ...
+-> qemu_wait_io_event +-> qemu_wait_io_event_common /* * 执行之前放入vcpu work_list里的任务,执行kvm_arch_put_registers。 * 语义是把CPUState中的寄存器保存到list寄存器,再把list寄存器的 * 数据通过ioctl KVM_SET_ONE_REG配置到KVM里。 */ +-> process_queued_cpu_work }
|
展开看下kvm_arch_put_registers的细节:
1 2 3 4 5 6 7 8 9 10
| kvm_arch_put_registers +-> write_cpustate_to_list /* 得到系统寄存器对应ARMCPRegInfo描述结构 */ +-> ri = get_arm_cp_reginfo /* 基本上对于CPU ID寄存器,是从cpu->isar.xxx的域段得到值 */ +-> newval = read_raw_cp_reg +-> if (kvm_sync) +-> cpu->cpreg_values[i] = newval
+-> write_list_to_kvmstate
|
系统寄存器访问
从get_arm_cp_reginfo可以看出,系统寄存器被保存在名为cp_regs的一个哈希表里,这个
函数就是通过指令的各个域段作为key找到相关系统寄存器的描述结构体,寄存器的相关操
作函数都定义在这个结构体里,在系统初始化的时候插入到cp_regs哈希表里:
1 2 3 4 5 6 7 8
| /* target/arm/cpu.c */ arm_cpu_realizefn +-> register_cp_regs_for_features /* 在V8这个分支定义相关和注册的寄存器 */ +-> if (arm_feature(env, ARM_FEATURE_V8)) [...] /* 底层就是把定义的寄存器插入到cp_regs哈希表里 */ +-> define_arm_cp_regs
|
内存模拟
基于KVM的QEMU虚机,会在QEMU里定义内存的基本拓扑、创建虚机内存管理基本框架,但是
实际虚机内存管理数据面上的行为还是由KVM承载。
QEMU中内存定义
qemu里内存模拟相关的基本数据结构有:AddressSpace/MemoryRegion/MemoryListener/MemorySlot等。
AddressSpace表示地址空间,收集归拢MemoryRegion和MemoryListener等。MemoryRegion表示
地址空间上的一段子空间,比如一个外设的MMIO空间就可以是一个MemoryRegion,一段内存
也可以是一个MemoryRegion。MemoryListener向AddressSpace注册一组回调函数,当AddressSpace
发生变化的时候,调用对应的回调函数,完成相关操作。
虚机的内存布局一般定义在虚拟机器平台里,比如,ARM上我们常用的virt就是一种虚拟机器
平台(hw/arm/virt.c)。virt的物理地址空间可能是如下,一般定义在virt.c的base_memmap[]。
1 2 3 4 5 6 7 8 9 10 11 12 13
| [...] 0x00000000 - 0x08000000 flash/ROM 0x08000000 - 0x0a000000 GIC(GICD, GICC, GICV2M, GICH, GICV, ITS, GICR) 0x09000000 - 0x09060000 UART, RTC, fw_cfg, GPIO, SMMU 0x0a000000 - 0x0c000000 virtio MMIO (每块512B, 共512个) 0x0c000000 - 0x0e000000 platform bus 0x0e000000 - 0x10000000 secure memory 0x10000000 - 0x40000000 PCIe (MMIO+IO+ECAM) /* * 注意,这里定义的内存的起点。内存大小/NUMA/大页等需要根据qemu cmdline传入的参数 * 在如上A处初始化。 */ 0x40000000+ RAM (1 GiB起)
|
根MemoryRegion和AddressSpace的创建,初始化address_space_memory/address_space_io/
system_memory/system_io,前两者是address space,后两者是memory region。
1 2 3 4 5
| memory_map_init // system/physmem.c +-> system_memory = container, size=UINT64_MAX <--- 所有内存树的根 +-> address_space_init(&address_space_memory, system_memory, "memory") +-> system_io (I/O空间, 64K) +-> address_space_init(&address_space_io, system_io, "I/O")
|
Host KVM中的虚机内存管理
如上地址空间中定义的内存,需要qemu分配host内存来支持,所以被分配出来的虚机内存实际
上有多个访问或管理接口:最核心的是guest内的访存指令直接访问,第二个是KVM hyp的管理,
第三个是qemu进程访问,第四个是host内核内存管理机制的管理。
如下是KVM虚拟内存管理示意图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| +----------------------------------------------------------------------+ | QEMU process | | | | | | QEMU main thread | vCPU0 thread | vCPU1 thread | | | (host EL0) | (host EL0) | | | | | | +----------------+ <-- host VA | | | | \ \ | | | | \ \ | | | +----------------------------------+-----------------+-----------------+ | \ \ | | | | \ \ | +----+ | +----+ | <-- guest VA | \ \ | \ | / | | | \ host S1 map \ | \ | / | | guest S1 map | ... ... | \ / KVM | v | \ \ | +----------------+ | <-- IPA | \ \ +------------ / --------------------+ | | \ -------+------------/ HYP | | | \ / \ | | | guest S2 map +-------------------------/--------+-----------------------------------+ | | \ / \ | v | Memory +----------------+ | <-- PA | | +----------------------------------------------------------------------+
|
虚机的物理地址是在QEMU主线程里分配的一段QEMU进程的虚拟地址,IPA的地址是在QEMU里
定义的,QEMU通过ioctl(KVM_SET_USER_MEMORY_REGION)把这个信息传递给KVM。
直观上看,虚机内会自己管理自己的S1 map,KVM hypervisor会管理虚机的S2 map。虚机内存
实际上是QEMU进程的内存,对应的内存会受到host内核各种内存管理机制的管理,比如,透明
大页、传统大页、页面迁移等。host内核内存管理如果涉及到guest物理内存,需要通过mmu
notifier的机制通知到KVM hypvisor,hypervisor一般的应对手段是unmap掉对应的guest S2 map。
KVM memslot的同步 — MemoryListener机制(accel/kvm/kvm-all.c):
kvm_init阶段通过kvm_memory_listener_register注册一个KVMMemoryListener到address_space_memory上。
该listener的回调函数将QEMU的内存变更转化为KVM ioctl。
1 2 3 4 5 6 7
| kvm_init [...] +-> kvm_memory_listener_register +-> 创建kvm listener,增加相关回调kvm_region_add/del/commit, kvm_log_start/stop/sync/clear/sync_global等 +-> memory_listener_register(&kml->listener, &address_space_memory) /* 注册时立即同步当前已有的FlatView */ +-> listener_add_address_space -> kvm_region_add -> kvm_set_phys_mem
|
machvirt_init调用memory_region_add_subregion的逻辑,最后会调用到如上注册的kvm_region_commit,
其中会调用kvm ioctl把这段内存注册给内核KVM。machvirt_init里内存相关操作(hw/arm/virt.c)如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| machvirt_init /* * 这里的system就是如上创建的system_memory,system_memory是整个地址空间? * machine->ram是内存对应的MemoryRegion。 */ +-> memory_region_add_subregion(sysmem, VIRT_MEM.base, machine->ram) /* 每个设备通过sysbus_mmio_map把各自的MMIO region挂到sysmem下 */ +-> create_gic -> sysbus_mmio_map -> memory_region_add_subregion +-> create_uart -> memory_region_add_subregion +-> create_rtc +-> create_pcie +-> create_smmu +-> create_virtio_devices +-> create_platform_bus
|
memory_region_add_subregion逻辑展开如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| memory_region_add_subregion / sysbus_mmio_map [...] +-> memory_region_transaction_commit <--- 内存树变更完成时触发 +-> address_space_update_topology_pass /* 遍历FlatView,对每个FlatRange调用MEMORY_LISTENER_UPDATE_REGION */ +-> kvm_region_add <--- 排队到transaction_add +-> MEMORY_LISTENER_CALL_GLOBAL(commit) <--- 全局commit +-> kvm_region_commit /* 先删后加,处理重叠区域 */ +-> kvm_set_phys_mem(mem, section, add) /* 关键过滤: 非RAM区域直接跳过, 不注册KVM memslot */ +-> if (!memory_region_is_ram(mr)) return /* RAM区域分配KVMSlot, 调用KVM_SET_USER_MEMORY_REGION ioctl */ +-> kvm_set_user_memory_region -> KVM_SET_USER_MEMORY_REGION
|
注意,MMIO region不会传给KVM,客户机访问MMIO时发生VM exit,由QEMU的MemoryRegionOps->read/write处理。
只有RAM通过KVM memslot映射到host的QEMU进程地址空间。
KVMSlot结构(include/system/kvm_int.h):
1 2 3 4 5 6 7
| start_addr <--- GPA起始地址 memory_size <--- 区域大小 ram <--- host虚拟地址 (QEMU进程的mmap地址) slot <--- KVM slot编号 flags <--- KVM_MEM_LOG_DIRTY_PAGES等 ram_start_offset <--- 在RAMBlock里的偏移 guest_memfd <--- KVM_SET_USER_MEMORY_REGION2专用
|
KVM运行时的内存管理基本在KVM hypvisor内处理,注意的骨架是各种KVM S2 PTW(page table walk)、
host内存管理和KVM hypvisor的交互。
热迁移相关的内存管理逻辑在另外的文章中总结。
GIC模拟
QEMU-KVM虚拟化中,GIC有两种模拟方式:in-kernel irqchip和用户态模拟。in-kernel irqchip
将GICD/GICR的MMIO访问直接在KVM处理,避免退出到QEMU处理。用户态模拟在TCG或KVM不支持
in-kernel irqchip时使用。
GIC版本选择(finalize_gic_version, hw/arm/virt.c):
1 2 3 4 5
| finalize_gic_version /* KVM + in-kernel irqchip时, 通过ioctl探测host支持的GIC版本 */ +-> kvm_arm_vgic_probe <--- KVM_DEV_ARM_VGIC_GRP_NR_IRQS /* KVM不支持in-kernel irqchip时只支持GICv2 */ /* TCG下GICv2总是可用, GICv3需检查arm-gicv3模块 */
|
选出的版本决定了设备class:
- GICv2: kvm-arm-gic (KVM)或arm_gic (TCG)
- GICv3: kvm-arm-gicv3 (KVM)或arm-gicv3 (TCG)
GIC创建(create_gic, hw/arm/virt.c):
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| create_gic +-> 选设备class名(gic_class_name/gicv3_class_name) +-> 设置属性(revision, num-cpu, num-irq, redist-region-count...) +-> sysbus_realize_and_unref | /* KVM路径: kvm_arm_gicv3_realize in arm_gicv3_kvm.c */ | +-> kvm_create_device(KVM_DEV_TYPE_ARM_VGIC_V3) | +-> kvm_device_access(KVM_DEV_ARM_VGIC_GRP_CTRL, KVM_DEV_ARM_VGIC_CTRL_INIT) | /* 把GIC的MMIO region注册给KVM */ | +-> kvm_arm_register_device(iomem_dist, KVM_VGIC_V3_ADDR_TYPE_DIST) | +-> kvm_arm_register_device(redist_iomem, KVM_VGIC_V3_ADDR_TYPE_REDIST) | /* 配置IRQ路由 */ | +-> kvm_irqchip_add_irq_route (for each SPI) +-> sysbus_mmio_map (GICD @ 0x08000000, GICR @ 0x080A0000) +-> wiring: CPU timer PPI -> GIC -> CPU IRQ/FIQ
|
要点: KVM in-kernel GIC创建后,GICD/GICR的MMIO访问直接在KVM里处理,不需要
VM exit到QEMU。中断注入通过kvm_arm_gicv3_set_irq -> kvm_arm_set_irq -> KVM
ioctl实现。
KVM GICv3的寄存器访问(热迁移/调试用):
KVM_DEV_ARM_VGIC_GRP_DIST_REGS — distributor寄存器
KVM_DEV_ARM_VGIC_GRP_REDIST_REGS — redistributor寄存器(用MPIDR选CPU)
KVM_DEV_ARM_VGIC_GRP_CPU_SYSREGS — CPU interface系统寄存器
KVM_DEV_ARM_VGIC_GRP_LEVEL_INFO — 中断线电平
ITS (arm_gicv3_its_kvm.c):
- KVM in-kernel ITS: kvm_create_device(KVM_DEV_TYPE_ARM_VGIC_ITS)
- MSI注入: kvm_its_send_msi -> KVM_SIGNAL_MSI ioctl (带device ID)
- 设置 kvm_msi_use_devid=true, kvm_gsi_direct_mapping=false
SMMU模拟
QEMU的SMMU模拟主要分软件模拟和硬件加速两条路径:软件模拟通过IOMMUMemoryRegion的
translate回调完成地址翻译;硬件加速(smmuv3-accel)通过iommufd将翻译卸载到物理SMMUv3硬件。
SMMU创建(create_smmu, hw/arm/virt.c):
- 条件: -machine iommu=smmuv3 时 vms->iommu == VIRT_IOMMU_SMMUV3
- MMIO位于 0x09050000 (VIRT_SMMU), 4个SPI中断(irq 74-77)
- 默认stage=”nested”, 挂到PCIe primary bus
SMMU类继承:
TYPE_ARM_SMMUV3 <- TYPE_ARM_SMMU <- TYPE_SYS_BUS_DEVICE
SMMUv3State包含SMMUState作为首个字段(C继承), SMMUState包含:
configs (STE/CD缓存), iotlb (TLB缓存), smmu_pcibus_by_busptr,
iommu_ops (PCIIOMMUOps)
SMMU与PCIe的IOMMU挂钩(smmu_base_realize, smmu-common.c):
1 2 3 4 5 6 7 8 9 10 11 12
| smmu_base_realize +-> pci_setup_iommu(primary_bus, &smmu_ops, s) /* smmu_ops = { .get_address_space = smmu_find_add_as } */ /* * PCI设备DMA时调用pci_device_iommu_address_space() * 沿PCI总线树向上找到iommu_ops -> get_address_space() * 返回SMMUDevice->as, 这个AddressSpace的后端是IOMMUMemoryRegion */
smmu_find_add_as <--- smmu-common.c +-> smmu_init_sdev -> memory_region_init_iommu <--- 创建IOMMUMemoryRegion /* IOMMUMemoryRegion的translate回调 = smmuv3_translate */
|
SMMU翻译流程(smmu_translate, smmu-common.c):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| smmuv3_translate <--- smmuv3.c /* 检查CR0.SMMUEN, 如果禁用则检查GBPA.ABORT */ +-> smmuv3_get_config <--- 从缓存或客户机内存解码STE/CD +-> smmuv3_do_translate +-> smmu_translate <--- smmu-common.c /* 先在模拟IOTLB里找 */ +-> smmu_iotlb_lookup /* 未命中则做PTW(页表遍历) */ +-> smmu_ptw +-> smmu_ptw_64_s1 (VMSAv8-64 stage-1) +-> smmu_ptw_64_s2 (stage-2) +-> combine_tlb (nested: 组合S1+S2结果) +-> smmu_iotlb_insert /* 故障记入event queue, 触发SMMU_IRQ_EVTQ */ +-> smmuv3_record_event
|
命令队列(CmdQ, 客户机->SMMU)和事件队列(EvtQ, SMMU->客户机):
- 客户机通过CMDQ_BASE/CMDQ_PROD提交命令, SMMU处理并更新CMDQ_CONS
- 翻译故障时SMMU写入EVENTQ_BASE处的循环缓冲区, 触发事件中断
- 命令16字节, 事件32字节
StreamID: SMMU用PCI BDF (bus:dev.fn)作为StreamID, 每个PCI设备有独立的SMMUDevice
硬件加速(accel=on, smmuv3-accel.c):
- 需要iommufd支持的KVM
- 将真实SMMUv3硬件配置为nested模式,VFIO直通设备直接使用硬件SMMU
- 嵌套: KVM模拟S1翻译, 硬件处理S2翻译
- 此模式下热迁移被禁止
SMMU的qemu模拟逻辑可以参考这里。
todo: 基于iommufd的vSMMU逻辑
热迁移的逻辑
所有热迁移涉及的部件都有一个VMStateDescription结构,qemu用这个结构描述热迁移的相
关信息,ARM的定义在target/arm/machine.c里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| VMStateDescription vmstate_arm_cpu /* * 源端会把kvm里的CPU寄存器得到,并保存在list寄存器,最后保存到热迁移专用 * 的cpreg_vmstate_indexes/values等寄存器。 */ +-> cpu_pre_save +-> write_kvmstate_to_list +-> memcpy(cpreg_vmstate_indexes, cpreg_indexes, ...) +-> memcpy(cpreg_vmstate_values, cpreg_values, ...) +-> cpu_post_save +-> cpu_pre_load /* * 目的端会把cpreg_vmstate_values保存到list寄存器,并把list寄存器的值保存 * 到kvm和qemu cpustate寄存器里。 */ +-> cpu_post_load +-> cpu->cpreg_values[i] = cpu->cpreg_vmstate_values[v] +-> write_list_to_kvmstate(cpu, KVM_PUT_FULL_STATE) +-> write_list_to_cpustate(cpu)
|
todo: 标记脏页逻辑
ACPI表构建
1 2 3
| virt_machine_done +-> virt_acpi_setup +-> todo: ...
|