# xv6 超页(Superpage)实现复盘 ## 项目概述 在 xv6 内核中实现 2MB 超页支持,当用户程序调用 `sbrk()` 时指定的大小为 2MB 或更大,并且新创建的地址范围包含一个或多个 2MB 对齐且至少为 2MB 大小的区域时,内核应使用单个超页(而不是数百个普通页)。 ## 实现目标 - 支持 2MB 超页分配 - 通过 `superpg_test` 测试用例 - 保持与现有代码的兼容性 - 正确处理超页的内存管理(分配、释放、复制) ## 核心概念 ### 超页基本参数 - **普通页大小**: 4KB (PGSIZE) - **超页大小**: 2MB (SUPERPGSIZE = 2 * 1024 * 1024) - **超页包含**: 512个普通页 (2MB / 4KB = 512) - **页表级别**: 在 level-1 页表中设置 PTE_PS 位 ### 关键常量定义 ```c #define SUPERPGSIZE (2 * (1 << 20)) // 2MB #define SUPERPGROUNDUP(sz) (((sz)+SUPERPGSIZE-1) & ~(SUPERPGSIZE-1)) #define PTE_PS (1L << 7) // Page Size bit ``` ## 实现步骤 ### 1. 超页内存分配器(kernel/kalloc.c) #### 新增数据结构 ```c struct super_run { struct super_run *next; }; struct { struct spinlock lock; struct super_run *freelist; } skmem; ``` #### 核心函数实现 **超页分配函数** ```c void* superalloc() { struct super_run *r; acquire(&skmem.lock); r = skmem.freelist; if(r) skmem.freelist = r->next; release(&skmem.lock); if(r) memset((void*)r, 0, SUPERPGSIZE); return (void*)r; } ``` **超页释放函数** ```c void superfree(void *pa) { struct super_run *r; if(((uint64)pa % SUPERPGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP) panic("superfree"); memset(pa, 1, SUPERPGSIZE); r = (struct super_run *)pa; acquire(&skmem.lock); r->next = skmem.freelist; skmem.freelist = r; release(&skmem.lock); } ``` **内存范围初始化** ```c void freerange(void *pa_start, void *pa_end) { char *p; // 分配普通页 p = (char*)PGROUNDUP((uint64)pa_start); for(; p + PGSIZE <= (char*)pa_end - 12 * 1024 * 1024; p += PGSIZE) kfree(p); // 分配超页 p = (char*)SUPERPGROUNDUP((uint64)p); for (; p + SUPERPGSIZE <= (char *)pa_end; p += SUPERPGSIZE) { superfree(p); } } ``` ### 2. 页表管理(kernel/vm.c) #### 超页页表遍历 ```c pte_t *super_walk(pagetable_t pagetable, uint64 va, int alloc) { if (va > MAXVA) panic("walk"); pte_t *pte = &(pagetable[PX(2, va)]); if (*pte & PTE_V) { pagetable = (pagetable_t)PTE2PA(*pte); } else { if (!alloc || (pagetable = (pde_t*)kalloc()) == 0) return 0; memset(pagetable, 0, PGSIZE); *pte = PA2PTE(pagetable) | PTE_V; } return &pagetable[PX(1, va)]; // 返回 level-1 PTE } ``` #### 修改 walk 函数支持超页检测 ```c pte_t *walk(pagetable_t pagetable, uint64 va, int alloc) { // ... 现有代码 ... for(int level = 2; level > 0; level--) { pte_t *pte = &pagetable[PX(level, va)]; if(*pte & PTE_V) { pagetable = (pagetable_t)PTE2PA(*pte); #ifdef LAB_PGTBL if (*pte & PTE_PS) { return pte; // 遇到超页时返回该PTE } #endif } // ... 其他代码 ... } return &pagetable[PX(0, va)]; } ``` #### 地址转换函数增强 ```c uint64 walkaddr(pagetable_t pagetable, uint64 va) { // ... 现有代码 ... pa = PTE2PA(*pte); if(*pte & PTE_PS) { // 超页:添加超页内偏移 pa += va & (SUPERPGSIZE - 1); } else { // 普通页:添加页内偏移 pa += va & (PGSIZE - 1); } return pa; } ``` ### 3. 内存分配策略(uvmalloc) **关键实现逻辑**: ```c uint64 uvmalloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz, int xperm) { // 检查是否应该使用超页 if (newsz - oldsz >= SUPERPGSIZE) { uint64 super_start = SUPERPGROUNDUP(oldsz); uint64 super_end = newsz & ~(SUPERPGSIZE - 1); // 1. 分配超页边界前的普通页 for(a = oldsz; a < super_start; a += PGSIZE) { // 分配普通页 } // 2. 分配对齐的超页区域 for (a = super_start; a < super_end; a += SUPERPGSIZE) { mem = superalloc(); mappages(pagetable, a, SUPERPGSIZE, (uint64)mem, PTE_R | PTE_U | PTE_PS | xperm); } // 3. 分配超页边界后的普通页 for(a = super_end; a < newsz; a += PGSIZE) { // 分配普通页 } } else { // 小于2MB的分配使用普通页 } } ``` ### 4. 内存操作函数适配 #### mappages 函数 ```c int mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm) { if ((perm & PTE_PS) == 0) { // 普通页映射逻辑 // 使用 walk() 函数 } else { // 超页映射逻辑 // 使用 super_walk() 函数 last = va + size - SUPERPGSIZE; for (;;) { pte = super_walk(pagetable, a, 1); *pte = PA2PTE(pa) | perm | PTE_V; a += SUPERPGSIZE; pa += SUPERPGSIZE; } } } ``` #### uvmunmap 函数 ```c void uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free) { for(a = va; a < va + npages*PGSIZE; a += sz){ sz = PGSIZE; pte = walk(pagetable, a, 0); if ((*pte & PTE_PS)) { // 超页释放 if(do_free) superfree((void*)PTE2PA(*pte)); *pte = 0; a += SUPERPGSIZE - sz; } else { // 普通页释放 if(do_free) kfree((void*)PTE2PA(*pte)); *pte = 0; } } } ``` #### uvmcopy 函数(fork支持) ```c int uvmcopy(pagetable_t old, pagetable_t new, uint64 sz) { for(i = 0; i < sz; i += szinc){ szinc = PGSIZE; pte = walk(old, i, 0); flags = PTE_FLAGS(*pte); if ((flags & PTE_PS) == 0) { // 复制普通页 mem = kalloc(); memmove(mem, (char*)pa, PGSIZE); mappages(new, i, PGSIZE, (uint64)mem, flags); } else { // 复制超页 mem = superalloc(); memmove(mem, (char*)pa, SUPERPGSIZE); mappages(new, i, SUPERPGSIZE, (uint64)mem, flags); szinc = SUPERPGSIZE; } } } ``` #### copy操作函数适配 ```c // copyout, copyin, copyinstr 都需要类似的修改 uint64 pgsize = (*pte & PTE_PS) ? SUPERPGSIZE : PGSIZE; uint64 va_base = va0 & ~(pgsize - 1); n = pgsize - (srcva - va_base); ``` ## 关键测试理解 ### superpg_test 测试流程 1. **分配8MB内存**: `sbrk(N)` 其中 N = 8MB 2. **计算超页起始地址**: `SUPERPGROUNDUP(end)` 3. **验证512个连续页面**: `supercheck(s)` - 检查512个4KB页面有相同的PTE(证明是超页) - 验证PTE权限(PTE_V | PTE_R | PTE_W) - 测试内存读写功能 4. **Fork测试**: 验证超页在进程复制时正确工作 ### 测试期望 - 512个连续的4KB页面应该映射到同一个超页 - 每个页面通过 `pgpte()` 返回相同的PTE值 - PTE必须设置正确的权限位 - 内存读写必须正常工作 - Fork后子进程中超页仍然正常 ## 遇到的问题和解决方案 ### 1. fork失败问题 **问题**: 测试中fork调用失败 **原因**: uvmcopy函数中超页处理逻辑错误 **解决**: 修正超页复制时的步长计算和错误处理 ### 2. 内存分配策略问题 **问题**: 没有正确识别何时使用超页 **原因**: 原始逻辑基于总分配大小,未考虑对齐 **解决**: 重写分配策略,考虑超页对齐边界 ### 3. 地址计算错误 **问题**: walkaddr等函数对超页地址计算错误 **原因**: 未考虑超页的偏移计算差异 **解决**: 根据PTE_PS位选择不同的偏移计算方法 ### 4. 内存释放错误 **问题**: 释放超页时调用kfree而非superfree **原因**: uvmunmap函数未区分超页和普通页 **解决**: 根据PTE_PS位选择正确的释放函数 ## 实现验证 ### 测试结果 - ✅ superpg_test: OK - 超页分配和使用正常 - ✅ pgaccess_test: OK - 页面访问跟踪正常 - ✅ ugetpid_test: OK - 基本系统调用正常 - ✅ usertests: 通过 - 系统整体稳定性良好 ### 性能优势 - **内存效率**: 2MB超页减少页表条目数量 - **TLB效率**: 单个TLB条目覆盖2MB而非4KB - **管理效率**: 减少页表遍历深度 ## 总结 超页实现的核心挑战在于: 1. **双重内存管理**: 同时支持4KB普通页和2MB超页 2. **智能分配策略**: 自动识别何时使用超页 3. **页表处理**: 正确处理不同级别的页表项 4. **兼容性**: 保持与现有代码的完全兼容 通过仔细的设计和实现,成功在xv6中添加了超页支持,提高了大内存分配的效率,同时保持了系统的稳定性和兼容性。