QEMU softmmu
虚拟机(Guest)内存模拟的首要任务是将虚拟机虚拟地址(GVA)转换成实际存储的宿主机(Host)的物理地址(HPA)。
虚拟机提供了虚拟的硬件环境,GVA到到虚拟机物理地址(GPA)的过程可以由Guest OS完成,而QEMU要做的就是提供一个GPA到HPA的转换的实现。
1 | GVA |
MemoryRegion
MemoryRegion基本信息及含义:
- MemoryRegion表示虚拟机的一段内存区域,即GPA。用于管理虚拟机的内存,是GPA与RAMBlock(管理HVA的结构)联系的桥梁
- 树状结构维护,每个MemoryRegion树代表一类作用的内存,如qemu中的两个全局MemoryRegion:系统内存空间(
system_memory
)或IO内存空间(system_io
) - 叶子节点表示实际分配给虚拟机的物理内存或者MMIO(即实体MemoryRegion),中间节点表示内存总线,内存控制器是其他MemoryRegion的别名
MemoryRegion可以分为如下几类
- 根级MemoryRegion
- 没有自己的内存,用于管理subregion,如
system_memory
- 通过
memory_region_init
初始化 - 别名mr都是根级mr的subregion
- 没有自己的内存,用于管理subregion,如
- 实体MemoryRegion
- 有具体的内存,从QEMU进程地址空间分配内存
- 通过
memory_region_init_ram
初始化,实际由qemu_ram_alloc
分配实际内存。如RAM、ROM、ROM device等 - 分配内存返回HVA,保存在RAMBlock成员的host域
- MemoryRegion通过对应的RAMBlock管理HVA(RAMBlock的
host
域)
- 别名MemoryRegion
- 表示实体mr的不同分段,没有自己的内存,是实体mr的一部分
- 通过
memory_region_init_alias
初始化 - 通过
alias
成员 指向实体MemoryRegion ,alias_offset
表示该别名mr在实体内存中的偏移量
1 | struct MemoryRegion { |
addr
表示在父mr(即container
的指向)中的偏移alias_offset
表示在实体mr中的偏移- 注意:实体mr并不是别名mr的父mr或者说container,因为别名mr就是实体mr的一部分
虚拟机申请ram时一次性申请完成,然后再在该ram的基础上按照size划分出若干subregion。每个subregion又可以通过alias
找到原始的mr,alias_offset
记录其在原始mr中的偏移。
RAMBlock
RAMBlock结构体用来关联GPA和HVA,其记录实际分配的内存地址信息,host
域指向申请到的qemu进程地址(HVA),以此作为虚拟机的物理地址。
通过offset+ram_block->host
即可完成逻辑GPA到HVA的转换。
1 | struct RAMBlock { |
- RAMBlock中几个重要的域
host
,表示虚拟机物理内存对应的QEMU进程地址空间的虚拟内存,即HVAoffset
,表示在ram_list
中的偏移
使用全局变量ram_list
以链表形式维护所有RAMBlock,新分配的RAMBlock会被插入ram_list
头部。要查找地址对应的RAMBlock则遍历ram_list
链表。
通过qemu_ram_alloc
创建一个新的RAMBlock,qemu_ram_alloc
是对qemu_ram_alloc_internal
的封装,其给new_block
的一些成员赋值,然后用ram_block_add
申请HVA赋给host
域,并添加到全局ram_list
中。
1 | RAMBlock *qemu_ram_alloc_internal(ram_addr_t size, ram_addr_t max_size, |
如果没启用xen,则ram_block_add
通过调用phys_mem_alloc()
分配实际的内存,其内部调用的是mmap
,于是host
域就得到了HVA值
1 | new_block->host = phys_mem_alloc(new_block->max_length, |
其他设备,如rom、flash等都会申请一个实体MemoryRegion,然后通过ram_block_add
添加到全局的ram_list
通过如下命令启动虚拟机,则ram_list
的内容是这样的
1 | qemu-system-x86_64 --enable-kvm -m 1G -hda Resery.img -vnv :0 -smp4 |
总结一下大概关系:根级mr可找到所有别名mr,别名mr通过其alias
域找到实体MemoryRegion。实体mr对应一个RAMBlock,可以找到其对应的HVA
AddressSpace
如果说RAMBlock维护了实际内存HVA。那么AddressSpace
就维护了虚拟机的一片地址空间。
AddressSpace用来表示Guest侧CPU/设备视角的地址空间,不同设备使用的地址空间不同,内存地址空间address_spaces_memory
和IO地址空间address_spaces_io
。其root
域指向根级MemoryRegion,从而可以找到一系列subregion
1 | /** |
AddressSpace还有个作用,就是是把MemoryRegion和FlatView联系起来,当mr发生变化时,对应的FlatView也应发生变化。可以通过dispatch_listener
在mr发生变化时做的一系列更新。
AddressSpaceDispatch
结构可以根据GPA找到HVA
1 | struct AddressSpaceDispatch { |
- Node类型就是一个大小为512的PhysPageEntry数组
- 最后一级页表的index就是sections数组的索引,于是找到了MemoryRegionSection
如通过
iotlb_to_section()
可以在AddressSpace中根据addr找到对应section
MemoryListener
当AddressSpace中的MemoryRegion发生变化,则触发注册的listener,处理region变更的事件
通过listener_add_address_space()
将listener注册到其对应的AddressSpace上
1 | FOR_EACH_FLAT_RANGE(fr, view) { |
FlatView
FlatView是MemoryRegion的平坦化表示,将树状的MemoryRegion展开成线性的FlatView,是QEMU管理内存的另一种方式。
因为要使用KVM_SET_USER_MEMORY_REGION
向KVM注册,将HVA和GPA的对应关系注册到KVM模块才能生成EPT。
1 | /* |
FlatView的range
域是一个FlatRange数组,每个FlatRange对应一段虚拟机物理地址区间,即一段GPA。可以通过AddrRange的start域拿到GPA的首地址
平坦化方法
通过调用static FlatView *generate_memory_topology(MemoryRegion *mr)
生成一个根mr的FlatView,其中的核心函数是render_memory_region()
用于填充FlatRange。
1 | static FlatView *generate_memory_topology(MemoryRegion *mr) |
render_memory_region()
1 | render_memory_region(view, mr, int128_zero(), |
了解几个变量的含义就可以尝试读源码了:
clip
表示映射区间的范围base
当前映射的小区间remain
表示clip还没映射的大小offset_in_region
需要映射的部分在其所属mr中的偏移int128_sub()
128位减法,同理ini128_xxx()
MemoryRegionSection
表示MemoryRegion中的片段。将MemoryRegion平坦化后,由于可能重叠,本来完整的mr可能就被分成了数片MemoryRegionSection。
1 | /** |
其中偏移offset_within_region
描述的是该section在其所属的MR中的偏移,因为一个MemoryRegion
可能有多个MemoryRegionSection
构成。而offset_within_address_space
是在整个地址空间中的偏移,是全局的offset,如果AddressSpace为系统内存,则该偏移则为GPA的起始地址
通过MemoryRegionSection,根据其在AddressSpace中的偏移offset_within_address_space
,加上修正就得到了GPA
通过该section所属mr的RAMBlock得到HVA,再加上section在所属的mr中的偏移offset_in_region
,再加上对齐修正,就得到了HVA
- HVA =
memory_region_get_ram_ptr(mr)
+section->offset_within_region
+ 页面对齐delta- 通过
memory_region_get_ram_ptr(mr)
得到当前Section对应的Mr的HVA起始地址。如果mr是alias则向上追溯,offset叠加,知道找到实体mr。ram_block->host+offset
- 通过
- GPA =
offset_within_address_space
+ 页对齐修正delta
实例说明
可以通过以下调用找到MemoreySection
1 | address_space_translate_internal |
使用找到的MemoryRegionSection找到ram,从而完成GPA到HVA的转换
1 | memory_region_get_ram_ptr(section->mr) |