xv6 Lab: page tables - Use superpages(巨页)

题目分析

修改xv6内核使能巨页,需要实现巨页物理内存的分配与释放,以及巨页虚拟内存的管理。结合xv6已给提示,从sbrk入手,进行分析。

由上图,我们首先需要为巨页实现物理内存的分配(super_alloc()/super_free())以及虚拟地址映射(super_walk)。

物理内存分配/释放

完全可以仿照xv6的链式管理方式。

struct super_run
{
  struct super_run *next;
};

struct {
  struct spinlock lock;
  struct super_run *freelist;
} skmem;

void
kinit()
{
  initlock(&kmem.lock, "kmem");
  initlock(&skmem.lock, "skem");
  freerange(end, (void*)PHYSTOP);
}

void
freerange(void *pa_start, void *pa_end)
{
  char *p;
  p = (char*)PGROUNDUP((uint64)pa_start);
  for(; p + PGSIZE <= (char*)pa_end - 12 * 1024 * 1024; p += PGSIZE) //留5个巨页
    kfree(p);
  
  p = (char*)SUPERPGROUNDUP((uint64)p);
  for (; p + SUPERPGSIZE <= (char *)pa_end; p += SUPERPGSIZE) {
    superfree(p);
  }
}

void *
superalloc(void)
{
  struct super_run *r;

  acquire(&skmem.lock);
  r = skmem.freelist;
  if (r) 
    skmem.freelist = r->next;
  release(&skmem.lock);

  if (r)
    memset((void *)r, 5, SUPERPGSIZE);

  return (void *)r;
}

虚拟内存管理

内存分配

添加PTE_S(页表项巨页标识位)

添加在riscv.h中

#define PTE_S (1L << 8) // 是否是巨页

修改uvmalloc()

uint64
uvmalloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz, int xperm)
{
  char *mem;
  uint64 a;
  int sz;
  uint64 soldsz = 0;
  if(newsz < oldsz)
    return oldsz;

  oldsz = PGROUNDUP(oldsz);

  if (newsz - oldsz < SUPERPGSIZE) {
    oldsz = PGROUNDUP(oldsz);
    for(a = oldsz; a < newsz; a += sz){
      sz = PGSIZE;
      mem = kalloc();
      if(mem == 0){
        uvmdealloc(pagetable, a, oldsz);
        return 0;
      }
  #ifndef LAB_SYSCALL
      memset(mem, 0, sz);
  #endif
      if(mappages(pagetable, a, sz, (uint64)mem, PTE_R|PTE_U|xperm) != 0){
        kfree(mem);
        uvmdealloc(pagetable, a, oldsz);
        return 0;
      }
    }
  } else {
    soldsz = SUPERPGROUNDUP(oldsz);

    for(a = oldsz; a < soldsz; a += sz){ /* 分配并映射空页 */
      sz = PGSIZE;
      mem = kalloc();
      if(mem == 0){
        uvmdealloc(pagetable, a, oldsz);
        return 0;
      }
  #ifndef LAB_SYSCALL
      memset(mem, 0, sz);
  #endif
      if(mappages(pagetable, a, sz, (uint64)mem, PTE_R|PTE_U|xperm) != 0){
        kfree(mem);
        uvmdealloc(pagetable, a, oldsz);
        return 0;
      }
    }

    for (a = soldsz; a < newsz; a+= sz) {
      sz = SUPERPGSIZE;
      mem = superalloc();
      if (mem == 0) {
        return 0;
      }
      if (mappages(pagetable, a, sz, (uint64)mem, PTE_R | PTE_U | PTE_S | xperm) != 0) {
        superfree(mem);
        return 0;
      }
    }
  }
 
  return newsz;
}

当需要分配的内存大于2M时,分配巨页。
注意:
1. 分配巨页前,一定要按SUPERPGSIZE对齐。若按PGSIZE对齐,会出现巨页覆盖先前正常页的情况(后文分析)。
2. 向上SUPERPGSIZE对齐后,在巨页首地址和对齐前的地址之间,会有很多未分配和映射的正常页,在释放内存遍历到这些空页时,会报错,所以需要将这些空页分配物理内存并映射。

修改mappages()

由于巨页使用level-1页表作为叶子页表,而原walk函数使用level-0页表作叶子页表,所以需要新的walk函数(super_walk())。应该是可以通过扩展walk()的第三个参数的值来实现两种walk()的复用,时间关系我也没再修改了。

int
mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
{
  uint64 a, last;
  pte_t *pte;

  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;

  if ((perm & PTE_S) == 0) {  /*不使用巨页*/
    last = va + size - PGSIZE;
    for(;;){
      if((pte = walk(pagetable, a, 1)) == 0)
        return -1;
      if(*pte & PTE_V)
        panic("mappages: remap");
      *pte = PA2PTE(pa) | perm | PTE_V;
      if(a == last)
        break;
      a += PGSIZE;
      pa += PGSIZE;
    }
  } else {   /* 使用巨页 */
    last = va + size - SUPERPGSIZE;
    for (;;) {
      if ((pte = super_walk(pagetable, a, 1)) == 0)
        return -1;
      if (*pte & PTE_V)
        panic("super mappages: remap");
      *pte = PA2PTE(pa) | perm | PTE_V;
      if (a == last) 
        break;
      a += SUPERPGSIZE;
      pa += SUPERPGSIZE;
    }
  }

  return 0;
}

新增super_walk()

依然是直接仿照xv6原有walk(),由于只循环一次,删掉了walk原有的for循环。

pte_t *
super_walk(pagetable_t pagetable, uint64 va, int alloc)
{
  if (va > MAXVA)
    panic("walk");
  
  pte_t *pte = &(pagetable[PX(2, va)]);
  if (*pte & PTE_V) {
    pagetable = (pagetable_t)PTE2PA(*pte);
  } else {
    if (!alloc || (pagetable = (pde_t*)kalloc()) == 0)
      return 0;
    memset(pagetable, 0, PGSIZE);
    *pte = PA2PTE(pagetable) | PTE_V;
  }

  return &pagetable[PX(1, va)];
}

修改walk()

在代码的其他地方,会使用walk()进行寻址,而walk()本身是无法满足巨页需要的两级页表寻址的,所以需要修改。
其实也很简单,在遍历level-1页表时,遇到PTE_S直接return就行。

pte_t *
walk(pagetable_t pagetable, uint64 va, int alloc)
{
  if(va >= MAXVA)
    panic("walk");

  for(int level = 2; level > 0; level--) {
    pte_t *pte = &pagetable[PX(level, va)];
    if(*pte & PTE_V) {
      pagetable = (pagetable_t)PTE2PA(*pte);
#ifdef LAB_PGTBL
      if (*pte & PTE_S) {
        return pte;
      }
#endif
    } else {
      if(!alloc || (pagetable = (pde_t*)kalloc()) == 0)
        return 0;
      memset(pagetable, 0, PGSIZE);
      *pte = PA2PTE(pagetable) | PTE_V;
    }
  }
  return &pagetable[PX(0, va)];
}

修改uvmcopy()

在fork的时候,子进程需要从父进程复制所有内存,原有uvmcopy()无法满足巨页的复制要求,所以需要修改。

int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
  pte_t *pte;
  uint64 pa, i;
  uint flags;
  char *mem;
  int szinc;

  for(i = 0; i < sz; i += szinc){
    szinc = 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);

    if ((flags & PTE_S) == 0) {
      if((mem = kalloc()) == 0)
        goto err;
      memmove(mem, (char*)pa, PGSIZE);
      if(mappages(new, i, PGSIZE, (uint64)mem, flags) != 0){
        kfree(mem);
        goto err;
      }
    } else {
      if ((mem = superalloc()) == 0)
        goto err;
      if (mappages(new, i, SUPERPGSIZE, (uint64)mem, flags) != 0) {
        superfree(mem);
        goto err;
      }
      memmove(mem, (char*)pa, SUPERPGSIZE);
      i += SUPERPGSIZE - szinc; /* 跳过巨页空间 */
    }
  }
  return 0;

 err:
  uvmunmap(new, 0, i / PGSIZE, 1);
  return -1;
}

改动不大,就是按正常页遍历并copy的过程中,当walk返回巨页的页表项时,直接copy后面一个巨页的内存,并将这段虚拟地址跳过。

行文至此,巨页的物理内存分配、虚拟内存映射、fork()内存复制均以实现。接下来还需实现巨页的释放。

内存释放

只需修改uvmunmap(),和uvmcopy的改动相似,也是只需在正常页遍历并free的过程中,当walk返回巨页的页表项时,直接free后面一个巨页的内存,并将这段虚拟地址跳过。

void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
  uint64 a;
  pte_t *pte;
  int sz;

  if((va % PGSIZE) != 0)
    panic("uvmunmap: not aligned");
  for(a = va; a < va + npages*PGSIZE; a += sz){
    sz = PGSIZE;
    if((pte = walk(pagetable, a, 0)) == 0)
      panic("uvmunmap: walk");
    if((*pte & PTE_V) == 0) {
      printf("va=%ld pte=%ld\n", a, *pte);
      panic("uvmunmap: not mapped");
    }
    if(PTE_FLAGS(*pte) == PTE_V)
      panic("uvmunmap: not a leaf");
    if ((*pte & PTE_S)) { /* 释放巨页 */
      a += SUPERPGSIZE - sz;
    } 
    if(do_free){
      uint64 pa = PTE2PA(*pte);
      kfree((void*)pa);
    }
    *pte = 0;
  }
}

总结

代码本身不难,难的是厘清xv6的物理内存链式管理方式,以及基于三级页表的虚拟内存管理方式。
本实验从sbrk()入手,可谓是虽管中窥豹,但也可见一斑

本log行文简略,我也并无太多时间精心编辑,只求作个记录,各位看客见谅。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值