本文将介绍 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:

-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 文件映射。