1 Qemu内存分布
2 内存初始化
Qemu中的内存模型,简单来说就是Qemu申请用户态内存并进行管理,并将该部分申请的内存注册到对应的加速器(如KVM)中。这样的模型有如下好处:
- 策略与机制分离。加速的机制由KVM负责,而如何调用加速的机制由Qemu负责
- 可以由Qemu设置多种内存模型,如UMA、NUMA等等
- 方便Qemu对特殊内存的管理(如MMIO)
- 内存的分配、回收、换出等都可以采用Linux原有的机制,不需要为KVM单独开发。
-
兼容其他加速器模型(或者无加速器,单纯使用Qemu做模拟)
Qemu需要做的有两方面工作:向KVM注册用户态内存空间,申请用户态内存空间。
Qemu主要通过如下结构来维护内存:
/ A system address space - I/O, memory, etc. /
struct AddressSpace {
char name;
MemoryRegion root;
FlatView current_map;
int ioeventfd_nb;
MemoryRegionIoeventfd ioeventfds;
struct AddressSpaceDispatch dispatch;
struct AddressSpaceDispatch next_dispatch;
MemoryListener dispatch_listener;
QTAILQ_ENTRY(AddressSpace) address_spaces_link;
};
"memory"的root是static MemoryRegion system_memory;
使用链表address_spaces保存虚拟机的内存,该链表保存AddressSpace address_space_io和AddressSpace address_space_memory等信息
void address_space_init(AddressSpace as, MemoryRegion root, const char *name)
{
if (QTAILQ_EMPTY(&address_spaces)) {
memory_init();
}memory_region_transaction_begin();
as->root = root;
as->current_map = g_new(FlatView, 1);
flatview_init(as->current_map);
as->ioeventfd_nb = 0;
as->ioeventfds = NULL;
QTAILQ_INSERT_TAIL(&address_spaces, as, address_spaces_link);
as->name = g_strdup(name ? name : "anonymous");
address_space_init_dispatch(as);
memory_region_update_pending |= root->enabled;
memory_region_transaction_commit();
}
static void memory_map_init(void)
{
system_memory = g_malloc(sizeof(*system_memory));
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");
memory_listener_register(&core_memory_listener, &address_space_memory);
}
AddressSpace设置了一段内存,其主要信息存储在root成员 中,root成员是个MemoryRegion结构,主要存储内存区的结构。在Qemu中最主要的两个AddressSpace是 address_space_memory和address_space_io,分别对应的MemoryRegion变量是system_memory和 system_io。
Qemu的主函数是vl.c中的main函数,其中调用了configure_accelerator(),是KVM初始化的配置部分。
configure_accelerator中首先根据命令行输入的参数找到对应的accelerator,这里是KVM。之后调用accel_list[i].init(),即kvm_init()。
在kvm_init()函数中主要做如下几件事情: - s->fd = qemu_open("/dev/kvm", O_RDWR),打开kvm控制的总设备文件/dev/kvm
- s->vmfd = kvm_ioctl(s, KVM_CREATE_VM, 0),调用创建虚拟机的API
- kvm_check_extension,检查各种extension,并设置对应的features
- ret = kvm_arch_init(s),做一些体系结构相关的初始化,如msr、identity map、mmu pages number等等
- kvm_irqchip_create,调用kvm_vm_ioctl(s, KVM_CREATE_IRQCHIP)在KVM中虚拟IRQ芯片
- memory_listener_register,该函数是初始化内存的主要函数
memory_listener_register调用了两次,分别注册了 kvm_memory_listener和kvm_io_listener,即通用的内存和MMIO是分开管理的。以通用的内存注册为例,函数首先在全局 的memory_listener链表中添加了kvm_memory_listener,之后调用listener_add_address_space 分别将该listener添加到address_space_memory和address_space_io中, address_space_io是虚机的io地址空间(设备的io port就分布在这个地址空间里)。
然后调用listener的region_add(即 kvm_region_add()),该函数最终调用了kvm_set_user_memory_region(),其中调用 kvm_vm_ioctl(s, KVM_SET_USER_MEMORY_REGION, &mem),该调用是最终将内存区域注册到kvm中的函数。
之后在vl.c的main函数中调用了cpu_exec_init_all() => memory_map_init(),设置system_memory和system_io。
至此初始化好了所有Qemu中需要维护的相关的内存结构,并完成了在KVM中的注册。下面需要初始化KVM中的MMU支持。
ram_size内存大小从内存被读取到ram_size中,在vl.c的main中调用machine->init()来初始化,machine是命令行指定的机器类型,默认的init是pc_init_pci
pc_memory_init
memory_region_allocate_system_memory
memory_region_add_subregion
memory_region_add_subregion_common
memory_region_update_container_subregions
memory_region_transaction_commit
address_space_update_topology
generate_memory_topology
render_memory_region
flatview_insert
3 内存分配
内存的分配实现函数为 ram_addr_t qemu_ram_alloc(ram_addr_t size, MemoryRegion *mr),输出为该次分配的内存在所有分配内存中的顺序偏移(即下图中的红色数字).
该函数最终调用phys_mem_alloc分配内存, 并将所分配的全部内存块, 串在一个ram_blocks开头的链表中, 如下示意:
上图中分配了4个内存块, 每次分配时偏移offset顺序累加, host指向该内存块在主机中的虚拟地址.
调用memory_listener_register注册
4 内存映射
使用的相关结构体如下:
/ Range of memory in the global map. Addresses are absolute. /
struct FlatRange {
MemoryRegion mr;
hwaddr offset_in_region;
AddrRange addr;
uint8_t dirty_log_mask;
bool romd_mode;
bool readonly;
};
/ Flattened global view of current active memory hierarchy. Kept in sorted order./
struct FlatView {
unsigned ref;
FlatRange ranges;
unsigned nr;
unsigned nr_allocated;
};
映射是将上面分配的地址块映射为客户机的物理地址, 函数如下, 输入为映射后的物理地址, 内存偏移,通用内存块的地址
static void memory_region_add_subregion_common(MemoryRegion mr, hwaddr offset, MemoryRegion subregion)
MemoryRegion mr:对应的是system_memory或者system_io,通过memory_listener_register函数注册内存块。
通用栈如下:
memory_region_update_container_subregions
memory_region_transaction_commit
address_space_update_topology
generate_memory_topology
address_space_update_topology_pass
memory_region_update_container_subregions函数在链表中寻找合适的位置插入,
/插入指定的位置/
QTAILQ_FOREACH(other, &mr->subregions, subregions_link) {
if (subregion->priority >= other->priority) {
QTAILQ_INSERT_BEFORE(other, subregion, subregions_link);
goto done;
}
}
QTAILQ_INSERT_TAIL(&mr->subregions, subregion, subregions_link);
memory_region_transaction_commit中引入了新的结构address_spaces(AS),内存有不同的应用类型,address_spaces以链表形式存在,commit函数则是对所有AS执行 address_space_update_topology,先看AS在哪里注册的,就是前面提到的kvm_init里面,执行 memory_listener_register,注册了address_space_memory和address_space_io两个,涉及的另 外一个结构体则是MemoryListener,有kvm_memory_listener和kvm_io_listener,就是用于监控内存映射关系 发生变化之后执行回调函数。
address_space_update_topology_pass函数比较之前的内存块,做相应的处理
MEMORY_LISTENER_UPDATE_REGION函数,将变化的FlatRange构造一个MemoryRegionSection,然后 遍历所有的memory_listeners,如果memory_listeners监控的内存区域和MemoryRegionSection一样,则执 行第四个入参函数,如region_del函数,即kvm_region_del函数,这个是在kvm_init中初始化的。 kvm_region_add主要是kvm_set_phys_mem函数,主要是将MemoryRegionSection有效值转换成KVMSlot 形式,在kvm_set_user_memory_region中使用kvm_vm_ioctl(s, KVM_SET_USER_MEMORY_REGION, &mem)传递给kernel。
5 客户机物理地址到主机虚拟地址的转换
5.1 地址属性
内存映射是以页为单位的, 也就意味着phys_offset的低12bit为0, Qemu使用这些bit标识地址属性:
Bit 11-3 Bit 2 Bit 1 Bit 0
MMIO索引, 其中4个固定分配 SUBWIDTH SUBPAGE ROMD
0: RAM
1: ROM
2: UNASSIGNED
3: NOTDIRTY
5.2 客户机物理地址到主机虚拟地址的转换步骤
虚拟机因mmio退出时,qemu处理该退出事件,相关的函数:
void cpu_physical_memory_rw(hwaddr addr, uint8_t buf, int len, int is_write)
该函数实现虚拟机的物理地址到主机虚拟地址的转换
- 查找该地址所应的MemoryRegionSection结构, 函数为 static MemoryRegionSection phys_page_find(PhysPageEntry lp, hwaddr addr, Node nodes, MemoryRegionSection *sections), 即将客户物理地址分为4段, 取每一段的索引查找下一段, 直至找到Level 3的MemoryRegionSection结构.
- 调函数void qemu_get_ram_ptr(ram_addr_t addr), 取主机虚拟地址起始位置, 再加上页内偏移, 即为对应的主机虚拟地址
6 Kvm映射
static void kvm_set_phys_mem(MemoryRegionSection section, bool add)
该函数把guest机的物理内存映射主机的虚拟内存
typedef struct KVMSlot
{
hwaddr start_addr; /guest物理地址/
ram_addr_t memory_size; /内存大小/
void ram; /对应的虚拟地址/
int slot; /对应的插槽号*/
int flags;
} KVMSlot;
Qemu支持kvm时, 还需通知kvm将客户机物理内存进行映射, 方法为先定义一个映射结构:
struct kvm_userspace_memory_region memory = {
.memory_size = len,
.guest_phys_addr = phys_start, // 客户机物理地址
.userspace_addr = userspace_addr, // 主机虚拟地址, 而非上面的偏移
.flags = log ? KVM_MEM_LOG_DIRTY_PAGES : 0,
};
然后调用kvm的ioctl r = kvm_vm_ioctl(kvm_state, KVM_SET_USER_MEMORY_REGION, &memory);同时, qemu的kvm用户空间代码, 还定义了一些结构如mapping/slot, 用于地址空间的管理, 如防止重复映射等.