Skip to content

MemoryRegion

点击听全文

基本介绍

从 CPU 的角度来说,一切访存行为都是对目标地址操作的,CPU 并不关心这个地址背后对应的是什么设备,只要能读写到正确结果即可。

比如执行一条访存指令:

Note

1. CPU 在计算(通过算术逻辑单元 ALU)出目标地址以后,将其发送到地址总线上,同时 CPU 还会给出读写的控制信号;

2. 地址对应的设备,可能是一块普通内存,也可能是一个 I/O 设备(这里特指外设),会对地址总线的信号进行响应;

3. 如果是读操作,则将该地址对应的数据,按照 CPU 指定的位宽大小,通过总线传输回去,一般是存放到 CPU 访存指令给出的寄存器内;

4. 如果是写操作,则会把总线传递过来的数据,按照 CPU 指定的位宽大小,写入指定地址里面,如果是 I/O 设备,一般是更新了这个地址对应的寄存器,并可能产生副作用。

为了实现以上流程,QEMU 提供了一套内存模拟的机制,当然实际上会更复杂,我们挑几个比较重要的概念,尽量以通俗易懂的方式讲出来。

为了能够模拟内存/外设的行为,QEMU 至少要实现以下机制:

Tip

1. 基本的地址空间管理,能够根据 CPU 投递过来的地址,区分是什么设备;

2. 实现地址的离散映射,有些外设的地址不一定是连续的;

3. 实现地址的重映射,比如 MCS-51 的 RAM、XRAM 都是从 0 地址开始的;

为此,QEMU 提供了两个概念,address-space 和 memory-region(下文简称为 mr),前者用于描述整个地址空间的映射关系(不同部件看到的地址空间可能不同),后者用于描述某个具体地址范围内的映射规则。

地址空间布局

我们通过 QEMU 的如下命令进入控制台,打印以下 RISC-V 的 virt machine 作为参考:

qemu-system-riscv64 -M virt -monitor stdio -s -S -display none
QEMU 10.0.50 monitor - type 'help' for more information
(qemu) 

然后我们输入 info mtree 命令可以看到地址空间的布局:

(qemu) info mtree
address-space: I/O
  0000000000000000-000000000000ffff (prio 0, i/o): io

address-space: cpu-memory-0
address-space: memory
  0000000000000000-ffffffffffffffff (prio 0, i/o): system
    0000000000001000-000000000000ffff (prio 0, rom): riscv_virt_board.mrom
    0000000000100000-0000000000100fff (prio 0, i/o): riscv.sifive.test
    0000000000101000-0000000000101023 (prio 0, i/o): goldfish_rtc
    0000000002000000-0000000002003fff (prio 0, i/o): riscv.aclint.swi
    0000000002004000-000000000200bfff (prio 0, i/o): riscv.aclint.mtimer
    0000000003000000-000000000300ffff (prio 0, i/o): gpex_ioport_window
      0000000003000000-000000000300ffff (prio 0, i/o): gpex_ioport
    0000000004000000-0000000005ffffff (prio 0, i/o): platform bus
    000000000c000000-000000000c5fffff (prio 0, i/o): riscv.sifive.plic
    0000000010000000-0000000010000007 (prio 0, i/o): serial
    0000000010001000-00000000100011ff (prio 0, i/o): virtio-mmio
    ...
    0000000010008000-00000000100081ff (prio 0, i/o): virtio-mmio
    0000000010100000-0000000010100007 (prio 0, i/o): fwcfg.data
    0000000010100008-0000000010100009 (prio 0, i/o): fwcfg.ctl
    0000000010100010-0000000010100017 (prio 0, i/o): fwcfg.dma
    0000000020000000-0000000021ffffff (prio 0, romd): virt.flash0
    0000000022000000-0000000023ffffff (prio 0, romd): virt.flash1
    0000000030000000-000000003fffffff (prio 0, i/o): alias pcie-ecam @pcie-mmcfg-mmio 0000000000000000-000000000fffffff
    0000000040000000-000000007fffffff (prio 0, i/o): alias pcie-mmio @gpex_mmio_window 0000000040000000-000000007fffffff
    0000000080000000-0000000087ffffff (prio 0, ram): riscv_virt_board.ram
    0000000400000000-00000007ffffffff (prio 0, i/o): alias pcie-mmio-high @gpex_mmio_window 0000000400000000-00000007ffffffff

address-space: gpex-root
  0000000000000000-ffffffffffffffff (prio 0, i/o): bus master container

memory-region: pcie-mmcfg-mmio
  0000000000000000-000000000fffffff (prio 0, i/o): pcie-mmcfg-mmio

memory-region: gpex_mmio_window
  0000000000000000-ffffffffffffffff (prio 0, i/o): gpex_mmio_window
    0000000000000000-ffffffffffffffff (prio 0, i/o): gpex_mmio

memory-region: system
  0000000000000000-ffffffffffffffff (prio 0, i/o): system
    0000000000001000-000000000000ffff (prio 0, rom): riscv_virt_board.mrom
    0000000000100000-0000000000100fff (prio 0, i/o): riscv.sifive.test
    0000000000101000-0000000000101023 (prio 0, i/o): goldfish_rtc
    0000000002000000-0000000002003fff (prio 0, i/o): riscv.aclint.swi
    0000000002004000-000000000200bfff (prio 0, i/o): riscv.aclint.mtimer
    0000000003000000-000000000300ffff (prio 0, i/o): gpex_ioport_window
      0000000003000000-000000000300ffff (prio 0, i/o): gpex_ioport
    0000000004000000-0000000005ffffff (prio 0, i/o): platform bus
    000000000c000000-000000000c5fffff (prio 0, i/o): riscv.sifive.plic
    0000000010000000-0000000010000007 (prio 0, i/o): serial
    0000000010001000-00000000100011ff (prio 0, i/o): virtio-mmio
    ...
    0000000010008000-00000000100081ff (prio 0, i/o): virtio-mmio
    0000000010100000-0000000010100007 (prio 0, i/o): fwcfg.data
    0000000010100008-0000000010100009 (prio 0, i/o): fwcfg.ctl
    0000000010100010-0000000010100017 (prio 0, i/o): fwcfg.dma
    0000000020000000-0000000021ffffff (prio 0, romd): virt.flash0
    0000000022000000-0000000023ffffff (prio 0, romd): virt.flash1
    0000000030000000-000000003fffffff (prio 0, i/o): alias pcie-ecam @pcie-mmcfg-mmio 0000000000000000-000000000fffffff
    0000000040000000-000000007fffffff (prio 0, i/o): alias pcie-mmio @gpex_mmio_window 0000000040000000-000000007fffffff
    0000000080000000-0000000087ffffff (prio 0, ram): riscv_virt_board.ram
    0000000400000000-00000007ffffffff (prio 0, i/o): alias pcie-mmio-high @gpex_mmio_window 0000000400000000-00000007ffffffff

结合这个输出,我们来讲讲 QEMU 是如何实现内存虚拟化(准确说是模拟)的。

一个 Guest(表示被模拟的对象,这里指 virt machine)可以有多个 address-space,每个 address-space 描述的地址映射关系不一定相同,典型的是 I/O 和 memory。

每个 address-space 对应一个 mr 树,比如 address-space: memory 对应的 mr 的根节点是 system,子节点按照地址大小顺序排列。

由于 mr 描述的是某个具体地址范围内的映射规则,因此可以很方便地实现设备的离散映射。

mr 支持同一级之间地址范围重叠,重叠的部分按照优先级呈现,高优先级的重叠部分作为访问目标。(prio 0, type) 中的 prio 后跟着的是优先级,virt 的外设之间没有地址重叠,因此优先级都是 0。

这里举例说明:

0x8000   0x70000  0x60000  0x50000  0x40000  0x30000  0x20000  0x10000    0
  |--------|--------|--------|--------|--------|--------|--------|--------|
A:[-----------------------------------------------------------------------] prio:0
B:[-----------------------------------------------------] prio:1
C:[-----------------------------------] prio:2
D:[-----------------] prio:3

对于 mr A 来说,它的地址范围可以看成:

0x8000   0x70000  0x60000  0x50000  0x40000  0x30000  0x20000  0x10000    0
  |--------|--------|--------|--------|--------|--------|--------|--------|
A:[DDDDDDDDDDDDDDDDD|CCCCCCCCCCCCCCCCC|BBBBBBBBBBBBBBBBB|AAAAAAAAAAAAAAAAA]

为了实现以上机制,QEMU 使用 alias 来描述 mr 中重叠的部分,使用 alias 可以将一个 mr 的一部分放到另外一个 mr 上,以此来简化内存模拟的复杂度。

初始化流程

我们从 QEMU 初始化过程,来理解 mr 和 address-space 的关系:

main() // system/main.c
|--qemu_init(argc, argv) // system/vlc.c
|  |--cpu_exec_init_all() // system/physmem.c
|  |  |--io_mem_init()
|  |  |  |--memory_region_init_io(&io_mem_unassigned, NULL, &unassigned_mem_ops, NULL, NULL, UINT64_MAX)
|  |  |--memory_map_init()
|  |  |  |--memory_region_init(system_memory, NULL, "system", UINT64_MAX)
|  |  |  |--address_space_init(&address_space_memory, system_memory, "memory")
|  |  |  |--memory_region_init_io(system_io, NULL, &unassigned_io_ops, NULL, "io", 65536)
|  |  |  |--address_space_init(&address_space_io, system_io, "I/O")

这里我们重点关注 memory_region_init() 和 address_space_init()。

对于 memory_region_init() ,最终调用到 memory_region_do_init() :

static void memory_region_do_init(MemoryRegion *mr,
                                  Object *owner,
                                  const char *name,
                                  uint64_t size)
{
    mr->size = int128_make64(size);
    if (size == UINT64_MAX) {
        mr->size = int128_2_64();
    }
    mr->name = g_strdup(name);
    mr->owner = owner;
    mr->dev = (DeviceState *) object_dynamic_cast(mr->owner, TYPE_DEVICE);
    mr->ram_block = NULL;

    if (name) {
        char *escaped_name = memory_region_escape_name(name);
        char *name_array = g_strdup_printf("%s[*]", escaped_name);

        if (!owner) {
            owner = machine_get_container("unattached");
        }

        object_property_add_child(owner, name_array, OBJECT(mr));
        object_unref(OBJECT(mr));
        g_free(name_array);
        g_free(escaped_name);
    }
}

在这段代码会完成 mr 一些关键字段的初始化,比如:

/** MemoryRegion:
 *
 * A struct representing a memory region.
 */
struct MemoryRegion {
    Object parent_obj;

    /* private: */
    Object *owner;
    const MemoryRegionOps *ops;
    Int128 size;
    QTAILQ_HEAD(, MemoryRegion) subregions;
    QTAILQ_ENTRY(MemoryRegion) subregions_link;
    ...
};

ops 指向 mr 访存的实际接口;而 subregions 指向其他 mr,通过 subregions,可以将所有关联的 mr 串起来。

这部分初始化代码,有一些是注册的函数回调,静态 review 代码不太方便理清中间的逻辑,可以借助 gdb 来操作。

system_memory 是一个全局变量指针,指向 mr 的根节点,我们可以对 system_memory->ops 和 system_memory->subregions 进行监视,看看是在哪个函数内被初始化的。 首先观察 system_memory->ops,命令和流程如下(为了方便阅读,对 GDB 打印信息做了简化处理):

$gdb ./build/qemu-system-riscv64
(gdb) b memory_map_initBreakpoint 1 at 0x6a1626: file ../system/physmem.c, line 2557.
(gdb) run
(gdb) watch system_memory->ops
(gdb) c
Old value = <unreadable>
New value = (const MemoryRegionOps *) 0xde4df4fda8189d90
memory_map_init () at ../system/physmem.c:2559
(gdb) c
Old value = (const MemoryRegionOps *) 0xde4df4fda8189d90
New value = (const MemoryRegionOps *) 0x00x00007ffff6a58ee3 in ?? () from /usr/lib/libc.so.6
(gdb) c
Old value = (const MemoryRegionOps *) 0x0
New value = (const MemoryRegionOps *) 0x5555562e76a0 <unassigned_mem_ops>
memory_region_initfn (obj=<optimized out>) at ../system/memory.c:1277
(gdb)

第一次和第二次命中监视点,是对 ops 进行 reset 操作,第三次命中,是真正初始化的地方,我们可以观察一下调用栈:

(gdb) bt
#0  memory_region_initfn (obj=) at ../system/memory.c:1277
#1  object_init_with_type (obj=, ti=) at ../qom/object.c:429
#2  object_initialize_with_type (obj=obj@entry=, size=size@entry=272, type=) at ../qom/object.c:571
#3  object_initialize (data=data@entry=, size=size@entry=272, typename=typename@entry= "memory-region") at ../qom/object.c:595
#4  memory_region_init (mr=, owner=0x0, name= "system", size=) at ../system/memory.c:1224
#5  memory_map_init () at ../system/physmem.c:2559
#6  cpu_exec_init_all () at ../system/physmem.c:3071
#7  qemu_create_machine (qdict=) at ../system/vl.c:2120
#8  qemu_init (argc=<optimized out>, argv=) at ../system/vl.c:3664
#9  main (argc=<optimized out>, argv=<optimized out>) at ../system/main.c:47
(gdb)

可以看到 memory_region_initfn() 是在 object_init_with_type() 中被调用,这是 QEMU 的 QOM 模块,可以简单理解为是对 mr 对象的初始化,这个初始化方法是注册的一个函数指针。

这块儿暂时不去深究,主要是给大家演示通过 GDB 来理解代码意图的方法。

以此类推,我们可以得到 system_memory->subregions 是在哪里被初始化的,你可以自己尝试,如何获得下面的输出:

Thread 1 "qemu-system-ris" hit Hardware watchpoint 3: system_memory->subregions
Old value = {
  tqh_first = 0x0,
  tqh_circ = {
    tql_next = 0x0,
    tql_prev = 0x0
  }
}

New value = {
  tqh_first = 0x0,
  tqh_circ = {
    tql_next = 0x0,
    tql_prev = 0x5555564de378
  }
}
...

in memory_region_initfn (obj=<optimized out>) at ../system/memory.c:1281QTAILQ_INIT(&mr->coalesced);

接着我们将它的函数调用栈打印出来:

(gdb) bt
#0  memory_region_initfn (obj=<optimized out>) at ../system/memory.c:1281
#1  object_init_with_type (obj=..., ti=...) at ../qom/object.c:429
#2  object_initialize_with_type (obj=obj@entry=..., size=size@entry=272, type=...) at ../qom/object.c:571
#3  object_initialize (data=data@entry=0x5555564de2c0, size=size@entry=272, typename=typename@entry=... "memory-region") at ../qom/object.c:595
#4  memory_region_init (mr=..., owner=0x0, name=... "system", size=...) at ../system/memory.c:1224
#5  memory_map_init () at ../system/physmem.c:2559
#6  cpu_exec_init_all () at ../system/physmem.c:3071
#7  qemu_create_machine (qdict=...) at ../system/vl.c:2120
#8  qemu_init (argc=<optimized out>, argv=...) at ../system/vl.c:3664
#9  main (argc=<optimized out>, argv=<optimized out>) at ../system/main.c:47

可以看到,system_memory->subregions 同样是在 memory_region_initfn() 内部被完成初始化的。

节点间关系

如果进一步监视 system_memory->subregions,你将得到这个其他 mr 节点是被如何添加进来的:

(gdb) c
Thread 1 "qemu-system-ris" hit Hardware watchpoint 3: system_memory->subregions
Old value ={
  tqh_first = 0x0,
  tqh_circ = {
    tql_next = 0x0,
    tql_prev = 0x5555564de378
  }
}
New value = {
  tqh_first = 0x5555567e16f0,
  tqh_circ = {
    tql_next = 0x5555567e16f0,
    tql_prev = 0x5555564de378
  }
}
memory_region_update_container_subregions (subregion=0x5555567e16f0) at ../system/memory.c:26452645
memory_region_update_pending |= mr->enabled && subregion->enabled;
...

(gdb) bt
#0  memory_region_update_container_subregions (subregion=) at ../system/memory.c:2645
#1  memory_region_add_subregion_common (mr=<optimized out>, offset=<optimized out>, subregion=) at ../system/memory.c:2661
#2  riscv_aclint_swi_create (addr=addr@entry=, hartid_base=hartid_base@entry=0,num_harts=num_harts@entry=1, sswi=sswi@entry=false) at ../hw/intc/riscv_aclint.c:546
#3  spike_board_init (machine=) at ../hw/riscv/spike.c:248
#4  machine_run_board_init (machine=, mem_path=<optimized out>, errp=<optimized out>, errp@entry= <error_fatal>) at ../hw/core/machine.c:1548
#5  qemu_init_board () at ../system/vl.c:2613
#6  qmp_x_exit_preconfig (errp= <error_fatal>) at ../system/vl.c:2705
#7  qemu_init (argc=<optimized out>, argv=<optimized out>) at ../system/vl.c:3739
#8  main (argc=<optimized out>, argv=<optimized out>) at ../system/main.c:47

memory_region_update_container_subregions() 的过程很简单,最终执行的结果如下:

                          struct MemoryRegion
                        +------------------------+
                        |subregions              |
                        |    QTAILQ_HEAD()       |
                        +------------------------+
                                        |
                +-------------------+---------------------+
                |                                         |
                |                                         |
        struct MemoryRegion                      struct MemoryRegion
    +------------------------+                +------------------------+
    |subregions              |                |subregions              |
    |    QTAILQ_HEAD()       |                |    QTAILQ_HEAD()       |
    +------------------------+                +------------------------+
          ...                                            ...

是不是很像一个树形结构?其实这就是红黑树。

address-space 内有一个 root 字段,指向 memory-region 的根节点,这样就实现了一个 address-space 对应一个 memory-region 树,如下:

                        AddressSpace
                   +-------------------------+
                   |name                     |
                   |   (char *)              |
                   |                         |     MemoryRegion(system_memory/system_io)
                   +-------------------------+          +------------------------+
                   |root                     |          |subregions              |
                   |   (MemoryRegion *)      | -------->|    QTAILQ_HEAD()       |
                   +-------------------------+          +------------------------+
                                                                     |
                                                                     |
                                                 +-------------------+---------------------+
                                                 |                                         |
                                      struct MemoryRegion                          struct MemoryRegion
                                      +------------------------+                   +------------------------+
                                      |subregions              |                   |subregions              |
                                      |    QTAILQ_HEAD()       |                   |    QTAILQ_HEAD()       |
                                      +------------------------+                   +------------------------+

每个 mr 会对应到具体的内存块 RAMBlock,这个内存块从 Host 申请,作为 Guest 外围设备的存储。

mr 提供了一些类型,用于描述存储设备,常见的有 RAM、ROM、IOMMU、container。

我们回到 QEMU 的交互终端,使用如下命令,我们可以打印 virt 的 mr 分布和对应的外设:

(qemu) info qom-tree
(qemu) info qom-tree
/machine (virt-machine)
  /fw_cfg (fw_cfg_mem)
    /\x2from@etc\x2facpi\x2frsdp[0] (memory-region)
    /\x2from@etc\x2facpi\x2ftables[0] (memory-region)
    /\x2from@etc\x2ftable-loader[0] (memory-region)
    /fwcfg.ctl[0] (memory-region)
    /fwcfg.data[0] (memory-region)
    /fwcfg.dma[0] (memory-region)
  /peripheral (container)
  /peripheral-anon (container)
  /soc0 (riscv.hart_array)
    /harts[0] (rv64-riscv-cpu)
      /riscv.cpu.rnmi[0] (irq)
      /riscv.cpu.rnmi[10] (irq)
      ...

对于 mr container 类型,它包含了其他的 mr,记录每个 mr 的 offset。

在实际应用场景,我们可以利用 mr container 创建不同的地址层级关系,可以在地址空间层面,清晰的描述不同子系统的关系,对于实现模块化有很好的帮助。