QEMU中的内存管理机制详解
QEMU中的内存管理机制详解
QEMU作为一款开源的虚拟机监视器,其内存管理机制一直是研究的重点。本文将从数据结构入手,详细解析QEMU如何将虚拟机内存映射到宿主机物理内存的过程。
1. 数据结构
1.1 RAMBLOCK
RAMBLOCK是QEMU中真正分配宿主机内存的地方,可以将其理解为一个内存条。每个RAMBLOCK都有一个唯一的MemoryRegion对应,但不是每个MemoryRegion都有RAMBLOCK对应的。
typedef struct RAMBlock {
uint8_t *host; // 对应宿主的内存地址hva
ram_addr_t offset; // block在ramlist中的偏移 gpa
ram_addr_t length; // block长度
char idstr[256]; // block名字
QLIST_ENTRY(RAMBlock) next;
} RAMBlock;
typedef struct RAMList {
uint8_t *phys_dirty; // list的head
QLIST_HEAD(ram, RAMBlock) blocks;
} RAMList;
1.2 MemoryRegion
MemoryRegion用于管理虚拟机内存,通过内存属性、GUEST物理地址等特点对内存分类。这些MemoryRegion通过树状组织起来,挂接到根MemoryRegion下。QEMU中两个全局的MemoryRegion分别是system_memory和system_io。
MemoryRegion可以分为三类:
- 根MemoryRegion:不分配真正的物理内存,通过subregions将所有的子MemoryRegion管理起来,如图中的system_memory
- 实体MemoryRegion:这种MemoryRegion中真正的分配物理内存,最主要的就是pc.ram和pci。分配的物理内存返回的是HVA,被保存到host域。同时这个结构还会为本段虚拟机内存分配虚拟机物理地址空间起始地址,该起始地址(GPA)保存到ram_addr域,该段内存大小为size。通过实体MemoryRegion就可以将HOST地址HVA和GUEST地址GPA对应起来,这种实体MemoryRegion起到了转换的作用。
- 别名MemoryRegion:这种MemoryRegion中不分配物理内存,代表了实体MemoryRegion的一个部分,通过alias域指向实体MemoryRegion,alias_offset代表了该别名MemoryRegion所代表内存起始GPA相对于实体MemoryRegion所代表内存起始GPA的偏移量,通常用来计算别名MemoryRegion对应的物理内存的HVA值:HVA = 起始HVA + alias_offset。如图中的ram_above_4g和ram-below-4g
1.3 Address_space
Address_space是QEMU内存管理中的一个抽象层,用于将MemoryRegion映射到虚拟机的物理地址空间。在QEMU X86中,主要有两种Address_space:address_space_memory(虚机的线性地址空间)和address_space_io(虚机的io地址空间)。
初始化过程如下:
main -> cpu_exec_init_all -> memory_map_init -> address_space_init(system_memory) 和 address_space_init(system_io)-> TAILQ_INSERT_TAIL(&address_spaces, as, address_spaces_link); 最后调用memory_region_transaction_commit()提交本次修改。
1.4 Flatview、Flatrange、MemoryRegionSection
Flatview用于将树状的MemoryRegion展平为一维的物理内存表示,便于通过KVM_SET_USER_MEMORY_REGION注册到KVM模块中。每个FlatRange对应一段虚拟机物理地址区间,各个FlatRange不会重叠,按照地址的顺序保存在数组中。
Flatview的原理如下:
- FlatView由一组FlatRange组成,每个FlatRange代表了虚拟机上的一段内存。
- 每个FlatView代表了某一类内存的组合,通常与特定用途的Address_Space关联。
- FlatRange数组在FlatView初始化时为0个,当进行flatview_insert()操作时才会动态分配。
- 为了简化FlatView,通常会将地址空间上连续的FlatRange进行合并。
1.5 KVMslot、kvm_userspace_memory_region
KVMslot和kvm_userspace_memory_region更接近KVM内核模块,用于将用户空间的内存映射信息传递给KVM。在MEMORY_LISTENER_CALL中调用kvm_region_add和kvm_region_del,执行kvm_set_phys_mem,组装KVMSlot,再把对应信息转给kvm_userspace_memory_region,将其通过kvm_vm_ioctl传给KVM用于更新kvm->memslots。
2. qemu到kvm实际分配vm内存流程
QEMU在pc_init1调用pc_cpus_init创建完vcpu返回后,走到初始化内存pc_memory_init:
- memory_region_init_ram-》qemu_ram_alloc-》qemu_ram_alloc_from_ptr-》
- 使用find_ram_offset赋值给new block的offset(find_ram_offset在线性区间内找到没有使用的一段空间,可以完全容纳新申请的ramblock length大小,找到满足新申请length的最小区间,把ramblock安插进去即可,返回的offset即是新分配区间的开始地址);
- new_block->host = kvm_vmalloc(size) 分配真正物理内存,内部qemu_vmalloc使用qemu_memalign页对齐分配内存。后续的都是对RAMBlock的插入等处理。
以上:memory_region_init_ram已经将qemu内存模型和实际的物理内存初始化了(严格意义讲真实内存空间分配,是在QEMU发生缺页host做分配的时候。tdp_page_fault函数只是做的guest物理地址到host线性地址的映射关系,不真正涉及guest真实物理空间的分配。)
- 从memory_region_init_ram退出到pc_memory_init时已经初始化完成MemoryRegion ram,然后执行vmstate_register_ram_global,负责将前面提到的ramlist中的ramblock和memory region的初始地址对应一下,将mr->name填充到ramblock的idstr里面,就是让二者有确定的对应关系,如此mr就有了物理内存使用
- memory_region_add_subregion-》memory_region_transaction_commit修改虚拟机的内存。引入了新的结构address_spaces(AS),对所有AS执行address_space_update_topology该函数内address_space_get_flatview直接获取当前的FlatView,然后generate_memory_topology根据前面已经变化的mr重新生成FlatView,然后调用address_space_update_topology_pass:
- 两个FlatView逐条的FlatRange进行对比,以后一个FlatView为准,如果前面FlatView的FlatRange和后面的不一样,则对前面的FlatView的这条FlatRange进行处理。
- 比较结束后,主要是MEMORY_LISTENER_UPDATE_REGION函数,将变化的FlatRange构造一个MemoryRegionSection,然后遍历所有的memory_listeners,如果memory_listeners监控的内存区域和MemoryRegionSection一样,则执行第四个入参函数,如region_del函数,即kvm_region_del函数,这个是在kvm_init中初始化的(不一样则利用kvm_region_add添加内存到KVM的kvm->memslots:)。
- kvm_region_del主要是kvm_set_phys_mem函数,主要是将MemoryRegionSection有效值转换成KVMSlot形式,然后调用kvm_set_user_memory_region中使用kvm_vm_ioctl(s, KVM_SET_USER_MEMORY_REGION, &mem);传递给kernel。(其中mem就是kvm_userspace_memory_region结构)
参考资料
- oenhen的qemu下内存结构
- oenhen的内存虚拟化
- 六六哥的一系列关于qemu内存数据结构分析
- jackchen的qemu-kvm内存分析1-2
- jessica的qemu对虚机地址空间管理