qemu内存管理之内存分配
——lvyilong316
这篇文章主要根据qemu2.10版本代码分析一下qemu内存分配的流程。这里说的qemu内存分配实际上是指qemu为guset分配内存,其根本原理就是调用mmap去分配一块内存给guset使用。但是根据不同的需要,mmap的调用方式不同,比如是private的mmap还是share的mmap,是匿名的mmap,还是文件的mmap。这些不同也对应着不同的调用路径。
不管何种方式的内存分配,都需要从pc_init1开始(关于main函数如何调用到pc_init1的,我们在module_call_init机制中分析)。
pc_init1中调用pc_memory_init完成内存的分配,和相关结构的初始化(qemu内存组织结构非常多,这里只是初始化了一部分)。
-
if (!xen_enabled()) {
-
pc_memory_init(machine, system_memory,
-
below_4g_mem_size, above_4g_mem_size,
-
rom_memory, &ram_memory, guest_info);
-
}
其中machine中存放的是创建虚拟机的一些特性,system_memory是一个static MemoryRegion类型(后面简称MR)的全局变量,其描述了一段内存空间。qemu启动后会定义两个全局变量:
static MemoryRegion *system_memory;
static MemoryRegion *system_io;
分别代表着guest的io ram和mem ram,这两个指针指向的全局变量是通过以下路径初始化的:
mainà cpu_exec_init_allà memory_map_init
关于io ram的分配这里先不讲。below_4g_mem_size, above_4g_mem_size分别表示根据guest总内存大小划分的低端内存和高端内存大小。rom_memory表示guest的rom内存区,也是在pc_memory_init中初始化的,但不是我们的重点。ram_memory代表着系统的ram内存区,这是pc_memory_init 初始化的重点。guest_info代表guest的一些信息,其中很多是从qemu参数中得到的。
l pc_memory_init
-
FWCfgState *pc_memory_init(MachineState *machine,
-
MemoryRegion *system_memory,
-
ram_addr_t below_4g_mem_size,
-
ram_addr_t above_4g_mem_size,
-
MemoryRegion *rom_memory,
-
MemoryRegion **ram_memory,
-
PcGuestInfo *guest_info)
-
{
-
int linux_boot, i;
-
MemoryRegion *ram, *option_rom_mr;
-
MemoryRegion *ram_below_4g, *ram_above_4g;
-
FWCfgState *fw_cfg;
-
PCMachineState *pcms = PC_MACHINE(machine);
-
-
assert(machine->ram_size == below_4g_mem_size + above_4g_mem_size);
-
linux_boot = (machine->kernel_filename != NULL);
-
-
/* 分配pc.ram MemoryRegion,作为整个guset的ram */
-
ram = g_malloc(sizeof(*ram));
-
memory_region_allocate_system_memory(ram, NULL, "pc.ram",
-
machine->ram_size);
-
*ram_memory = ram;
-
-
/* 分配并初始化ram-below-4g MemoryRegion,其父MemoryRegion为system_memory */
-
ram_below_4g = g_malloc(sizeof(*ram_below_4g));
-
memory_region_init_alias(ram_below_4g, NULL, "ram-below-4g", ram,
-
0, below_4g_mem_size);
-
memory_region_add_subregion(system_memory, 0, ram_below_4g);
-
e820_add_entry(0, below_4g_mem_size, E820_RAM);
-
-
/* 分配并初始化ram-above-4g MemoryRegion,其父MemoryRegion为system_memory */
-
if (above_4g_mem_size > 0) {
-
ram_above_4g = g_malloc(sizeof(*ram_above_4g));
-
memory_region_init_alias(ram_above_4g, NULL, "ram-above-4g", ram,
-
below_4g_mem_size, above_4g_mem_size);
-
memory_region_add_subregion(system_memory, 0x100000000ULL,
-
ram_above_4g);
-
e820_add_entry(0x100000000ULL, above_4g_mem_size, E820_RAM);
-
}
-
-
...... /* 省略热插拔相关内存的初始化 */
-
/* Initialize PC system firmware */
-
/* 初始化rom_memory MemoryRegion */
-
pc_system_firmware_init(rom_memory, guest_info->isapc_ram_fw);
-
-
option_rom_mr = g_malloc(sizeof(*option_rom_mr));
-
memory_region_init_ram(option_rom_mr, NULL, "pc.rom", PC_ROM_SIZE,
-
&error_abort);
-
vmstate_register_ram_global(option_rom_mr);
-
memory_region_add_subregion_overlap(rom_memory,
-
PC_ROM_MIN_VGA,
-
option_rom_mr,
-
1);
-
-
...... /* 省略了对rom中的bios,system firmware相关初始化 */
-
}
通过分析pc_memory_init的代码,我们可以看到,其中核心的部分就是创建4个MemoryRegion(MR):pc.ram、ram-below-4g、ram-above-4g、pc.rom。我们只关注前三个ram MR,这三个MR以及之前我们说的全局变量system_memory MR之间是什么关系呢?通过pc_memory_init初始化后其结构关系如下图所示:
整个qemu中的MR是以树状结构维护的,单这个树有两个维度,或者说两个根,其中一个根是system_memory,但是它只是描述一块内存空间不对应真正的物理内存,另一个根如pc.ram这种,会对应真正的物理内存。
为了方便描述,我将MemoryRegion分为三类:
(1) 根MemoryRegion:不分配真正的物理内存,通过其subregions将所有的子MemoryRegion管理起来,如图中的system_memory;
(2) 实体MemoryRegion:这种MemoryRegion中真正的分配物理内存,最主要的就是pc.ram和pci。分配的物理内存的作用分别是内存、PCI地址空间以及fireware空间。时这个结构还会为本段虚拟机内存分配虚拟机物理地址空间起始地址,该起始地址(GPA)保存到ram_addr域,该段内存大小为size。通过实体MemoryRegion就可以将HOST地址HVA和GUEST地址GPA对应起来,这种实体MemoryRegion起到了转换的作用;
(3) 别名MemoryRegion:这种MemoryRegion中不分配物理内存,代表了实体MemoryRegion的一个部分,通过alias域指向实体MemoryRegion,alias_offset代表了该别名MemoryRegion所代表内存起始GPA相对于实体MemoryRegion所代表内存起始GPA的偏移量,通常用来计算别名MemoryRegion对应的物理内存的HVA值:HVA = 起始HVA + alias_offset。如图中的ram_above_4g和ram-below-4g;
所有实体MemoryRegion都会被插在主板上,如上图pc.ram就被插在I440FX主板的ram_memory成员中
由于只有pc.ram这个实体MR会真正分配内存,下面我们就注意关注下pc.ram这个MemoryRegion这个过程是由memory_region_allocate_system_memory完成的。
l memory_region_allocate_system_memory
-
void memory_region_allocate_system_memory(MemoryRegion *mr, Object *owner,
-
const char *name,
-
uint64_t ram_size)
-
{
-
uint64_t addr = 0;
-
int i;
-
/* 只有nb_numa_nodes为0时才会进入 */
-
if (nb_numa_nodes == 0 || !have_memdevs) {
-
allocate_system_memory_nonnuma(mr, owner, name, ram_size);
-
return;
-
}
-
/*有numa 的情况*/
-
/* 初始化pc.ram的QOM相关成员(MR也是一种QOM),初始化这个MR的name和size */
-
memory_region_init(mr, owner, name, ram_size);
-
for (i = 0; i < MAX_NODES; i++) {
-
Error *local_err = NULL;
-
/* size为当前numa node上的内存大小 */
-
uint64_t size = numa_info[i].node_mem;
-
HostMemoryBackend *backend = numa_info[i].node_memdev;
-
if (!backend) {
-
continue;
-
}
-
/* 获取当前numa HostMemoryBackend对应的MR */
-
MemoryRegion *seg = host_memory_backend_get_memory(backend, &local_err);
-
if (local_err) {
-
error_report_err(local_err);
-
exit(1);
-
}
-
/* HostMemoryBackend.MR是否已经mmap,虚拟机启动这里是false */
-
if (memory_region_is_mapped(seg)) {
-
char *path = object_get_canonical_path_component(OBJECT(backend));
-
error_report("memory backend %s is used multiple times. Each "
-
"-numa option must use a different memdev value.",
-
path);
-
exit(1);
-
}
-
/* 将当前HostMemoryBackend.MR作为一个字MR加入pc.ram MR */
-
memory_region_add_subregion(mr, addr, seg);
-
vmstate_register_ram_global(seg);
-
addr += size;
-
}
-
}
这个函数根据是否有numa参数的情况,执行不同的内存分配路径。而在没有设置numa时调用allocate_system_memory_nonnuma分配内存:
l allocate_system_memory_nonnuma
-
static void allocate_system_memory_nonnuma(MemoryRegion *mr, Object *owner,
-
const char *name,
-
uint64_t ram_size)
-
{
-
/* 使用tmpfs或hugepage需要指定mem_path */
-
if (mem_path) {
-
#ifdef __linux__
-
Error *err = NULL;
-
memory_region_init_ram_from_file(mr, owner, name, ram_size, false,
-
mem_path, &err);
-
-
/* Legacy behavior: if allocation failed, fall back to
-
* regular RAM allocation.
-
*/
-
if (err) {
-
error_report_err(err);
-
memory_region_init_ram(mr, owner, name, ram_size, &error_abort);
-
}
-
#else
-
fprintf(stderr, "-mem-path not supported on this host\n");
-
exit(1);
-
#endif
-
} else {
-
memory_region_init_ram(mr, owner, name, ram_size, &error_abort);
-
}
-
vmstate_register_ram_global(mr);
-
}
其中有会根据传入的mem_path是否为NULL执行不同的内存分配路径。所以整个内存路径就分为了三个:
(1) 没有设置numa参数,且没有设置mem_path;
(2) 没有设置numa参数,但设置了mem_path;
(3) 设置了numa参数;
具体过程如下图:
其中图中每种颜色代表一种情况的执行路径,下面我们就逐个分析。
没有设置numa参数,且没有设置mem_path
如果没有设置mempath会执行memory_region_init_ram分配内存。那么什么时候使用这种非numa且不设置mem_path的的场景呢?这种组合其实是最常见的方式,一般我们启动vm没有特殊内存要求(如不使用hugepage),没有vm区分numa的需求,就不需要指定numa参数,也不需要设置mem_path,那么guset的内存分配方式就是这种。下面看具体分配过程。
l memory_region_init_ram
-
void memory_region_init_ram(MemoryRegion *mr,
-
Object *owner,
-
const char *name,
-
uint64_t size,
-
Error **errp)
-
{
-
memory_region_init(mr, owner, name, size);
-
mr->ram = true;
-
mr->terminates = true;
-
mr->destructor = memory_region_destructor_ram;
-
mr->ram_addr = qemu_ram_alloc(size, mr, errp);
-
mr->dirty_log_mask = tcg_enabled() ? (1 << DIRTY_MEMORY_CODE) : 0;
-
}
该函数首先使用memory_region_init初始化pc.ram这个MR的name和size等信息,然后初始化MR的一些成员。其中关键是调用qemu_ram_alloc分配内存。
l qemu_ram_alloc
qemu_ram_alloc只是qemu_ram_alloc_internal的封装。
l qemu_ram_alloc_internal
-
static
-
ram_addr_t qemu_ram_alloc_internal(ram_addr_t size, ram_addr_t max_size,
-
void (*resized)(const char*,
-
uint64_t length,
-
void *host),
-
void *host, bool resizeable,
-
MemoryRegion *mr, Error **errp)
-
{
-
RAMBlock *new_block;
-
ram_addr_t addr;
-
Error *local_err = NULL;
-
-
size = TARGET_PAGE_ALIGN(size);
-
max_size = TARGET_PAGE_ALIGN(max_size);
-
new_block = g_malloc0(sizeof(*new_block));
-
new_block->mr = mr;
-
new_block->resized = resized;
-
new_block->used_length = size;
-
new_block->max_length = max_size;
-
assert(max_size >= size);
-
new_block->fd = -1;
-
new_block->host = host;
-
if (host) {
-
new_block->flags |= RAM_PREALLOC;
-
}
-
if (resizeable) {
-
new_block->flags |= RAM_RESIZEABLE;
-
}
-
addr = ram_block_add(new_block, &local_err);
-
if (local_err) {
-
g_free(new_block);
-
error_propagate(errp, local_err);
-
return -1;
-
}
-
return addr;
-
}
这个函数又引入了一个新结构RAMBlock,这个结构表示底层得物理内存。其mr成员指向对应的实体MR(RAMBlock和实体MR是对应的)。根据pc.ram这个MR初始化新建的RAMBlock结构,然后调用ram_block_add将这个RAMBlock插入到全局链表ram_list.blocks。那真正的内存是在哪里分配的呢,我们看ram_block_add。
l ram_block_add
-
static ram_addr_t ram_block_add(RAMBlock *new_block, Error **errp)
-
{
-
RAMBlock *block;
-
RAMBlock *last_block = NULL;
-
ram_addr_t old_ram_size, new_ram_size;
-
-
old_ram_size = last_ram_offset() >> TARGET_PAGE_BITS;
-
qemu_mutex_lock_ramlist();
-
new_block->offset = find_ram_offset(new_block->max_length);
-
-
if (!new_block->host) {
-
if (xen_enabled()) {
-
……
-
} else {
-
/* 分配物理内存 */
-
new_block->host = phys_mem_alloc(new_block->max_length,
-
&new_block->mr->align);
-
memory_try_enable_merging(new_block->host, new_block->max_length);
-
}
-
}
-
-
new_ram_size = MAX(old_ram_size,
-
(new_block->offset + new_block->max_length) >> TARGET_PAGE_BITS);
-
/* 如果内存变大了,需要扩展用来记录热迁移的脏页位图dirty_memory */
-
if (new_ram_size > old_ram_size) {
-
migration_bitmap_extend(old_ram_size, new_ram_size);
-
}
-
/* Keep the list sorted from biggest to smallest block. Unlike QTAILQ,
-
* QLIST (which has an RCU-friendly variant) does not have insertion at
-
* tail, so save the last element in last_block.
-
*/
-
/* 将RAMBlock按照又大到小的顺序插入全局链表ram_list.blocks */
-
QLIST_FOREACH_RCU(block, &ram_list.blocks, next) {
-
last_block = block;
-
if (block->max_length < new_block->max_length) {
-
break;
-
}
-
}
-
if (block) {
-
QLIST_INSERT_BEFORE_RCU(block, new_block, next);
-
} else if (last_block) {
-
QLIST_INSERT_AFTER_RCU(last_block, new_block, next);
-
} else { /* list is empty */
-
QLIST_INSERT_HEAD_RCU(&ram_list.blocks, new_block, next);
-
}
-
ram_list.mru_block = NULL;
-
……
-
return new_block->offset;
-
}
这个函数首先调用phys_mem_alloc分配物理内存,然后将当前创建的RAMBlock按照又大到小的顺序插入全局链表ram_list.blocks。其中phys_mem_alloc被初始化为qemu_anon_ram_alloc:
static void *(*phys_mem_alloc)(size_t size, uint64_t *align) = qemu_anon_ram_alloc;
qemu_anon_ram_alloc不再展开,其中会调用mmap分配内存(注意是匿名、私有方式mmap):
mmap(0, total, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
其mmap的返回地址(host虚拟地址)赋值给RAMBlock->host。我们再注意一下ram_block_add的返回值是什么。new_block->offset也就是当然RAMBlock在整个地址空间(这个地址空间可以理解为guest的物理地址控制)的偏移,也就是对应guset的物理地址块的起始地址。这个值最终会被赋值给MR->ram_addr。
执行完整个数据结构关系如下图所示:
没有设置numa参数,但设置了mem_path
下面看没有设置numa参数,但设置了mem_path的情况。什么情况下回使用这种内存分配方式呢?例如想使用hugepage,tmpfs这种内存时,而又没有内存共享的需求时就可以使用这种。为什么不能用于内存共享的情况呢?我们注意allocate_system_memory_nonnuma中对memory_region_init_ram_from_file的调用方式:
memory_region_init_ram_from_file(mr, owner, name, ram_size, false, mem_path, &err);
其中第五个参数share为false,所以导致之后mmap的内存不能share,所以说如果后端网络使用vhost_user一定需要指定numa参数,否则就无法共享qemu内存。下面我们看具体实现。
l memory_region_init_ram_from_file
-
void memory_region_init_ram_from_file(MemoryRegion *mr,
-
struct Object *owner,
-
const char *name,
-
uint64_t size,
-
bool share,
-
const char *path,
-
Error **errp)
-
{
-
/* 初始化pc.ram MemoryRegion,size为vm内存大小:machine->ram_size */
-
memory_region_init(mr, owner, name, size);
-
mr->ram = true;
-
mr->terminates = true;
-
mr->destructor = memory_region_destructor_ram;
-
mr->ram_addr = qemu_ram_alloc_from_file(size, mr, share, path, errp);
-
mr->dirty_log_mask = tcg_enabled() ? (1 << DIRTY_MEMORY_CODE) : 0;
-
}
老样子,调用memory_region_init对当前MR(pc.ram)进行一些初始化,然后调用qemu_ram_alloc_from_file分配内存。
l qemu_ram_alloc_from_file
-
ram_addr_t qemu_ram_alloc_from_file(ram_addr_t size, MemoryRegion *mr,
-
bool share, const char *mem_path,
-
Error **errp)
-
{
-
RAMBlock *new_block;
-
ram_addr_t addr;
-
Error *local_err = NULL;
-
-
size = TARGET_PAGE_ALIGN(size);
-
new_block = g_malloc0(sizeof(*new_block));
-
new_block->mr = mr;
-
new_block->used_length = size;
-
new_block->max_length = size;
-
new_block->flags = share ? RAM_SHARED : 0;
-
/* 调用file_ram_alloc分配内存 */
-
new_block->host = file_ram_alloc(new_block, size,
-
mem_path, errp);
-
if (!new_block->host) {
-
g_free(new_block);
-
return -1;
-
}
-
-
addr = ram_block_add(new_block, &local_err);
-
if (local_err) {
-
g_free(new_block);
-
error_propagate(errp, local_err);
-
return -1;
-
}
-
return addr;
-
}
和之前没有指定mem_path的情况一样,还是先分配一个RAMBlock,然后初始化,不同的是这里是调用file_ram_alloc进行物理内存的分配的。然后还是调用ram_block_add将RAMBlock加入全局链表ram_list.blocks中。之前我们看到ram_block_add也会分配物理内存,这不是就和file_ram_alloc分配物理内存重复了吗?其实不会,ram_block_add会检查new_block->host,如果不为NULL,说明之前已经分配过了,就不再分配内存,值进行链表插入操作。
-
static void *file_ram_alloc(RAMBlock *block,
-
ram_addr_t memory,
-
const char *path,
-
Error **errp)
-
{
-
char *filename;
-
char *sanitized_name;
-
char *c;
-
void *area = NULL;
-
int fd;
-
uint64_t hpagesize;
-
Error *local_err = NULL;
-
-
hpagesize = gethugepagesize(path, &local_err);
-
block->mr->align = hpagesize;
-
/* 检查内存大小不能小于内存文件系统的页大小,如使用1G的hugepage,不能创建512M内存的vm */
-
if (memory < hpagesize) {
-
error_setg(errp, "memory size 0x" RAM_ADDR_FMT " must be equal to "
-
"or larger than huge page size 0x%" PRIx64,
-
memory, hpagesize);
-
goto error;
-
}
-
-
/* Make name safe to use with mkstemp by replacing '/' with '_'. */
-
sanitized_name = g_strdup(memory_region_name(block->mr));
-
for (c = sanitized_name; *c != '\0'; c++) {
-
if (*c == '/')
-
*c = '_';
-
}
-
/* 获取内存临时文件名称 */
-
filename = g_strdup_printf("%s/qemu_back_mem.%s.XXXXXX", path,
-
sanitized_name);
-
g_free(sanitized_name);
-
/* 创建内存临时文件 */
-
fd = mkstemp(filename);
-
/* 删除内存临时文件,注意由于进程还在打开,所以文件不会真的被删除 */
-
unlink(filename);
-
g_free(filename);
-
-
memory = (memory+hpagesize-1) & ~(hpagesize-1);
-
-
if (ftruncate(fd, memory)) {
-
perror("ftruncate");
-
}
-
/* mmap 内存文件 */
-
area = mmap(0, memory, PROT_READ | PROT_WRITE,
-
(block->flags & RAM_SHARED ? MAP_SHARED : MAP_PRIVATE),
-
fd, 0);
-
-
if (mem_prealloc) {
-
os_mem_prealloc(fd, area, memory);
-
}
-
-
block->fd = fd;
-
return area;
-
}
整个过程比较简单,就是在指定目录下创建一个临时的内存文件,然后进行mmap。
设置了numa参数
最后我们看下设置了numa参数的情况,我们刚才说之前两种情况都不能满足vhost_user的使用,因为无法内存共享。那么我们看下使用vhost_user时的qemu参数的例子:
-object memory-backend-file,id=ram-node0,prealloc=yes,mem-path=/dev/hugepages/,share=yes,size=17179869184 -numa node,nodeid=0,cpus=0-29,memdev=ram-node0
没错我们需要指定numa参数,同时也要指定mem-path。numa参数的会导致在parse_numa函数中解析,并对nb_numa_nodes++。这里还有注意,指定了numa时也指定了memory-backend-file参数,这会导致qemu创建一个struct HostMemoryBackend内存对象(object)。
下面我们回到memory_region_allocate_system_memory具体分析。
l memory_region_allocate_system_memory
-
void memory_region_allocate_system_memory(MemoryRegion *mr, Object *owner,
-
const char *name,
-
uint64_t ram_size)
-
{
-
uint64_t addr = 0;
-
int i;
-
/* 只有nb_numa_nodes为0时才会进入 */
-
if (nb_numa_nodes == 0 || !have_memdevs) {
-
allocate_system_memory_nonnuma(mr, owner, name, ram_size);
-
return;
-
}
-
/*不开启numa的情况*/
-
/* 初始化pc.ram的QOM相关成员(MR也是一种QOM),初始化这个MR的name和size */
-
memory_region_init(mr, owner, name, ram_size);
-
for (i = 0; i < MAX_NODES; i++) {
-
Error *local_err = NULL;
-
/* size为当前numa node上的内存大小 */
-
uint64_t size = numa_info[i].node_mem;
-
HostMemoryBackend *backend = numa_info[i].node_memdev;
-
if (!backend) {
-
continue;
-
}
-
/* 获取当前numa HostMemoryBackend对应的MR */
-
MemoryRegion *seg = host_memory_backend_get_memory(backend, &local_err);
-
if (local_err) {
-
error_report_err(local_err);
-
exit(1);
-
}
-
/* HostMemoryBackend.MR是否已经mmap,虚拟机启动这里是false */
-
if (memory_region_is_mapped(seg)) {
-
char *path = object_get_canonical_path_component(OBJECT(backend));
-
error_report("memory backend %s is used multiple times. Each "
-
"-numa option must use a different memdev value.",
-
path);
-
exit(1);
-
}
-
/* 将当前HostMemoryBackend.MR作为一个子MR加入pc.ram MR */
-
memory_region_add_subregion(mr, addr, seg);
-
vmstate_register_ram_global(seg);
-
addr += size;
-
}
-
}
当nb_numa_nodes不为0时,实际上并没有分配真正的内存,而只是将对应numa 的HostMemoryBackend.MR作为子MR加入pc.ram MR。
那么这种情况下的物理内存是在什么时候分配的呢?事实上是通过后续的如下调用路径:
调用的具体过程我们不再展开,以后有机会再写吧。然后我们看一下分配完内存后的数据结构关系图: