前言
- qemu模拟虚机内存,核心是维护虚机物理地址空间。这个地址空间既要方便qemu管理,向虚机侧提供内存,又要方便展示和导出,向平台侧提供内存视图。因此qemu抽象的内存区域有两种组织结构,一种是树状的,方便qemu管理并模拟内存,一种是扁平的,用于展示和导出内存视图,方便传递给KVM
- 树状视图有两个元素,一是AddressSpace,表示一个cpu可访问的地址空间;一是MemoryRegion,表示一段逻辑内存区域。AddressSpace地址空间由许多逻辑内存区域MemoryRegion组成
- 扁平化视图同样有两个元素,一是FlatView,cpu可访问地址空间的扁平化表示;一是FlatRange,逻辑内存区域的扁平化描述,表示一段段内存区域。同样地,FlatView由许多FlatRange组成。
- 本章介绍树状视图
MemoryRegion
数据结构
struct MemoryRegion {
......
bool ram; /* 标记是否为ram类型的MR */
bool readonly; /* For RAM regions,标记是否为ROM类型的MR */
bool rom_device; /* 是否为rom_device设备 */
bool is_iommu; /* 是否为iommu设备 */
RAMBlock *ram_block; /* 是否关联一段真实的虚拟内存 */
const MemoryRegionOps *ops; /* 是否为MMIO类型的MR */
MemoryRegion *container; /* 指向包含此MR的容器MR */
Int128 size; /* 虚机内存的物理地址大小 */
hwaddr addr; /* 虚机内存的绝对物理地址 */
MemoryRegion *alias; /* 指向别名MR的MR */
hwaddr alias_offset; /* 别名MR在所属MR内的偏移*/
QTAILQ_HEAD(, MemoryRegion) subregions; /* 容器MR的子MR组成的链表头部 */
QTAILQ_ENTRY(MemoryRegion) subregions_link; /* 用于将子MR组织成链表的成员 */
unsigned ioeventfd_nb; /* MR包含的ioeventfd个数 */
MemoryRegionIoeventfd *ioeventfds; /* MR包含的ioeventfd数组,用于和内核通信 */
......
};
分类
- MemoryRegion表示一段逻辑内存区域,它的后端可以是以下类型:
- RAM:普通内存,qemu通过向主机申请虚拟内存来实现。一旦qemu成功申请这段内存并给出虚机地址,guest对这段内存的读写就和普通进程对虚拟内存的读写一样,没有物理页就触发缺页,有物理页就正常读写。guest对RAM内存的读写不需要qemu干涉,qemu也感知不到有guest对RAM内存进行了读写,qemu成功申请到虚拟内存给guest就算完成了自己的任务,其它的事情就交给了内核的内存管理来做。普通内存的ram字段必为true。
- MMIO:映射内存,MMIO模拟的不是普通内存。想像pci设备配置空间的第一个字段device id,它是只读的,通过读取这个字段可以获取设备id用来识别是什么类型的pci设备(网卡,显卡,virtio设备)。在物理环境下,设备id的信息由硬件设备提供,在qemu模拟的环境下,信息就需要qemu提供。因此当guest读取这个字段时,qemu需要做一些“动作”来模拟这个信息。所以读取这个字段时会触发callback,callback种实现对信息的模拟。再考虑配置空间的command字段,它的功能是用来控制pci设备,使能pci设备的某些硬件特性。它是可读写的,当guest写这个字段时,意味着对这个设备进行了配置,qemu需要感知这个“配置”从而模拟该配置的结果,因此也要触发callback,callback种实现对配置结果的模拟。简单讲,MMIO内存在读写时会触发一系列的操作,这些操作由MemoryRegionOps实现,初始化这种类型的MemoryRegion时需要提供MemoryRegionOps。映射内存的ops字段必不为空。
- ROM:只读内存,只读内存的读操作和RAM相同,禁止写操作,ROM内存的readonly字段必为true。
- ROM device:只读设备,读操作和RAM行为相同,只读设备的允许写操作,写操作和MMIO行为相同,会触发callback。
- IOMMU region:将对一段内存的访问转发到另一段内存上,这种类型的内存只用于模拟IOMMU的场景。
- container:容器,管理多个MR的MR,用于将多个MR组织成一个内存区域,比如一个pci配置空间,可以抽象成一个容器,它由普通内存(RAM)和映射内存(MMIO)组成。再比如整个虚机的内存地址区域,它被抽象成一个容器,包括了所有虚拟的内存区间。
- alias:alias MR指向另一段MR,可以将一段连续的内存空间分割成多个不连续的内存空间,alias MR可以指向其它类型的MR,也可以指向alias MR,但不能指向其本身,否则就回环了。alias MR的alias字段和alias_offset字段必不为空,alias字段指向一段完整的内存空间,alias_offset表示本MR在alias指向的MR的偏移
MR实例
- 数据结构实例,下面的截图打印了4个MR,类型各不相同,分别是:
- container MR:组织管理多个MR,这里是两个,container MR的subregions字段指向其余3个MR组成的链表的头
- MMIO MR:内存映射MR,当虚机对这段内存有写操作时,会触发回调
- alias MR:指向另一段MR,它是另一段MR的一部分
- RAM MR:表示一段真实内存,它的ram字段必为true,并且关联一个RAMBlock
- 各个数据结构关系,system MR是acpi MR和ram-blew-4g MR的容器,subregions_link将acpi MR和ram-blew-4g MR链接到一起。pc.ram MR则关联了可用的虚拟内存
- 通过
virsh qemu-monitor-command vm --hmp info mtree
命令可以打印虚机的MR
AddressSpace
数据结构
/**
* AddressSpace: describes a mapping of addresses to #MemoryRegion objects
*/
struct AddressSpace {
......
MemoryRegion *root; /* 关联的根MR,地址空间拥有了它,就拥有了整棵MR树的内存信息,结构体初始化时这一字段作为输入 */
/* Accessed via RCU. */
struct FlatView *current_map; /* Root MR对应的扁平化内存视图 */
int ioeventfd_nb;
struct MemoryRegionIoeventfd *ioeventfds;
QTAILQ_HEAD(, MemoryListener) listeners; /* 用于当地址空间发生变化时通知qemu其它模块或者内核 */
QTAILQ_ENTRY(AddressSpace) address_spaces_link;
};
- 从数据结构标注的解释看,地址空间将MR转换成更接近虚拟机侧的内存地址映射。它是一个中间层,起到连结MemoryRegion和FlatView的作用。对比来看,MemoryRegion描述的内存接近主机侧,AddressSpace作为向FlatView转换的中间层,更接近虚机侧。对于qemu模拟的cpu,如果是tcg,一个cpu可以拥有多个地址空间;如果是kvm,只支持一个地址空间(系统地址空间),在其下,可以管理多个子地址空间,比如IO地址空间或内存地址空间。qemu的cpu结构体的cpu_ases是存放地址空间的字段,如下:
/**
struct CPUState {
......
CPUAddressSpace *cpu_ases; /* 地址空间数组 */
int num_ases;
AddressSpace *as; /* 指向第一个内存地址空间,as和cpu_ases[0]->as都存放了首个内存地址空间的指针 */
MemoryRegion *memory;
......
}
* CPUAddressSpace: all the information a CPU needs about an AddressSpace
* @cpu: the CPU whose AddressSpace this is
* @as: the AddressSpace itself
* @memory_dispatch: its dispatch pointer (cached, RCU protected)
* @tcg_as_listener: listener for tracking changes to the AddressSpace
*/
struct CPUAddressSpace {
CPUState *cpu;
AddressSpace *as;
struct AddressSpaceDispatch *memory_dispatch;
MemoryListener tcg_as_listener;
};
AdressSpace初始化
- 地址空间的初始化有三个地方,1是静态全局链表,2是qemu准备cpu执行环境时,3是qemu初始化特定硬件类型时。下面分别介绍
- 全局链表初始化:qemu有链表将所有地址空间组织到一起,全局变量address_spaces指向这个链表的头部
static QTAILQ_HEAD(, AddressSpace) address_spaces = QTAILQ_HEAD_INITIALIZER(address_spaces);
- qemu准备cpu执行环境:将系统内存和IO内存的Root MR和地址空间都进行了初始化。Root MR作为地址空间初始化的输入
cpu_exec_init_all
memory_map_init
system_memory = g_malloc(sizeof(*system_memory)); /* Root MemoryRegion */
memory_region_init(system_memory, NULL, "system", UINT64_MAX);
address_space_init(&address_space_memory, system_memory, "memory"); /* 初始化系统地址空间并添加到全局链表中 */
system_io = g_malloc(sizeof(*system_io));
memory_region_init_io(system_io, NULL, &unassigned_io_ops, NULL, "io", 65536);
address_space_init(&address_space_io, system_io, "I/O"); /* 初始化系统IO空间并添加到全局链表中 */
初始化后的地址空间和根MR关系图如下,除了内存和IO地址空间外,还有cpu的地址空间
通过virsh qemu-monitor-command vm --hmp info mtree
命令可以打印一个虚机的所有地址空间,内存地址空间如下:
IO地址空间如下:
- cpu初始化:每个cpu都有自己可访问的地址空间,因此在cpu对象中有一个cpu_ases数组用来保存地址空间,如果是kvm模拟的cpu,只允许拥有一个地址空间;如果是tcg模拟的cpu,允许有多个地址空间。
machine_run_board_init
pc_init1
pc_cpus_init
......
x86_cpu_realizefn
if (tcg_enabled()) {
cpu->cpu_as_mem = g_new(MemoryRegion, 1);
cpu->cpu_as_root = g_new(MemoryRegion, 1);
/* Outer container... */
memory_region_init(cpu->cpu_as_root, OBJECT(cpu), "memory", ~0ull);
memory_region_set_enabled(cpu->cpu_as_root, true);
cs->num_ases = 2;
cpu_address_space_init(cs, 0, "cpu-memory", cs->memory);
cpu_address_space_init(cs, 1, "cpu-smm", cpu->cpu_as_root);
......
cpu地址空间初始化后拓扑如下,每个cpu的第一个地址空间都指向pc_memory_init中初始化的系统内存地址空间
AdressSpace Listener初始化
- 地址空间还有一个关键数据结构listeners,表示所有监听地址空间变化的对象实体(因此叫Listener),它是一个链表头,链表的每个元素是一个MemoryListener成员。当qemu模拟的内存地址空间发生变化时,需要有机制通知到其它模块,对于kvm的模拟,内存地址空间变化后要通知内核,从而保证内核与用户态内存信息一致。对于tcg的模拟,虽然不通知到内核,但在内存地址信息变化时也有其它事情需要做,MemoryListener结构体就是为此而设计,每个地址空间都维护了这样一个结构体的链表,当内存信息变化时,会触发相关的回调。同时还有一个全局的链表维护所有注册的Listener结构体,这些结构体在链表内通过优先级排序
/**
* 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); /* 1 */
void (*commit)(MemoryListener *listener);
void (*region_add)(MemoryListener *listener, MemoryRegionSection *section);
void (*region_del)(MemoryListener *listener, MemoryRegionSection *section);
void (*region_nop)(MemoryListener *listener, MemoryRegionSection *section);
void (*log_start)(MemoryListener *listener, MemoryRegionSection *section,
int old, int new);
void (*log_stop)(MemoryListener *listener, MemoryRegionSection *section,
int old, int new);
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);
unsigned priority; /* 2 */
AddressSpace *address_space; /* 3 */
QTAILQ_ENTRY(MemoryListener) link;
QTAILQ_ENTRY(MemoryListener) link_as;
......
};
1. 当内存地址空间有变化时,比如添加一个MR或者删除一个MR,整个地址空间都会变化,某些感兴趣的实体(比如内核)可能想要让自己被通知到并调用提前注册的钩子函数,这些函数的原型就在这里定义。一个MemoryListener可以只实现其中部分
2. MemoryListener代表的是某个对地址空间感兴趣的实体,这些实体不只一个,需要被管理起来,有两个地方管理这些实体,一是全局链表memory_listeners,它管理所有注册的Listener,结构体的link成员用作连接到这个链表。二是地址空间,它管理对自己感兴趣的Listener,地址空间的listeners成员维护这个链表头,结构体的link_as成员用作链接到这个链表。成员的address_space指向这个所属的地址空间。同时所有listener有一个优先级,由priority表示,决定了在链表中的顺序。
3. address_space指向listener监听的地址空间,一个地址空间上可以有多个listener在监听。以address_space_memory地址空间举例,当有内存热添加操作时,热添加内存所在的address_space_memory发生变化,需要通知地址空间下面的所有listener,对kvm作为地址空间的一个listener,它收到通知后,做的事情就是注册热添加的内存区域到kvm内核。
- tcg模拟的cpu地址空间初始化时,会注册这个Listener,它只实现了Listener结构体中的commit方法
cpu_address_space_init
if (tcg_enabled()) {
newas->tcg_as_listener.commit = tcg_commit; /* 注册commit */
memory_listener_register(&newas->tcg_as_listener, as);
}
- kvm模拟的情况下, 会注册以下方法
kvm_init
kvm_memory_listener_register(s, &s->memory_listener, &address_space_memory, 0) /* 3 */
......
kml->listener.region_add = kvm_region_add;
kml->listener.region_del = kvm_region_del;
kml->listener.log_start = kvm_log_start;
kml->listener.log_stop = kvm_log_stop;
kml->listener.log_sync = kvm_log_sync;
kml->listener.priority = 10;
memory_listener_register(&kml->listener, as);
memory_listener_register(&kvm_io_listener, &address_space_io); /* 4 */
static MemoryListener kvm_io_listener = { /* 5 */
.eventfd_add = kvm_io_ioeventfd_add,
.eventfd_del = kvm_io_ioeventfd_del,
.priority = 10,
}
4. 系统内存空间的Listener注册
5. 系统IO空间的Listener注册
6. IO空间Listener的接口实现,这里实现ioeventfd的两个回调函数,两个函数在IO空间更新的时候会被调用
Memory Listener的注册分析
void memory_listener_register(MemoryListener *listener, AddressSpace *as)
{
......
QTAILQ_FOREACH(other, &memory_listeners, link) {
if (listener->priority < other->priority) {
break;
}
}
QTAILQ_INSERT_BEFORE(other, listener, link); /* 6 */
......
QTAILQ_FOREACH(other, &as->listeners, link_as) {
if (listener->priority < other->priority) {
break;
}
}
QTAILQ_INSERT_BEFORE(other, listener, link_as); /* 7 */
listener_add_address_space(listener, as); /* 8 */
}
7. 将Listener按优先级加入到全局链表memory_listerners中
8. 将Listener按优先级加入到地址空间的link_as成员中
9. TODO
Q&A
Q:qemu代码中有两个类型的内存地址,ram_addr_t和hwaddr,它们有啥区别?
A:ram_addr_t是qemu内部用来表示内存地址的标准类型,它主要用来统一变量的长度,不同虚拟化实现中的内存地址都需要将其转化成这种类型的地址。说白了,ram_addr_t类型的内存地址是从代码移植性角度定义了一个统一的内存地址类型,它没有任何现实意义。hwaddr表示的是虚机内部看到的物理地址。