qemu中的内存管理
qemu负责模拟虚机的外设,因此虚机的线性地址空间主要由qemu进行管理,也就是确定线性地址空间中哪段地址属于哪个设备或者DRAM或者其他的什么。
1、数据结构
1、RAMBLOCK
(最直接接触host内存,有hva)
RAMBLOCK才是真正分配了host内存的地方,如果把它直接理解成一个内存条也是非常合适的,但实际上不仅仅如此,还有设备自有内存,显存。ram_list则是RAMBlock的链表。
每个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;
2、MemoryRegion
管理虚拟机内存,通过内存属性,GUEST物理地址等特点对内存分类,就形成了多个MemoryRegion,这些MemoryRegion 通过树状组织起来,挂接到根MemoryRegion下。每个MemoryRegion树代表了一类作用的内存,qemu中两个全局的MemoryRegion,分别是system_memory和system_io
为了方便描述,将MemoryRegion分为三类:
1. 根MemoryRegion:不分配真正的物理内存,通过subregions将所有的子MemoryRegion管理起来,如图中的system_memory
2. 实体MemoryRegion:这种MemoryRegion中真正的分配物理内存,最主要的就是pc.ram和pci。分配的物理内存的作用分别是内存、PCI地址空间以及fireware空间。QEMU是用户空间代码,分配的物理内存返回的是HVA,被保存到host域。同时这个结构还会为本段虚拟机内存分配虚拟机物理地址空间起始地址,该起始地址(GPA)保存到ram_addr域,该段内存大小为size。通过实体MemoryRegion就可以将HOST地址HVA和GUEST地址GPA对应起来,这种实体MemoryRegion起到了转换的作用。
3. 别名MemoryRegion:这种MemoryRegion中不分配物理内存,代表了实体MemoryRegion的一个部分,通过alias域指向实体MemoryRegion,alias_offset代表了该别名MemoryRegion所代表内存起始GPA相对于实体MemoryRegion所代表内存起始GPA的偏移量,通常用来计算别名MemoryRegion对应的物理内存的HVA值:HVA = 起始HVA + alias_offset。如图中的ram_above_4g和ram-below-4g
3、Address_space
(qemu的内存管理在交付给KVM管理时,中间又加了一个抽象层。MR管理的host的内存,那么address_space管理的更偏向于虚拟机。把MR映射到虚拟机的物理地址空间)
不管是DRAM还是设备的资源都要通过memory region添加到address space里。
本来不同的设备使用的地址空间不同,但是QEMU X86里面只有两种,address_space_memory(虚机的线性地址空间(设备的mmio分布在这个地址空间))和address_space_io(虚机的io地址空间(设备的io port就分布在这个地址空间里))。
初始化: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()提交本次修改。
注册为回调(保持内核和用户空间的内存信息的一致性):kvm_init最后执行memory_listener_register,注册了address_space_memory和address_space_io两个。涉及的另外一个结构体则是MemoryListener,有kvm_memory_listener和kvm_io_listener,就是用于监控内存映射关系发生变化之后执行回调函数
实际内存分配:下面的pc_memory_init:分配全局ram(一整个memory region),然后根据below_4g_mem_size、above_4g_mem_size分别对ram进行划分,形成子MR,并注册子MR到root MR system_memory 的subregions链表中。最后需要调用memory_region_transaction_commit()函数提交修改。
4、Flatview、Flatrange、MemoryRegionSection
(每个FlatRange对应一段虚拟机物理地址区间,各个FlatRange不会重叠,按照地址的顺序保存在数组中)
MR和Flatview关系:MemoryRegion是QEMU管理内存的树状结构,便于按照功能、属性分类;但这只是管理结构。但虚拟机的内存需要通过KVM_SET_USER_MEMORY_REGION,将HVA和GPA的对应关系注册到KVM模块的memslot,才可以生效成为EPT。如果QEMU直接使用MemoryRegion进行注册,那么注册的过程将会很麻烦,也容易不断的出现重叠判断等。所以在通过KVM_SET_USER_MEMORY_REGION注册前,加了一层转换机制,先将树状的MemoryRegion展开物理内存样子的一维区间结构,然后再通过KVM_SET_USER_MEMORY_REGION将这个展开的物理内存注册到KVM内核模块中,就方便了许多。这个转换机制是FlatView模型。整个转换过程:函数address_space_update_topology,将指定的AddressSpace下的MemoryRegion树进行展平,形成了对应一维内存逻辑表示的FlatView,然后再address_space_update_topology_pass中将FlatView模型通过KVM_SET_USER_MEMORY_REGION注册到KVM模块中。
当memory region发生变化的时候,执行memory_region_transaction_commit,address_space_update_topology,address_space_update_topology_pass最终完成更新FlatView的目标。【此处就涉及到KVM中更新kvm->memslots】
Flatview原理:
1. 首先FlatView模型是通过FlatView和FlatRange两个对象组成。
2. FlatView是该段内存的整体视图的管理结构,一个FlatView由一组FlatRange组成。
3. 每个FlatRange代表了虚拟机上的一段内存,多个FlagRange就组成了一个内存视图,这些FlatRange在物理地址空间上不一定是相邻的。
3. 每个FlatView代表了某一类内存的组合,用作特殊的用途(如系统内存空间,MMIO内存地址空间),通常一个FlatView同一个特定用途的Address_Space进行关联
4. 每个FlatRange通过AddrRange标记该段GUEST内存的大小和长度。
5. FlatRange数组在FlatView初始化的时候为0个,也就是没有分配数组。当进行flatview_insert()的操作的时候,才会动态分配出来
6. 为了简化FlatView,通常将地址空间上连续的FlatRange进行合并,合并为1个FlatRange。如图中的r3,r4,r5就可以进行合并的区间,合并后都合并为r3。r4和r5的内容将被后面的FlatRange的数组元素覆盖掉
每一个flat range都投射到同一个地址空间的平面上,而上图中的R1,R2等对应的则是struct MemoryRegionSection(MemoryRegionSection对应于FlatRange,一个FlatRange代表一个物理地址空间的片段,但是其偏向于address-space,而MemoryRegionSection则在MR端显示的表明了分片)
5、KVMslot、kvm_userspace_memory_region
(更接近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-》
1、memory_region_init_ram-》qemu_ram_alloc-》qemu_ram_alloc_from_ptr-》
(1)使用find_ram_offset赋值给new block的offset(find_ram_offset在线性区间内找到没有使用的一段空间,可以完全容纳新申请的ramblock length大小,找到满足新申请length的最小区间,把ramblock安插进去即可,返回的offset即是新分配区间的开始地址);
(2)new_block->host = kvm_vmalloc(size) 分配真正物理内存,内部qemu_vmalloc使用qemu_memalign页对齐分配内存。后续的都是对RAMBlock的插入等处理。
以上:memory_region_init_ram已经将qemu内存模型和实际的物理内存初始化了(严格意义讲真实内存空间分配,是在QEMU发生缺页host做分配的时候。tdp_page_fault函数只是做的guest物理地址到host线性地址的映射关系,不真正涉及guest真实物理空间的分配。)
2、从memory_region_init_ram退出到pc_memory_init时已经初始化完成MemoryRegion ram,然后执行vmstate_register_ram_global,负责将前面提到的ramlist中的ramblock和memory region的初始地址对应一下,将mr->name填充到ramblock的idstr里面,就是让二者有确定的对应关系,如此mr就有了物理内存使用
3、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:
(1)两个FlatView逐条的FlatRange进行对比,以后一个FlatView为准,如果前面FlatView的FlatRange和后面的不一样,则对前面的FlatView的这条FlatRange进行处理。
(2)比较结束后,主要是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结构)
参考: