1 Qemu内存分布
2 内存初始化
Qemu中的内存模型,简单来说就是Qemu申请用户态内存并进行管理,并将该部分申请的内存注册到对应的加速器(如KVM)中。这样的模型有如下好处:
-
策略与机制分离。加速的机制由KVM负责,而如何调用加速的机制由Qemu负责
-
可以由Qemu设置多种内存模型,如UMA、NUMA等等
-
方便Qemu对特殊内存的管理(如MMIO)
-
内存的分配、回收、换出等都可以采用Linux原有的机制,不需要为KVM单独开发。
-
兼容其他加速器模型(或者无加速器,单纯使用Qemu做模拟) Qemu需要做的有两方面工作:向KVM注册用户态内存空间,申请用户态内存空间。 Qemu主要通过如下结构来维护内存: /* A system address space - I/O, memory, etc. */ struct AddressSpace { char *name; MemoryRegion *root; FlatView current_map; int ioeventfd_nb; MemoryRegionIoeventfd *ioeventfds; struct AddressSpaceDispatch *dispatch; struct AddressSpaceDispatch *next_dispatch; MemoryListener dispatch_listener; QTAILQ_ENTRY(AddressSpace) address_spaces_link; }; "memory"的root是static MemoryRegion *system_memory; 使用链表address_spaces保存虚拟机的内存,该链表保存AddressSpace address_space_io和AddressSpace address_space_memory等信息 void address_space_init(AddressSpace *as, MemoryRegion *root, const char *name) { if (QTAILQ_EMPTY(&address_spaces)) { memory_init(); }
memory_region_transaction_begin(); as->root = root; as->current_map = g_new(FlatView, 1); flatview_init(as->current_map); as->ioeventfd_nb = 0; as->ioeventfds = NULL; QTAILQ_INSERT_TAIL(&address_spaces, as, address_spaces_link); as->name = g_strdup(name ? name : "anonymous"); address_space_init_dispatch(as); memory_region_update_pending |= root->enabled; memory_region_transaction_commit(); } static void memory_map_init(void) { system_memory = g_malloc(sizeof(*system_memory)); memory_region_init(system_memory, NULL, "system", UINT64_MAX); address_space_init(&address_space_memory, system_memory, "memory");
system_io = g_malloc(sizeof(*system_io)); memory_region_init_io(system_io, NULL, &unassigned_io_ops, NULL, "io",65536); address_space_init(&address_space_io, system_io, "I/O"); memory_listener_register(&core_memory_listener, &address_space_memory); } AddressSpace设置了一段内存,其主要信息存储在root成员 中,root成员是个MemoryRegion结构,主要存储内存区的结构。在Qemu中最主要的两个AddressSpace是 address_space_memory和address_space_io,分别对应的MemoryRegion变量是system_memory和 system_io。 Qemu的主函数是vl.c中的main函数,其中调用了configure_accelerator(),是KVM初始化的配置部分。 configure_accelerator中首先根据命令行输入的参数找到对应的accelerator,这里是KVM。之后调用accel_list[i].init(),即kvm_init()。 在kvm_init()函数中主要做如下几件事情:
-
s->fd = qemu_open("/dev/kvm", O_RDWR),打开kvm控制的总设备文件/dev/kvm
-
s->vmfd = kvm_ioctl(s, KVM_CREATE_VM, 0),调用创建虚拟机的API
-
kvm_check_extension,检查各种extension,并设置对应的features
-
ret = kvm_arch_init(s),做一些体系结构相关的初始化,如msr、identity map、mmu pages number等等
-
kvm_irqchip_create,调用kvm_vm_ioctl(s, KVM_CREATE_IRQCHIP)在KVM中虚拟IRQ芯片
-
memory_listener_register,该函数是初始化内存的主要函数 memory_listener_register调用了两次,分别注册了 kvm_memory_listener和kvm_io_listener,即通用的内存和MMIO是分开管理的。以通用的内存注册为例,函数首先在全局 的memory_listener链表中添加了kvm_memory_listener,之后调用listener_add_address_space 分别将该listener添加到address_space_memory和address_space_io中, address_space_io是虚机的io地址空间(设备的io port就分布在这个地址空间里)。 然后调用listener的region_add(即 kvm_region_add()),该函数最终调用了kvm_set_user_memory_region(),其中调用 kvm_vm_ioctl(s, KVM_SET_USER_MEMORY_REGION, &mem),该调用是最终将内存区域注册到kvm中的函数。 之后在vl.c的main函数中调用了cpu_exec_init_all() => memory_map_init(),设置system_memory和system_io。 至此初始化好了所有Qemu中需要维护的相关的内存结构,并完成了在KVM中的注册。下面需要初始化KVM中的MMU支持。 ram_size内存大小从内存被读取到ram_size中,在vl.c的main中调用machine->init()来初始化,machine是命令行指定的机器类型,默认的init是pc_init_pci pc_memory_init memory_region_allocate_system_memory memory_region_add_subregion memory_region_add_subregion_common memory_region_update_container_subregions memory_region_transaction_commit address_space_update_topology generate_memory_topology render_memory_region flatview_insert
3 内存分配
内存的分配实现函数为 ram_addr_t qemu_ram_alloc(ram_addr_t size, MemoryRegion *mr),输出为该次分配的内存在所有分配内存中的顺序偏移(即下图中的红色数字).
该函数最终调用phys_mem_alloc分配内存, 并将所分配的全部内存块, 串在一个ram_blocks开头的链表中, 如下示意:
上图中分配了4个内存块, 每次分配时偏移offset顺序累加, host指向该内存块在主机中的虚拟地址.
调用memory_listener_register注册
4 内存映射 使用的相关结构体如下: /* Range of memory in the global map. Addresses are absolute. / struct FlatRange { MemoryRegion mr; hwaddr offset_in_region; AddrRange addr; uint8_t dirty_log_mask; bool romd_mode; bool readonly; }; / Flattened global view of current active memory hierarchy. Kept in sorted order./ struct FlatView { unsigned ref; FlatRange *ranges; unsigned nr; unsigned nr_allocated; }; 映射是将上面分配的地址块映射为客户机的物理地址, 函数如下, 输入为映射后的物理地址, 内存偏移,通用内存块的地址 static void memory_region_add_subregion_common(MemoryRegion *mr, hwaddr offset, MemoryRegion *subregion) MemoryRegion *mr:对应的是system_memory或者system_io,通过memory_listener_register函数注册内存块。 通用栈如下: memory_region_update_container_subregions memory_region_transaction_commit address_space_update_topology generate_memory_topology address_space_update_topology_pass memory_region_update_container_subregions函数在链表中寻找合适的位置插入, /插入指定的位置/ QTAILQ_FOREACH(other, &mr->subregions, subregions_link) { if (subregion->priority >= other->priority) { QTAILQ_INSERT_BEFORE(other, subregion, subregions_link); goto done; } } QTAILQ_INSERT_TAIL(&mr->subregions, subregion, subregions_link); memory_region_transaction_commit中引入了新的结构address_spaces(AS),内存有不同的应用类型,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_pass函数比较之前的内存块,做相应的处理 MEMORY_LISTENER_UPDATE_REGION函数,将变化的FlatRange构造一个MemoryRegionSection,然后 遍历所有的memory_listeners,如果memory_listeners监控的内存区域和MemoryRegionSection一样,则执 行第四个入参函数,如region_del函数,即kvm_region_del函数,这个是在kvm_init中初始化的。 kvm_region_add主要是kvm_set_phys_mem函数,主要是将MemoryRegionSection有效值转换成KVMSlot 形式,在kvm_set_user_memory_region中使用kvm_vm_ioctl(s, KVM_SET_USER_MEMORY_REGION, &mem)传递给kernel。 5 客户机物理地址到主机虚拟地址的转换 5.1 地址属性 内存映射是以页为单位的, 也就意味着phys_offset的低12bit为0, Qemu使用这些bit标识地址属性: Bit 11-3 Bit 2 Bit 1 Bit 0 MMIO索引, 其中4个固定分配 SUBWIDTH SUBPAGE ROMD 0: RAM 1: ROM 2: UNASSIGNED 3: NOTDIRTY 5.2 客户机物理地址到主机虚拟地址的转换步骤 虚拟机因mmio退出时,qemu处理该退出事件,相关的函数: void cpu_physical_memory_rw(hwaddr addr, uint8_t *buf, int len, int is_write) 该函数实现虚拟机的物理地址到主机虚拟地址的转换
- 查找该地址所应的MemoryRegionSection结构, 函数为 static MemoryRegionSection *phys_page_find(PhysPageEntry lp, hwaddr addr, Node *nodes, MemoryRegionSection *sections), 即将客户物理地址分为4段, 取每一段的索引查找下一段, 直至找到Level 3的MemoryRegionSection结构.
- 调函数void *qemu_get_ram_ptr(ram_addr_t addr), 取主机虚拟地址起始位置, 再加上页内偏移, 即为对应的主机虚拟地址 6 Kvm映射 static void kvm_set_phys_mem(MemoryRegionSection *section, bool add) 该函数把guest机的物理内存映射主机的虚拟内存 typedef struct KVMSlot { hwaddr start_addr; /guest物理地址/ ram_addr_t memory_size; /内存大小/ void *ram; /对应的虚拟地址/ int slot; /对应的插槽号/ int flags; } KVMSlot; Qemu支持kvm时, 还需通知kvm将客户机物理内存进行映射, 方法为先定义一个映射结构: struct kvm_userspace_memory_region memory = { .memory_size = len, .guest_phys_addr = phys_start, // 客户机物理地址 .userspace_addr = userspace_addr, // 主机虚拟地址, 而非上面的偏移 .flags = log ? KVM_MEM_LOG_DIRTY_PAGES : 0, }; 然后调用kvm的ioctl r = kvm_vm_ioctl(kvm_state, KVM_SET_USER_MEMORY_REGION, &memory);同时, qemu的kvm用户空间代码, 还定义了一些结构如mapping/slot, 用于地址空间的管理, 如防止重复映射等.