从零实现 xv6 懒分配:在缺页中断中动态分配堆内存
1. 懒分配机制的核心思想懒分配Lazy Allocation是操作系统中一种常见的内存管理优化技术。它的核心思想是延迟物理内存的分配时机。传统的内存分配方式如xv6原生的growproc()会在调用sbrk()时立即分配物理内存并建立页表映射而懒分配模式下内核仅记录虚拟地址范围的变化调整p-sz实际物理页的分配会推迟到首次访问该内存时触发缺页异常Page Fault才进行。举个例子假设用户程序调用sbrk(4096)申请4KB内存传统模式内核立即分配物理页并在页表中建立映射消耗实际资源懒分配模式内核只把p-sz增加4096等程序真正读写[p-sz-4096, p-sz)范围内的地址时才通过缺页异常处理程序分配物理页这种机制的优势非常明显提升性能避免立即分配大量可能不会使用的内存如稀疏数组降低开销大内存申请如1GB不再需要处理26万次页分配操作简化设计用户程序无需关心内存是否实际分配由内核透明处理2. 修改sbrk系统调用实现懒分配的第一步是修改sys_sbrk()移除原有的物理内存分配逻辑。以下是具体实现要点// kernel/sysproc.c uint64 sys_sbrk(void) { int addr; int n; struct proc *p myproc(); if(argint(0, n) 0) return -1; addr p-sz; // 处理负数参数内存释放 if(n 0) { if(addr n PGROUNDUP(p-trapframe-sp)) { return -1; // 不能释放栈内存 } p-sz uvmdealloc(p-pagetable, addr, addr n); } // 正数参数仅调整sz else if(addr n addr) { // 防止整数溢出 p-sz n; } else { return -1; } return addr; }关键修改点删除原有的growproc(n)调用正数参数时仅增加p-sz负数参数时检查不能释放到用户栈之下通过PGROUNDUP(p-trapframe-sp)判断调用uvmdealloc实际释放内存因为缩小内存必须立即生效测试时运行echo hi会出现缺页异常这是因为shell尝试写入新申请但未实际分配的内存区域触发scause15Store Page Fault。3. 缺页异常处理当用户程序访问未映射的地址时CPU会触发缺页异常流程如下陷入内核态执行usertrap()kernel/trap.c通过r_scause()判断异常类型13Load Page Fault15Store/AMO Page Fault通过r_stval()获取触发异常的虚拟地址以下是处理缺页异常的核心代码// kernel/trap.c void usertrap(void) { // ... else if(r_scause() 13 || r_scause() 15) { uint64 va r_stval(); struct proc *p myproc(); // 检查地址合法性 if(va p-sz || va PGROUNDUP(p-trapframe-sp)) { p-killed 1; goto end; } // 分配物理页 char *pa kalloc(); if(pa 0) { p-killed 1; goto end; } memset(pa, 0, PGSIZE); // 建立映射 if(mappages(p-pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)pa, PTE_W|PTE_R|PTE_U) ! 0) { kfree(pa); p-killed 1; } } end: // ... }关键安全检查va p-sz访问超过进程内存范围va PGROUNDUP(p-trapframe-sp)访问栈保护区Guard Pagekalloc()失败物理内存耗尽注意必须使用PGROUNDDOWN(va)对齐页边界否则会导致映射混乱。我曾遇到过因未对齐导致uvmunmap时出现freewalk: leaf panic的问题。4. 修改页表相关函数懒分配会导致页表出现空洞虚拟地址有效但无物理页映射需要修改以下函数4.1 uvmunmap修改原版uvmunmap()遇到未映射的页会panic需改为跳过// kernel/vm.c 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; // 页表项不存在 if((*pte PTE_V) 0) continue; // 页未映射 // ...原有释放逻辑... } }4.2 uvmcopy修改fork()时会调用uvmcopy复制父进程页表需处理懒分配页// kernel/vm.c 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; // ...复制有效页... } }5. 处理系统调用中的懒分配当系统调用如read/write使用懒分配的内存时不会触发用户态缺页异常因为发生在内核态需要在walkaddr()中处理// kernel/vm.c uint64 walkaddr(pagetable_t pagetable, uint64 va) { // ... if(pte 0 || (*pte PTE_V) 0) { if(va p-sz || va PGROUNDUP(p-trapframe-sp)) return 0; // 分配物理页 char *pa kalloc(); if(pa 0) return 0; memset(pa, 0, PGSIZE); if(mappages(pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)pa, PTE_W|PTE_R|PTE_U) ! 0) { kfree(pa); return 0; } return (uint64)pa; } // ... }6. 测试与调试通过以下测试验证实现基础测试echo hi应能正常运行压力测试lazytests测试大内存、边界条件等全面测试usertests常见问题及解决方案uvmunmap: not mapped未正确处理未映射的PTEfreewalk: leaf页表未正确对齐或重复映射内核崩溃使用kernel.asm查找sepc定位问题我在实现时曾遇到一个典型问题当sbrk(1)后访问地址时由于未对stval进行页对齐导致映射了错误的页范围。通过添加PGROUNDDOWN解决了该问题。7. 性能优化思考虽然基础实现已能工作但还有优化空间批量分配连续缺页时可预分配多个页零页优化只读页可映射到统一的零页内存压力在kalloc失败时优先回收懒分配页这些优化需要更复杂的设计但能进一步提升懒分配的性能优势。