本文将介绍 QEMU 中内存后端文件参数用法和代码实现,基于的版本为 QEMU 6.2。
参数用法
首先是参数的用法,下面是 官方文档 中对此的说明:
memory-backend=’id’
An alternative to legacy -mem-path and mem-prealloc options. Allows to use a memory backend as main RAM.
For example:
1 2 3
| -object memory-backend-file,id=pc.ram,size=512M,mem-path=/hugetlbfs,prealloc=on,share=on -machine memory-backend=pc.ram -m 512M
|
接下来我们做一个测试,首先创建一个空文件 mem,使用 du 查看其空间占用大小:
然后利用参数指定该文件为内存后端文件,并启动一个模拟器,启动参数如下:
1 2 3 4 5 6 7 8 9 10
| qemu-system-riscv64 \ -cpu rv64 \ -object memory-backend-file,id=pc.ram,size=512M,mem-path=mem,prealloc=on,share=on \ -machine virt,memory-backend=pc.ram \ -m 512M \ -smp 4 \ -kernel Image \ -append "rootwait root=/dev/vda ro" \ -drive file=rootfs.ext4,format=raw,id=hd0 -device virtio-blk-device,drive=hd0 \ -nographic \
|
模拟器启动后关闭再次查看其空间占用大小:
可以看到,该文件大小已经增长为了 512M,即我们指定的模拟器内存空间大小,说明 QEMU 的确将该文件作为了内存的后端,且由于我们启动了 share=on,模拟器运行中带来的内存数据的更改将同步到内存后端文件 mem 中。
我们还可以进行进一步的测试,首先使用 shasum 计算文件 mem 的哈希值:
1 2
| $ shasum mem ed1d4cf3845092b3e409acc5c3c1c75a387ed23a mem
|
然后重新启动模拟器运行,并执行一些命令,造成内存数据的更改,然后重新使用 shasum 计算文件 mem 的哈希值:
1 2
| $ shasum mem 5e2d5106d36f10a81d597b99c27071ebb353fa66 mem
|
可以看到,文件的哈希值发生了更改,说明文件的数据发生了变化。
代码实现分析
接下来我们具体分析 QEMU 中是如何实现这样的内存后端文件机制的。首先是函数的调用栈,从对象 mem-backend-file 的创建开始看,其调用栈如下:
1 2 3 4 5 6 7 8 9
| (struct UserCreatableClass).complete -> host_memory_backend_memory_complete -> (struct HostMemoryBackendClass).alloc -> file_backend_memory_alloc -> memory_region_init_ram_from_file -> qemu_ram_alloc_from_file -> qemu_ram_alloc_from_fd -> file_ram_alloc -> qemu_ram_mmap
|
以下是各具体代码片段的分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
static void host_memory_backend_memory_complete(UserCreatable *uc, Error **errp) { HostMemoryBackend *backend = MEMORY_BACKEND(uc); HostMemoryBackendClass *bc = MEMORY_BACKEND_GET_CLASS(uc); Error *local_err = NULL; void *ptr; uint64_t sz;
if (bc->alloc) { bc->alloc(backend, &local_err); if (local_err) { goto out; }
ptr = memory_region_get_ram_ptr(&backend->mr); sz = memory_region_size(&backend->mr);
[...] } out: error_propagate(errp, local_err); }
|
上述函数的主要作用是调用一个统一的回调函数 alloc,对各不同的内存后端(ram, file, memfd)进行分别的内存初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
static void file_backend_memory_alloc(HostMemoryBackend *backend, Error **errp) { [...] HostMemoryBackendFile *fb = MEMORY_BACKEND_FILE(backend); uint32_t ram_flags; gchar *name;
[...]
name = host_memory_backend_get_name(backend); ram_flags = backend->share ? RAM_SHARED : 0; ram_flags |= backend->reserve ? 0 : RAM_NORESERVE; ram_flags |= fb->is_pmem ? RAM_PMEM : 0; memory_region_init_ram_from_file(&backend->mr, OBJECT(backend), name, backend->size, fb->align, ram_flags, fb->mem_path, fb->readonly, errp); g_free(name); [...] }
|
上述函数则是针对后端为一个文件(命名文件)的情况,进行内存数据结构 memory_region 的初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
void memory_region_init_ram_from_file(MemoryRegion *mr, Object *owner, const char *name, uint64_t size, uint64_t align, uint32_t ram_flags, const char *path, bool readonly, Error **errp) { Error *err = NULL; memory_region_init(mr, owner, name, size); mr->ram = true; mr->readonly = readonly; mr->terminates = true; mr->destructor = memory_region_destructor_ram; mr->align = align; mr->ram_block = qemu_ram_alloc_from_file(size, mr, ram_flags, path, readonly, &err); [...] }
|
上述函数初始化了 memory_region 的一系列元数据属性(是否只读、对齐规则等),最后调用 qemu_ram_alloc_from_file 创建其内存块结构 ram_block。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
RAMBlock *qemu_ram_alloc_from_file(ram_addr_t size, MemoryRegion *mr, uint32_t ram_flags, const char *mem_path, bool readonly, Error **errp) { int fd; bool created; RAMBlock *block;
fd = file_ram_open(mem_path, memory_region_name(mr), readonly, &created, errp); [...]
block = qemu_ram_alloc_from_fd(size, mr, ram_flags, fd, 0, readonly, errp); [...]
return block; }
|
上述函数则是一个进一步的封装,划分为打开文件得到文件描述符 fd 和根据 fd 进行 ram_block 的创建两步。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
|
RAMBlock *qemu_ram_alloc_from_fd(ram_addr_t size, MemoryRegion *mr, uint32_t ram_flags, int fd, off_t offset, bool readonly, Error **errp) { RAMBlock *new_block; Error *local_err = NULL; int64_t file_size, file_align;
[...] size = HOST_PAGE_ALIGN(size); file_size = get_file_size(fd); if (file_size > 0 && file_size < size) { error_setg(errp, "backing store size 0x%" PRIx64 " does not match 'size' option 0x" RAM_ADDR_FMT, file_size, size); return NULL; }
file_align = get_file_align(fd); if (file_align > 0 && file_align > mr->align) { error_setg(errp, "backing store align 0x%" PRIx64 " is larger than 'align' option 0x%" PRIx64, file_align, mr->align); return NULL; }
new_block = g_malloc0(sizeof(*new_block)); new_block->mr = mr; new_block->used_length = size; new_block->max_length = size; new_block->flags = ram_flags; new_block->host = file_ram_alloc(new_block, size, fd, readonly, !file_size, offset, errp); if (!new_block->host) { g_free(new_block); return NULL; }
ram_block_add(new_block, &local_err); if (local_err) { g_free(new_block); error_propagate(errp, local_err); return NULL; } return new_block;
}
|
上述函数根据 fd 所指向的文件属性创建 ram_block 结构,最核心的操作为调用 file_ram_alloc 分配宿主机的一片内存空间,并将该空间起始地址赋值给 host 字段,ram_block 创建完成后,插入 memory_region 中进行更新。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
static void *file_ram_alloc(RAMBlock *block, ram_addr_t memory, int fd, bool readonly, bool truncate, off_t offset, Error **errp) { uint32_t qemu_map_flags; void *area;
[...]
qemu_map_flags = readonly ? QEMU_MAP_READONLY : 0; qemu_map_flags |= (block->flags & RAM_SHARED) ? QEMU_MAP_SHARED : 0; qemu_map_flags |= (block->flags & RAM_PMEM) ? QEMU_MAP_SYNC : 0; qemu_map_flags |= (block->flags & RAM_NORESERVE) ? QEMU_MAP_NORESERVE : 0; area = qemu_ram_mmap(fd, memory, block->mr->align, qemu_map_flags, offset); if (area == MAP_FAILED) { error_setg_errno(errp, errno, "unable to map backing store for guest RAM"); return NULL; }
block->fd = fd; return area; }
|
上述函数则是如何根据 fd 所指向的文件创建一片内存空间,可以看到,核心操作就是 mmap 文件映射。