5.14.2.3. MemoryRegion
本小节看看qemu的内存管理逻辑。对于VM来说,它有它视角中的内存,当这个内存被VM中的CPU或者设备访问,我们还需要Host中有backend去支撑这个访问,所以,qemu有Host视角中的内存。Qemu使用MemoryRegion描述这个视角的内存。它包含如下一些子概念:
- MemoryRegion
这表示一个面向VM的内存区,以下简称MR。请注意了,MR是一片内存区的描述,而不是那片内存本身。MR的要素是base_address, size这些信息,而不是void *ptr这样的内存本身。整个系统的所有内存就是一个MR,整个系统的所有IO空间(不是说mmio,是说x86的LPC的IO)也是一个MR。MR内部包含多个不同设备的mmio也是一个MR。
但部分基础的内存层是真的分配和Host一侧用于支持前端的backend内存的,这个这个真正的内存指针在MR->ram_block中。
- RAMBlock
这是MR的ram_block的类型,表示一段真实的Host一侧的内存,它可以是创建的时候就分配的,也可能是用Lazy算法动态一点点增加的。
- MemoryRegionSection
MR中的一个分段,简称MRS。当多个MR叠在一起的时候,MR会被隔离成一段一段,每段就是一个MRS。MRS的行为决定于它所在的MR。
- Container
包含其他MR的MR叫Container。没有RAM或者IO属性的Container叫纯Container,不影响理解的时候也可以简单叫Container。纯Container是透明的,要判断一段MRS的行为,如果它属于纯Container,就要看它下一层MR的定义了。
- AddressSpace
这表示一个VM眼中的地址空间,以下简称AS。同一个VM可以有多个不同角度的AS,比如系统内存,这是一个AS,这个AS中的不同地址会map不同的属性的内存片段(MR/MRS)。但比如x86还支持用io指令去访问IO的地址空间,这个也是一个AS,但就不是前面那个AS了。如果VM中而某个设备观察它可以访问的地址空间,看到的可能和CPU看到的不一样,这就会有这个设备自己的AS。
- FlatView
这表示看到的地址空间,本文简称FV。这个概念比较绕。我们这样说:AS是立体的,里面的MR是相互独立的,他们可以交叠,转义,动态开关等。但当你去访问的时候,某个时刻,某个物理地址总是对应着某个MR中(某段MRS)的地址,FlatView用来表示层叠的结果。另外它也提供多个访问源互斥的锁。
- MemoryRegionCache
IO MR中访问过的数据可以放在Cache中,这个Cache简称MRC,现在主要就是给virtio用。
综合来说,我们用MR定义一个有特定属性的内存区(比如RAM或者IO),然后把它们叠起来构成一个AS,backend用这个AS去访问内存,首先压平为一个FV,然后匹配到一个MRS,最终用这个MRS所在MR决定如何访问。
CPU眼中的AS是全局变量,可以用address_space_meory和address_space_io直接访问(没有io指令的平台没有后者),这两个AS对应的基础container可以通过get_system_memory()和get_system_io()获得。
整个概念可以用下图展示:
我们通过例子看看MR的创建方法。
RISCV的系统RAM是这样创建的:
memory_region_init_ram(main_mem, NULL, "riscv_virt_board.ram",
machine->ram_size, &error_fatal);
memory_region_add_subregion(system_memory, memmap[VIRT_DRAM].base,
main_mem);
system_memory是个纯container,初始化的时候就可以创建,真正的内存用memory_region_init_ram()创建,然后用memory_region_add_subregion()加到container中。
如果其中有一段IO的空间,这个通常是你有了某个系统总线的IO设备的时候才会有,这由驱动来创建,通常长这样:
memory_region_init_io(&ar->pm1.evt.io, memory_region_owner(parent),
&acpi_pm_evt_ops, ar, "acpi-evt", 4);
memory_region_add_subregion(parent, 0, &ar->pm1.evt.io);
如果是系统一级的IO,这里的parent就是system_memory container了。
Guest访问的时候有两种可能,一种是Guest的CPU直接做地址访问,这会变成TLB的访问过程。在这个过程中,CPU模拟程序把系统的AS压平为FV,然后找到对应的MR,最后根据MR的属性去回调IO或者直接访问RAM(RAM MR中有ram_block的地址)。
另一种是backend的设备直接调函数去访问地址了,这样的调用:
dma_memory_rw(&address_space_memory, pa, buf, size, direction);
pci_dma_rw(pdev, addr, buffer, len, direction);
pci的调用本质还是对dma_memory_rw的封装,只是有可能用比如iommu这样的手段做一个地址转换而已。
这个实现和前面CPU的访问没有什么区别,仍从AS开始,从AS得到FV,然后定位MRS,最终找到MR。之后作为RAM处理还是IO处理,就由MR的属性决定了。这个代码是这样的:
static MemTxResult flatview_write(FlatView *fv, hwaddr addr, MemTxAttrs attrs,
const void *buf, hwaddr len)
{
...
mr = flatview_translate(fv, addr, &addr1, &l, true, attrs);
result = flatview_write_continue(fv, addr, attrs, buf, len,
addr1, l, mr);
...
}
还有一种Device Backend的DMA访问方法是这样的:
dma_memory_map(address_space, pa, len, direction);
dma_memory_unmap(address_space, buffer, len, direction, access_len);
就是说,你有一个VM意义上的pa,你不调用前面的函数去访问它,而是map它变成一个指针,之后,你可以直接访问上面的内容。
这两个函数的原理是:如果这片MR背后有直接分配的内存,那最好办,直接把本地内存的指针拿过来就可以了,unmap的时候保证发起相关的通知即可。如果没有,那可以使用Bounce DMA Buffer机制。也就是说,直接另外分配一片内存,到时映射过来就是了。
MR有很多类型,比如RAM,ROM,IO等,本质都是io,ram和container的变体:
memory_region_init(mr, owner, name, size);
memory_region_init_alias(mr, owner, name, orig, offset, size);
memory_region_init_io(mr, owner, ops, opaque, name, size);
memory_region_init_iommu(_iommu_mr, instance_size, mrtypename, owner, name, size);
memory_region_init_ram_nomigrate(mr, owner, name, size, errp);
memory_region_init_ram_shared_nomigrate(mr, owner, name, size, share, errp);
memory_region_init_ram_shared_nomigrate(mr, owner, name, size, share, errp);
memory_region_init_ram(mr, owner, name, size, errp);
memory_region_init_ram_ptr(mr, owner, name, size, ptr);
memory_region_init_ram_device_ptr(mr, owner, name, size, ptr);
memory_region_init_ram_from_fd(mr, owner, name, size, share, fd, errp);
memory_region_init_ram_from_file(mr, owner, name, size, align, ram_flags, path, errp);
memory_region_init_rom(mr, owner, name, size, errp);
memory_region_init_rom_device(mr, owner, ops, opaque, name, size, errp);
memory_region_init_rom_device(mr, owner, ops, opaque, name, size, errp);
memory_region_init_rom_device_nomigrate(mr, owner, ops, opaque, name, size, errp);
memory_region_init_rom_device_nomigrate(mr, owner, ops, opaque, name, size, errp);
其中,iommu是最特别的一种MR,它一般用于实现IOMMU,放在设备视角的MR和AS中(而不放在系统MR和AS中)。
5.14.2.3.1. IOMMU MR
IOMMU MR不放入系统MR和AS空间中,因为系统MR和AS相当于物理地址空间,但加了IOMMU,设备访问的就不是物理地址了,它必须是针对每个设备的虚拟地址。
下面是这种MR的一个应用实例(这个例子是ARM SMMU的,但由于ARM的SMMU在qemu中是专门为PCI子系统定制的,我们在例子中把两个模块中的流程进行了组合和化简,突出关键逻辑):
// 为设备创建设备自己的AS,包含一个代表物理空间的container
memory_region_init(&dev->bus_master_container_region, OBJECT(dev),
"bus master container", UINT64_MAX);
address_space_init(&dev->bus_master_as,
&dev->bus_master_container_region, dev->name);
// 创建一个设备的iommu,TYPE_SMMUV3_IOMMU_MEMORY_REGION是iommu的类型名称
memory_region_init_iommu(&dev->iommu_mr, sizeof(dev->iommu_mr),
TYPE_SMMUV3_IOMMU_MEMORY_REGION,
OBJECT(s), name, 1ULL << SMMU_MAX_VA_BITS);
// 创建iommu MR的别名,以便可以动态开启和关闭
memory_region_init_alias(&dev->bus_master_enable_region,
OBJECT(dev), "bus master",
dev->iommu_mr, 0, memory_region_size(dev->iommu_mr));
// 初始化的时候先关掉iommu,等设备启动的时候再让它生效
// 对于PCI设备来说,通常是设备被下了PCI_COMMAND_BUS_MASTER命令的时候,才会开启
memory_region_set_enabled(&dev->bus_master_enable_region, false);
// 加到设备的container MR中
memory_region_add_subregion(&dev->bus_master_container_region, 0,
&dev->bus_master_enable_region);
这样创建出来的dev->bus_master_as就是可以用于dma_memory_rw()访问的AS了。有人可能奇怪,为什么这个AS中没有包含system MR。答案在translate的实现中可以找到:
static IOMMUTLBEntry smmuv3_translate(IOMMUMemoryRegion *mr, hwaddr addr,
IOMMUAccessFlags flag, int iommu_idx)
{
..
IOMMUTLBEntry entry = {
.target_as = &address_space_memory,
.iova = addr,
.translated_addr = addr,
.addr_mask = ~(hwaddr)0, //地址空间长度掩码,如果要求的读写范围超过这个限度,会分多次翻译
.perm = IOMMU_NONE,
};
...
return entry;
}
static void smmuv3_iommu_memory_region_class_init(ObjectClass *klass, void *data)
{
...
imrc->translate = smmuv3_translate;
imrc->notify_flag_changed = smmuv3_notify_flag_changed;
}
static const TypeInfo smmuv3_iommu_memory_region_info = {
.parent = TYPE_IOMMU_MEMORY_REGION,
.name = TYPE_SMMUV3_IOMMU_MEMORY_REGION,
.class_init = smmuv3_iommu_memory_region_class_init,
};
所以答案是,iommu自己提供目标AS是什么(这个例子中就是address_space_memory)。
在qemu的当前实现中,大部分iommu都作为PCI的总线属性的一部分来设计,当你创建一个iommu设备的时候,通过primary_master属性(一个link)指定所述的PCI总线,从而调用pci_setup_iommu()设置回调,之后每个EP注册到这个总线上,就会创建一个针对这个设备的IOMMU设备(以及相应的IOMMU MR)。
但这个设计其实是有毛病的。主要有两个问题:
这个是人为限定了虚拟设备的硬件结构:真实的硬件可不是每个设备都有一个IOMMU设备的,按现在的实际,保证功能是没有问题的,但要模拟一个真实硬件的行为,这是不够的。
translate函数只有VA和属性作为输入。但现代IOMMU设备支持多页表(ASID Index),这个接口需要通过iommu_idx参数索引MemTxAttrs,现在的版本MemTxAttrs不支持pasid,需要增加上去才能支持。这个地方其实设计得不是很好看,因为iommu_idx这个名字就预期这只是一个index,而不是一个值,但要把pasid编码进来,未来如果有更多参数,这就不好发展了。