QEMU源码全解析 —— 内存虚拟化(8)

接前一篇文章: QEMU源码全解析 —— 内存虚拟化(7)

本文内容参考:

《趣谈 Linux操作系统 》 —— 刘超, 极客时间

QEMU /KVM源码解析与应用》 —— 李强,机械工业出版社

QEMU内存管理模型

浅谈QEMU Memory Region 与 Address Space

【QEMU系统分析之实例篇(七)】-CSDN博客

特此致谢!

2. QEMU虚拟机内存初始化

内存作为虚拟机的基础部分,其初始化除了在pc_init1函数中进行部分工作外,在cpu_exec_init_all函数中也进行了一部分初始化工作。再来回顾一下,cpu_exec_init_all函数在softmmu/physmem.c中,代码如下:

void cpu_exec_init_all(void)
{
    qemu_mutex_init(&ram_list.mutex);
    /* The data structures we set up here depend on knowing the page size,
     * so no more changes can be made after this point.
     * In an ideal world, nothing we did before we had finished the
     * machine setup would care about the target page size, and we could
     * do this much later, rather than requiring board models to state
     * up front what their requirements are.
     */
    finalize_target_page_bits();
    io_mem_init();
    memory_map_init();
    qemu_mutex_init(&map_client_list_lock);
}

cpu_exec_init_all函数中有两个函数的调用与内存相关:io_mem_init()和memory_map_init()。

上一回讲到了cpu_exec_init_all()中的memory_map_init(),再来回顾一下。memory_map_init函数在softmmu/physmem.c中,代码如下:

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_map_init函数中,对于系统内存区域system_memory和用于I/O的内存区域system_io,都进行了初始化,并且关联到了相应的地址空间AddressSpace。在memory_map_init函数中会创建两个AddressSpace:address_space_memory和address_space_io,分别用来表示虚拟机的内存地址空间和I/O地址空间,其对应根MemoryRegion分别是system_memory和system_io。

上一回我们看了memory_region_init函数的代码即内存区域初始化的代码,本回来看address_space_init函数的代码即地址空间初始化及内存区域关联的代码。

address_space_init函数在softmmu/memory.c中,代码如下:

void address_space_init(AddressSpace *as, MemoryRegion *root, const char *name)
{
    memory_region_ref(root);
    as->root = root;
    as->current_map = NULL;
    as->ioeventfd_nb = 0;
    as->ioeventfds = NULL;
    QTAILQ_INIT(&as->listeners);
    QTAILQ_INSERT_TAIL(&address_spaces, as, address_spaces_link);
    as->name = g_strdup(name ? name : "anonymous");
    address_space_update_topology(as);
    address_space_update_ioeventfds(as);
}

对于系统内存地址空间address_space_memory,需要将其内存区域的根root设置为system_memory。

设置好系统内存区域的根之后,还要调用address_space_update_topology函数更新内存拓扑。address_space_update_topology函数在softmmu/memory.c中,代码如下:

static void address_space_update_topology(AddressSpace *as)
{
    MemoryRegion *physmr = memory_region_get_flatview_root(as->root);

    flatviews_init();
    if (!g_hash_table_lookup(flat_views, physmr)) {
        generate_memory_topology(physmr);
    }
    address_space_set_flatview(as);
}

address_space_update_topology函数中会生成AddressSpace的flatview。由于这里内存处于初始化的阶段,属于首次建立,因此会执行到generate_memory_topology函数,第一次生成flat_views。

这个flatview(代码中的flat_views)结构代表的是地址空间(AddressSpace)中与树形结构(MemoryRegion)并列的“平面”结构。为什么要弄两个结构表示同一事物?因为树形内存结构更便于管理;而平的内存结构则更方便与内核通信,以请求物理内存。虽然操作系统内核中也是使用树形结构来表示内存区域的,但是用户态向内核申请内存的时候,会按照平的、连续的模式进行申请。由于QEMU在用户态,因此需要做这样一个转换。

接下来看一下address_space_set_flatview函数,其也在softmmu/memory.c中,代码如下:

static void address_space_set_flatview(AddressSpace *as)
{
    FlatView *old_view = address_space_to_flatview(as);
    MemoryRegion *physmr = memory_region_get_flatview_root(as->root);
    FlatView *new_view = g_hash_table_lookup(flat_views, physmr);

    assert(new_view);

    if (old_view == new_view) {
        return;
    }

    if (old_view) {
        flatview_ref(old_view);
    }

    flatview_ref(new_view);

    if (!QTAILQ_EMPTY(&as->listeners)) {
        FlatView tmpview = { .nr = 0 }, *old_view2 = old_view;

        if (!old_view2) {
            old_view2 = &tmpview;
        }
        address_space_update_topology_pass(as, old_view2, new_view, false);
        address_space_update_topology_pass(as, old_view2, new_view, true);
    }

    /* Writes are protected by the BQL.  */
    qatomic_rcu_set(&as->current_map, new_view);
    if (old_view) {
        flatview_unref(old_view);
    }

    /* Note that all the old MemoryRegions are still alive up to this
     * point.  This relieves most MemoryListeners from the need to
     * ref/unref the MemoryRegions they get---unless they use them
     * outside the iothread mutex, in which case precise reference
     * counting is necessary.
     */
    if (old_view) {
        flatview_unref(old_view);
    }
}

对于address_space_set_flatview函数的解析,请看下回。