QEMU内存后端文件的用法及分析
本文将介绍 QEMU 中内存后端文件参数用法和代码实现,基于的版本为 QEMU 6.2。
参数用法
首先是参数的用法,下面是 官方文档 中对此的说明:
memory-backend=’id’
An alternative to legacy
-mem-path
andmem-prealloc
options. Allows to use a memory backend as main RAM.For example:
-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
查看其空间占用大小:
$ du -sh mem
0 mem
然后利用参数指定该文件为内存后端文件,并启动一个模拟器,启动参数如下:
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 \
模拟器启动后关闭再次查看其空间占用大小:
$ du -sh mem
512M mem
可以看到,该文件大小已经增长为了 512M,即我们指定的模拟器内存空间大小,说明 QEMU 的确将该文件作为了内存的后端,且由于我们启动了 share=on
,模拟器运行中带来的内存数据的更改将同步到内存后端文件 mem
中。
我们还可以进行进一步的测试,首先使用 shasum
计算文件 mem
的哈希值:
$ shasum mem
ed1d4cf3845092b3e409acc5c3c1c75a387ed23a mem
然后重新启动模拟器运行,并执行一些命令,造成内存数据的更改,然后重新使用 shasum
计算文件 mem
的哈希值:
$ shasum sum
5e2d5106d36f10a81d597b99c27071ebb353fa66 mem
可以看到,文件的哈希值发生了更改,说明文件的数据发生了变化。
代码实现分析
接下来我们具体分析 QEMU 中是如何实现这样的内存后端文件机制的。首先是函数的调用栈,从对象 mem-backend-file
的创建开始看,其调用栈如下:
(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
以下是各具体代码片段的分析:
/* backends/hostmem.c */
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) {
/* 调用 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)进行分别的内存初始化。
/* backends/hostmem-file.c */
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;
/* 根据文件后端文件来创建 MR */
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
的初始化。
/* softmmu/memory.c */
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;
/* 初始化 MR */
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 根据内存后端文件来创建 */
mr->ram_block = qemu_ram_alloc_from_file(size, mr, ram_flags, path,
readonly, &err);
[...]
}
上述函数初始化了 memory_region
的一系列元数据属性(是否只读、对齐规则等),最后调用 qemu_ram_alloc_from_file
创建其内存块结构 ram_block
。
/* softmmu/physmem.c */
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);
[...]
/* 根据内存后端文件来创建 ram_block */
block = qemu_ram_alloc_from_fd(size, mr, ram_flags, fd, 0, readonly, errp);
[...]
return block;
}
上述函数则是一个进一步的封装,划分为打开文件得到文件描述符 fd 和根据 fd 进行 ram_block
的创建两步。
/* softmmu/physmem.c */
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;
/* ram_block 的宿主机地址根据内存后端文件分配 */
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 的新增 */
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
中进行更新。
/* softmmu/physmem.c */
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;
/* 宿主机内存区域根据内存后端文件 mmap 得到 */
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
文件映射。