Page fault发生时的map流程
Guest在发生异常的时候,会触发vm_exit从guest切换到host,xvisor作为当前的host触发中断,通过stvec寄存器,调用在xvisor/arch/riscv/cpu/generic/cpu_entry.S
中定义的_handle_hyp_exception
异常处理函数。
HANDLE_EXCEPTION
定义的do_handle_exception
是在arch_cpu_irq_setup
中将其地址写入到了CSR_STVEC寄存器中,stvec寄存器是用来保存处理中断函数地址的寄存器。arch_cpu_irq_setup
函数可以追溯到vmm_entry
中的cpu_init
。
.global _handle_hyp_exception
_handle_hyp_exception:
SAVE_ALL
SAVE_HSTATUS
HANDLE_EXCEPTION
RESTORE_HSTATUS
RESTORE_ALL
sret
_handle_hyp_exception
首先对通用寄存器和虚拟化h寄存器的内容进行保存然后跳转到HANDLE_EXCEPTION,HANDLE_EXCEPTION
会调用do_handle_exception
。xvisor/arch/riscv/cpu/generic/cpu_exception.c
中do_handle_exception函数调用对应的中断或者异常,此处进入do_handle_trap
,根据寄存器cause的值,判断是哪一种trap,此处我们要处理的page fault将会根据case中跳转到cpu_vcpu_page_fault
中。
switch (cause) {
/* ly:fetch guest page fault,
* load guest page fault, store guest page fault
*/
case CAUSE_FETCH_GUEST_PAGE_FAULT:
case CAUSE_LOAD_GUEST_PAGE_FAULT:
case CAUSE_STORE_GUEST_PAGE_FAULT:
msg = "page fault failed";
if (regs->hstatus & HSTATUS_SPV) {
trap.sepc = regs->sepc;
trap.scause = cause;
trap.stval = csr_read(CSR_STVAL);
trap.htval = csr_read(CSR_HTVAL);
trap.htinst = csr_read(CSR_HTINST);
rc = cpu_vcpu_page_fault(vcpu, regs, &trap);
panic = FALSE;
} else {
rc = VMM_EINVALID;
}
cpu_vcpu_page_fault
中首先会获取fault_addr,此处的fault_addr可以理解为guest的gpa。接着根据这个地址先去guest的region_list中判断当前的fault_addr是否在region中,如果识别到当前的地址指向的是一个VMM_REGION_VIRTUAL
的虚拟设备,则直接通过cpu_vcpu_emulate_load/store
进行模拟的读写操作。如果当前地址不是虚拟的设备,则通过map的方式寻找到gpa对应hpa。接下来我们将详细讲述以上描述的过程。
vmm_guest_find_region
reg = vmm_guest_find_region(vcpu->guest, fault_addr,
VMM_REGION_VIRTUAL | VMM_REGION_MEMORY,
FALSE);
// Find region corresponding to a guest physical address
首先要理解,两阶段地址转换是不会触发page fault的,guest在正常运行的过程中,首先会在vsatp寄存器中获取页表的地址,然后再hgatp寄存器中获取第二阶段页表的地址,通过nest的方式完成寻址。发生page fault是因为访问硬件设备,或者申请的虚拟地址并没有真正分配内存的时候。cpu_vcpu_page_fault
函数处理的流程就是根据fault_addr,也就是guest的gpa来判断是否能够找到对应的region,且这个region还是一个virtual的device,这样的话就可以直接调用调用cpu_vcpu_emulate_load/stroe
去完成。
vmm_guest_find_region
中首先会根据reg的flag判断设备到底是属IO/Memory,在xvisor中将所有的设备都定义为了memory形式。然后根据flag信息获得设备树的根节点,因为IO/Memory是不同的设备树,拥有不同的根节点。在获取设备树的根节点之后,判断传入的fault_addr也就是gpa在哪个节点。如此就通过树的方式找到是否找到了gpa对应的region。
pos = root->rb_node;
while (pos) {
reg = rb_entry(pos, struct vmm_region, head);
if (gphys_addr < VMM_REGION_GPHYS_START(reg)) { // gphys_addr < (reg)->gphys_addr
pos = pos->rb_left;
} else if (VMM_REGION_GPHYS_END(reg) <= gphys_addr) { // (reg)->gphys_addr + (reg)->phys_size <= gphys_addr
pos = pos->rb_right;
} else {
if ((reg->flags & cmp_flags) == cmp_flags) {
found = TRUE;
}
break;
}
}
vmm_guest_find_region
函数会根据传入的gpa和flag信息找到对应的region,如果可以找到,则表明当前的gpa访问的是一个虚拟的设备。就可以直接进行模拟的读写操作。在vmm_devemu_emulate_read
的模拟读的操作中,真正执行读操作的是devemu_doread
,传入的参数就有reg->devemu_priv
,也就是在region_add
中给virtual device
传入的模拟设备的结构体变量,devemu_priv
是一个指针,指向的emulator设备。有了定义的模拟的设备的结构体,就可以去对应模拟设备中执行操作,比如读取rtc的时间。Guest读取rtc的操作是在guest启动的时候从vmm中获取一个起始时间,加上系统运行的时间就获得了实时的时间。
rc = devemu_doread(reg->devemu_priv,
gphys_addr - reg->gphys_addr,
dst, dst_len, dst_endian);
cpu_vcpu_stage2_map
如果传入的gpa访问的不是虚拟的设备,那么就需要找到gpa对应的hpa。这个操作就是由cpu_vcpu_stage2_map
来完成的。
/* Mapping does not exist hence create one */
return cpu_vcpu_stage2_map(vcpu, regs, fault_addr);
cpu_vcpu_stage2_map中首先定义了一个页struct mmu_page pg;
的结构体,用来保存gpa,hpa,size和flag等信息。
struct mmu_page {
physical_addr_t ia; // 虚拟地址
physical_addr_t oa; // 最终物理地址的ppn的地址
physical_size_t sz;
arch_pgflags_t flags;
};
定义一个地址将gpa的后12位清零,因为gpa->hpa的转换后12位为页内偏移,是不会变化的,所以想要找到gpa对应的hpa只要找到gpa的页对应的hpa对应的物理页即可。
inaddr = fault_addr & PGTBL_L0_MAP_MASK;
在获得gpa对应的页之后,尝试将页去guest对应的设备树中寻找对应的hpa页。vmm_guest_physical_map
函数首先是继续将获得页作为gpa去vmm_guest_find_region
。
rc = vmm_guest_physical_map(vcpu->guest, inaddr, size,
&outaddr, &availsz, ®_flags);
// Map guest physical address to some host physical address
如果转化成页的gpa还没有找到对应的region,则表明cpu_vcpu_stage2_map
阶段失败,将直接返回并打印出错误的gpa地址。
reg = vmm_guest_find_region(guest, gphys_addr,
VMM_REGION_MEMORY, FALSE);
if (!reg) {
return VMM_EFAIL;
}
在guest的region_list中找到gpa页对应的region之后,就可以去region中去找对应的hpa了,vmm_guest_find_mapping
用来完成这个步骤。vmm_guest_find_mapping-> mapping_find
根据gpa的地址寻找到mapping数组中对应的i。在region_add章节中可以知道,mapping是一个数组,比如mem0代表guest内存的部分,就有一百多个数组。
map = mapping_find(guest, reg, &i, gphys_addr);
在获得数组的对应的i之后,就可以根据region的起始地址加上i对应的偏移量之后,得到region中对应的gpa。Hphys是mapping数组的起始hpa,加上偏移量就得到了数组中对应的hpa的地址。将这个得到的hphys传给指针hphys_addr指针指向的地址。
map_gphys_addr = reg->gphys_addr + mapping_gphys_offset(reg, i);
hphys = map->hphys_addr + (gphys_addr - map_gphys_addr);
到这里vmm_guest_physical_map
中传入的参数hphys_addr就已经获取到了hpa的对应地址,此时要将对应的gpa,hpa,size等信息填充到mmu_page这个结构体中。
pg.ia = inaddr; // 虚拟地址
pg.sz = size;
pg.oa = outaddr; // 得到了要映射的页的物理地址
pg_reg_flags = reg_flags;
(TODO:在cpu_vcpu_stage2_map中对RAM/ROM和64位的情况下有另外的操作,比如对于64位的情况不再是对页进行映射,而是对vpn[3]作为gpa进行地址进行映射,需要进行分析这么做的原因是什么)
填充页表信息 mmu_map_page
在拥有了gpa,hpa,size和flag等信息之后,需要将该部分内容填充到页表中。虽然页表的寻址是通过mmu,tlb等硬件完成的,但是页表项pte等内容是由软件来完成的。arch_mmu_pgflags_set
根据获得region flag来填充pg结构体中flag的内容。注意在xvisor中,MMU_STAGE2是用来描述gpa->hpa的,但是MMU_STAGE1并不是用来描述guest在第一阶段的地址转换,而是xvisor自身的mmu转换,这个和文档中的内容有所不同。
arch_mmu_pgflags_set(&pg.flags, MMU_STAGE2, pg_reg_flags);
接着根据pg结构体的内容来填充stage2的页表内容。riscv_guest_priv(vcpu->guest)->pgtbl
是从guest结构体中获得arch_priv指针指向的riscv_guest_priv结构体,并获得pgtbl页表的指针。页表结构体中用来存放虚拟地址,页的地址,pte页表项的内容信息。
/* Try to map the page in Stage2 */
rc = mmu_map_page(riscv_guest_priv(vcpu->guest)->pgtbl, &pg);
mmu_map_page首先根据sz的判断,找到页表的对应的最后一级页表,此处采用的是递归的方式。因为我们知道页表指向的最后一级页表的PPN就是对应的物理地址,所以此处要填充的一定是最后一级页表中PPN的地址。mmu_pgtbl_get_child顾名思义,获取下一级页表返回一个页表结构体给child。
if (pg->sz < blksz) {
child = mmu_pgtbl_get_child(pgtbl, pg->ia, TRUE);
if (!child) {
return VMM_EFAIL;
}
return mmu_map_page(child, pg);
}
mmu_pgtbl_get_child
中将gpa对应的vpn[i]中的内容取出,变成了pte数组中的index。Pte指向的是上一级页表中的pte页表的pte数组首地址,pte_val就是数组中对应的pte的值。Pte_val本身也是一个数组,即页表项,由PPN和flag组成。
index = arch_mmu_level_index(map_ia, parent->stage, parent->level);
pte = (arch_pte_t *)parent->tbl_va;
vmm_spin_lock_irqsave_lite(&parent->tbl_lock, flags);
pte_val = pte[index];
arch_mmu_pte_is_valid
用来判断pte的有效位是不是为1,如果为1就可以将页表项中的PPN通过偏移转换成地址赋给tbl_pa。mmu_pgtbl_find ->mmu_pgtbl_nonpool_find
,去页表池中根据地址找到空闲的页表返回给child并且对child页表结构体进行初始化mmu_pgtbl_alloc
。
找到需要填充的页表,并获得结构体之后,需要对虚拟地址对应的pte页表项内容进行填充,依然是根据虚拟地址找到对应的index,和pte数组的起始地址。
index = arch_mmu_level_index(pg->ia, pgtbl->stage, pgtbl->level); pte = (arch_pte_t *)pgtbl->tbl_va;
根据pg结构体中的内容填充pgtbl中pte页表项的内容。arch_mmu_pte_set(&pte[index], pgtbl->stage, pgtbl->level, pg->oa, &pg->flags);
arch_mmu_pte_set
传入的参数arch_pte_t *pte就是&pte[index],是vpn[]对应的pte,pte本身也是一个数组,首先将pa也就是pg->oa,是region中找到的hpa填充到pte对应的PPN中。
void arch_mmu_pte_set(arch_pte_t *pte, int stage, int level,
physical_addr_t pa, arch_pgflags_t *flags)
{
*pte = pa & arch_mmu_level_map_mask(stage, level);
*pte = *pte >> PGTBL_PAGE_SIZE_SHIFT;
*pte = *pte << PGTBL_PTE_ADDR_SHIFT;
*pte |= ((arch_pte_t)flags->rsw << PGTBL_PTE_RSW_SHIFT) &
PGTBL_PTE_RSW_MASK;
*pte |= ((arch_pte_t)flags->dirty << PGTBL_PTE_DIRTY_SHIFT) &
PGTBL_PTE_DIRTY_MASK;
*pte |= ((arch_pte_t)flags->accessed << PGTBL_PTE_ACCESSED_SHIFT) &
PGTBL_PTE_ACCESSED_MASK;
*pte |= ((arch_pte_t)flags->global << PGTBL_PTE_GLOBAL_SHIFT) &
PGTBL_PTE_GLOBAL_MASK;
*pte |= ((arch_pte_t)flags->user << PGTBL_PTE_USER_SHIFT) &
PGTBL_PTE_USER_MASK;
*pte |= ((arch_pte_t)flags->execute << PGTBL_PTE_EXECUTE_SHIFT) &
PGTBL_PTE_EXECUTE_MASK;
*pte |= ((arch_pte_t)flags->write << PGTBL_PTE_WRITE_SHIFT) &
PGTBL_PTE_WRITE_MASK;
*pte |= ((arch_pte_t)flags->read << PGTBL_PTE_READ_SHIFT) &
PGTBL_PTE_READ_MASK;
*pte |= PGTBL_PTE_VALID_MASK;
}
再来回顾一下sv32中pte页表项的内容,注意虽然PPN划分成了PPN[0]和PPN[1],但这两个在使用的时候是作为一个整体的。在经历过arch_mmu_pte_set
后,gpa->hpa第二阶段页表中vpn[]对应的页表项的内容已经根据region寻找到了对应hpa并填充在对应的pte页表项pte中。这样硬件mmu就可以从多级页表的结构中寻找到gpa对应的hpa。多级页表的填充是同过递归的方式来实现的。
为了提高效率,会刷新stage2的tlb(TODO:TLB研究)
arch_mmu_stage2_tlbflush(TRUE, pg->ia, blksz);
至此就完整的完成了guest在创建时gpa和hpa的绑定,第二阶段的地址转换发生page fault时如何根据gpa寻找到对应的hpa并填充到页表对应的pte页表项中。