xv6 lazy page allocation 实验详解:从 page fault 到 3 处关键代码修改

xv6 lazy page allocation 实验详解:从 page fault 到 3 处关键代码修改
xv6 惰性页分配实验全解析从缺页异常到核心代码改造1. 实验背景与核心挑战在传统操作系统中当用户程序通过sbrk()系统调用请求堆内存时内核会立即分配物理内存并建立页表映射。这种**预先分配Eager Allocation**策略存在两个显著问题性能损耗大内存请求如1GB需要分配26万多个4KB页面即使单个分配很快总量也相当可观资源浪费程序可能申请远超实际需要的内存如稀疏数组或提前分配但延迟使用惰性分配Lazy Allocation通过以下方式优化sbrk()仅记录内存范围不实际分配物理页首次访问时触发缺页异常内核再按需分配将O(n)的分配时间分摊到实际使用时// 修改前的sbrk实现立即分配 uint64 sys_sbrk(void) { int addr myproc()-sz; if(growproc(n) 0) return -1; // 立即分配物理内存 return addr; }2. 实验实现路径拆解2.1 阶段一消除sbrk的立即分配核心修改sysproc.c删除growproc()调用仅增加进程的sz字段处理负数参数时直接收缩内存// 修改后的sbrk实现惰性分配 uint64 sys_sbrk(void) { int addr myproc()-sz; if(n 0){ myproc()-sz n; // 仅扩展虚拟地址空间 } else if(addr n PGROUNDUP(p-trapframe-sp)) { myproc()-sz uvmdealloc(..., addr n); // 负参数实际释放 } return addr; }典型问题场景执行echo hi时触发缺页异常scause15控制流停在0x1258查看user/sh.asm缺页地址stval0x4008用户态未映射地址2.2 阶段二缺页异常处理关键识别逻辑trap.cvoid usertrap(void) { if(r_scause() 8) { /* 系统调用 */ } else if(r_scause() 13 || r_scause() 15) { uint64 va r_stval(); if(va p-sz || va p-trapframe-sp) { p-killed 1; // 非法地址访问 } else { char* pa kalloc(); mappages(p-pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)pa, PTE_W|PTE_R|PTE_U); } } }处理流程检查scause值13读缺页15写缺页通过stval获取故障地址验证地址有效性在堆空间内且高于栈顶分配物理页并建立映射2.3 阶段三配套机制改造2.3.1 页表操作改造vm.cuvmunmap修改void uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free) { for(a va; a va npages*PGSIZE; a PGSIZE){ if((pte walk(pagetable, a, 0)) 0) continue; // 原版会panic if((*pte PTE_V) 0) continue; // 允许无效PTE if(do_free) kfree((void*)PTE2PA(*pte)); *pte 0; } }uvmcopy修改fork时int uvmcopy(pagetable_t old, pagetable_t new, uint64 sz) { for(i 0; i sz; i PGSIZE){ if((pte walk(old, i, 0)) 0) continue; if((*pte PTE_V) 0) continue; // 跳过未分配页 // ...复制有效页... } }2.3.2 系统调用处理walkaddr当read/write访问未分配页时uint64 walkaddr(pagetable_t pagetable, uint64 va) { if(pte 0 || (*pte PTE_V) 0) { if(va PGROUNDUP(p-trapframe-sp) va p-sz) { char* pa kalloc(); mappages(pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)pa, PTE_W|PTE_R|PTE_U); return (uint64)pa; } } return 0; }3. 关键问题与调试技巧3.1 典型错误场景错误现象可能原因解决方案uvmunmap: not mapped未处理无效PTE将panic改为continuefreewalk: leaf页表未正确释放检查PGROUNDDOWN(va)使用惰性测试失败未处理sbrk负参数添加栈空间检查3.2 调试工具链异常信息解读usertrap(): unexpected scause 0x0000000f pid3 sepc0x0000000000001258 stval0x0000000000004008scause0xf存储缺页异常sepc故障指令地址stval访问的虚拟地址页表检查// 在trap.c中添加调试输出 printf(page table: %p, va: %p, pte: %p\n, p-pagetable, va, walk(p-pagetable, va, 0));汇编对照$ grep -n 1258 user/sh.asm 1258: 00001597 auipc a1,0x14. 进阶实现考量4.1 边界条件处理内存不足场景if((pa kalloc()) 0) { p-killed 1; goto end; // 避免继续执行 }地址有效性检查if(va p-sz || va PGROUNDUP(p-trapframe-sp)) { printf(usertrap(): invalid va%p (sz%p, sp%p)\n, va, p-sz, p-trapframe-sp); p-killed 1; }4.2 性能优化方向批量预分配连续缺页时批量分配多个页面零页优化对只读页延迟清零写时再处理页回收策略定期回收长时间未使用的惰性页5. 测试验证体系5.1 标准测试流程# 基础功能测试 $ make qemu $ echo hi # 完整测试套件 $ lazytests $ usertests5.2 测试用例覆盖测试类型验证要点关键检查点lazy alloc基本分配缺页次数统计lazy unmap内存释放uvmunmap不panicout of memory内存不足进程正确终止sbrkarg系统调用参数copyin/out处理6. 与写时复制(COW)的对比特性惰性分配写时复制触发时机首次访问写入操作复制单位整个页面被修改页面典型应用堆内存扩展fork()优化实现复杂度中等需改缺页处理高需引用计数实际项目中常组合使用这两种技术例如在Rust的std::vec::Vec实现中就同时应用了惰性分配和写时复制策略。