当前位置: 首页 > news >正文

(undone) xv6-labs-2020 补充 LAB lazy page allocation (Day11 xv6-2020 LAB5 懒分配)

url: https://pdos.csail.mit.edu/6.S081/2020/labs/lazy.html


操作系统利用页表硬件可以实现诸多巧妙技巧,其中之一便是用户空间堆内存的惰性分配。Xv6应用程序通过sbrk()系统调用向内核申请堆内存。在我们提供的基准内核中,sbrk()会直接分配物理内存并将其映射到进程的虚拟地址空间。对于大规模内存请求,内核执行分配和映射操作可能耗时极长——例如1GB内存相当于262,144个4096字节的页,即便单次分配开销很小,如此庞大的数量级也令人望而生畏。更何况,某些程序分配的内存会超出实际使用量(比如实现稀疏数组时),或者过早提前分配内存。为了让sbrk()在这些场景下能快速返回,成熟的内核会采用惰性分配策略:即sbrk()调用时不立即分配物理内存,仅记录被分配的用户地址范围,并在用户页表中将这些地址标记为无效。当进程首次尝试访问任一惰性分配的内存页时,CPU会触发缺页异常,此时内核才真正分配物理内存、进行清零操作并建立映射。本实验将为xv6实现这一惰性分配特性。
这段总结:直接分配内存开销大,且遇到用户进程分配过多内存 crash 时会造成 overhead 浪费。因此采用 lazy allocation (记录已分配的范围 + 缺页异常)

这次实验相关的代码:

  • kernel/trap.c
  • kernel/vm.c
  • kernel/sysproc.c

任务1:Eliminate allocation from sbrk() (easy) (完成)

你的首要任务是从 sbrk(n) 系统调用的实现(即 sysproc.c 中的 sys_sbrk() 函数)中移除物理页分配逻辑。sbrk(n) 系统调用会将进程的内存大小(size)增加 n 字节,并返回新分配区域的起始地址(即原先的内存大小)。修改后的 sbrk(n) 应当仅将进程的 myproc()->sz 增加 n 并返回旧的大小值,而不再实际分配内存——因此你需要删除对 growproc() 的调用(但仍需确保进程的 size 正确增长!)。

尝试猜测这一修改会导致什么结果:哪些功能会出问题?
回答:会导致缺页异常

进行此修改后,启动xv6并在shell中输入echo hi。你应该会看到类似以下的输出:

init: starting sh
$ echo hi
usertrap(): unexpected scause 0x000000000000000f pid=3sepc=0x0000000000001258 stval=0x0000000000004008
va=0x0000000000004000 pte=0x0000000000000000
panic: uvmunmap: not mapped

“usertrap(): …” 这条消息来自 trap.c 文件中的用户态陷阱处理程序;它捕获了一个无法处理的异常。请确保你理解为何会发生这一缺页错误。“stval=0x0…04008” 表明引发缺页错误的虚拟地址是 0x4008。
回答:导致缺页错误的原因是用户程序访问了一个实际上没有被映射到内存空间的地址

把 sysproc.c 的 sys_sbrk 修改如下:

uint64
sys_sbrk(void)
{int addr;int n;if(argint(0, &n) < 0)return -1;// addr = myproc()->sz;// if(growproc(n) < 0)//   return -1;// p->sz = sz;struct proc* p = myproc();addr = p->sz;p->sz += n;return addr;
}

运行 echo hi 后报错如下:
基本与讲义一致

$ echo hi
usertrap(): unexpected scause 0x000000000000000f pid=3sepc=0x00000000000012ac stval=0x0000000000004008
panic: uvmunmap: not mapped

任务2:Lazy allocation (moderate) (完成)

修改 trap.c 中的代码,使其能够处理用户空间的缺页错误:在触发缺页的地址处映射一个新分配的物理内存页,然后返回到用户空间,让进程继续执行。你应该在生成 “usertrap(): …” 消息的 printf 调用之前添加你的代码。同时,根据需要修改其他 xv6 内核代码,以确保 echo hi 能够正常工作。

以下是一些提示:

  • 您可以通过在usertrap()中检查r_scause()是否为13或15来确定故障是否为页面错误。
    syscall();} else if(r_scause() == 13 || r_scause() == 15) {// load page fault 和 write page fault}else if((which_dev = devintr()) != 0){// ok} else {
  • r_stval()返回RISC-V stval寄存器的值,其中包含导致页面错误的虚拟地址。
  } else if(r_scause() == 13 || r_scause() == 15) {// load page fault 和 write page fault// 使用 stval 获取发生缺页异常的用户空间虚拟地址uint64 va = r_stval();}
  • 从vm.c中的uvmalloc()中窃取代码,这是sbrk()调用的(通过growproc())。您需要调用kalloc()和mappages()。
  • 使用PGROUNDDOWN(va)将错误的虚拟地址向下舍入到页边界。
  } else if(r_scause() == 13 || r_scause() == 15) {// load page fault 和 write page fault// 使用 stval 获取发生缺页异常的用户空间虚拟地址uint64 va = r_stval();// 获取虚拟地址下界va = PGROUNDDOWN(va);// 分配一页char *mem = kalloc();if(mem == 0)panic("no memory when page fault in usertrap");// 置空一页memset(mem, 0, PGSIZE);// 把这一页映射给用户空间 vaif(mappages(p->pagetable, va, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0)panic("mapping failure when page fault in usertrap");}
  • uvmunmap()将会panic;修改它,如果某些页面未映射,则不会panic。
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");for(a = va; a < va + npages*PGSIZE; a += PGSIZE){// 如果某些页面未映射,则不会panic。if((pte = walk(pagetable, a, 0)) == 0)continue;//   panic("uvmunmap: walk");if((*pte & PTE_V) == 0)continue;//   panic("uvmunmap: not mapped");if(PTE_FLAGS(*pte) == PTE_V)panic("uvmunmap: not a leaf");if(do_free){uint64 pa = PTE2PA(*pte);kfree((void*)pa);}*pte = 0;}
}
  • 如果内核崩溃,请在kernel/kernel.asm中查找sepc。
  • 使用您在pgtbl实验中的vmprint函数来打印页表的内容。
  • 如果您看到错误“incomplete type proc”,请包含“spinlock.h”然后是“proc.h”。

如果一切顺利,您的懒惰分配代码应该会使echo hi工作。您应该至少得到一个页面错误(以及懒惰分配),也许两个。

经过测试,echo hi 顺利工作了
在这里插入图片描述


任务3:Lazytests and Usertests (moderate) (doing)

我们已经为您提供了lazytests,这是一个xv6用户程序,用于测试可能对您的懒惰内存分配器造成压力的一些特定情况。修改您的内核代码,以便lazytests和usertests都能通过。

  • 处理负sbrk()参数。
uint64
sys_sbrk(void)
{int addr;int n;if(argint(0, &n) < 0)return -1;struct proc* p = myproc();if(n >= 0) {addr = p->sz;p->sz += n;}else {if(p->sz + n < 0)return -1;addr = p->sz;p->sz = uvmdealloc(p->pagetable, p->sz, p->sz + n);}return addr;
}
  • 如果一个进程在用sbrk()分配的虚拟内存范围之外的地址上发生页面错误,则杀死该进程。
  } else if(r_scause() == 13 || r_scause() == 15) {// load page fault 和 write page fault// 使用 stval 获取发生缺页异常的用户空间虚拟地址uint64 va = r_stval();// 如果一个进程在用sbrk()分配的虚拟内存范围之外的地址上发生页面错误,则杀死该进程。if(va >= p->sz)p->killed = 1;// 获取虚拟地址下界va = PGROUNDDOWN(va);// 分配一页char *mem = kalloc();...
  • 正确处理fork()中的父进程到子进程的内存复制。

进入 kernel/proc.c : fork 源码查看,发现使用 uvmcopy 拷贝用户内存到子进程,进入 uvmcopy 查看,修改如下:

  for(i = 0; i < sz; i += PGSIZE){if((pte = walk(old, i, 0)) == 0)continue;// panic("uvmcopy: pte should exist");if((*pte & PTE_V) == 0)continue;// panic("uvmcopy: page not present");pa = PTE2PA(*pte);flags = PTE_FLAGS(*pte);if((mem = kalloc()) == 0)...
  • 处理进程将sbrk()的有效地址传递给read或write等系统调用,但该地址的内存尚未分配的情况。

之前处理 page fault 是在 trap.c : usertrap 里处理的。这种处理方式只能处理用户程序发生的缺页异常,而无法处理 read/write 等系统调用在内核发生缺页时的错误。

以 write 系统调用为例,在内核中的调用链:sys_write → filewrite → writei → either_copyin → copyin。在 copyin 中使用 walkaddr 查找虚拟地址的物理地址。此时由于没有映射,walkaddr 就会返回0。所以这里要做页表映射,如下:

int
copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
{uint64 n, va0, pa0;while(len > 0){va0 = PGROUNDDOWN(srcva);pa0 = walkaddr(pagetable, va0);if(pa0 == 0) {struct proc* p = myproc();if(srcva >= p->sz) return -1;// 缺页异常,做页表映射// 分配一页char *mem = kalloc();if(mem == 0) return -1;// 清空这页memset(mem, 0, PGSIZE);// 把这一页映射给用户空间 vaif(mappages(p->pagetable, va0, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0)panic("mapping failure when page fault in copyin");// 再查找一次物理地址pa0 = walkaddr(pagetable, va0);}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;
}

同理可分析 read 系统调用,copyout 做相似的修改即可

这里很难使用 kernelvec + 页表硬件去实现缺页异常。主要是无法判断缺页异常的 stval 来自于用户空间虚拟地址还是内核空间虚拟地址

  • 正确处理内存不足的情况:如果在页面错误处理程序中kalloc()失败,则杀死当前进程。
  • 处理比用户栈栈顶更低的无效地址的页面故障。

加一个判断就行

  } else if(r_scause() == 13 || r_scause() == 15) {// load page fault 和 write page fault// 使用 stval 获取发生缺页异常的用户空间虚拟地址uint64 va = r_stval();// 如果一个进程在用sbrk()分配的虚拟内存范围之外的地址上发生页面错误,则杀死该进程。// 处理比用户栈栈顶更低的无效地址的页面故障。if(va >= p->sz || va < p->trapframe->sp)p->killed = 1;// 获取虚拟地址下界va = PGROUNDDOWN(va);// 分配一页char *mem = kalloc();if(mem == 0) {p->killed = 1;// panic("no memory when page fault in usertrap");}else {// 置空一页memset(mem, 0, PGSIZE);// 把这一页映射给用户空间 vaif(mappages(p->pagetable, va, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0)panic("mapping failure when page fault in usertrap");}}

用户栈下方的无效页面故障只能是发生在用户态的异常,故可知该异常由 usertrap() 处理。
从 memlayout.h 可以看到用户程序的地址空间排布

// User memory layout.
// Address zero first:
//   text
//   original data and bss
//   fixed-size stack
//   expandable heap
//   ...
//   TRAPFRAME (p->trapframe, used by the trampoline)
//   TRAMPOLINE (the same page as in the kernel)

p→trapframe→sp

TODO: here

如果您的内核通过了lazytests和usertests,那么您的解决方案是可以接受的:

$  lazytests
lazytests starting
running test lazy alloc
test lazy alloc: OK
running test lazy unmap...
usertrap(): ...
test lazy unmap: OK
running test out of memory
usertrap(): ...
test out of memory: OK
ALL TESTS PASSED
$ usertests
...
ALL TESTS PASSED
$

TODO: here


相关文章:

  • py实现win自动化自动登陆qq
  • Android View#post()源码分析
  • tinyrenderer笔记(Shader)
  • C语言数组和函数的实践———扫雷游戏
  • APP自动化测试(一)
  • 9-4 USART串口数据包
  • [HOT 100] 1377. T 秒后青蛙的位置
  • 在若依里创建新菜单
  • uniapp开发11-v-for动态渲染list列表数据
  • Beetle 树莓派RP2350 - 桌面时钟摆件
  • 探索Hello Robot开源移动操作机器人Stretch 3的技术亮点与市场定位
  • Banana Pi BPI-CM6 是一款八核 RISC-V 模块,兼容 Raspberry Pi CM 载板
  • POI创建Excel文件
  • Android setContentView()源码分析
  • [学习]RTKLib详解:rtkcmn.c与rtkpos.c
  • Java实现堆排序算法
  • 【省电级子印章系统建设方案】
  • 一款独立于游戏外的键盘源按键辅助工具他来了
  • 2025年AI视觉革命:Dual-Stream Diffusion如何让PS下岗?
  • 交换机 MUX VLAN
  • 上海一中院一审公开开庭审理被告人胡欣受贿案
  • 申活观察|精致精准精细,城市“双面镜”照见怎样的海派活力
  • 严正交涉!我驻日使馆:如日方采取新的挑衅举动,中方必坚决反制
  • 新加坡国会选举投票抽样结果公布,执政党已获超半数议席
  • 中虎跳峡封闭仍有游客逆行打卡,景区:专人值守防意外
  • 重庆渝中警方:男子点燃摩托车欲寻衅滋事,被民警和群众合力制服