Guest创建
Vmm_guest结构体
首先来看guest结构体的内容,其中apace是用来管理物理内存的,设备树的绑定,gpa->hpa
的对应关系就是在apace中完成的,将在接下来的章节中将会详细讲述。arch_priv是guest对应的页表。此处只是一个空地址,但是在guest创建的时候对该部分进行了定义。
Guest结构体中对应的页表arch_priv
vmm_manager.c-> vmm_manager_guest_create-> arch_guest_init
中完成了对stage2阶段,guest对应的页表的定义。如下图,113行中guest->arch_priv = vmm_malloc(sizeof(struct riscv_guest_priv))
;原来的void类型的arch_priv
被malloc一个riscv_guest_priv
类型的结构体,并且在117行中,结构体priv = riscv_guest_priv(guest)
。
riscv_guest_priv结构体是用来描述页表的结构体,结构体中的struct mmu_pgtbl *pgtbl;就是真正用来描述页表的结构体。
mmu_pgtbl是用来描述页表的结构,其中tbl_pa存放的就是页表的起始地址,之所以得到这个结论是在vcpu切换的时候,hgatp寄存器的内容就是从中获取的。其中tbl_va是页表结构体中描述页表项数组的起始,因为虚拟地址的页表项的获得是&pte[index],而pte = (arch_pte_t *)pgtbl->tbl_va。该部分内容将在page fault中详细讲述。
hgatp
Guest运行在vcpu上,vcpu进行切换的时候将进行上下文的保护,其中就涉及到两阶段地址转换中第二阶段地址转换gpa->hpa是的hgatp寄存器。如下图,展示的就是vcpu的切换和上下文保护,mmu_stage2_change_pgtbl(vcpu->guest->id, riscv_guest_priv(vcpu->guest)->pgtbl)
;其中id是guest的id 号,riscv_guest_priv(vcpu->guest)->pgtbl)
是将vcpu中对应的guest,取((struct riscv_guest_priv *)((guest)->arch_priv))
获得arch_priv指针的地址,该地址指向的是riscv_guest_priv
结构体,再取出改结构体中的struct mmu_pgtbl *pgtbl
页表结构体。
mmu_stage2_change_pgtbl
将调用架构对应的arch_mmu_stage2_change_pgtbl(vmid, pgtbl->tbl_pa)
。在riscv中,调用的是arch_mmu_stage2_change_pgtbl
,完成的是hgatp寄存器内容的填充,并写入到CSR_HGATP中。
此时再来回顾一下hgatp寄存器的构成:
和satp寄存器的内容类似,其中MODE位表达的是映射的方式,当MODE为0时,MMU关闭,当为1时为32位的映射等等。其中PPN表示的是地址转换的第二阶段guest对应的最高级页表的地址。然后在页表对应的页表项中,又指向的是下一级页表的地址。于是可以看到在
arch_mmu_stage2_change_pgtbl
中,hgatp首先是将地址映射的方式通过位移,然后与上页表对应的页数。PPN是页号的意思,此处传入的tbl_phys是页表结构体中用来描述最高级页表的起始地址的。在从hgatp寄存器中取值进行地址转换的时候,pte是将PPN*pagesize + va.vpn[i]*ptesize
,也就是说tbl_phys中的后12位将会被舍弃。
同时从vcpu的切换可以看出,guest是运行在vcpu上的,一个vcpu中对hgatp寄存器的保存是基于guest结构体中用来描述页表的变量mmu_pgtbl的,而一个guest结构体中用来描述stage2的页表的内容也就一个,即arch_priv。这意味着每个guest对应的操作系统在进行stage2转换的时候,只有一个页表。对于linux这样的操作系统来说,每个进程都有属于自己的页表地址,在进程创建的时候会在进程描述符中分配进程对应的页表地址,然后填充到stap寄存器,并且在进程切换的时候的切换stap寄存器中的内容,不同的进程stap寄存器中的内容是不同的。
Guest address space物理内存管理
Guest结构体中struct vmm_guest_aspace aspace结构体用来描述guest对应的物理内存管理,这里就涵盖了gpa->hpa的映射。先来看vmm_guest_aspace结构体:
Node是设备树,guest指向的是当前aspace指向的guest。接下来的一长串从reg_iotree_lock到reg_memprobe_list都是用来描述Region的,在设计文档中称作Region List: A set of "Guest Regions"。这部分可以参考xvisor/docs/DesignDoc:553行对guest,vcpu和region的概述。
Region
先来认识region结构体,每一个设备将对应一个region。Region结构体的填充都是依据分配给guest的设备树来的,其中可以读取各个设备对应的gpa,hpa等,flag表示的region读取的设备到底是哪种设备,比如ram,是需要vmm在内存中真正的给guest分配的内存空间,所以是real类型的。而对于rtc,plic这种,vmm采用的方式是使用软件emulator模拟的方式,并不是一个真实分配的物理硬件,就是virtual虚拟的。gphys_addr就是gpa,vmm_region_mapping *maps
结构体中存放的是hpa,真实的物理地址。
关于不同region的描述:
Each Guest Region has a unique Guest Physical Address (i.e. Physical addressat which region is accessible to Guest VCPUs) and Physical Size (i.e. Size of Guest Region). Further a Guest Region can be one of the three forms:
- Real Guest Region: A Real Guest Region gives direct access to a Host Machine Device/Memory (e.g. RAM, UART, etc). This type of regions directly map guest physical address to Host Physical Address (i.e. Physical address in Host Machine).
- Virtual Guest Region: A Virtual Guest Region gives access to an emulated device (e.g. emulated PIC, emulated Timer, etc.). This type of region is typically linked with an emulated device. The architecture specific code is responsible for redirecting virtual guest region read/write access to the Xvisor device emulation framework.
- Aliased Guest Region: An Aliased Guest Region gives access to another Guest Region at an alternate Guest Physical Address.
设备树是是xvisor开辟guest的时候就设置了,xvisor/tests/riscv/virt64/virt64-guest.dts
中有关于设备树的内容,描述了给guest分配各个设备的信息。在nbuild/Makefile:138行
,将设备树dts通过dtc设备树编译器编译成二进制dtb。在guest创建时,调用vmm_guest_aspace_init->region_add
,通过循环遍历设备树的方式初始化guest的地址空间。
在xvisor启动之后,可以通过命令查看各个region对应的信息。此处输入guest region_list guest0来查看guest0对应的region_list。
先来看guest对应的内存mem0,对应的region内容如下。对于mem0来说,分配了256M地址,region flag是0x00001169,对应的gpa->hpa的数量为128个。根据region的flag表示的内容,mem0对应的属性分别是:alloced,bufferable,cacheable,memory,real。
对于plic来说,flag为0x40a,对应的属性分别是:isdevice,memory,virtual。
其他的设备也可以通过查看region_list的方式查看具体的信息。对于设备树具体的解析过程,代码中并没有详细讲述,不过在通过guest中列出的region_list列表对应的设备关系,可以反向推导guest是如何完成region的初始化,地址的绑定的。
Region_add
vmm_manager.c->vmm_manager_guest_create->vmm_guest_aspace_init
中,vmm_devtree_for_each_child
去每个设备树获得设备树的节点,然后根据设备树节点的结构体描述符去填充region,并将region结构体以树的形式添加到guest的aspace中形成region_list。
vmm_devtree_for_each_child(rnode, aspace->node) {
rc = region_add(guest, rnode, NULL, NULL, TRUE);
if (rc) {
vmm_devtree_dref_node(rnode);
return rc;
}
}
Region_add中传入的参数是对应的guest结构体,对应的设备树节点node,一个新的也是空的,即将被填充的region结构体变量new_reg。
static int region_add(struct vmm_guest *guest,
struct vmm_devtree_node *rnode,
struct vmm_region **new_reg,
void *rpriv,
bool add_probe_list)
{
Region_add中首先重新定义了一个region结构体的变量reg,初始化。
/* Allocate region instance */
reg = vmm_zalloc(sizeof(struct vmm_region));
RB_CLEAR_NODE(®->head);
INIT_LIST_HEAD(®->phead);
/* Fillup region details */
reg->node = rnode;
reg->aspace = aspace;
reg->flags = 0x0;
接着从reg对应的node读取设备树的类型。vmm_devtree_read_string
是从设备树中根据输入的字符串信息,此处是manifest_type
,去设备树中寻找对应的节点,并且把节点转化成对应的结构体vmm_devtree_attr
,将value的值赋给aval。这意味着首先设备要满足manifest_type
,在定义中就是为real,virtual或者aliased三者中的一种。
/* Determine manifest_type */
rc = vmm_devtree_read_string(reg->node, VMM_DEVTREE_MANIFEST_TYPE_ATTR_NAME, &aval);
// 此处的意思是,region必须是real,virtual或者aliased三者中的一种
if (rc) {
goto region_free_fail;
}
接着再去设备树中,根据传入的字符串数组"device_type"将对应的内容赋给aval,并根据aval填充reg(region)的flag。flag是region的属性,比如是内存ram,还是设备device等等。
Gpa
,即guest physical address
,是客户机物理地址,也是从设备树中获取的。Xvisor是硬件辅助的type1类型的虚拟机,对于正常的guest是直接通过vcpu运行在物理cpu上的,但是一些异常,中断和io或一些硬件设备的访问是需要被vmm截获的。对于这种情况,是需要将gpa转化成hpa真实的物理地址,在guest创建的时候,根据给guest分配的设备树中,就可以直接获得gpa的地址。对于guest操作系统来说,获得了gva->gpa
的绑定是软件来完成的,填充pte页表项的内容在xvisor发生page fault的时候也有。
/* Determine region guest physical address */
rc = vmm_devtree_read_physaddr(reg->node,
VMM_DEVTREE_GUEST_PHYS_ATTR_NAME,
®->gphys_addr);
if (rc) {
goto region_free_fail;
}
Region size也是在设备树中获取的,对于不同的设备来说,占用的大小是不相同的。比如mem0是guest的内存,分配的空间是256M。对于设备比如rtc,分配的是地址空间是一个页大小为4k。
/* Determine region size */
rc = vmm_devtree_read_physsize(reg->node, // 可以理解为该设备占据的内存空间,比如mem0就占据了256M
VMM_DEVTREE_PHYS_SIZE_ATTR_NAME,
®->phys_size);
不同的guest会有内容重叠的部分,所以在region中有shared memory
。但是这个共享内存是如何使用的,(TODO:后续需要继续研究)。
Region结构体有一个变量为mapping order
,可以理解为mapping的地址分配的大小,比如rtc分配的就是4k,是一个页的大小。
/* Compute default mapping order for guest region */
reg->map_order = VMM_PAGE_SHIFT;
for (i = VMM_PAGE_SHIFT; i < 64; i++) {
if (reg->phys_size <= ((u64)1 << i)) {
reg->map_order = i;
break;
}
}
但是对于alloced的RAM/ROM regions
,需要重写mapping order的值,为align_order。Mem0就是属于alloced region,所以复写mapping order为align order为2M。这个align_order是从设备树中读取的,也就意味着对ram区域单位的大小,是在设备树中定义的。
/*
* Overwrite mapping order for alloced RAM/ROM regions
* based on align_order or map_order DT attribute
*/
if (!(reg->flags & (VMM_REGION_ALIAS | VMM_REGION_VIRTUAL)) && // 对于mem0,Overwrite map_order,将其设置为align_order为21
(reg->flags & (VMM_REGION_ISRAM | VMM_REGION_ISROM)) &&
(reg->flags & VMM_REGION_ISALLOCED)) {
if ((VMM_PAGE_SHIFT <= reg->align_order) &&
(reg->align_order < reg->map_order)) {
reg->map_order = reg->align_order;
}
有了从设备树中读取的physical size和计算出的单位地址的大小,map order,就可以计算出当前region中mapping的数量。
/* Compute number of mappings for guest region */
reg->maps_count = reg->phys_size >> reg->map_order; // 总大小phys_size/单位map_order 得到map的数量maps_count
if ((((physical_size_t)reg->maps_count) << reg->map_order)
< reg->phys_size) {
reg->maps_count++;
}
Maps数组中存放的就是gpa->hpa的映射关系,如图region_list一样。首先是根据获得maps_counts来初始化maps数组。初始化的时候,hpa的值就是gpa的值。
/* Allocate mappings for guest region */
reg->maps = vmm_zalloc(sizeof(*reg->maps) * reg->maps_count); // 初始化maps,maps是一个数组,其中保存的就是hpa
if (!reg->maps) {
rc = VMM_ENOMEM;
goto region_dref_shm_fail;
}
reg->maps[0].hphys_addr = reg->gphys_addr + // 初始化maps[0]的值,对于maps[0]来说,gpa与hpa是一样的
mapping_gphys_offset(reg, 0);
reg->maps[0].flags = 0;
for (i = 1; i < reg->maps_count; i++) {
reg->maps[i].hphys_addr = reg->gphys_addr + // 那mem0举例,gpa只有一个,map order是每一个gpa与gpa之间的间隔,即2M, 1<<21, map order为21
mapping_gphys_offset(reg, i); // hpa = gpa + i<<map_order
reg->maps[i].flags = 0;
}
对于guest需要直接支配的真实real设备,region会从设备树中直接读取hpa的值,并将其写入maps中。对于真实需要在内存条上给guest分配内存的ram来说,真正分配由vmm_host_ram_reserve
来完成。(TODO:host_ram_reserve分析)
/* Reserve host RAM for reserved RAM/ROM regions */
if (!(reg->flags & (VMM_REGION_ALIAS | VMM_REGION_VIRTUAL)) &&
(reg->flags & (VMM_REGION_ISRAM | VMM_REGION_ISROM)) &&
(reg->flags & VMM_REGION_ISRESERVED)) {
for (i = 0; i < reg->maps_count; i++) {
rc = vmm_host_ram_reserve(reg->maps[i].hphys_addr, // Reserve a portion of RAM forcefully 强制保留一部分内存
mapping_phys_size(reg, i));
在ram和real device设备的gpa和hpa都绑定好之后,剩下的就是virtual device。将reg(region)和guest作为参数传入到vmm_devemu_probe_region
中对模拟的设备列表进行遍历寻找,判断是否能够找到对应的模拟设备。
/* Probe device emulation for real & virtual device regions */
if ((reg->flags & VMM_REGION_ISDEVICE) &&
!(reg->flags & VMM_REGION_ALIAS)) {
if ((rc = vmm_devemu_probe_region(guest, reg))) {
goto region_ram_free_fail;
}
}
devemu_probe_edev函数是用来遍历模拟设备列表的,在找到对应的设备之后,给结构体为vmm_emulator的edev赋值,并将reg的devemu_priv指针指向该结构体。
reg->devemu_priv = edev;
到此,属于该设备树节点的region的内容已经被填充完成了,接下来就是将该region添加在guest的apace中的region list中。这个时候会判断当前的reg的gpa+pyh_size属于树的那个地址范围,然后插入,完成region_add。
new = &root->rb_node;
while (*new) {
pnode = *new;
pnode_reg = rb_entry(pnode, struct vmm_region, head);
if (VMM_REGION_GPHYS_END(reg) <=
VMM_REGION_GPHYS_START(pnode_reg)) {
new = &pnode->rb_left;
} else if (VMM_REGION_GPHYS_END(pnode_reg) <=
VMM_REGION_GPHYS_START(reg)) {
new = &pnode->rb_right;
} else {
rc = VMM_EINVALID;
vmm_write_unlock_irqrestore_lite(root_lock, flags);
goto region_arch_del_fail;
}
}
这一步完成之后表示一个设备树的节点已经被添加到guest中,region_add是每次一个节点的添加就调用一次,需要遍历完整个设备树完成所有节点和region的添加。完成循环之后,guest的aspace物理空间地址的管理已经大致完成。