美文网首页
Lec 4,5 - Lab 3 page table

Lec 4,5 - Lab 3 page table

作者: 西部小笼包 | 来源:发表于2023-11-05 09:40 被阅读0次

    为什么需要虚拟内存

    shell进程由于bug,引发了随机写入某些内存地址,这些内存地址可能是其他进程使用的,而可能影响内核或其他进程的执行。

    所以我们需要引入虚拟内存来实现隔离的进程的内存地址空间。每个进程拥有自己的内存地址空间,它们能够读写自己的内存,但不能访问其他进程的内存。实现多个地址空间与物理内存之间多路复用的挑战在于如何保持隔离性。

    页表(page table)

    xv6操作系统使用RISC-V架构的分页硬件来实现地址空间(AS)。
    页表提供了一个间接层来处理地址映射,CPU通过内存管理单元(MMU)映射到物理内存(RAM)。内核告诉MMU如何将每个虚拟地址(VA)映射到物理地址(PA)。


    image.png

    为了实现不同的地址空间,需要多个页表并在切换过程中更换页表。
    MMU有一个satp寄存器,用于内核写入以更改页表。
    页表存在于内存中,satp寄存器保存当前页表的物理地址。
    MMU并不会保存page table,它只会从内存中读取page table,然后完成翻译。

    页表的大小和结构

    1699189384090.png
    • RISC-V映射4KB的"页面",因此页表只需要为每个页面有一个条目。
    • RISC-V使用了一个三级页表结构来节省空间,通过索引位逐级查找页面表条目(PTE)。
      • 如果是直接映射,虽然我们只使用了一个page, 我们还是需要2^27个PTE(因为有27位用来索引)。这个方案中,我们只需要3 * 512个PTE(3个9位来索引,512是2^9)。所需的空间大大减少了。这是实际上硬件采用这种层次化的3级page directory结构的主要原因。
    • 每个PTE有64位,其中只有54位被使用,包括44位物理页面号(PPN)和10位标志位。
    • PTE中的标志位包括有效(V)、可写(W)、可读(R)、可执行(X)和用户(U)。
    • 如果V位未设置或尝试写入时W位未设置,会触发"页错误",导致控制权转移到内核。

    TLB

    • 对于一个虚拟内存地址的寻址,需要读三次内存,这里代价有点高.
    • 所以实际中,几乎所有的处理器都会对于最近使用过的虚拟地址的翻译结果有缓存。这个缓存被称为:Translation Lookside Buffer(通常翻译成页表缓存)
    • 当处理器第一次查找一个虚拟地址时,硬件通过3级page table得到最终的PPN,TLB会保存虚拟地址到物理地址的映射关系。这样下一次当你访问同一个虚拟地址时,处理器可以查看TLB,直接得到物理地址
    • 如果你切换了page table,TLB中的缓存将不再有用,它们需要被清空,否则地址翻译可能会出错。所以操作系统知道TLB是存在的,要切换page table时,会发送清空TLB的指令,sfence_vma。

    kernel pagetable

    1699190362825.png

    右半部分

    KERNBASE

    图中的右半部分的结构完全由硬件设计者决定。如你们上节课看到的一样,当操作系统启动时,会从地址0x80000000开始运行,这个地址其实也是由硬件设计者决定的。
    主板的设计人员决定了,在完成了虚拟到物理地址的翻译之后,如果得到的物理地址大于0x80000000会走向DRAM芯片,如果得到的物理地址低于0x80000000会走向不同的I/O设备。

    • PLIC是中断控制器(Platform-Level Interrupt Controller)我们下周的课会讲。
    • CLINT(Core Local Interruptor)也是中断的一部分。所以多个设备都能产生中断,需要中断控制器来将这些中断路由到合适的处理函数。
    • UART0(Universal Asynchronous Receiver/Transmitter)负责与Console和显示器交互。
    • VIRTIO disk,与磁盘进行交互。

    左半部分

    当机器刚刚启动时,还没有可用的page,XV6操作系统会设置好内核使用的虚拟地址空间,也就是这张图左边的地址分布。


    1699190587957.png

    guard page

    有一些page在虚拟内存中的地址很靠后,比如kernel stack在虚拟内存中的地址就很靠后。这是因为在它之下有一个未被映射的Guard page,这个Guard page对应的PTE的Valid 标志位没有设置,这样,如果kernel stack耗尽了,它会溢出到Guard page,但是因为Guard page的PTE中Valid标志位未设置,会导致立即触发page fault,这样的结果好过内存越界之后造成的数据混乱。立即触发一个panic(也就是page fault),你就知道kernel stack出错了。同时我们也又不想浪费物理内存给Guard page,所以Guard page不会映射到任何物理内存,它只是占据了虚拟地址空间的一段靠后的地址。

    权限

    • Kernel text page被标位R-X,意味着你可以读它,也可以在这个地址段执行指令,但是你不能向Kernel text写数据
    • Kernel data需要能被写入,所以它的标志位是RW-,但是你不能在这个地址段运行指令,所以它的X标志位未被设置

    user pagetable

    1699190823363.png
    • 用户空间的虚拟地址从0开始。这样做的好处在于可预测性和简化编译器生成代码的工作。编译器可以假设所有地址都是从一个固定的基点开始的,这样可以生成更简洁的机器代码。
    • 这些地址是连续的,非常适合比如说大数组这样的数据结构。虽然虚拟地址连续,但它们并不需要映射到连续的物理内存上,从而避免了内存碎片问题。
    • 用户地址空间提供了大量的地址范围以供扩展,这对于动态增长的数据结构来说非常有用。

    内核与用户空间都映射了跳板页(trampoline page):

    跳板页是一种特殊的内存页面,它既存在于用户空间的地址映射中,也存在于内核空间的地址映射中,虽然用户位(U bit)没有被设置。
    这样的设置方便了用户空间与内核空间之间的转换。当用户程序需要进行系统调用,即从用户模式切换到内核模式时,通过跳板页可以更加顺畅地进行。因为切换了pagetable之后,理论上同样的VA会用不同的页表找到不同的PA,但是这2块映射是一致的,那么不同的页表,也可以保证代码可以顺畅执行。

    内核如何使用用户虚拟地址?

    • 当用户程序调用如read()这样的系统调用时,它会将虚拟地址传递给内核。内核不能直接使用这些用户虚拟地址,因为内核有自己的地址空间。
    • 因此,内核必须将这些用户虚拟地址转换为内核空间中的虚拟地址。这一过程涉及到查询当前进程的页表,找到用户虚拟地址对应的物理地址,然后通过内核的页表再将该物理地址映射回内核的虚拟地址。
    • 通过这种方式,内核可以安全地访问和操作用户程序传递过来的数据。

    xv6代码结构

    • 内核页表设置在启动时(kvminit())由内核建立,大部分直接映射,允许内核使用物理地址作为虚拟地址。
    • 每个进程都有自己的地址空间,内核为每个进程创建独立的页表。
      • fork(void) -> uvmcopy(p->pagetable, np->pagetable, p->sz)
      • exec() -> pagetable = proc_pagetable(p)
    • 用户地址空间的布局从0开始,为编译器生成代码提供了便利。
    • 内核地址空间的设置:在内核启动时分页还未启用,所以地址是物理地址。
    • kvmmake() 函数在 vm.c 中创建内核的页表。
    • kvmmap() 函数在构建页表时添加PTE。
    • mappages() 函数在 vm.c 中为一系列虚拟地址添加映射到相应物理地址的映射。
    • walk() 函数模拟了寻址硬件找到一个地址的PTE的过程。

    Lab 3 Optional challenge

    Unmap the first page of a user process so that dereferencing a null pointer will result in a fault. You will have to change user.ld to start the user text segment at, for example, 4096, instead of 0.

    这道题目的难点有2,当xv6 启动时,我们开启的第一个进程是initcode。这个进程强制设置了代码的起始点从地址0开始。同时vmprint的test函数也需要当进程号为1的时候,打印含有0地址的3级页表项。
    所以这道题,在理解了整个新的进程初始化时页表怎么分配后,我们需要对非1号进程,进行Unmap the first page的操作。
    那么理解进程的页表怎么创建就非常重要。除了1号进程,进程的创建分为2种。第一种就是纯fork,第二种就是fork+exec。fork里的操作就是复制父进程的页表到子进程。所以我们保证父进程没有0号地址的页表项。那么子进程也不会复制这个页表项。那么2号进程 shell (exec sh) 其实就作为其他进程的父进程,他的页表需要最先做unmap。 那么当 fork 返回后,如果是接着exec,会进到exec.c的exec中。
    这里做的事情是开启一个新的页表,然后把ELF要执行的代码段(就是user space下注册的命令的main函数里的代码)加载进新页表的text 和 data段。随后创建guard page 和 stack page。然后把原来fork出来的pagetable 给free掉,同时把新创建的pagetable用作这个进程的pagetable.
    那么核心代码,其实就是,对于1号进程,从ADDRESS 0开始free. 对其他进程的新的pagetable做一次unmap,然后因为FORK出来的oldpagetable,已经不含有第一页了。再free的时候得从第二页开始。具体代码改动如下:

    1699196919666.png 1699195074895.png
    1699195130318.png

    然后uvmcopy, 就得从地址PGSIZE开始,因为0开始的那页已经被unmap了。


    1699195307668.png

    测试方案

    写一个用户命令,nulltest

    #include "kernel/types.h"
    #include "user/user.h"
    
    int main(int argc, char *argv[]) {
        char *ptr = (char *)0x0;
        printf("%c\n", *ptr);  // deference null
        exit(0);
    }
    

    同时写一个vmprint命令,以及一个vmprint系统调用,可以使得在shell段去打印pagetable来验证是否没有0号PAGE。

    #include "kernel/types.h"
    #include "user/user.h"
    
    int main(int argc, char *argv[]) {
      if(argc <= 1){
        fprintf(2, "usage: vmprint 0 or 1, which mean abbr or not\n");
        exit(1);
      }
      vmprint(atoi(argv[1]));
      exit(0);
    }
    
    1699195856983.png

    Add a system call that reports dirty pages (modified pages) using PTE_D.

    这道题其实和LAB 3里的第三问的做法差不多,只不过是CHECK PTE_A 变成了 PTE_D; 然后REPORT我的方案是打印所有DIRTY PAGE。

    1699196062393.png

    测试方案

    同样写一个用户端的命令去作为测试。

    #include "kernel/param.h"
    #include "kernel/fcntl.h"
    #include "kernel/types.h"
    #include "kernel/riscv.h"
    #include "user/user.h"
    
    int
    main(int argc, char *argv[])
    {
      char *buf = malloc(32 * PGSIZE);
      // record init dirty page, check delta
      dirtypages();
      // write two page
      buf[PGSIZE] += 1;
      buf[PGSIZE * 3] += 1;
      dirtypages();
      // read only
      char c = buf[PGSIZE * 4];
      c++;
      dirtypages();
      exit(0);
    }
    
    1699196402739.png

    符合预期。

    Use super-pages to reduce the number of PTEs in page tables.

    这一问,我花了非常久的时间,代码改动量非常多。但是也彻底帮我理解vm.c每个函数的作用。因为一旦涉及到了大页的支持,基本这里面每个函数都要修改。
    这一问,我直接贴全部代码,小伙伴们可以根据需要自己比对我有哪些改动。
    下面我来说一下要实现大页的基本代码思路。

    1. kalloc.c

    维护2个freelist,一个用来分配普通页,一个用来分配大页。
    然后初始化的时候,对不同地址空间进行不同的分工。指定一个地址,低于这个地址为普通页,高于这个地址为大页。
    这样做的弊端是大页和普通页没法互相转化。我选择这么做其实也是为了简化问题难度。不然要写的代码会更多。


    1699196736942.png
    1699196773624.png
    1699196831866.png

    2. risv.h

    1699197059956.png

    3. vm.c

    原理是当一个page是leaf page,(有PTE_R或者PTE_X) 就不可能是NON-LEAF PAGE。并且当且层级不为2时,就代表它索引到的地址是一个巨页的开始。因为我们不使用额外的一个BIT去表明这个页是否为巨页。这个信息如果外层需要,理论上我们需要在函数签名里一起返回。但是因为我们固定了巨页和普通页的地址分割线。所以我们只需要用一次PTE2PA,然后和SUPSTART比较,就可以知道当前是否为巨页。


    1699200566770.png

    然后就是根据需求,去改动VM.C里的各种函数,根据是否为巨页,需要写不同的逻辑处理。这里有个注意事项是,要分配巨页的前提条件,除了要求分配的内存大于单个巨页的SIZE,同时也要求需要映射的VA,根据2MB对齐。

    要求超级页面按其大小(例如,2 MiB的超级页面需按2MB对齐)进行对齐,是由于MMU在将虚拟地址转换为物理地址和访问内存时的工作方式。

    下面是对齐的原因:

    • 地址转换效率:
      虚拟地址的低位通常用作页面内的偏移量,而高位用来索引页面表。对于标准的4 KiB页面,低12位用作偏移量(因为2^12 = 4096)。对于2 MiB超级页面,低21位用作偏移量(因为2^21 = 2097152,即2 MiB)。如果一个超级页面没有进行2 MiB对齐,地址的低位就不能正确代表页面内的偏移量。

    • 简化页面表:
      通过使超级页面大小对齐,可以绕过一个或多个页面表级别。例如,在使用多级页面表的系统中,使用超级页面可以允许单个页面表条目映射大块内存,减少了表条目的数量,从而减少了内存占用和遍历这些表所需的时间。

    • 硬件要求:
      CPU的MMU期望超级页面按其大小对齐。这是硬件的要求;MMU被设计为以这种方式工作。如果页面没有正确对齐,MMU将无法正确翻译虚拟地址到物理地址,导致不正确的内存访问和可能的系统崩溃。

    • 缓存性能:
      正确对齐也有助于优化缓存线的利用。未对齐的大页面可能导致缓存利用率低效,结果产生更多缓存未命中和较低的性能。

    如果超级页面的起始虚拟地址没有按2MB边界对齐,这将意味着该地址不能清晰地映射到页面表的单个条目,这违背了使用超级页面进行高效内存管理的目的。它将需要特殊处理以在一个超级页面的末端映射部分页面,并在另一个的开头映射剩余部分,增加了复杂性,并可能抵消性能优势。

    // Return the address of the PTE in page table pagetable
    // that corresponds to virtual address va.  If alloc!=0,
    // create any required page-table pages.
    //
    // The risc-v Sv39 scheme has three levels of page-table
    // pages. A page-table page contains 512 64-bit PTEs.
    // A 64-bit virtual address is split into five fields:
    //   39..63 -- must be zero.
    //   30..38 -- 9 bits of level-2 index.
    //   21..29 -- 9 bits of level-1 index.
    //   12..20 -- 9 bits of level-0 index.
    //    0..11 -- 12 bits of byte offset within the page.
    pte_t *
    walk(pagetable_t pagetable, uint64 va, int alloc)
    {
      if(va >= MAXVA)
        panic("walk");
    
      int level = 2;
      int end = alloc == SUPPGSIZE ? 1 : 0;  
      for(; level > end; level--) {
        pte_t *pte = &pagetable[PX(level, va)];
        if(*pte & PTE_V) {
          pagetable = (pagetable_t)PTE2PA(*pte);
          if ((*pte & PTE_R) || (*pte & PTE_X)) {
            if (level != 1) panic("walk_level");
            return pte;
          }
        } else {
          if(!alloc || (pagetable = (pde_t*)kalloc()) == 0)
            return 0;
          memset(pagetable, 0, PGSIZE);
          *pte = PA2PTE(pagetable) | PTE_V;
        }
      }
      
      if (level > 1 || level < 0) panic("walk_level");
      return &pagetable[PX(level, va)];
    }
    
    // Look up a virtual address, return the physical address,
    // or 0 if not mapped.
    // Can only be used to look up user pages.
    uint64
    walkaddr(pagetable_t pagetable, uint64 va)
    {
      pte_t *pte;
      uint64 pa;
    
      if(va >= MAXVA)
        return 0;
    
      pte = walk(pagetable, va, 0);
      if(pte == 0)
        return 0;
      if((*pte & PTE_V) == 0)
        return 0;
      if((*pte & PTE_U) == 0)
        return 0;
      pa = PTE2PA(*pte);
      return pa;
    }
    
    // add a mapping to the kernel page table.
    // only used when booting.
    // does not flush TLB or enable paging.
    void
    kvmmap(pagetable_t kpgtbl, uint64 va, uint64 pa, uint64 sz, int perm)
    {
      if(mappages(kpgtbl, va, sz, pa, perm) != 0)
        panic("kvmmap");
    }
    
    // Create PTEs for virtual addresses starting at va that refer to
    // physical addresses starting at pa.
    // va and size MUST be page-aligned.
    // Returns 0 on success, -1 if walk() couldn't
    // allocate a needed page-table page.
    int
    mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
    {
      uint64 a, last;
      pte_t *pte;
    
      int suppg = (size >= SUPPGSIZE) && !(va % SUPPGSIZE);
      int pgsize = suppg ? SUPPGSIZE : PGSIZE;
      if((va % pgsize) != 0)
        panic("mappages: va not aligned");
    
      if((size % pgsize) != 0)
        panic("mappages: size not aligned");
    
      if(size == 0)
        panic("mappages: size");
      
      a = va;
      last = va + size - pgsize;
      for(;;){
        if((pte = walk(pagetable, a, pgsize)) == 0)
          return -1;
        if(*pte & PTE_V)
          panic("mappages: remap");
        *pte = PA2PTE(pa) | perm | PTE_V;
        if(a == last)
          break;
        a += pgsize;
        pa += pgsize;
      }
      return 0;
    }
    
    // Remove npages of mappings starting from va. va must be
    // page-aligned. The mappings must exist.
    // Optionally free the physical memory.
    void
    uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
    {
      uint64 a;
      pte_t *pte;
    
      if((va % PGSIZE) != 0)
        panic("uvmunmap: not aligned");
      int pgsize = PGSIZE;  
      for(a = va; a < va + npages*PGSIZE; a += pgsize){
        
        if((pte = walk(pagetable, a, 0)) == 0)
          panic("uvmunmap: walk");
        
        if((*pte & PTE_V) == 0)
          panic("uvmunmap: not mapped");
        if(PTE_FLAGS(*pte) == PTE_V)
          panic("uvmunmap: not a leaf");
        uint64 pa = PTE2PA(*pte);
        int suppg = pa >= SUPSTART;
        pgsize = suppg ? SUPPGSIZE : PGSIZE;  
        if(do_free){
          suppg ? kfree_suppage((void*)pa) : kfree((void*)pa);
        }
        *pte = 0;
      }
    }
    
    // create an empty user page table.
    // returns 0 if out of memory.
    pagetable_t
    uvmcreate()
    {
      pagetable_t pagetable;
      pagetable = (pagetable_t) kalloc();
      if(pagetable == 0)
        return 0;
      memset(pagetable, 0, PGSIZE);
      return pagetable;
    }
    
    // Load the user initcode into address 0 of pagetable,
    // for the very first process.
    // sz must be less than a page.
    void
    uvmfirst(pagetable_t pagetable, uchar *src, uint sz)
    {
      char *mem;
    
      if(sz >= PGSIZE)
        panic("uvmfirst: more than a page");
      mem = kalloc();
      memset(mem, 0, PGSIZE);
      mappages(pagetable, 0, PGSIZE, (uint64)mem, PTE_W|PTE_R|PTE_X|PTE_U);
      memmove(mem, src, sz);
    }
    
    // Allocate PTEs and physical memory to grow process from oldsz to
    // newsz, which need not be page aligned.  Returns new size or 0 on error.
    uint64
    uvmalloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz, int xperm)
    {
      char *mem;
      uint64 a;
    
      if(newsz < oldsz)
        return oldsz;
      uint64 sz = newsz - oldsz;
      int suppg = sz >= SUPPGSIZE && PGROUNDUP(oldsz) == SUPPGROUNDUP(oldsz);
      int pgsize = suppg ? SUPPGSIZE : PGSIZE;  
      oldsz = PGROUNDUP(oldsz);
      if (suppg) newsz = SUPPGROUNDUP(newsz);
      for(a = oldsz; a < newsz; a += pgsize){
        mem = suppg ? kalloc_suppage() : kalloc();
        if(mem == 0){
          uvmdealloc(pagetable, a, oldsz);
          return 0;
        }
        memset(mem, 0, pgsize);
        if(mappages(pagetable, a, pgsize, (uint64)mem, PTE_R|PTE_U|xperm) != 0){
          suppg ? kfree_suppage(mem) : kfree(mem);
          uvmdealloc(pagetable, a, oldsz);
          return 0;
        }
      }
      return newsz;
    }
    
    // Deallocate user pages to bring the process size from oldsz to
    // newsz.  oldsz and newsz need not be page-aligned, nor does newsz
    // need to be less than oldsz.  oldsz can be larger than the actual
    // process size.  Returns the new process size.
    uint64
    uvmdealloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz)
    {
      if(newsz >= oldsz)
        return oldsz;
    
      uint64 sz = oldsz - newsz;
      int suppg = sz >= SUPPGSIZE && PGROUNDUP(newsz) == SUPPGROUNDUP(newsz);;
    
      if (suppg) {
        if(SUPPGROUNDUP(newsz) < SUPPGROUNDUP(oldsz)){
          int npages = (SUPPGROUNDUP(oldsz) - SUPPGROUNDUP(newsz)) / SUPPGSIZE;
          uvmunmap(pagetable, SUPPGROUNDUP(newsz), npages, 1);
        }
      } else {
        if(PGROUNDUP(newsz) < PGROUNDUP(oldsz)){
          int npages = (PGROUNDUP(oldsz) - PGROUNDUP(newsz)) / PGSIZE;
          if (newsz == 0) {
            newsz = PGSIZE;
            npages--;
          }
          uvmunmap(pagetable, PGROUNDUP(newsz), npages, 1);
        }
      }
    
      return newsz;
    }
    
    // Recursively free page-table pages.
    // All leaf mappings must already have been removed.
    void
    freewalk(pagetable_t pagetable)
    {
      // there are 2^9 = 512 PTEs in a page table.
      for(int i = 0; i < 512; i++){
        pte_t pte = pagetable[i];
        if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
          // this PTE points to a lower-level page table.
          uint64 child = PTE2PA(pte);
          freewalk((pagetable_t)child);
          pagetable[i] = 0;
        } else if(pte & PTE_V){
          panic("freewalk: leaf");
        }
      }
      kfree((void*)pagetable);
    }
    
    // Free user memory pages,
    // then free page-table pages.
    void
    uvmfree(pagetable_t pagetable, uint64 sz)
    {
      if(sz > 0)
        uvmunmap(pagetable, 0, PGROUNDUP(sz)/PGSIZE, 1);
      freewalk(pagetable);
    }
    
    // Given a parent process's page table, copy
    // its memory into a child's page table.
    // Copies both the page table and the
    // physical memory.
    // returns 0 on success, -1 on failure.
    // frees any allocated pages on failure.
    int
    uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
    {
      pte_t *pte;
      uint64 pa, i;
      uint flags;
      char *mem;
      int pgsize = PGSIZE;
      for(i = PGSIZE; i < sz; i += pgsize){
        if((pte = walk(old, i, 0)) == 0)
          panic("uvmcopy: pte should exist");
        if((*pte & PTE_V) == 0)
          panic("uvmcopy: page not present");
        pa = PTE2PA(*pte);
        flags = PTE_FLAGS(*pte);
        int suppg = pa >= SUPSTART;
        pgsize = suppg ? SUPPGSIZE : PGSIZE;
        if((mem = suppg ? kalloc_suppage() : kalloc()) == 0)
          goto err;
        memmove(mem, (char*)pa, pgsize);
        if(mappages(new, i, pgsize, (uint64)mem, flags) != 0){
          suppg ? kfree_suppage(mem) : kfree(mem);
          goto err;
        }
      }
      return 0;
    
     err:
      uvmunmap(new, 0, i / PGSIZE, 1);
      return -1;
    }
    
    // mark a PTE invalid for user access.
    // used by exec for the user stack guard page.
    void
    uvmclear(pagetable_t pagetable, uint64 va)
    {
      pte_t *pte;
      
      pte = walk(pagetable, va, 0);
      if(pte == 0)
        panic("uvmclear");
      *pte &= ~PTE_U;
    }
    
    // Copy from kernel to user.
    // Copy len bytes from src to virtual address dstva in a given page table.
    // Return 0 on success, -1 on error.
    int
    copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
    {
      uint64 n, va0, pa0;
      pte_t *pte;
      int suppg = dstva >= SUPSTART;
      int pgsize = suppg ? SUPPGSIZE : PGSIZE;
      while(len > 0){
        va0 = suppg ? SUPPGROUNDDOWN(dstva) : PGROUNDDOWN(dstva);
        if(va0 >= MAXVA)
          return -1;  
        pte = walk(pagetable, va0, 0);
        if(pte == 0 || (*pte & PTE_V) == 0 || (*pte & PTE_U) == 0 ||
           (*pte & PTE_W) == 0)
          return -1;
        pa0 = PTE2PA(*pte);
        n = pgsize - (dstva - va0);
        if(n > len)
          n = len;
        memmove((void *)(pa0 + (dstva - va0)), src, n);
    
        len -= n;
        src += n;
        dstva = va0 + pgsize;
      }
      return 0;
    }
    
    // Copy from user to kernel.
    // Copy len bytes to dst from virtual address srcva in a given page table.
    // Return 0 on success, -1 on error.
    int
    copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
    {
      uint64 n, va0, pa0;
      
      int suppg = srcva >= SUPSTART;
      int pgsize = suppg ? SUPPGSIZE : PGSIZE;
      while(len > 0){
        va0 = suppg ? SUPPGROUNDDOWN(srcva) : PGROUNDDOWN(srcva);
        pa0 = walkaddr(pagetable, va0);
        if(pa0 == 0)
          return -1;
        n = pgsize - (srcva - va0);
        if(n > len)
          n = len;
        memmove(dst, (void *)(pa0 + (srcva - va0)), n);
    
        len -= n;
        dst += n;
        srcva = va0 + pgsize;
      }
      return 0;
    }
    
    // Copy a null-terminated string from user to kernel.
    // Copy bytes to dst from virtual address srcva in a given page table,
    // until a '\0', or max.
    // Return 0 on success, -1 on error.
    int
    copyinstr(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max)
    {
      uint64 n, va0, pa0;
      int got_null = 0;
      int suppg = srcva >= SUPSTART;
      int pgsize = suppg ? SUPPGSIZE : PGSIZE;
      
      while(got_null == 0 && max > 0){
        va0 = suppg ? SUPPGROUNDDOWN(srcva) : PGROUNDDOWN(srcva);
        pa0 = walkaddr(pagetable, va0);
        if(pa0 == 0)
          return -1;
        n = pgsize - (srcva - va0);
        if(n > max)
          n = max;
    
        char *p = (char *) (pa0 + (srcva - va0));
        while(n > 0){
          if(*p == '\0'){
            *dst = '\0';
            got_null = 1;
            break;
          } else {
            *dst = *p;
          }
          --n;
          --max;
          p++;
          dst++;
        }
    
        srcva = va0 + pgsize;
      }
      if(got_null){
        return 0;
      } else {
        return -1;
      }
    }
    

    上述完成,我们其实已经可以在kvmmake(void)时,验证KERNEL在初始化PAGETABLE时,是否使用了巨页优化。

      // PLIC
      kvmmap(kpgtbl, PLIC, PLIC, 0x400000, PTE_R | PTE_W);
    

    我们简单做个KERNEL PAGETABLE的VMPRINT来验证:


    1699201102087.png

    可以看到这里2页,直接用到了巨页优化,因为并没有创建LEVEL 2的叶子节点。并且物理地址是根据2MB对齐。


    1699201149163.png

    4. umalloc.c

    下面为了写一个用户命令来验证malloc可以使用巨页优化,我们需要对sbrk做一些改动。之前malloc最小申请的内存单位是PGSIZE,所以我们可以确保进程实际用的memory size 是连续的,那么sbrk就可以直接返回上次的addr,然后进程继续往后读是没有问题的。但是引入了巨页后,情况不一样了。
    比如我们申请的内存为SUPPGSIZE + 4096,我们实际会分配2个巨页大小。也就是说发生了我们实际申请的size 和系统给的size有GAP,为了不浪费这段多申请的内存,我们需要有个途径返回实际得到的size。
    针对这段测试代码:

    #include "kernel/types.h"
    #include "user/user.h"
    
    
    int main(int argc, char *argv[]) {
        vmprint(1);
        // 让VA 和2MB对齐
        char *long_content = malloc(2076656);
        long_content[0] = 'c';
        printf("content:%c\n", long_content[0]);
        vmprint(1);
        // 分配一整个巨页,-16是因为有sizeof(Header) overhead.
        char *long_content2 = malloc(2097152-16);
        long_content2[0] = 'b';
        printf("content:%c\n", long_content2[0]);
        vmprint(1);
        char *long_content3 = malloc(2097152);
        long_content3[0] = 'a';
        printf("content:%c\n", long_content3[0]);
        vmprint(1);
        char *long_content4 = malloc(4096 - 16);
        long_content4[0] = 'd';
        printf("content:%c\n", long_content4[0]);
        vmprint(1);
        char *long_content5 = malloc(2097152 - 32 - 4096);
        long_content5[0] = 'e';
        printf("content:%c\n", long_content5[0]);
        vmprint(1);
    
        free(long_content5);
        free(long_content4);
        free(long_content3);
        free(long_content2);
        free(long_content);
        
        exit(0);
    }
    

    可以看到优化前, 用了2个大页,之后还要开512普通页。但是内存花的


    1699234164537.png

    优化后,继续利用之前大页没用完的内存,内存空间得到节约:


    1699234250535.png
    下面是优化代码:
    1699234307649.png

    exec.c, sysproc.c

    这块loadseg 也需要enhance一下,巨页逻辑

    static int
    loadseg(pagetable_t pagetable, uint64 va, struct inode *ip, uint offset, uint sz)
    {
      uint i, n;
      uint64 pa;
      int pgsize = PGSIZE;
      for(i = 0; i < sz; i += pgsize){
        pa = walkaddr(pagetable, va + i);
        if (pa >= SUPSTART) pgsize = SUPPGSIZE;
        if(pa == 0)
          panic("loadseg: address should exist");
        if(sz - i < pgsize)
          n = sz - i;
        else
          n = pgsize;
        if(readi(ip, 0, (uint64)pa, offset+i, n) != n)
          return -1;
      }
      
      return 0;
    }
    

    之前pgaccess 加的也要ENHANCE一下巨页逻辑

    answers-pgtbl.txt

    Q: Which other xv6 system call(s) could be made faster using this shared page? Explain how.

    A:
    uptime(): This system call returns the current uptime of the system. Although this value changes constantly, the kernel could update the shared page at fixed time intervals (e.g., every 10 milliseconds).
    how: add timeticks value in usyscall structure, and when clockintr, it assign latest value to this page. and in userspace, when it call uuptime, it just fetch this value from USYSCALL Page like ugetpid().

    Q: For every leaf page in the vmprint output, explain what it logically contains and what its permission bits are. Figure 3.4 in the xv6 book might be helpful, although note that the figure might have a slightly different set of pages than the init process that's being inspected here.

    A:
    for example,
    .. .. .. 0: pte 0x00000000219da81b pa 0x000000008676a000
    pte use last 10 bit to save flag, and 54-10 bit to save pa;
    so pa = (pte >> 10) << 12;
    the last 10 bit is 0000011011, which means V, R, X, U is enabled.

    相关文章

      网友评论

          本文标题:Lec 4,5 - Lab 3 page table

          本文链接:https://www.haomeiwen.com/subject/bixmidtx.html