关于MemoryRegion请参考我的博客
《MemoryRegion模型原理,以及同FlatView模型的关系(QEMU2.0.0)》,今天主要讲一下QEMU如何生成内存拓扑。
主要涉及的函数为:FlatView *generate_memory_topology(MemoryRegion *mr),输入一个MR,将MR平展的FlatView返回
该函数的主要目的是为一个MemoryRegion所管理的内存模型生成对应的FlatView内存拓扑模型
/* * 将MR所管理的内存展开成FlatView后返回 */ static FlatView *generate_memory_topology(MemoryRegion *mr) { FlatView *view; /*新分配一个FlatView并初始化*/ view = g_new(FlatView, 1); flatview_init(view); if (mr) { /* * 将mr所管理的内存进行平坦展开为FlatView,通过view返回 * addrrange_make(int128_zero(), int128_2_64()) --> 指定一个GUEST内存空间,起始GPA等于0,大小等于2^64,可以想象成GUEST的整个物理地址空间 */ render_memory_region(view, mr, int128_zero(), addrrange_make(int128_zero(), int128_2_64()), false); } /*简化FlatView,将View中的FlatRange能合并的都合并*/ flatview_simplify(view); return view; } |
先看最主要的函数render_memory_region。TMD,这个函数太难描述了,先看几个图了解一个原理,大家根据原理再自己体会一下
下面口语化一点口语化的情况下
第一次进入render_memory_region时候,传了一个2^64大小的超级大的GUEST内存(clip参数代表),base代表了相对这个超大内存物理其实地址,开始为0;然后将MR代表的地址空间和这个这个超大的内存相与,得到新的clip,新的clipMR所代表的地址空间,base指向了MR所代表地址空间的起始位置
这里先说一下offset_in_region变量的作用,这个值表示的是该alias类型MR表示的起始GPA相对于分配了物理内存的实体MR起始GPA的偏移量,我们通过ram_above_4g为例来查看一下
ram_above_4g是一个alias类型的MR,其源头(alias成员指向)是名为pc.ram的MR,pc.ram中真正分配的物理内存,并记录了这块物理内存的起始HVA。如上图所示base指向了GPA=0x0的位置,ram_above_4g就是本函数里的clip,二者之间产生了一个偏移量,就是offset_in_region。
这个offset_in_region通常用来计算ram_above_4g这段MR里的GPA地址在物理内存RAM上的HVA。还是以上图为例,ram_above_4g中起始GPA对应的HVA=pc.ram起始HVA+offset_in_region。依次类推ram_above_4g范围内其他GPA对应的HVA,这样就可以建立GPA到HVA的对应关系了。
细心的有人会问,直接加上偏移量不就可以了吗,为什么还要搞offset_in_region。我们前面在
《MemoryRegion模型原理,以及同FlatView模型的关系(QEMU2.0.0)》里面讲过,MR是需要平展成多个FlatRange后才注册到KVM模块中的,offset_in_region就是给了这些被展开的FlatRange,而每个FlatRange在GPA地址空间上是不一定相邻的,通过offset_in_region就可以计算FlatRange其实GPA地址对以的HVA的值了。
下面我们来讲一下展开的过程,对应的函数是render_memory_region
先界定一下其实场景
GUEST物理地址空间中,当前MR中已经有r1~r3,3个FlatRange已经展开了并填充了,GUEST地址空间起始GPA=0x0;黄色代表了待展开MR对应的地址空间,base指向了clip的起始GPA,remain代表了还有多长没有展开。
这里有一点需要注意,FlatView是属于AddressSpace的,AddressSpace代表了某一用途的内存,下面挂了一个MemoryRegion树。
所以一个FlatView可以对应多个MemoryRegion,就可能出现图中所示的,r1已经在别的MemoryRegion中被展开,而r1范围在当前clip所管理范围之外的情况。
另外,为什么会有r2和r3存在,是因为可能现在的操作是对虚拟机插入新的内存,在更新内存拓扑,这样原来的内存就是R2和R3。
base就是clip的其实地址,remian就是clip的长度。随着不断的展开,base会向后移动,remian会不断减小
下面开始展开过程
1. 比较r1,发现base比r1的结尾大,直接跳过r1。base和remain保持不变
2. 比较r2,发现base的其实地址在r2前面,需要将小于r2的部分展开到GUEST地址空间上,now代表了本次要展开的部分长度,展开后,base移动到r2之后,remian减小now长度。
3. 和r2重叠的部分直接跳过,不需要展开,因为已经展开过了。本次now为跳过的r2的长度,base向后移动到now后面,remian减少now长度
4. 同r2一样,处理r3,这里不在赘述
5. 当所有已经存在的FlatRange都遍历完了,发现还有一部分内存clip没有被展开,那么展开这块内存到GUEST物理地址空间上
到此为止,MR所代表的内存就全部展开为FlatView下的FlatRange了
原理讲完了,上代码
/* Render a memory region into the global view. Ranges in @view obscure * ranges in @mr. */ /* * render: 致使;提出;实施;着色;以…回报 */ /* * 将MR中所管理的内存,在clip指定的地址空间,逐个形成FlatRange后,将所有的FlatRange加入FlatView中 * * @view: 待形成的View * @mr: 待展平的Mr * @clip: 待展开的内存将被展开在clip所在的区域内,目前clip代表了整个内存 * @base: 可以理解为clip的起始地址 * @readonly: 读写属性 */ static void render_memory_region(FlatView *view, MemoryRegion *mr, Int128 base, AddrRange clip, bool readonly) { MemoryRegion *subregion; unsigned i; hwaddr offset_in_region; /*在region对应的真实物理内存的偏移量*/ Int128 remain; /*待展开内存的长度*/ Int128 now; /*本次展开的长度或跳过的长度*/ FlatRange fr; AddrRange tmp; if (!mr->enabled) { return; } /*base改为MR的起始地址*/ int128_addto(&base, int128_make64(mr->addr)); readonly |= mr->readonly; /*取得mr所表示的物理地址范围tmp*/ tmp = addrrange_make(base, mr->size); /*更新clip为MR所代表的地址空间*/ if (!addrrange_intersects(tmp, clip)) { return; } clip = addrrange_intersection(tmp, clip); /*如果是alias类型的MR,首先对原始MR进行FlatView展开*/ if (mr->alias) { /*将base指向alias源MR的起始地址位置*/ int128_subfrom(&base, int128_make64(mr->alias->addr)); int128_subfrom(&base, int128_make64(mr->alias_offset)); render_memory_region(view, mr->alias, base, clip, readonly); return; } /* Render subregions in priority order. */ /* 对所有子MR递归进行FlatView展开 */ QTAILQ_FOREACH(subregion, &mr->subregions, subregions_link) { render_memory_region(view, subregion, base, clip, readonly); } if (!mr->terminates) { return; } /* * 运行都这里说明MemoryRegion的子MR和aliaseMR都已经展开了 */ /* * 更新offset_in_region,offset_in_region是base相对于MR起始位置的偏移量,以后用来计算本FR对应MR的物理内存的HVA * 只有是alias类型的MemoryRegion,相应的offset_in_region才不为0, */ offset_in_region = int128_get64(int128_sub(clip.start, base)); /* * 准备展开MR为FlatRange,所有的FlatRange组成FlatView * clip为待展开MR * 更新base为clip的起始,remain为待展开的长度 */ base = clip.start; remain = clip.size; fr.mr = mr; fr.dirty_log_mask = mr->dirty_log_mask; fr.romd_mode = mr->romd_mode; fr.readonly = readonly; /* Render the region itself into any gaps left by the current view. */ /* 开始展开 */ for (i = 0; i < view->nr && int128_nz(remain); ++i) { /*跳过FlatView中在clip前面的FR*/ if (int128_ge(base, addrrange_end(view->ranges[i].addr))) { continue; } /* * 处理clip起始小于当前range起始的情况 * 展开 */ if (int128_lt(base, view->ranges[i].addr.start)) { /*计算填空部分大小*/ now = int128_min(remain, int128_sub(view->ranges[i].addr.start, base)); /*填充新的Fr信息*/ fr.offset_in_region = offset_in_region; fr.addr = addrrange_make(base, now); /*将新的Fr信息填充到插入到FlatView的当前位置,以前该位置往后的FlatRange都向后顺移了一位*/ flatview_insert(view, i, &fr); /*i++执行原来插入位置FlatRange*/ ++i; int128_addto(&base, now); offset_in_region += int128_get64(now); int128_subfrom(&remain, now); } /*跳过重叠的部分*/ /*计算重叠部分的长度*/ now = int128_sub(int128_min(int128_add(base, remain), addrrange_end(view->ranges[i].addr)), base); /*跳过重叠部分*/ int128_addto(&base, now); offset_in_region += int128_get64(now); int128_subfrom(&remain, now); } /*遍历完所有现有的FlatRange后,最后发现还有未展开的内存,这里处理其展开*/ if (int128_nz(remain)) { /*填入FR的信息*/ fr.offset_in_region = offset_in_region; fr.addr = addrrange_make(base, remain); /*插入该FR*/ flatview_insert(view, i, &fr); } } |
如果是alias类型的MR,递归调用render_memory_region,先对源MR进行展开后,在展开自己,注意,这种情况下,offset_in_region就不为0了
如果有子MR,对所有子MR进行递归展开
展开完了,回到generate_memory_topology的最后一步,就是对FlatView进行简化合并,如下图的r3,r4,r5就可以合并为一个,都合并为r3,具体的请参考博文
《QEMU内存管理之FlatView模型(QEMU2.0.0) 》
参考文章