6.S081-6缺页异常 - lazy allocation - Page Fault

6.S081-6缺页异常Page Fault

这一节课,可以帮我们完成2个实验:

题目要求链接:Lab: xv6 lazy page allocation
对应做法链接:6.S081 Lab4 Lazy allocation

题目要求链接:Lab: Copy-on-Write Fork for xv6
对应做法链接:

1. page fault都需要什么(处理page fault都需要哪些输入信息)

  • (内存)虚拟地址。引发page fault的虚拟地址 (存储在STVAL寄存器)
  • 原因(类型)。对不同场景的page fault 有不同的相应。(比如,到底是由于load 还是 store 或者 jump指令出发的)
  • 指令地址(PC)。触发page fault的指令的地址。从上节课可以知道,作为trap处理代码的一部分,这个地址存放在SEPC(Supervisor Exception Program Counter)寄存器中,并同时会保存在trapframe->epc中。 - 因为能够恢复因为page fault中断的指令运行是很重要的。所以需要保存出错的PC。

page fault和其他的异常使用与系统调用相同的trap机制来从用户空间切换到内核空间。如果是因为page fault触发的trap机制并且进入到内核空间,STVAL寄存器和SCAUSE寄存器都会有相应的值。

2. 内存分配allocation

(0) sbrk

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

如果看不到下图可以看图片链接

操作系统内存分配示意图

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

用户程序倾向于申请多于自己需求的内存数量,sbrk的内存申请是eager allocation(一旦调用,就会去申请空间,让用户的heap变大)——但是很多内存可能永远都用不上。

(1) Lazy page allocation

使用lazy allocation——思想:sbrk的时候,do nothing,只是p->sz会增加相应的page数量,但是并没有真正的分配内存。之后的某个时间段,用户如果用到了那部分并没有被真正分配的内存,就会触发page fault,因为这个时候还没有把新的内存映射到page table。——如果我们发现,我们调用的page地址并不存在(page fault),并且这个地址小于p->sz(因为p->sz是目前真正拥有地址 + n ,即理应拥有的page 数量),这个时候我们希望OS可以为我们分配一个内存page,并重新开始执行指令。

过程:

if (page fault) {
	if (stack < page fault vm_address < p -> sz) {
		page fault handler {
			kalloc 1 page and initialize it to 0;
			map this page to user page table;
			return to the page fault address;
		}
	}
}

<1> 修改sys_brk() —— 触发page fault

原始代码👇:

uint64
sys_sbrk(void)
{
  int addr;
  int n;

  if(argint(0, &n) < 0)
    return -1;
  addr = myproc()->sz;
  if(growproc(n) < 0)
    return -1;
  return addr;
}

修改函数,让她不分配内存,只是将p->sz增加n👇

uint64
sys_sbrk(void)
{
  int addr;
  int n;

  if(argint(0, &n) < 0)
    return -1;
  addr = myproc()->sz;
  myproc -> sz = myporc() -> sz + n;
  // if(growproc(n) < 0)
  // return -1;
  return addr;
}

shell 执行echo hi,得到page fault👇(熟悉的panic)

$echo hi
usertrap(): unexpected scause 0x000000000000000f pid = 3
			sepc=0x00000000000012a4 stval=0x0000000000004008
panic: uvmunmap: not mapped
  • SCAUSE寄存器 = 15,说明这是一个store page fault

  • pid = 3,这个应该是shell的pid

  • sepc寄存器的值 : 0x12a4

  • 出错的虚拟内存地址(stval) 0x4008

page fault是出现在malloc的实现代码中。这也非常合理,在malloc的实现中,我们使用sbrk系统调用来获得一些内存,之后会初始化我们刚刚获取到的内存,在0x12a4位置,刚刚获取的内存中写入数据,但是实际上我们在向未被分配的内存写入数据。

<2> Lazy allocation 的实现:如何处理page fault

首先打开usertrap函数👇(由于我20年曾经做过这个实验,所以可能和原始代码有出入,即else 里面的注释部分):

//
// handle an interrupt, exception, or system call from user space.
// called from trampoline.S
//
void
usertrap(void)
{
  int which_dev = 0;

  if((r_sstatus() & SSTATUS_SPP) != 0)
    panic("usertrap: not from user mode");

  // send interrupts and exceptions to kerneltrap(),
  // since we're now in the kernel.
  w_stvec((uint64)kernelvec);

  struct proc *p = myproc();
  
  // save user program counter.
  p->tf->epc = r_sepc();
  
  if(r_scause() == 8){
    // system call

    if(p->killed)
      exit(-1);

    // sepc points to the ecall instruction,
    // but we want to return to the next instruction.
    p->tf->epc += 4;

    // an interrupt will change sstatus &c registers,
    // so don't enable until done with those registers.
    intr_on();

    syscall();
  } else if((which_dev = devintr()) != 0){
    // ok
  } else {
    printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
    printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
    // printf("page down:%d\n",PGROUNDDOWN(r_stval()));
    // printf("r :%d\n",r_scause());
    // int sz = 0;
    // while( !(r_stval()>=sz && r_stval()<sz+4096) )
    // {
    //   sz = sz + 4096;
    // }
    // printf("sz:%d\n",sz);
    p->killed = 1;
  }

  if(p->killed)
    exit(-1);

  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2)
    yield();

  usertrapret();
}

6.S081 附加Lab1 用户执行系统调用的过程(Trap)中已经提到,r_scause() == 8是系统调用,所以这里增加一个trap的处理即r_scause() == 15:此时我们需要一些定制化的处理。

什么处理?

检查p->sz是否大于STVAL,如果大于就分配物理内存。

  else if(r_scause() == 15)
  {
    uint64 va = r_stval();
    printf("page fault %p\n", va);
    uint64 ka = (uint64)kalloc();
    if (ka == 0) {
      p -> killed  = 1;
    } else {
      memset((void *)ka, 0, PGSIZE);
      va = PGROUNDDOWN(va);
      if (mappages(p -> pagetable, va, PGSIZE, ka, PTE_W | PTE_U | PTE_R) != 0) {
        kfree((void *)ka);
        p -> killed = 1;
      }
    }
  }

增加上面的代码,会分配一个物理page,如果ka == 0,说明没有足够的物理内存,需要kill当前进程。如果有物理内存,首先会将内存内容设置为0,之后将物理内存page指向用户地址空间中合适的虚拟内存地址。具体来说,我们首先将虚拟地址向下取整,这里引起page fault的虚拟地址是0x4008,向下取整之后是0x4000。之后我们将物理内存地址跟取整之后的虚拟内存地址的关系加到page table中。对应的PTE需要设置常用的权限标志位,在这里是u,w,r bit位。

接下来继续echo hi,发现报错信息反而变多了👇。 —— unmap的是之前lazy allocated,但是又还没有用到的地址。——因为都是调用sbrk实现的所以对于这个内存,并没有对应的物理内存。所以在uvmunmap函数中,当PTE的v标志位为0并且没有对应的mapping,这并不是一个实际的panic,这是我们预期的行为。

$ echo hi
page fault 0x0000000000004008
page fault 0x0000000000013f48
panic: uvmunmap: not mapped
// Remove mappings from a page table. The mappings in
// the given range must exist. Optionally free the
// physical memory.
void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 size, int do_free)
{
  uint64 a, last;
  pte_t *pte;
  uint64 pa;

  a = PGROUNDDOWN(va);
  last = PGROUNDDOWN(va + size - 1);
  for(;;){
    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");
    if(do_free){
      pa = PTE2PA(*pte);
      kfree((void*)pa);
    }
    *pte = 0;
  }
}

找到👇(此时不再是panic,而是do nothing)

if((*pte & PTE_V) == 0)
   panic("uvmunmap: not mapped");      

可以修改为

if((*pte & PTE_V) == 0)
  continue;

接下来,我们再重新编译XV6,并执行“echo hi”。——仍然有page fault,但是我们成功执行了。

$ echo hi
page fault 0x0000000000004008
page fault 0x0000000000013f48
hi

——这就是最基本的lazy allocation的实现。

3. Zero Fill On Demand

(0) 思想 (和大致做法流程)

这是一个很类似于lazy allocation的思想。思想:很多内存都被设置为0,那么我们有很多全0的page table——实际上,我们并不需要分配那么多全0的pagtable,只需要一个就可以了(但是不允许对这个page 进行写操作)。只有当对这个页执行写入(store)指令的时候,进行一定的操作就行了。——当执行store的时候,首先触发page fault,然后申请一个全0的pagetable,并且将它的写flag置为1(允许写)。

(1) 好处 + 坏处

<1>好处 (节省内存,初始化变快)

  1. 节省内存;
  2. exec的内容减少了,程序可以启动的更快,因为只需要分配一个全0的pagetable就可以。(当然了write(update)操作会变慢)

<2>坏处 (trap有代价)

  1. 每次更新的时候,都需要page fault(也就是每次都涉及user mode和kernel mode的切换,user和kernel页表的切换等)

    ——其实就是trap的代价。

4. Copy On Write Fork

同样类似于Lazy allocation & Zero fill on demand

(1)背景:

当shell执行指令的时候会fork,shell子进程的第一件事就是调用exec,执行我们想要执行的命令。如果fork创建了shell地址空间的完整copy,而exec的第一件事就是丢弃这个空间,这样太浪费了 (前面的课程中讲过,通常来说exec系统调用不会返回,因为exec会完全替换当前进程的内存,相当于当前进程不复存在了,所以exec系统调用已经没有地方能返回了。)。

(2) 思想:(具体流程)

  1. fork之后,我们直接让父进程的物理内存page被共享 —— 让子进程的PTE指向父进程对应的物理内存page。——为了保证隔离性,可以将父、子进程的PTE标志位都设置成只读。

  2. 当我们需要更改内存的时候,就会触发page fault (因为现在正在向一个只读PTE的进行写数据的操作)。

  3. page fault后的处理:

    1. page fault之后重新分配一个物理内存page
    2. 然后copy page fault对应的物理内存的内容到新建的page
    3. 并将新分配的page映射到子进程
    4. 注意:这时候新分配的子进程的page和之前父进程的page对应的PTE都设置成可读可写

(3) 实验链接

6.S081 Lab5 Copy-on-Write Fork for xv6

5. Demand paging

这里的意思是参考前面的lazy的思想,直接OS会加载的text,data区域,以lazy的方式映射到pagetable(如果为被用到,就不去真正分配)。

如果内存不够用了,怎样决定哪个page被换出?

LRU;

是选择dirty-page (一个page只要最近被写过,那么就会是dirty的) 还是 no-dirty-page 被撤回?

选择的是no-dirty-page —— 没被写过,直接去掉map就行,如果被写过,就必须先map回去,写入,然后再删除这个map

6. Memory Mapped Files (mmap)

—— 将一个文件映射到page上,这样可以使用load / store操作这些数据。

mmap将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零。mmap在用户空间映射调用系统中作用很大。

mmap (va, len, protection, …)

这里也是用lazy的思想。

学生提问:有没有可能多个进程将同一个文件映射到内存,然后会有同步的问题?

Frans教授:好问题。这个问题其实等价于,多个进程同时通过read/write系统调用读写一个文件会怎么样?
这里的行为是不可预知的。write系统调用会以某种顺序出现,如果两个进程向一个文件的block写数据,要么第一个进程的write能生效,要么第二个进程的write能生效,只能是两者之一生效。在这里其实也是一样的,所以我们并不需要考虑冲突的问题。
的大小之和,最后一个页不被使用的空间将会清零。mmap在用户空间映射调用系统中作用很大。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值