全网最最实用--基于Mac ARM 芯片实现操作系统MIT 6.S081-lab5

实验五 惰性页面分配

一、代码理解

在开始编码之前,请阅读xv6 书的第 4 章(特别是 4.6)以及可能会修改的相关文件:

内核/trap.c
内核/vm.c
内核/sysproc.c

1.sys_sbrk()进程页面分配

sysproc.c中的sys_sbrk 函数是 sbrk 系统调用的实现,用于增加或减少进程的数据段(堆)的大小。这个系统调用通常用于动态内存分配,允许程序在运行时调整其内存占用。
以下是 sys_sbrk 函数的详细步骤:

  1. 参数获取

    if(argint(0, &n) < 0)
      return -1;
    

    这段代码从用户空间获取第一个参数 n,它表示要增加(正值)或减少(负值)的内存大小。argint 是一个辅助函数,用于从用户空间读取整数参数。如果参数获取失败,函数返回 -1

  2. 获取当前进程的内存大小

    addr = myproc()->sz;
    

    这里,myproc() 返回当前正在执行的进程的 struct proc 结构体指针。sz 是该结构体中的一个字段,表示进程的当前内存大小(即数据段的末尾地址)。addr 被赋值为当前进程的内存大小。

  3. 调整内存大小

    if(growproc(n) < 0)
      return -1;
    

    growproc 是一个内核函数,用于实际调整进程的内存大小。它根据参数 n 的值增加或减少进程的内存。如果 growproc 返回负值,表示内存调整失败,函数返回 -1

  4. 返回旧的内存末尾地址

    return addr;
    

    如果内存调整成功,函数返回调整前的内存末尾地址 addr。这个值是用户程序调用 sbrk 之前的堆的末尾地址,用户程序可以使用这个地址来管理新分配的内存。

2.usertrap() 用户入内核态

usertrap 函数是在 RISC-V 架构的操作系统中处理从用户空间进入内核的中断、异常或系统调用的核心函数。这个函数通常在用户程序执行系统调用、发生异常或硬件中断时被调用。

  1. 检查模式

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

    这段代码检查当前的中断是否确实来自用户模式。SSTATUS_SPPsstatus 寄存器中的一个位,表示上一个模式(用户模式或监管者模式)。如果不是从用户模式进入的,则触发 panic,表示这是一个错误情况。

  2. 设置陷阱向量

    w_stvec((uint64)kernelvec);
    

    将陷阱处理向量设置为 kernelvec,这样后续的中断和异常将由内核的陷阱处理程序处理。

  3. 获取当前进程

    struct proc *p = myproc();
    

    获取当前正在执行的进程的 proc 结构体指针。

  4. 保存用户程序计数器

    p->trapframe->epc = r_sepc();
    

    保存异常程序计数器(sepc)到进程的陷阱帧中,以便在返回用户空间时恢复程序执行位置。

  5. 处理系统调用

    if(r_scause() == 8){
      // system call
      if(p->killed)
        exit(-1);
      p->trapframe->epc += 4;
      intr_on();
      syscall();
    }
    

    如果 scause 寄存器的值为 8,表示这是一个系统调用。首先检查进程是否已被标记为 killed,如果是,则退出。然后,增加 epc 以跳过导致系统调用的 ecall 指令,确保返回时执行下一条指令。接着,启用中断并调用 syscall() 函数来处理系统调用。

  6. 处理设备中断

    else if((which_dev = devintr()) != 0){
      // ok
    }
    

    调用 devintr() 函数来处理设备中断。如果 devintr() 返回非零值,表示成功处理了设备中断。

  7. 处理意外情况

    else {
      printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
      printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
      p->killed = 1;
    }
    

    如果 scause 不是系统调用也不是已知的设备中断,打印错误信息并将进程标记为 killed。

  8. 检查进程状态

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

    如果进程已被标记为 killed,则退出进程。

  9. 处理定时器中断

    if(which_dev == 2)
      yield();
    

    如果中断是由定时器引起的(which_dev == 2),则调用 yield() 函数让出 CPU。

  10. 返回用户空间

    usertrapret();
    

    调用 usertrapret() 函数准备返回用户空间,恢复用户模式下的执行状态。

二、Eliminate allocation from sbrk()

1.题目描述

您的首要任务是从 sbrk(n) 系统调用实现中删除页面分配,即 sysproc.c 中的 sys_sbrk() 函数。sbrk(n) 系统调用将进程的内存大小增加 n 个字节,然后返回新分配区域的起始位置(即旧大小)。
您的新 sbrk(n) 应该只是将进程的大小(myproc()->sz)增加 n 并返回旧大小。它不应该分配内存 - 因此您应该删除对 growproc() 的调用(但您仍然需要增加进程的大小!)。

2.解答

通过对上文的sys_sbrk 函数分析不难看出,我们只需要将分配物理内存那一条改成指针指向修改。

// kernel/sysproc.c
uint64
sys_sbrk(void)
{
  int addr;
  int n;
  struct proc *p = myproc();
  acquire(&p->lock);  // 确保对进程大小的修改是原子的
  if(argint(0, &n) < 0)
    return -1;
  addr = p->sz;
  if(n < 0) {
    uvmdealloc(p->pagetable, p->sz, p->sz+n); // 缩小就马上释放
  }
  p->sz += n; // 懒分配
  return addr;
}

三、Lazy allocation

1.题目描述

修改 trap.c 中的代码,通过在错误地址映射新分配的物理内存页面,然后返回用户空间以让进程继续执行,来响应来自用户空间的页面错误。
您应该在产生“usertrap(): ...”消息的printf调用之前添加代码。修改您需要的任何其他 xv6 内核代码,以使echo hi正常工作。

2.解答

四、Lazytests and Usertests

1.中断处理

usertrap 函数负责处理从用户空间进入内核的各种情况,包括系统调用、设备中断和异常,确保正确地处理这些事件并安全地返回到用户空间。修改 usertrap 用户态陷阱处理函数,为缺页异常添加检测。
usertrap 添加条件分支

uint64 va = r_stval();
    if((r_scause() == 13 || r_scause() == 15) && uvmshouldtouch(va)){ // 缺页异常,并且发生异常的地址进行过懒分配
      uvmlazytouch(va); // 分配物理内存,并在页表创建映射
2.页表处理

uvmlazytouch 函数用于确保一个虚拟地址(va)对应的页面被实际分配并映射到物理内存中。

// 触摸一个惰性分配的页面,使其映射到一个实际的物理页面。
void uvmlazytouch(uint64 va) {
  struct proc *p = myproc();
  char *mem = kalloc();
  if (mem == 0) {
    printf("lazy alloc: 内存分配失败,虚拟地址 %p\n", va);
    p->killed = 1;
    return; // 出现错误时提前退出
  } else {
    memset(mem, 0, PGSIZE);
    int map_result = mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U);
    if (map_result != 0) {
      printf("lazy alloc: 页面映射失败,虚拟地址 %p, 错误码 %d\n", va, map_result);
      kfree(mem);  // 映射失败时释放已分配的内存
      p->killed = 1;
      return; // 出现错误时提前退出
    }
  }
  // 如需调试信息可以取消下行注释:
  // printf("lazy alloc: 成功分配并映射虚拟地址 %p\n", PGROUNDDOWN(va));
}

uvmshouldtouch 函数用于判断给定的虚拟地址(va)是否属于那些已经被惰性分配了但还未被实际触及(即分配物理内存并映射)的页面。

// 判断是否一个页面之前已经惰性分配但尚未触摸使用。
int uvmshouldtouch(uint64 va) {
  struct proc *p = myproc();

  if (va >= p->sz) {
    // 地址超出进程的内存大小范围
    return 0;
  }

  if (PGROUNDDOWN(va) == r_sp()) {
    // 地址是栈保护页
    return 0;
  }

  pte_t *pte = walk(p->pagetable, va, 0);
  if (pte == 0 || (*pte & PTE_V) == 0) {
    // 页表项不存在或不有效
    return 1;
  }

  return 0; // 页面存在且有效
}

1.题目描述

我们为您提供了lazytests,这是一个 xv6 用户程序,用于测试可能给您的惰性内存分配器带来压力的一些特定情况。修改您的内核代码,以便惰性测试 和用户测试均能通过。

处理负的 sbrk() 参数。
如果某个进程在高于使用 sbrk() 分配的虚拟内存地址上发生页面错误,则终止该进程。
正确处理 fork() 中父进程到子进程的内存复制。
处理进程将有效地址从 sbrk() 传递给系统调用(例如 read 或 write),但该地址的内存尚未分配的情况。
正确处理内存不足:如果页面错误处理程序中的 kalloc() 失败,则终止当前进程。
处理用户堆栈下无效页面的故障。
如果你的内核通过了惰性测试和用户测试,那么你的解决方案就是可以接受的

2.解答

1. 处理负的 sbrk() 参数

sys_sbrk() 系统调用中,检查 n 参数是否为负数。如果是负数,则需要减少进程的大小,并确保不会减少到低于初始堆栈位置。

uint64
sys_sbrk(void)
{
  int n;
  uint64 addr;
  struct proc *p = myproc();

  if(argint(0, &n) < 0)
    return -1;

  addr = p->sz;
  if(n < 0) {
    if(addr + n < 0) {
      return -1; // 防止减少到负地址
    }
    p->sz = addr + n;
  } else {
    p->sz = addr + n;
  }
  return addr;
}
2. 处理高于 sbrk() 分配地址的页面错误

在页面错误处理程序中,检查错误地址是否在进程的合法内存范围内。如果不是,则终止进程。

void
trap(struct trapframe *tf)
{
  if(tf->trapno == T_PGFLT) {
    uint64 va = r_stval();
    struct proc *p = myproc();

    if(va >= p->sz || va < PGROUNDDOWN(p->trapframe->sp)) {
      p->killed = 1;
      return;
    }

    if(uvmshouldtouch(va)) {
      uvmlazytouch(va);
    }
  }
}
3. 正确处理 fork() 中的内存复制

fork() 系统调用中,确保父进程的内存正确复制到子进程。

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

  for(i = 0; i < sz; i += PGSIZE){
    if((pte = walk(old, i, 0)) == 0)
      continue; // 如果一个页不存在,则认为是懒加载的页,忽略即可
    if((*pte & PTE_V) == 0)
      continue; // 如果一个页不存在,则认为是懒加载的页,忽略即可
    pa = PTE2PA(*pte);
    flags = PTE_FLAGS(*pte);
    if((mem = kalloc()) == 0)
      goto err;
    memmove(mem, (char*)pa, PGSIZE);
    if(mappages(new, i, PGSIZE, (uint64)mem, flags) != 0){
      kfree(mem);
      goto err;
    }
  }
  return 0;

 err:
  uvmunmap(new, 0, i / PGSIZE, 1);
  return -1;
}
4. 处理系统调用中的未分配内存

在内核/用户之间互相拷贝数据处理程序中,如 copyout copyout,检查传递的地址是否已经分配,在每个函数加上判断条件分支。


  if(uvmshouldtouch(dstva))
    uvmlazytouch(dstva);
5. 处理内存不足

在页面错误处理程序中,如果 kalloc() 失败,则终止当前进程。

void
uvmlazytouch(uint64 va)
{
  struct proc *p = myproc();
  char *mem = kalloc();
  if (mem == 0) {
    printf("lazy alloc: out of memory when allocating for va %p\n", va);
    p->killed = 1;
    return;
  }
  memset(mem, 0, PGSIZE);
  if(mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0) {
    printf("lazy alloc: failed to map page for va %p\n", va);
    kfree(mem);
    p->killed = 1;
  }
}
6. 处理用户堆栈下无效页面的故障

在页面错误处理程序中,检查错误地址是否在用户堆栈保护页之下。

void
trap(struct trapframe *tf)
{
  if(tf->trapno == T_PGFLT) {
    uint64 va = r_stval();
    struct proc *p = myproc();

    if(va >= p->sz || va < PGROUNDDOWN(p->trapframe->sp)) {
      p->killed = 1;
      return;
    }

    if(uvmshouldtouch(va)) {
      uvmlazytouch(va);
    }
  }
}

五、心得体会

好饿!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值