简介:
内存虚拟化就是为虚拟机提供内存,使得虚拟机能够像在物理机上正常工作,这需要虚拟化软件为虚拟机展示一种物理内存的假象,内存虚拟化是虚拟化技术中关键技术之一。
qemu模拟虚机内存,核心是维护虚机物理地址空间。这个地址空间既要方便qemu管理,向虚机侧提供内存,又要方便展示和导出,向平台侧提供内存视图。因此qemu抽象的内存区域有两种组织结构,一种是树状的,用于qemu管理并模拟内存,一种是扁平的,用于展示和导出内存视图,也方便传递给KVM。
- 树状视图有两个元素,一是AddressSpace,表示一个cpu可访问的地址空间;一是MemoryRegion,表示一段逻辑内存区域。AddressSpace地址空间由许多逻辑内存区域MemoryRegion组成
- 扁平化视图同样有两个元素,一是FlatView,cpu可访问地址空间的扁平化表示;一是FlatRange,逻辑内存区域的扁平化描述,表示一段段内存区域。同样地,FlatView由许多FlatRange组成。
主要结构体间关系:
1. 关键数据结构
1.1 AddressSpace
结构体AddressSpace用来表示虚拟机的一片地址空间,不同的设备使用的地址空间不同,但qemu x86中只有两种, address_space_memory和address_space_io,这也是两个全局的address_space变量,所有设备的地址空间都被映射到了这两个上面。其root指向根MemoryRegion, 对于全局变量address_space_memory而言,其root指向系统全局的system_memory,address_space_io的root则指向system_io.由于根MR可能有自己的若干个subregion,因此每个AddressSpace一般包含一系列MemoryRegion,形成树状结构。
struct AddressSpace {
/* All fields are private. */
struct rcu_head rcu;
char *name;
MemoryRegion *root; //地址空间的根节点,其他的MemoryReagion会alia到这个节点
/* Accessed via RCU. */
struct FlatView *current_map; //平坦视图
int ioeventfd_nb;
struct MemoryRegionIoeventfd *ioeventfds;
QTAILQ_HEAD(memory_listeners_as, MemoryListener) listeners; //监听链表的头部,用于当地址空间发生变化时通知qemu其它模块或者内核
QTAILQ_ENTRY(AddressSpace) address_spaces_link; //地址空间的链表
};
AdressSpace初始化
- 地址空间的初始化有三个地方,1是静态全局链表,2是qemu准备cpu执行环境时,3是qemu初始化特定硬件类型时。下面分别介绍
1. 全局链表初始化:qemu有链表将所有地址空间组织到一起,全局变量address_spaces指向这个链表的头
static QTAILQ_HEAD(, AddressSpace) address_spaces = QTAILQ_HEAD_INITIALIZER(address_spaces);
- qemu准备cpu执行环境:将系统内存和IO内存的Root MR和地址空间都进行了初始化。Root MR作为地址空间初始化的输入
main() // vl.c
cpu_exec_init_all() // exec.c
memory_map_init() // exec.c
这里共创建了两个全局的AS: address_space_memory、address_space_io
初始化后的地址空间构造如下:
1.2 MemoryRegion
QEMU通过MemoryRegion来管理虚拟机内存,通过内存属性,GUEST物理地址等特点对内存分类,就形成了多个MemoryRegion,这些MemoryRegion 通过树状组织起来,挂接到根MemoryRegion下。每个MemoryRegion树代表了一类作用的内存,如系统内存空间(system_memory)或IO内存空间(system_io),这两个是qemu中的两个全局MemoryRegion。
struct MemoryRegion {
Object parent_obj;
bool ram; //标记是否为ram类型的MR
bool readonly; //For RAM regions,标记是否为ROM类型的MR
RAMBlock *ram_block; //实际申请的物理内存块信息,不为null则关联一段实际内存
const MemoryRegionOps *ops; // 是否为MMIO类型的MR
MemoryRegion *container; //MemoryRegion所属的父容器,当前MR为subregion
Int128 size; //虚机内存的物理地址大小
hwaddr addr; //虚机内存的绝对物理地址
bool terminates; //是否为叶子节点
MemoryRegion *alias; //指向别名所在的MR
hwaddr alias_offset; //相对别名MR的起始地址偏移
int32_t priority; //优先级
QTAILQ_HEAD(subregions, MemoryRegion) subregions; //容器MR的子MR组成的链表头部
QTAILQ_ENTRY(MemoryRegion) subregions_link; //用于将子MR组织成链表的成员
const char *name; //当前MR的名称,如根节点为system
};
pc.ram的初始化流程
pc.ram属于qemu初始化过程中申请的最大一部分内存,可以基于此分析其他内存块的申请
main() // vl.c
cpu_exec_init_all() // exec.c
memory_map_init() // exec.c
address_space_init() // memory.c
machine_run_board_init() // hw/core/machine.c
machine_class->init() // pc_init1, hw/i386/pc_piix.c
pc_cpus_init() // hw/i386/pc.c
pc_memory_init() // hw/i386/pc.c
memory_region_allocate_system_memory // numa.c
allocate_system_memory_nonnuma // numa.c
memory_region_init_ram_shared_nomigrate // memory.c
qemu_ram_alloc // exec.c
ram_block_add // exec.c
phys_mem_alloc // exec.c
qemu_anon_ram_alloc // util/oslib-win32.c
MemoryRegion的组织关系
AOSP中内存分布
1.3 MemoryListener
1. 当内存地址空间有变化时,比如添加一个MR或者删除一个MR,整个地址空间都会变化,某些感兴趣的实体可能想要让自己被通知到并调用提前注册的钩子函数,这些函数的原型就在这里定义。一个MemoryListener可以只实现其中部分
2. MemoryListener代表的是某个对地址空间感兴趣的实体,这些实体不只一个,需要被管理起来,有两个地方管理这些实体,一是全局链表memory_listeners,它管理所有注册的Listener,结构体的link成员用作连接到这个链表。二是地址空间,它管理对自己感兴趣的Listener,地址空间的listeners成员维护这个链表头,结构体的link_as成员用作链接到这个链表。成员的address_space指向这个所属的地址空间。同时所有listener有一个优先级,由priority表示,决定了在链表中的顺序。
/**
* MemoryListener: callbacks structure for updates to the physical memory map
*
* Allows a component to adjust to changes in the guest-visible memory map.
* Use with memory_listener_register() and memory_listener_unregister().
*/
struct MemoryListener {
void (*begin)(MemoryListener *listener);
void (*commit)(MemoryListener *listener); //执行内存变更所需的函数
void (*region_add)(MemoryListener *listener, MemoryRegionSection *section); //在添加region的时候被调用
void (*region_del)(MemoryListener *listener, MemoryRegionSection *section);
void (*region_nop)(MemoryListener *listener, MemoryRegionSection *section);
void (*log_start)(MemoryListener *listener, MemoryRegionSection *section,
int old, int nw); //跟脏页机制的开启合同步有关
void (*log_stop)(MemoryListener *listener, MemoryRegionSection *section,
int old, int nw);
void (*log_sync)(MemoryListener *listener, MemoryRegionSection *section);
void (*log_global_start)(MemoryListener *listener);
void (*log_global_stop)(MemoryListener *listener);
void (*eventfd_add)(MemoryListener *listener, MemoryRegionSection *section,
bool match_data, uint64_t data, EventNotifier *e);
void (*eventfd_del)(MemoryListener *listener, MemoryRegionSection *section,
bool match_data, uint64_t data, EventNotifier *e);
void (*coalesced_mmio_add)(MemoryListener *listener, MemoryRegionSection *section,
hwaddr addr, hwaddr len);
void (*coalesced_mmio_del)(MemoryListener *listener, MemoryRegionSection *section,
hwaddr addr, hwaddr len);
/* Lower = earlier (during add), later (during del) */
unsigned priority; //表示优先级, 优先级低的在田间是会被优先调用,在删除时会最后被调用
AddressSpace *address_space; //Listener对应的地址空间
QTAILQ_ENTRY(MemoryListener) link;
QTAILQ_ENTRY(MemoryListener) link_as;
};
1.3.1 全局的memory_listeners
首先系统有一个全局的memory_listeners,上面挂上了所有的MemoryListener
// memory.c
static QTAILQ_HEAD(, MemoryListener) memory_listeners
= QTAILQ_HEAD_INITIALIZER(memory_listeners);
1.3.2 memoryListener注册
MemoryListener是通过 memory_listener_register注册的,针对haxm的memoryListerner注册流程如下
hax_accel_class_init() //hax-all.c
hax_accel_init() //hax-all.c
hax_init() //hax-all.c
hax_memory_init() //hax-mem.c
memory_listener_register(&hax_memory_listener, &address_space_memory) //hax-mem.c
1.3.3 MemoryListener通知
MemoryListener注册的回调函数会在内存进行更新的时候被调用。
进行内存更新有很多情况,比如:
- 将一个mr添加到另一个mr的subregions中memory_region_add_subregion
- 更改了一端内存的属性memory_region_set_readonly
- 将一个mr设置使能或者非使能memory_region_set_enabled
以添加subregion为例,通知链调用执行流程如下:
void memory_region_add_subregion(MemoryRegion *mr,
hwaddr offset,
MemoryRegion *subregion)
{
subregion->priority = 0;
memory_region_add_subregion_common(mr, offset, subregion);
}
static void memory_region_update_container_subregions(MemoryRegion *subregion)
{
memory_region_transaction_begin();
...
memory_region_transaction_commit();
}
最终会通过memory_region_transaction_commit函数实现链表遍历
//memory.c
void memory_region_transaction_commit(void)
{
AddressSpace *as;
assert(memory_region_transaction_depth);
assert(qemu_mutex_iothread_locked());
--memory_region_transaction_depth;
if (!memory_region_transaction_depth) {
if (memory_region_update_pending) {
flatviews_reset();
MEMORY_LISTENER_CALL_GLOBAL(begin, Forward);
QTAILQ_FOREACH(as, &address_spaces, address_spaces_link) {
address_space_set_flatview(as);
address_space_update_ioeventfds(as);
}
memory_region_update_pending = false;
ioeventfd_update_pending = false;
MEMORY_LISTENER_CALL_GLOBAL(commit, Forward);
} else if (ioeventfd_update_pending) {
QTAILQ_FOREACH(as, &address_spaces, address_spaces_link) {
address_space_update_ioeventfds(as);
}
ioeventfd_update_pending = false;
}
}
}
#define MEMORY_LISTENER_CALL_GLOBAL(_callback, _direction, _args...)
do {
MemoryListener *_listener;
switch (_direction) {
case Forward:
QTAILQ_FOREACH(_listener, &memory_listeners, link) {
if (_listener->_callback) {
_listener->_callback(_listener, ##_args);
}
}
break;
case Reverse:
QTAILQ_FOREACH_REVERSE(_listener, &memory_listeners,
memory_listeners, link) {
if (_listener->_callback) {
_listener->_callback(_listener, ##_args);
}
}
break;
default:
abort();
}
} while (0)
1.4 FlatView/FlatRange
FlatView管理MR展开后得到的所有FlatRange,ranges是一个数组,记录FlatView下所有的FlatRange,每个FlatRange对应一段虚拟机物理地址区间,各个FlatRange不会重叠,按照地址的顺序保存在数组中。具体的范围由一个AddrRange结构描述,其描述了地址和大小。当memory region发生变化的时候,执行memory_region_transaction_commit,address_space_update_topology,address_space_update_topology_pass最终完成更新FlatView的目标。
将内存平坦完成的函数是:generate_memory_topology
/* Flattened global view of current active memory hierarchy. Kept in sorted
* order.
*/
struct FlatView {
struct rcu_head rcu;
unsigned ref; //引用计数,为0就销毁
FlatRange *ranges; //对应的flatrange数组
unsigned nr; //flatrange数目
unsigned nr_allocated;
struct AddressSpaceDispatch *dispatch;
MemoryRegion *root;
};
/* Range of memory in the global map. Addresses are absolute. */
struct FlatRange {
MemoryRegion *mr; //指向所属的MR
hwaddr offset_in_region; //在MR中的offset
AddrRange addr; //本FR代表的区间
uint8_t dirty_log_mask;
bool romd_mode;
bool readonly;
};
FlatView结构如下:
由图片可知每个FlatRange的中的AddrRange的start为该段内存区间GPA的首地址,size则描述了该段区间的大小。
1.5 MemoryRegionSection
MemoryRegionSection 指向 MemoryRegion 的一部分 ([offset_within_region, offset_within_region + size]),是注册到 KVM 的基本单位。
将 AddressSpace 中的 MemoryRegion 映射到线性地址空间后,由于重叠的关系,原本完整的 region 可能会被切分成片段,于是产生了 MemoryRegionSection。
其中偏移offset_within_region描述的是该section在其所属的MR中的偏移,一个address_space可能有多个MR构成,因此该offset是局部的。而offset_within_address_space是在整个地址空间中的偏移,是全局的offset,如果AddressSpace为系统内存,则该偏移则为GPA的起始地址。
/**
* MemoryRegionSection: describes a fragment of a #MemoryRegion
*
* @mr: the region, or %NULL if empty
* @address_space: the address space the region is mapped in
* @offset_within_region: the beginning of the section, relative to @mr's start
* @size: the size of the section; will not exceed @mr's boundaries
* @offset_within_address_space: the address of the first byte of the section
* relative to the region's address space
* @readonly: writes to this section are ignored
*/
struct MemoryRegionSection {
MemoryRegion *mr; // 指向所属 MemoryRegion
AddressSpace *address_space; // 所属 AddressSpace
hwaddr offset_within_region; // 起始地址 (HVA) 在 MemoryRegion 内的偏移量
Int128 size;
hwaddr offset_within_address_space; // 在 AddressSpace 内的偏移量,如果该 AddressSpace 为系统内存,则为 GPA 起始地址
bool readonly;
};
参考:
https://blog.csdn.net/huang987246510/article/details/104012839
https://www.cnblogs.com/ccxikka/p/9477530.html
https://blog.csdn.net/GerryLee93/article/details/106477323