1.内存虚拟化技术实现原理
内存虚拟化其实就是关于如何做Guest虚机到host宿主机物理内存之间的各种地址转换,KVM经历了三代的内存虚拟化技术,大大加快了内存的访问速率。
先看看虚拟化环境和非虚拟化环境,内存分配的差异:
非虚拟化环境,内存分配时逻辑地址需要转换为线性地址,然后由线性地址转换为物理地址。
逻辑地址 ==> 线性地址 ==> 物理地址
虚拟化环境下,由于qemu-kvm进程在宿主机上作为一个普通进程,那对于Guest而言,需要的转换过程就是这样:
GVA===>GPA===>HVA===>HPV
Guest Physical Address, GPA 客户机物理地址
Guest Virtual Address, GVA 客户机虚拟地址
Host Physical Address, HPA 宿主机物理地址
Host Virtual Address, HVA 宿主机虚拟地址
为了实现内存虚拟化,让客户机使用一个隔离的、从零开始且具有连续的内存空间,像KVM虚拟机引入一层新的地址空间,即客户机物理地址空间 (Guest Physical Address, GPA),这个地址空间并不是真正的物理地址空间,它只是宿主机虚拟地址空间在客户机地址空间的一个映射。对客户机来说,客户机物理地址空间都是从零开始的连续地址空间,但对于宿主机来说,客户机的物理地址空间并不一定是连续的,客户机物理地址空间有可能映射在若干个不连续的宿主机地址区间。
在虚拟化环境中,由于虚拟机物理地址不能直接用于宿主机物理MMU进行寻址,所以需要把虚拟机物理地址转换成宿主机虚拟地址 (Host Virtual Address, HVA)。运行在硬件之上的Hypervisor首先会对物理内存进行虚拟地址 (Host Virtual Address, HVA)转换,然后还需要对转换后的虚拟地址内存空间进行再次虚拟,然后输出给上层虚拟机使用,而在虚拟机中同样又要进行GVA转换到GPA操作。显然通过这种映射方式,虚拟机的每次内存访问都需要Hypervisor介入,并由软件进行多次地址转换,其效率是非常低的。
因此,为了提高GVA到HPA转换的效率,目前有两种实现方式来进行客户机虚拟地址到宿主机物理地址之间的直接转换。其一是基于纯软件的实现方式,也即通过影子页表(Shadow Page Table)来实现客户虚拟地址GVA到宿主机物理地址HPA之间的直接转换(KVM虚拟机是支持的)。其二是基于硬件辅助MMU对虚拟化的支持,来实现两者之间的转换。
其中Shadow Page Table(影子页表),其实现非常复杂,因为每一个虚拟机都需要有一个Shadow Page Table。并且这种情况会出现一种非常恶劣的结果,那就是TLB(Translation Lookaside Buffer,传输后备缓冲器)很难命中,尤其是由多个虚拟主机时,因为TLB中缓存的是GVA到GPA的转换关系,所以每一次虚拟主机切换都需要清空TLB,不然主机之间就会发生数据读取错误(因为各主机间都是GVA到GPA)。传输后备缓冲器是一个内存管理单元用于改进虚拟地址到物理地址转换后结果的缓存,而这种问题也会导致虚拟机性能低下。
此外,Intel的EPT(Extent Page Table) 技术和AMD的NPT(Nest Page Table) 技术都对内存虚拟化提供了硬件支持。这两种技术原理类似,都是在硬件层面上实现客户机虚拟地址到宿主机物理地址之间的转换。称为Virtualation MMU。当有了这种MMU虚拟化技术后,对于虚拟机进程来说还是同样把GVA通过内部MMU转换为GPA,并不需要改变什么,保留了完全虚拟化的好处。但是同时会自动把GVA通过Virtualation MMU技术转换为真正的物理地址(HPA)。很明显减少了由GPA到HPA的过程,提升虚拟机性能。
并且CPU厂商还提供了TLB硬件虚拟化技术,以前的TLB只是标记GVA到GPA的对应关系,就这两个字段,现在被扩充为了三个字段,增加了一个主机字段,并且由GVA到GPA以及对应变成了GVA到HPA的对应关系。明确说明这是哪个虚拟机它的GVA到HPA的映射结果。
这里主要讲讲EPT技术的原理:
CR3将客户机程序所见的客户机虚拟地址转化成客户机物理地址,然后通过EPT将客户机物理地址转换成宿主机物理地址,而这两次都是通过CPU硬件自动完成的。EPT是在原有的CR3的页表地址映射的基础上引入了另一次映射,这样就可以直接两次转换了。
EPT(extended page table)可以看做一个硬件的影子页表,在Guest中通过增加EPT寄存器,当Guest产生了CR3和页表的访问的时候,由于对CR3中的页表地址的访问是GPA,当地址为空时候,也就是Page fault后,产生缺页异常,如果在软件模拟或者影子页表的虚拟化方式中,此时会有VM退出,qemu-kvm进程接管并获取到此异常。但是在EPT的虚拟化方式中,qemu-kvm忽略此异常,Guest并不退出,而是按照传统的缺页中断处理,在缺页中断处理的过程中会产生EXIT_REASON_EPT_VIOLATION,Guest退出,qemu-kvm捕获到异常后,分配物理地址并建立GVA->HPA的映射,并保存到EPT中,将EPT载入到MMU,下次转换时候直接查询根据CR3查询EPT表来完成GVA->HPA的转换。以后的转换都由硬件直接完成,大大提高了效率,且不需要为每个进程维护一套页表,减少了内存开销。
2.qemu-kvm 内存虚拟化实现流程
在虚拟机启动时,qemu在qemu进程地址空间申请内存,即内存的申请是在用户空间完成的。通过kvm提供的API,把地址信息注册到KVM中,这样KVM中维护有虚拟机相关的slot,这些slot构成了一个完整的虚拟机物理地址空间。slot中记录了其对应的HVA,页面数、起始GPA等,利用它可以把一个GPA转化成HVA,想到这一点自然和硬件虚拟化下的地址转换机制EPT联系起来,不错,这正是KVM维护EPT的技术核心。整个内存虚拟化可以分为两部分:qemu部分和kvm部分。qemu完成内存的申请,kvm实现内存的管理。看起来简单,但是内部实现机制也并非那么简单。本文先介绍qemu部分。
1)qemu 部分设计的主要结构体
qemu中内存管理的数据结构主要涉及:MemoryRegion、AddressSpace、FlatView、FlatRange、MemoryRegionSection、RAMList、RAMBlock、KVMSlot、kvm_userspace_memory_region等,来张关系图:
1. Multiple types of MemoryRegion (MemoryRegion直接操作内存,每一棵MR树的树根对应一个RAMBlock,其host即为通过mmap()分配的HVA)
- RAM: a range of host memory that can be made available to the guest. e.g. “pc.ram”, “pc.bios”, “pc.rom”, ram/rom for devices like “vga.ram”, “vga.rom”, etc.
- IOMMU region: translate addresses esp. for each PCI device for DMA usage
- container: includes other memory regions, each at a different offset. Use memory_region_init() to initialize.
- alias: a subsection of another region. Initialize with memory_region_init_alias().
2. AddressSpace (代表某种用途的内存,比如"memory", "I/O", "cpu-memory"等,将其他几个内存相关的结构体联系到一起)
- Links all important structures together: MemoryRegion, MemoryRegionListener, FlatView, AddressSpaceDispatch, MemoryRegionIoevented, and so on.
- Initialize with address_space_init().
3. FlatView (将树状的MemoryRegion展成平坦型的FlatView,用于内存映射)
- Spread the MR-tree to Flat FlatView, which is filled with several FlatRange.
4. MemoryListener (用于监听内存以及内存更新)
- Callbacks structure for updates to the physical memory map
- Allows a component to adjust to changes in the guest-visible memory map. Use with memory_listener_register() and memory_listener_unregister().
注意:此图是网上找的,把里面的ram_addr暂时理解成RAMBlock *ram_block
这几个数据结构的确不太容易滤清,下面是个人的一些见解。 可以把qemu层的内存管理再分为三个层次,MemoryRegion就位于顶级抽象层或者说比较偏向于host端,qemu中两个全局的MemoryRegion,分别是system_memory和system_io,不过两个均是以指针的形式存在,在地址空间的时候才会对对其分配具体的内存并初始化。MemoryRegion负责管理host的内存,理论上是树状结构,但是实际上根据代码来看仅仅有两层,
struct MemoryRegion {
Object parent_obj;
/* All fields are private - violators will be prosecuted */
/* The following fields should fit in a cache line */
bool romd_mode;
bool ram;
bool subpage;
bool readonly; /* For RAM regions */
bool rom_device;
bool flush_coalesced_mmio;
bool global_locking;
uint8_t dirty_log_mask;
bool is_iommu;
RAMBlock *ram_block;
Object *owner;
const MemoryRegionOps *ops;
void *opaque;
MemoryRegion *container;
Int128 size;//mr的大小
hwaddr addr;//mr的起始地址
void (*destructor)(MemoryRegion *mr);
uint64_t align;
bool terminates;
bool ram_device;
bool enabled;
bool warning_printed; /* For reservations */
uint8_t vga_logging_count;
MemoryRegion *alias;
hwaddr alias_offset;//子mr在root mr中的偏移。
int32_t priority;
QTAILQ_HEAD(subregions, MemoryRegion) subregions;
QTAILQ_ENTRY(MemoryRegion) subregions_link;
QTAILQ_HEAD(coalesced_ranges, CoalescedMemoryRange) coalesced;
const char *name;
unsigned ioeventfd_nb;
MemoryRegionIoeventfd *ioeventfds;
};
MemoryRegion结构如上,其中parent指向父MR,默认是NULL,size表示区域的大小;默认是64位下的最大地址;RAMBlock ram_block才是真正分配了host内存的地方,如果把它直接理解成一个内存条也是非常合适的,但实际上不仅仅如此,还有设备自有内存,显存。它的主要元素就是mr,host, offset和used_length,size,addr,每一个ramblock都有一个对应的MemoryRegion。size表明此mr的大小,addr表明此mr的起始地址。alias表明该区域是某一类型的区域(先这么说吧),这么说不知是否合适,实际上虚拟机的ram申请时一次性申请的一个完成的ram,记录在一个MR中,之后又对此ram按照size进行了划分,形成subregion,而subregion 的alias便指向原始的MR,而alias_offset 便是在原始ram中的偏移。对于系统地址空间的ram,会把刚才得到的subregion注册到系统中,父MR是刚才提到的全局MR system_memory,subregions_link是链表节点。前面提到,实际关联host内存的是subregion->alias指向的MR,MR其ram_block是最直接的接触host的内存,看下其结构:
typedef struct RAMBlock {
struct MemoryRegion *mr;
uint8_t *host;/*block关联的内存,HVA*/
ram_addr_t offset;/*在vm物理内存中的偏移 GPA*/
ram_addr_t length;/*block的长度*/
uint32_t flags;
char idstr[256];
/* Reads can take either the iothread or the ramlist lock.
* Writes must take both locks.
*/
QTAILQ_ENTRY(RAMBlock) next;
int fd;
} RAMBlock;
仅有的几个字段意义比较明确,理论上一个RAMBlock 就代表一段虚拟内存,host指向申请的ram的虚拟地址,是HVA。所有的RAMBlock通过next字段连接起来,表头保存在一个全局的RAMList结构中,但是根据代码来看,原始MR分配内存时分配的是一整块block,之所以这样做也许是为了扩展用吧!!RAMList中有个字段mru_block指针,指向最近使用的block,这样需要遍历所有的block时,先判断指定的block是否是mru_block,如果不是再进行遍历从而提高效率。
qemu的内存管理在交付给KVM管理时,中间又加了一个抽象层,叫做address_space.如果说MR管理的host的内存,那么address_space管理的更偏向于虚拟机。正如其名字所描述的,它是管理地址空间的,qemu中有几个全局的AddressSpace,address_space_memory和address_space_io,很明显一个是管理系统地址空间,一个是IO地址空间。它是如何进行管理的呢?展开下AddressSpace的结构;
struct AddressSpace {
/* All fields are private. */
char *name;
MemoryRegion *root;
struct FlatView *current_map;/*对应的flatview*/
int ioeventfd_nb;
struct MemoryRegionIoeventfd *ioeventfds;
struct AddressSpaceDispatch *dispatch;
struct AddressSpaceDispatch *next_dispatch;
MemoryListener dispatch_listener;
QTAILQ_ENTRY(AddressSpace) address_spaces_link;
};
对于该结构,源码的注释或许更能解释:AddressSpace: describes a mapping of addresses to #MemoryRegion objects,很明显是把MR映射到虚拟机的物理地址空间。root指向根MR,对于address_space_memory来讲,root指向系统全局的MR system_memory,current_map指向一个FlatView结构,其他的字段咱们先暂时忽略,所有的AddressSpace通过结构中的address_spaces_link连接成链表,表头保存在全局的AddressSpace结构中。FlatView管理MR展开后得到的所有FlatRange,看下FlatView:
struct FlatView {
unsigned ref;//引用计数,为0时就销毁
FlatRange *ranges;/*对应的flatrange数组*/
unsigned nr;/*flatrange 的数目*/
unsigned nr_allocated;//当前数组的项数
};
各个字段的意义就不说了,ranges是一个数组,记录FlatView下所有的FlatRange,每个FlatRange对应一段虚拟机物理地址区间,各个FlatRange不会重叠,按照地址的顺序保存在数组中。FlatRange结构如下
struct FlatRange {
MemoryRegion *mr;/*指向所属的MR*/
hwaddr offset_in_region;/*在MR中的offset*/
AddrRange addr;/*本FR代表的区间*/
uint8_t dirty_log_mask;
bool romd_mode;
bool readonly;/*是否是只读*/
}
具体的范围由一个AddrRange结构描述,其描述了起始地址和大小,offset_in_region表示该区间在全局的MR中的offset,根据此可以进行GPA到HVA的转换,mr指向所属的MR。
到此为止,负责管理的结构基本就介绍完毕,剩余几个主要起中介的作用,MemoryRegionSection对应于FlatRange,一个FlatRange代表一个物理地址空间的片段,但是其偏向于address-space,而MemoryRegionSection则在MR端显示的表明了分片,其结构如下:
struct MemoryRegionSection {
MemoryRegion *mr;//所属的MemoryRegion
AddressSpace *address_space;//region关联的AddressSpace
hwaddr offset_within_region;//在region内部的偏移
Int128 size;//section的大小
hwaddr offset_within_address_space;//首个字节的地址在section中的偏移
bool readonly;//是否是只读
};
其中注意两个偏移,offset_within_region和offset_within_address_space。前者描述的是该section在整个MR中的偏移,一个address_space可能有多个MR构成,因此该offset是局部的。而offset_within_address_space是在整个地址空间中的偏移,是全局的offset。
KVMSlot也是一个中介,只不过更加接近kvm了,
typedef struct KVMSlot
{
{
hwaddr start_addr;//客户机物理地址 GPA
ram_addr_t memory_size;//内存大小
void *ram;//HVA qemu用户空间地址
int slot;//slot编号
int flags;
}
} KVMSlot;
kvm_userspace_memory_region是和kvm共享的一个结构,说共享不太恰当,但是其实最终作为参数给kvm使用的,kvm获取控制权后,从栈中复制该结构到内核,其中字段意思就很简单,不在赘述。
整体布局大致如图所示:
2)qemu分配内存的具体实现流程
qemu部分的内存申请流程上可以分为三小部分,分成三小部分主要是我在看代码的时候觉得这三部分耦合性不是很大,相对而言比较独立。众所周知,qemu起始于vl.c中的main函数,那么这三部分也按照在main函数中的调用顺序分别介绍。
2.1) 回调函数的注册
涉及函数:1)addressspace初始化:main()-->cpu_exec_init_all()-->memory_map_init()
cpu_exec_init_all()函数,该函数主要初始化了IO地址空间和系统地址空间。memory_map_init()函数初始化系统地址空间,有一个全局的MemoryRegion指针system_memory指向该区域的MemoryRegion结构。
static void memory_map_init(void)
{
/*为system_memory分配内存*/
system_memory = g_malloc(sizeof(*system_memory));
memory_region_init(system_memory, NULL, "system", UINT64_MAX);
/*初始化全局的address_space_memory*/
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");
}
所以在函数起始,就对system_memory分配了内存,然后调用了memory_region_init函数对其进行初始化,其中size设置为整个地址空间:如果是64位就是2^64.接着调用了address_space_init函数对address_space_memory进行了初始化。同理对io地址空间的的初始化也是一样。
void address_space_init(AddressSpace *as, MemoryRegion *root, const char *name)
{
memory_region_ref(root);
/*指定address_space_memory的root为system_memory*/
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);/*把address_space_memory插入全局链表*/
as->name = g_strdup(name ? name : "anonymous");
address_space_update_topology(as);
address_space_update_ioeventfds(as);
}
函数主要做了以下几个工作,设置addressSpace和MR的关联,address_space_memory加入到全局的address_spaces链表中,接下来调用address_space_update_topology更新FlatView,函数address_space_update_topology,将指定的AddressSpace下的MemoryRegion树进行展平,形成了对应一维内存逻辑表示的FlatView,然后再address_space_update_topology_pass中将FlatView模型通过KVM_SET_USER_MEMORY_REGION注册到KVM模块中(具体实现见下文);最后调用address_space_update_ioeventfds初始化设置MR的ioeventfds。回到memory_map_init()函数中,接下来按照同样的模式对IO区域system_io和IO地址空间address_space_io做了初始化。
MR和Flatview关系:MemoryRegion是QEMU管理内存的树状结构,便于按照功能、属性分类;但这只是管理结构。但虚拟机的内存需要通过KVM_SET_USER_MEMORY_REGION,将GPA与HVA的对应关系注册到KVM模块的memslot,才可以生效成为EPT。如果QEMU直接使用MemoryRegion进行注册,那么注册的过程将会很麻烦,也容易不断的出现重叠判断等。所以在通过KVM_SET_USER_MEMORY_REGION注册前,加了一层转换机制,先将树状的MemoryRegion展开物理内存样子的一维区间结构Flatview,然后再通过KVM_SET_USER_MEMORY_REGION将这个展开的物理内存注册到KVM内核模块中,就方便了许多。这个转换机制是FlatView模型。
2)addressspace中注册listener:configure_accelerator() ----->accel_init_machine()----->acc->machine()(kvm_init())----->memory_listener_register ()
这里所说的accelerator在这里就是kvm,初始化函数自然调用了kvm_init,该函数主要完成对kvm的初始化,包括一些常规检查如CPU个数、kvm版本等,还会通过ioctl和内核交互创建kvm结构,这些并非本文重点,不在赘述。在kvm_init函数的结尾调用了memory_listener_register。
kvm_memory_listener_register(s, &s->memory_listener, &address_space_memory, 0) ==> memory_listener_register(&kml->listener, as);
memory_listener_register(&kvm_io_listener, &address_space_io);
通过memory_listener_register函数,针对地址空间注册了lisenter,lisenter本身是一组函数表,当地址空间发生变化的时候,会调用到listener中的相应函数,从而保持内核和用户空间的内存信息的一致性。虚拟机包含有两个地址空间address_space_memory和address_space_io,很容易理解,正常系统也包含系统地址空间和IO地址空间。
看下memory_listener_register函数实现:
void memory_listener_register(MemoryListener *listener, AddressSpace *as)
{
MemoryListener *other = NULL;
listener->address_space = as;
/*如果全局的memory_listeners为空或者当前listener的优先级大于最后一个listener的优先级,则可以直接插入到memory_listeners中*/
if (QTAILQ_EMPTY(&memory_listeners)
|| listener->priority >= QTAILQ_LAST(&memory_listeners,
memory_listeners)->priority) {
QTAILQ_INSERT_TAIL(&memory_listeners, listener, link);
} else {/*listener按照优先级升序排列*/
QTAILQ_FOREACH(other, &memory_listeners, link) {
if (listener->priority < other->priority) {
break;
}
}
QTAILQ_INSERT_BEFORE(other, listener, link);/*插入listener*/
}
/*如果当前地址空间的as->listener链表为空或者当前listener的优先级大于最后一个listener的优先级,则可以直接插入到当前地址空间的as->listener链表中*/
if (QTAILQ_EMPTY(&as->listeners)
|| listener->priority >= QTAILQ_LAST(&as->listeners,
memory_listeners)->priority) {
QTAILQ_INSERT_TAIL(&as->listeners, listener, link_as);
} else {
QTAILQ_FOREACH(other, &as->listeners, link_as) {
if (listener->priority < other->priority) {
break;
}
}
QTAILQ_INSERT_BEFORE(other, listener, link_as);
}
listener_add_address_space(listener, as);
}
系统中可以存在多个listener,listener之间有着明确的优先级关系,通过链表进行组织,链表头是全局的memory_listeners。函数中,如果memory_listeners为空或者当前listener的优先级大于最后一个listener的优先级,即直接把当前listener插入。否则需要挨个遍历链表,找到合适的位置。具体按照优先级升序查找。注册listener到相应的地址空间链表&as->listeners中也是同理。在函数最后调用listener_add_address_space函数,该函数对其对应的address_space管理的flatrange,通过函数listener_add_address_space向KVM注册。当然,实际上此时listener里的函数数都未经过初始化,所以这里的循环其实是空循环。
3)实际内存的分配
前面注册listener也好,或是初始化addressspace也好,实际上均没有对应的物理内存,目前为止只是初始化好了所有Qemu中需要维护的相关的内存结构,并完成了在KVM中的注册。下面需要初始化KVM中的MMU支持。顺着main函数往下走,会调用到machine->init,实际上对应于pc_init1函数,在该函数中有pc_memory_init()函数对实际的内存做了分配。大致流程如下:
先直接从pc_memory_init()函数开始:
void pc_memory_init(PCMachineState *pcms,
MemoryRegion *system_memory,
MemoryRegion *rom_memory,
MemoryRegion **ram_memory)
{
int linux_boot, i;
MemoryRegion *ram, *option_rom_mr;
MemoryRegion *ram_below_4g, *ram_above_4g;
FWCfgState *fw_cfg;
MachineState *machine = MACHINE(pcms);
PCMachineClass *pcmc = PC_MACHINE_GET_CLASS(pcms);
assert(machine->ram_size == pcms->below_4g_mem_size +
pcms->above_4g_mem_size);
linux_boot = (machine->kernel_filename != NULL);
/* Allocate RAM. We allocate it as a single memory region and use
* aliases to address portions of it, mostly for backwards compatibility
* with older qemus that used qemu_ram_alloc().
*/
ram = g_malloc(sizeof(*ram));
//分配具体的内存
memory_region_allocate_system_memory(ram, NULL, "pc.ram",
machine->ram_size);
*ram_memory = ram;
ram_below_4g = g_malloc(sizeof(*ram_below_4g));
/*从整体ram进行划分出ram-below-4g、ram_above_4g子MR,并调用memory_region_add_subregion注册子MR到root MR system_memory 的subregions链表中,链表按照优先级升序组织,最后调用memory_region_transaction_commit提交修改*/
memory_region_init_alias(ram_below_4g, NULL, "ram-below-4g", ram,
0, pcms->below_4g_mem_size);
memory_region_add_subregion(system_memory, 0, ram_below_4g);
e820_add_entry(0, pcms->below_4g_mem_size, E820_RAM);
if (pcms->above_4g_mem_size > 0) {
ram_above_4g = g_malloc(sizeof(*ram_above_4g));
memory_region_init_alias(ram_above_4g, NULL, "ram-above-4g", ram,
pcms->below_4g_mem_size,
pcms->above_4g_mem_size);
memory_region_add_subregion(system_memory, 0x100000000ULL,
ram_above_4g);
e820_add_entry(0x100000000ULL, pcms->above_4g_mem_size, E820_RAM);
}
if (!pcmc->has_reserved_memory &&
(machine->ram_slots ||
(machine->maxram_size > machine->ram_size))) {
MachineClass *mc = MACHINE_GET_CLASS(machine);
error_report("\"-memory 'slots|maxmem'\" is not supported by: %s",
mc->name);
exit(EXIT_FAILURE);
}
//初始化设备内存
/* always allocate the device memory information */
machine->device_memory = g_malloc0(sizeof(*machine->device_memory));
/* initialize device memory address space */
if (pcmc->has_reserved_memory &&
(machine->ram_size < machine->maxram_size)) {
ram_addr_t device_mem_size = machine->maxram_size - machine->ram_size;
if (machine->ram_slots > ACPI_MAX_RAM_SLOTS) {
error_report("unsupported amount of memory slots: %"PRIu64,
machine->ram_slots);
exit(EXIT_FAILURE);
}
if (QEMU_ALIGN_UP(machine->maxram_size,
TARGET_PAGE_SIZE) != machine->maxram_size) {
error_report("maximum memory size must by aligned to multiple of "
"%d bytes", TARGET_PAGE_SIZE);
exit(EXIT_FAILURE);
}
//初始化设备内存在物理内存空间中的起始地址
machine->device_memory->base =
ROUND_UP(0x100000000ULL + pcms->above_4g_mem_size, 1 * GiB);
if (pcmc->enforce_aligned_dimm) {
/* size device region assuming 1G page max alignment per slot */
device_mem_size += (1 * GiB) * machine->ram_slots;
}
if ((machine->device_memory->base + device_mem_size) <
device_mem_size) {
error_report("unsupported amount of maximum memory: " RAM_ADDR_FMT,
machine->maxram_size);
exit(EXIT_FAILURE);
}
//初始化设备内存MR.
memory_region_init(&machine->device_memory->mr, OBJECT(pcms),
"device-memory", device_mem_size);
//将设备内存MR添加到全局的system_memeory root MR链表中。
memory_region_add_subregion(system_memory, machine->device_memory->base,
&machine->device_memory->mr);
}
/* Initialize PC system firmware */
pc_system_firmware_init(rom_memory, !pcmc->pci_enabled);
/ /初始化option_rom_mr
option_rom_mr = g_malloc(sizeof(*option_rom_mr));
memory_region_init_ram(option_rom_mr, NULL, "pc.rom", PC_ROM_SIZE,
&error_fatal);
if (pcmc->pci_enabled) {
memory_region_set_readonly(option_rom_mr, true);
}
//增加子MRoption_rom_mr,到root system_memory链表中
memory_region_add_subregion_overlap(rom_memory,
PC_ROM_MIN_VGA,
option_rom_mr,
1);
//初始化bios
fw_cfg = bochs_bios_init(&address_space_memory, pcms);
rom_set_fw(fw_cfg);
if (pcmc->has_reserved_memory && machine->device_memory->base) {
uint64_t *val = g_malloc(sizeof(*val));
PCMachineClass *pcmc = PC_MACHINE_GET_CLASS(pcms);
uint64_t res_mem_end = machine->device_memory->base;
if (!pcmc->broken_reserved_end) {
res_mem_end += memory_region_size(&machine->device_memory->mr);
}
*val = cpu_to_le64(ROUND_UP(res_mem_end, 1 * GiB));
fw_cfg_add_file(fw_cfg, "etc/reserved-memory-end", val, sizeof(*val));
}
if (linux_boot) {
load_linux(pcms, fw_cfg);
}
for (i = 0; i < nb_option_roms; i++) {
rom_add_option(option_rom[i].name, option_rom[i].bootindex);
}
pcms->fw_cfg = fw_cfg;
/* Init default IOAPIC address space */
pcms->ioapic_as = &address_space_memory;
}
上面函数中的memory_region_add_subregion函数会造成我们MR的变化,地址空间已经发生变化(分配变化了),自然要把变化和KVM(内存管理侧)进行同步,这一工作需要由memory_region_transaction_commit()来实现。
memory_region_transaction_commit在上述函数的调用流程
memory_region_add_subregion-->memory_region_add_subregion_common-->memory_region_update_container_subregions-->memory_region_transaction_commit
接下来看看memory_region_transaction_commit函数:
void memory_region_transaction_commit(void)
{
AddressSpace *as;
assert(memory_region_transaction_depth);
assert(qemu_mutex_iothread_locked());
--memory_region_transaction_depth;
if (!memory_region_transaction_depth) {
if (memory_region_update_pending) {
flatviews_reset();
MEMORY_LISTENER_CALL_GLOBAL(begin, Forward);
QTAILQ_FOREACH(as, &address_spaces, address_spaces_link) {
/*更新各个addressspace 拓扑结构和ioeventfds*/
address_space_set_flatview(as);
address_space_update_ioeventfds(as);
}
memory_region_update_pending = false;
ioeventfd_update_pending = false;
MEMORY_LISTENER_CALL_GLOBAL(commit, Forward);
} else if (ioeventfd_update_pending) {
QTAILQ_FOREACH(as, &address_spaces, address_spaces_link) {
address_space_update_ioeventfds(as);
}
ioeventfd_update_pending = false;
}
}
}
可以看到,这里listener的作用就凸显出来了。对于每个address_space,先调用flatviews_reset生成新mr的flat_view,并用hash表flat_views维护,然后调用address_space_set_flatview()执行更新。里面还涉及个重要的函数是 address_space_update_topology_pass,他根据oldview和newview对当前视图进行更新。
flatviews_reset,主要是遍历全局as链表address_spaces中每个as,如果hash表flat_views中没有mr被维护,则调用generate_memory_topology重新生成该as中MR对应的flat_view,并用hash表flat_views维护,我们先看下函数generate_memory_topology代码:
static FlatView *generate_memory_topology(MemoryRegion *mr)
{
int i;
FlatView *view;
/*mR是system_memory*/
view = flatview_new(mr);
/*addrrange_make生成一个0-2^64的地址空间*/
if (mr) {
/*最初是让MR 按照基址为0映射到地址空间中*/
render_memory_region(view, mr, int128_zero(),
addrrange_make(int128_zero(), int128_2_64()), false);
}
flatview_simplify(view);
view->dispatch = address_space_dispatch_new(view);
for (i = 0; i < view->nr; i++) {
//建立FlatView中nr个FlatRange与MemoryRegionSection一一对应关系
MemoryRegionSection mrs =
section_from_flat_range(&view->ranges[i], view);
flatview_add_to_dispatch(view, &mrs);
}
address_space_dispatch_compact(view->dispatch);
g_hash_table_replace(flat_views, mr, view);
return view;
}
先申请一个FlatView结构,并对其进行初始化,然后调用render_memory_region函数实现核心功能,然后调用flatview_simplify尝试合并相邻的FlatRange.,最后再建立MemoryRegionSection与FlatRange的关系。
void render_memory_region(FlatView *view,MemoryRegion *mr,Int128 base,AddrRange clip,bool readonly)是一个递归函数,参数中,view表示当前FlatView,mr最初为system_memory即全局的MR,base起初为0表示从地址空间的其实开始,clip最初为一个完整的地址空间。readonly标识是否只读。看下他的具体实现:
/* Render a memory region into the global view. Ranges in @view obscure
* ranges in @mr.
*/
static void render_memory_region(FlatView *view,
MemoryRegion *mr,
Int128 base,
AddrRange clip,
bool readonly)
{
MemoryRegion *subregion;
unsigned i;
hwaddr offset_in_region;
Int128 remain;
Int128 now;
FlatRange fr;
AddrRange tmp;
if (!mr->enabled) {
return;
}
//设置base,即当前mr的起始地址。
int128_addto(&base, int128_make64(mr->addr));
readonly |= mr->readonly;
/*获得当前MR的地址区间范围*/
tmp = addrrange_make(base, mr->size);
/*判断目标MR和clip是否有交叉,即MR应该落在clip的范围内*/
if (!addrrange_intersects(tmp, clip)) {
return;
}
/*缩小clip到交叉的部分*/
clip = addrrange_intersection(tmp, clip);
/*子区域才有alias字段*/
if (mr->alias) {
int128_subfrom(&base, int128_make64(mr->alias->addr));
int128_subfrom(&base, int128_make64(mr->alias_offset));
/*这里base应该为subregion对应的alias中的区间基址*/
render_memory_region(view, mr->alias, base, clip, readonly);
return;
}
/* Render subregions in priority order. */
QTAILQ_FOREACH(subregion, &mr->subregions, subregions_link) {
render_memory_region(view, subregion, base, clip, readonly);
}
if (!mr->terminates) {
return;
}
/*clip.start表示地址区间的起始,base为本次映射的基址,差值就为offset*/
offset_in_region = int128_get64(int128_sub(clip.start, base));
/***开始映射时,clip表示映射的区间范围,base作为一个移动指导每个FR的映射,remain表示clip总还没映射的大小*/
/*最初base=clip.start */
base = clip.start;
remain = clip.size;
fr.mr = mr;
fr.dirty_log_mask = memory_region_get_dirty_log_mask(mr);
fr.romd_mode = mr->romd_mode;
fr.readonly = readonly;
/* Render the region itself into any gaps left by the current view. */
for (i = 0; i < view->nr && int128_nz(remain); ++i) {
/*if base > addrrange_end(view->ranges[i].addr)即大于一个range的end*/
if (int128_ge(base, addrrange_end(view->ranges[i].addr))) {
continue;
}
/*如果base < = view->ranges[i].addr.start*/
/*now 表示已经存在的FR 或者本次填充的FR的长度*/
if (int128_lt(base, view->ranges[i].addr.start)) {
now = int128_min(remain,
int128_sub(view->ranges[i].addr.start, base));
/*fr.offset_in_region表示在整个地址空间中的偏移*/
fr.offset_in_region = offset_in_region;
fr.addr = addrrange_make(base, now);
flatview_insert(view, i, &fr);//将新增的fr插入到指定位置view->ranges[i]处。
++i;
int128_addto(&base, now);
offset_in_region += int128_get64(now);
int128_subfrom(&remain, now);
}
now = int128_sub(int128_min(int128_add(base, remain),
addrrange_end(view->ranges[i].addr)),
base);
int128_addto(&base, now);
offset_in_region += int128_get64(now);
int128_subfrom(&remain, now);
}
/*如果还有剩下的clip没有映射,则下面不会在发生冲突,直接一次性的映射完成*/
if (int128_nz(remain)) {
fr.offset_in_region = offset_in_region;
fr.addr = addrrange_make(base, remain);
flatview_insert(view, i, &fr);
}
}
此函数的核心工作起始于一个for循环,循环的条件是 view->nr && int128_nz(remain),表示当前还有未遍历的FR并且remain还有剩余。循环中如果MR base的值大于或者等于当前FR的end,则继续往后遍历FR,否则进入下面,如果base小于当前FR的start,则表明base到start之间的区间还没有映射,则为其进行映射,now表示要映射的长度,取remain和int128_sub(view->ranges[i].addr.start, base)之间的最小值,后者表示下一个FR的start和base之间的差值,当然按照clip为准。接下来就没难度了,设置FR的offset_in_region和addr,然后调用flatview_insert插入到FlatView的FlatRange数组中。不过由于FR按照地址顺序排列,如果插入位置靠前,则需要移动较多的项,不知道为何不用链表实现。下面就很自然了移动base,增加offset_in_region,减少remain等操作。出了if,此时base已经和FR的start对齐,所以还需要略过当前FR。就这么一直映射下去。
出了for循环,如果remain不为0,则表明还有没有映射的,但是现在已经没有已经存在的FR了,所以不会发生冲突,直接把remain直接映射成一个FR即可。
按照这个思路,把所有的subregion都映射下去,最终把FlatView返回。这样generate_memory_topology就算是介绍完了,下面的flatview_simplify是对数组表项的尝试合并,这里就不再介绍。到此为止,已经针对当前MR生成了一个新的FlatView,然后需要对as对应的FlatView进行更新。
address_space_set_flatview函数主要就对as对应的FlatView进行更新: atomic_rcu_set(&as->current_map, new_view); mr变->as的FlatView就需要更新
static void address_space_set_flatview(AddressSpace *as)
{
/*根据AddressSpace获取对应的old FlatView*/
FlatView *old_view = address_space_to_flatview(as);
/*根据AddressSpace对应的MR查找一个更新后的FlatView*/
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. */
atomic_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);
}
}
对old_view和new_view做对比后,接下来需要用address_space_update_topology_pass函数,连续调用了两次这个函数,不过最后的adding参数先后为false和true。进入函数分析下:
static void address_space_update_topology_pass(AddressSpace *as,
const FlatView *old_view,
const FlatView *new_view,
bool adding)
{
/*FR计数*/
unsigned iold, inew;
FlatRange *frold, *frnew;
/* Generate a symmetric difference of the old and new memory maps.
* Kill ranges in the old map, and instantiate ranges in the new map.
*/
iold = inew = 0;
while (iold < old_view->nr || inew < new_view->nr) {
if (iold < old_view->nr) {
frold = &old_view->ranges[iold];
} else {
frold = NULL;
}
if (inew < new_view->nr) {
frnew = &new_view->ranges[inew];
} else {
frnew = NULL;
}
if (frold
&& (!frnew
|| int128_lt(frold->addr.start, frnew->addr.start)
|| (int128_eq(frold->addr.start, frnew->addr.start)
&& !flatrange_equal(frold, frnew)))) {
/* In old but not in new, or in both but attributes changed. */
if (!adding) {
MEMORY_LISTENER_UPDATE_REGION(frold, as, Reverse, region_del);
}
++iold;
} else if (frold && frnew && flatrange_equal(frold, frnew)) {
/* In both and unchanged (except logging may have changed) */
if (adding) {
MEMORY_LISTENER_UPDATE_REGION(frnew, as, Forward, region_nop);
if (frnew->dirty_log_mask & ~frold->dirty_log_mask) {
MEMORY_LISTENER_UPDATE_REGION(frnew, as, Forward, log_start,
frold->dirty_log_mask,
frnew->dirty_log_mask);
}
if (frold->dirty_log_mask & ~frnew->dirty_log_mask) {
MEMORY_LISTENER_UPDATE_REGION(frnew, as, Reverse, log_stop,
frold->dirty_log_mask,
frnew->dirty_log_mask);
}
}
++iold;
++inew;
} else {
/* In new */
if (adding) {
MEMORY_LISTENER_UPDATE_REGION(frnew, as, Forward, region_add);
}
++inew;
}
}
}
该函数主体是一个while循环,循环条件是old_view->nr和new_view->nr,表示新旧view的可用FlatRange数目。这里依次对FR数组的对应FR 做对比,主要由下面几种情况:frold和frnew均存在、frold存在但frnew不存在,frold不存在但frnew存在。下面的if划分和上面的略有不同:
1、如果frold不为空&&(frnew为空||frold.start<frnew.start||frold.start=frnew.start)&&frold!=frnew 这种情况是新旧view的地址范围不一样,则需要调用lienter的region_del对frold进行删除。
2、如果frold和frnew均不为空且frold.start=frnew.start 这种情况需要判断日志掩码,如果frold->dirty_log_mask && !frnew->dirty_log_mask,调用log_stop回调函数;如果frnew->dirty_log_mask && !frold->dirty_log_mask,调用log_start回调函数。
3、frold为空但是frnew不为空 这种情况直接调用region_add回调函数添加region。
函数主体逻辑基本如上所述,那我们注意到,当adding为false时,执行的只有第一个情况下的处理,就是删除frold的操作,其余的处理只有在adding 为true的时候才得以执行。这意图就比较明确,首次执行先删除多余的,下次直接添加或者对日志做更新操作了.
这个函数的region_add会调用KVMMemoryListener初始化时注册的MemoryListener回调函数kvm_region_add,此函数最终调用kvm_set_user_memory_region,实现qemu内核与kvm内存的同步。
接下来再看看MEMORY_LISTENER_UPDATE_REGION是如何实现kvm_region_add的调用:
/* No need to ref/unref .mr, the FlatRange keeps it alive. */
#define MEMORY_LISTENER_UPDATE_REGION(fr, as, dir, callback, _args...) \
do { \
MemoryRegionSection mrs = section_from_flat_range(fr, \
address_space_to_flatview(as)); \
MEMORY_LISTENER_CALL(as, callback, dir, &mrs, ##_args); \
} while(0)
该宏实际上是另一个宏MEMORY_LISTENER_CALL的封装,在MEMORY_LISTENER_CALL中临时生成一个MemoryRegionSection结构,具体逻辑如下:
#define MEMORY_LISTENER_CALL(_as, _callback, _direction, _section, _args...) \
do { \
MemoryListener *_listener; \
struct memory_listeners_as *list = &(_as)->listeners; \
\
switch (_direction) { \
case Forward: \
QTAILQ_FOREACH(_listener, list, link_as) { \
if (_listener->_callback) { \
_listener->_callback(_listener, _section, ##_args); \
} \
} \
break; \
case Reverse: \
QTAILQ_FOREACH_REVERSE(_listener, list, memory_listeners_as, \
link_as) { \
if (_listener->_callback) { \
_listener->_callback(_listener, _section, ##_args); \
} \
} \
break; \
default: \
abort(); \
} \
} while (0)
这里有个_direction,其实就是遍历方向,因为listener按照优先级从低到高排列,所以这里其实就是确定让谁先处理。Forward就是从前向后,而reverse就是从后向前。知道这些,看这些代码就不成问题了。
如果是新增了mr,fr被更新了,kvm_region_add()函数将得到执行,那咱们就从kvm_region_add函数开始,对KVM部分的内存管理做介绍。kvm_region_add()函数是listener注册时初始化的region_add回调,在qemu申请好内存后,针对每个FR(宏转化成MemoryRegionSection),调用了listener的region_add函数。最终需要利用此函数把region信息告知KVM,KVM以此对内存信息做记录。咱们直奔主题,函数核心在static void kvm_set_phys_mem(MemoryRegionSection *section, bool add)函数
static void kvm_set_phys_mem(KVMMemoryListener *kml,
MemoryRegionSection *section, bool add)
{
KVMSlot *mem;
int err;
MemoryRegion *mr = section->mr;
/*是否可写*/
bool writeable = !mr->readonly && !mr->rom_device;
hwaddr start_addr, size;
void *ram;
/*如果不是rom,则不能进行写操作*/
if (!memory_region_is_ram(mr)) {
if (writeable || !kvm_readonly_mem_allowed) {
return;
} else if (!mr->romd_mode) {
/* If the memory device is not in romd_mode, then we actually want
* to remove the kvm memory slot so all accesses will trap. */
add = false;
}
}
//对size进行了对齐操作
size = kvm_align_section(section, &start_addr);
if (!size) {
return;
}
/* use aligned delta to align the ram address */
ram = memory_region_get_ram_ptr(mr) + section->offset_within_region +
(start_addr - section->offset_within_address_space);
//移除section对应的slot
if (!add) {
mem = kvm_lookup_matching_slot(kml, start_addr, size);
if (!mem) {
return;
}
if (mem->flags & KVM_MEM_LOG_DIRTY_PAGES) {
kvm_physical_sync_dirty_bitmap(kml, section);
}
/* unregister the slot */
mem->memory_size = 0;
mem->flags = 0;
err = kvm_set_user_memory_region(kml, mem, false);
if (err) {
fprintf(stderr, "%s: error unregistering slot: %s\n",
__func__, strerror(-err));
abort();
}
return;
}
/* 把当前的section转化成一个slot进行添加 */
/* register the new slot */
mem = kvm_alloc_slot(kml);
mem->memory_size = size;
mem->start_addr = start_addr;
mem->ram = ram;
mem->flags = kvm_mem_flags(mr);
err = kvm_set_user_memory_region(kml, mem, true);
if (err) {
fprintf(stderr, "%s: error registering slot: %s\n", __func__,
strerror(-err));
abort();
}
}
该函数首先获取section对应的MR的一些属性,如writeable、readonly_flag。获取section的start_addr和size,其中start_addr就是section中的offset_within_address_space也就是FR中的offset_in_region,接下来对size进行了对齐操作 。如果对应的MR关联的内存并不是作为ram存在,就要进行额外的验证。这种情况如果writeable允许写操作或者kvm不支持只读内存,那么直接返回。然后根据第三个布尔参数决定移除还是添加这个section。
接下来就调用kvm_set_user_memory_region把new slot映射进去。基本思路就是这样。下面看下核心函数kvm_set_user_memory_region
static int kvm_set_user_memory_region(KVMMemoryListener *kml, KVMSlot *slot, bool new)
{
KVMState *s = kvm_state;
struct kvm_userspace_memory_region mem;
int ret;
//slot转化成kvm_userspace_memory_region结构体,便于拷贝给内核
mem.slot = slot->slot | (kml->as_id << 16);
mem.guest_phys_addr = slot->start_addr;
mem.userspace_addr = (unsigned long)slot->ram;
mem.flags = slot->flags;
if (slot->memory_size && !new && (mem.flags ^ slot->old_flags) & KVM_MEM_READONLY) {
/* Set the slot size to 0 before setting the slot to the desired
* value. This is needed based on KVM commit 75d61fbc. */
mem.memory_size = 0;
kvm_vm_ioctl(s, KVM_SET_USER_MEMORY_REGION, &mem);
}
mem.memory_size = slot->memory_size;
//把mem通过ioctl拷贝到内核kvm模块。
ret = kvm_vm_ioctl(s, KVM_SET_USER_MEMORY_REGION, &mem);
slot->old_flags = mem.flags;
trace_kvm_set_user_memory(mem.slot, mem.flags, mem.guest_phys_addr,
mem.memory_size, mem.userspace_addr, ret);
return ret;
}
可以看到该函数使用了一个kvm_userspace_memory_region对应,该结构本质上作为参数传递给KVM,只是由于不能共享堆栈,在KVM中需要把该结构复制到内核空间,代码本身没什么难度,只是这里如果是只读的mem,需要调用两次kvm_vm_ioctl,第一次设置mem的size为0,具体原因,在kvm侧说明:
kvm对于kvm_vm_ioctl中命令字KVM_SET_USER_MEMORY_REGION的处理如下:
case KVM_SET_USER_MEMORY_REGION: {
3299 struct kvm_userspace_memory_region kvm_userspace_mem;
3300
3301 r = -EFAULT;
3302 if (copy_from_user(&kvm_userspace_mem, argp,
3303 sizeof(kvm_userspace_mem)))
3304 goto out;
3305
3306 r = kvm_vm_ioctl_set_memory_region(kvm, &kvm_userspace_mem);
3307 break;
3308 }
可以看到首要任务就是把用户态数据复制到内核kvm_userspace_mem,然后调用了kvm_vm_ioctl_set_memory_region()函数设置内核kvm结构。
1172 static int kvm_vm_ioctl_set_memory_region(struct kvm *kvm,
1173 struct kvm_userspace_memory_region *mem)
1174 {
1175 if ((u16)mem->slot >= KVM_USER_MEM_SLOTS)
1176 return -EINVAL;
1177
1178 return kvm_set_memory_region(kvm, mem);
1179
函数检查下slot编号如果超额,那没办法,无法添加,否则调用kvm_set_memory_region()函数。而该函数没做别的,直接调用了__kvm_set_memory_region。
* Allocate some memory and give it an address in the guest physical address
* space.
*
* Discontiguous memory is allowed, mostly for framebuffers.
*
* Must be called holding kvm->slots_lock for write.
*/
int __kvm_set_memory_region(struct kvm *kvm,
const struct kvm_userspace_memory_region *mem)
{
int r;
gfn_t base_gfn;
unsigned long npages;
struct kvm_memory_slot *slot;
struct kvm_memory_slot old, new;
struct kvm_memslots *slots = NULL, *old_memslots;
int as_id, id;
enum kvm_mr_change change;
r = check_memory_region_flags(mem);
if (r)
goto out;
r = -EINVAL;
as_id = mem->slot >> 16;
id = (u16)mem->slot;
/* General sanity checks */
if (mem->memory_size & (PAGE_SIZE - 1))//如果memory_size 不是页对齐的
goto out;
if (mem->guest_phys_addr & (PAGE_SIZE - 1))//如果mem的客户机物理地址不是页对齐的,也失败
goto out;
/* We can read the guest memory with __xxx_user() later on. */
//如果slot的id在合法范围内但是用户空间地址不是页对齐的或者地址范围内的不能正常访问,则失败
if ((id < KVM_USER_MEM_SLOTS) &&
((mem->userspace_addr & (PAGE_SIZE - 1)) ||
!access_ok((void __user *)(unsigned long)mem->userspace_addr,
mem->memory_size)))
goto out;
//如果slot id大于等于KVM_MEM_SLOTS_NUM,则失败
if (as_id >= KVM_ADDRESS_SPACE_NUM || id >= KVM_MEM_SLOTS_NUM)
goto out;
if (mem->guest_phys_addr + mem->memory_size < mem->guest_phys_addr)
goto out;
/*通过id,定位到指定slot*/
slot = id_to_memslot(__kvm_memslots(kvm, as_id), id);
base_gfn = mem->guest_phys_addr >> PAGE_SHIFT;//获取页框号
npages = mem->memory_size >> PAGE_SHIFT;//获取页数
if (npages > KVM_MEM_MAX_NR_PAGES)
goto out;
/*new 为用户空间传递过来的mem,old为和用户空间mem id一致的mem*/
new = old = *slot;
new.id = id;
new.base_gfn = base_gfn;
new.npages = npages;
new.flags = mem->flags;
/*new 为用户空间传递过来的mem,old为和用户空间mem id一致的mem*/
if (npages) {
if (!old.npages)
change = KVM_MR_CREATE;
else { /* Modify an existing slot. *//*否则修改已有的mem*/
if ((mem->userspace_addr != old.userspace_addr) ||
(npages != old.npages) ||
((new.flags ^ old.flags) & KVM_MEM_READONLY))
goto out;
/*如果两个mem映射的基址不同*/
if (base_gfn != old.base_gfn)
change = KVM_MR_MOVE;
/*如果标志位不同则更新标志位*/
else if (new.flags != old.flags)
change = KVM_MR_FLAGS_ONLY;
else { /* Nothing to change. *//*都一样的话就什么都不做*/
r = 0;
goto out;
}
}
} else {/*如果new的npage为0而old的npage不为0,则需要delete已有的*/
if (!old.npages)
goto out;
change = KVM_MR_DELETE;
new.base_gfn = 0;
new.flags = 0;
}
/*
这里如果检查都通过了,首先通过传递进来的slot的id在kvm维护的slot数组中找到对应的slot结构,此结构可能为空或者为旧的slot。然后获取物理页框号、页面数目。如果页面数目大于KVM_MEM_MAX_NR_PAGES,则失败;如果npages为0,则去除KVM_MEM_LOG_DIRTY_PAGES标志。使用旧的slot对新的slot内容做初始化,然后对new slot做设置,参数基本是从用户空间接收的kvm_userspace_memory_region的参数。然后进入下面的if判断
1、如果npages不为0,表明本次要添加slot此时如果old slot的npages为0,表明之前没有对应的slot,需要添加新的,设置change为KVM_MR_CREATE;如果不为0,则需要先修改已有的slot,注意这里如果old slot和new slot的page数目和用户空间地址userspace_addr必须相等,还有就是两个slot的readonly属性必须一致。如果满足上述条件,进入下面的流程。如果映射的物理页框号不同,则设置change KVM_MR_MOVE,如果flags不同,设置KVM_MR_FLAGS_ONLY,否则,什么都不做。
2、如果npages为0,而old.pages不为0,表明需要删除old slot,设置change为KVM_MR_DELETE。到这里基本是做一些准备工作,确定用户空间要进行的操作,接下来就执行具体的动作了。
*/
if ((change == KVM_MR_CREATE) || (change == KVM_MR_MOVE)) {
/* Check for overlaps */
r = -EEXIST;
kvm_for_each_memslot(slot, __kvm_memslots(kvm, as_id)) {
if (slot->id == id)
continue;
if (!((base_gfn + npages <= slot->base_gfn) ||
(base_gfn >= slot->base_gfn + slot->npages)))
goto out;
}
}
/* Free page dirty bitmap if unneeded */
if (!(new.flags & KVM_MEM_LOG_DIRTY_PAGES))
new.dirty_bitmap = NULL;
r = -ENOMEM;
if (change == KVM_MR_CREATE) {
new.userspace_addr = mem->userspace_addr;
if (kvm_arch_create_memslot(kvm, &new, npages))
goto out_free;
}
/* Allocate page dirty bitmap if needed */
if ((new.flags & KVM_MEM_LOG_DIRTY_PAGES) && !new.dirty_bitmap) {
if (kvm_create_dirty_bitmap(&new) < 0)
goto out_free;
}
slots = kvzalloc(sizeof(struct kvm_memslots), GFP_KERNEL_ACCOUNT);
if (!slots)
goto out_free;
memcpy(slots, __kvm_memslots(kvm, as_id), sizeof(struct kvm_memslots));
/*如果用户层请求释放*/
if ((change == KVM_MR_DELETE) || (change == KVM_MR_MOVE)) {
slot = id_to_memslot(slots, id);/*先根据id定位具体的slot*/
slot->flags |= KVM_MEMSLOT_INVALID;/*首先设置非法*/
old_memslots = install_new_memslots(kvm, as_id, slots);
/* From this point no new shadow pages pointing to a deleted,
* or moved, memslot will be created.
*
* validation of sp->gfn happens in:
* - gfn_to_hva (kvm_read_guest, gfn_to_pfn)
* - kvm_is_visible_gfn (mmu_check_roots)
*/
kvm_arch_flush_shadow_memslot(kvm, slot);
/*
* We can re-use the old_memslots from above, the only difference
* from the currently installed memslots is the invalid flag. This
* will get overwritten by update_memslots anyway.
*/
slots = old_memslots;
}
r = kvm_arch_prepare_memory_region(kvm, &new, mem, change);
if (r)
goto out_slots;
/* actual memory is freed via old in kvm_free_memslot below */
if (change == KVM_MR_DELETE) {
new.dirty_bitmap = NULL;
memset(&new.arch, 0, sizeof(new.arch));
}
update_memslots(slots, &new, change);
old_memslots = install_new_memslots(kvm, as_id, slots);
kvm_arch_commit_memory_region(kvm, mem, &old, &new, change);
kvm_free_memslot(kvm, &old, &new);
kvfree(old_memslots);
return 0;
out_slots:
kvfree(slots);
out_free:
kvm_free_memslot(kvm, &new, &old);
out:
return r;
}
EXPORT_SYMBOL_GPL(__kvm_set_memory_region);
这里就根据change来做具体的设置了,如果 KVM_MR_CREATE,则设置new.用户空间地址为新的地址。如果new slot要求KVM_MEM_LOG_DIRTY_PAGES,但是new并没有分配dirty_bitmap,则为其分配。如果change为KVM_MR_DELETE或者KVM_MR_MOVE,这里主要由两个操作,一是设置对应slot标识为KVM_MEMSLOT_INVALID,更新页表。二是增加slots->generation,撤销iommu mapping。接下来对于私有映射的话(memslot->id >= KVM_USER_MEM_SLOTS),如果是要创建,则需要手动建立映射。
接下来确保slots不为空,如果是KVM_MR_CREATE或者KVM_MR_MOVE,就需要重新建立映射,使用kvm_iommu_map_pages函数 ,而如果是KVM_MR_DELETE,就没必要为new设置dirty_bitmap,并对其arch字段的结构清零。最终都要执行操作install_new_memslots,不过当为delete操作时,new的memory size为0,那么看下该函数做了什么。
参照:https://www.cnblogs.com/ck1020/p/6729224.html
https://blog.csdn.net/Shirleylinyuer/article/details/83592614
http://oenhan.com/qemu-memory-struct