MIT6.S081_2020_Lab5_xv6LazyPageAllocation

个人博客:https://www.shui2000.top/posts/4fd45b61.html

一、什么是惰性分配(lazy page allocation)?

1.eager allocation

sbrk是xv6提供的系统调用,使得用户应用程序能扩大自己的heap当一个应用程序启动的时候,sbrk指向的是heap的最底端,同时也是stack的最顶端。这个位置通过代表进程的数据结构中的sz字段表示,这里以p->sz表示

当调用sbrk时,它的参数是整数,代表了你想要申请的字节数.

这意味着,当sbrk实际发生或者被调用的时候,内核会分配一些物理内存,并将这些内存映射到用户应用程序的地址空间,然后将内存内容初始化为0,再返回sbrk系统调用。这样,应用程序可以通过多次sbrk系统调用来增加它所需要的内存。类似的,应用程序还可以通过给sbrk传入负数作为参数,来减少或者压缩它的地址空间。

在XV6中,sbrk的实现默认是eager allocation。这表示了,一旦调用了sbrk,内核会立即分配应用程序所需要的物理内存。但是实际上,对于应用程序来说很难预测自己需要多少内存,所以通常来说,应用程序倾向于申请多于自己所需要的内存。这意味着,进程的内存消耗会增加许多,但是有部分内存永远也不会被应用程序所使用到。

2.lazy allocation的核心思想

lazy allocation核心思想非常简单,sbrk系统调基本上不做任何事情,唯一需要做的事情就是提升p->sz,将p->sz增加n,其中n是需要新分配的字节数。但是内核在这个时间点并不会分配任何物理内存。之后在某个时间点,应用程序使用到了新申请的那部分内存,这时会触发page fault,因为我们还没有将新的内存映射到page table。所以,如果我们解析一个大于旧的p->sz,但是又小于新的p->sz(注,也就是旧的p->sz + n)的虚拟地址,我们希望内核能够分配一个内存page,并且重新执行指令。

所以,当我们看到了一个page fault,相应的虚拟地址小于当前p->sz,同时大于stack,那么我们就知道这是一个来自于heap的地址,但是内核还没有分配任何物理内存。所以对于这个page fault的响应也理所当然的直接明了:在page fault handler中,通过kalloc函数分配一个内存page;初始化这个page内容为0;将这个内存page映射到user page table中;最后重新执行指令。比方说,如果是load指令,或者store指令要访问属于当前进程但是还未被分配的内存,在我们映射完新申请的物理内存page之后,重新执行指令应该就能通过了。

二、如何实现lazy allocation?

lazy allocation的核心思想简单易懂,我们根据思想对sbrk函数进行修改即可.注意:sbrk函数不仅用于增加物理内存,也用于减少物理内存

1.先分析sbrk函数原本的功能,再做修改

uint64
sys_sbrk(void)
{
  int addr;
  int n;
  //n为想要增加或减少的字节数
  //addr为p->sz 所指的地址
  if(argint(0, &n) < 0)
    return -1;
  addr = myproc()->sz;
  if(growproc(n) < 0)
    return -1;
  return addr;
}
​
// Grow or shrink user memory by n bytes.
// Return 0 on success, -1 on failure.
int
growproc(int n)
{
  uint sz;
  struct proc *p = myproc();
​
  sz = p->sz;
  if(n > 0){
    //立马分配n个字节给进程(eager allocation)
    if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
      return -1;
    }
  } else if(n < 0){
    //减少字节数
    sz = uvmdealloc(p->pagetable, sz, sz + n);
  }
  p->sz = sz;
  return 0;
}

可以看出来sbrk函数原本根据n的正负值来立刻分配或立刻减少n个字节给进程

根据lazy allcation的思想,我们在进程要求新增物理内存大小的时候只增加p->sz的值,如何操作看下一节,我们还需要思考,lazy allocation的思想是否可以用于减少物理内存的大小?

答案是不可以:因为我们是通过page fault会进入usertrap函数这一机制,在进程实际需要使用到内存的时候为其分配物理内存。

倘若n值为复数,我们仅仅将p-sz的值减少而不去释放物理内存,那么当进程访问本不应该存在的物理地址时,不会发生page fault

进而程序使用了本不应该使用的物理内存,而操作系统却没有报错。所以我们对n值为负数时,立刻释放物理内存的操作不做修改。

修改后的sbrk函数如下(为了避免对growproc函数的修改,将n值正负如何进行处理的代码选择放在了sbrk函数中):

uint64
sys_sbrk(void)
{
  int addr;
  int n;
​
  if(argint(0, &n) < 0)
    return -1;
​
  struct proc* p = myproc();
  addr = p->sz;
  uint64 sz = p->sz;
​
  if(n > 0) {
    // lazy allocation 仅增加p->sz的大小,并不实际分配物理内存
    p->sz += n;
  } else if(sz + n > 0) {
    // 减少n字节
    sz = uvmdealloc(p->pagetable, sz, sz + n);
    p->sz = sz;
  } else {
    // 减少n字节,p-sz 大小小于0,说明减少的字节太多,发生错误。
    return -1;
  }
  return addr;
​
}

2.在usertrap中增加对page fault的处理

2.1 trap流程与systemcall调用流程的区别

这里的 trap流程与systemcall类似,不同于systemcall的是,在进入内核态后,r_scause()值不同,原本的

xv6系统,虽然scause会根据不同的trap赋予不同的值,但实际上的usertrap函数中只有处理scause值为8(系统调用)的代码,我们需要在usertrap函数中自行添加对page fault的处理。

我们可以根据图片看出来xv6系统会根据不同的page fault生成三种不同的数值:12(因为指令执行引起的page fault)、13(因为load--读操作引起的page fault)、15(是因为store--写操作引起的page fault)

由于本身的xv6系统不存在实际上的scause为其他值的情况,我们可以认为所有scause值为其他值的情况都是由于我们的修改引起的。我们可以用print函数将scause值打印出来调试错误,实际上后续的实验过程中,由于lazy allocation引起的scause只有13或15两种值。

//
// handle an interrupt, exception, or system call from user space.
// called from trampoline.S
//
void
usertrap(void)
{
  ....
  ....
  // system call    
  if(r_scause() == 8){
    
    ...  
      
  } //page fault
    else if(r_scause() == 15||r_scause() == 13){
    ...
    ...
  } 
    ...
    ...
  usertrapret();
}
2.2 发生page fault的虚拟地址范围

这样我们在usertrap函数中可以通过r_stval得知发生page fault的虚拟地址

xv6中虚拟地址的范围在0-MAXVA之间,所以超出这个范围的sepc值属于严重的错误,我们应该将进程杀死,而不是对待page fault的方式对其进行处理。lazy allocation政策下发生page fault的情况是错以为有内存的情况下进行读写操作,所以发生page fault的虚拟地址应该小于进程认为自己拥有的字节大小(p->sz

其次,目前的lazy allocation仅在需要增加字节的时候使用,所以发生page fault的虚拟地址应该大于栈指针p->trapframe->sp(xv6最初的进程为栈分配了PGSIZE的值,后续的进程都是初始进程fork出来的,意味着所有进程都具有PGSIZE大小的栈,那么我们lazy allocation的地址一定是大于栈指针的)

所以,综合考虑下我们检测发生page fault的范围应该是p->trapframe->sp - p->sz

2.3 分配物理内存并进行映射

在判断发生虚拟地址的正确性后,我们需要做的就是为其分配物理地址并映射,应该注意的是当物理内存不足后,我们直接杀死了进程。其余更高级的系统会采用更加灵活的方式:

最终的代码应该是

//kernel/trap.c/usertrap函数中
​
else if(r_scause() == 15||r_scause() == 13){//page fault
    // printf("it is page fault\n");
    // printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
    // printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
    //stval寄存器中保存了造成页面错误的虚拟地址
    uint64 va=r_stval();
    //如果某个进程在高于sbrk()分配的任何虚拟内存地址上出现页错误,则终止该进程。
    //page guard
    if(PGROUNDUP(p->trapframe->sp) - 1 >= va ){
        p->killed=1;
    }else if(va>p->sz){
      p->killed=1;
    }else{
      uint64* ka=kalloc();
      if(ka==0){
        //printf("out of memory\n");
        p->killed=1;
      }else{
          //填空垃圾数据
          memset((void *)ka, 0, PGSIZE);
          if(mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)ka, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
            kfree((void *)ka);
            p->killed=1;
          }
        }
      }  
  }
  • 28
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值