内存管理技术四:xvisor实现源码分析2

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_exceptionxvisor/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, &reg_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页表项中。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容