8.8 KiB
8.8 KiB
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 位
关键常量定义
#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)
新增数据结构
struct super_run {
struct super_run *next;
};
struct {
struct spinlock lock;
struct super_run *freelist;
} skmem;
核心函数实现
超页分配函数
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;
}
超页释放函数
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);
}
内存范围初始化
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)
超页页表遍历
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 函数支持超页检测
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)];
}
地址转换函数增强
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)
关键实现逻辑:
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 函数
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 函数
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支持)
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操作函数适配
// copyout, copyin, copyinstr 都需要类似的修改
uint64 pgsize = (*pte & PTE_PS) ? SUPERPGSIZE : PGSIZE;
uint64 va_base = va0 & ~(pgsize - 1);
n = pgsize - (srcva - va_base);
关键测试理解
superpg_test 测试流程
- 分配8MB内存:
sbrk(N)其中 N = 8MB - 计算超页起始地址:
SUPERPGROUNDUP(end) - 验证512个连续页面:
supercheck(s)- 检查512个4KB页面有相同的PTE(证明是超页)
- 验证PTE权限(PTE_V | PTE_R | PTE_W)
- 测试内存读写功能
- 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
- 管理效率: 减少页表遍历深度
总结
超页实现的核心挑战在于:
- 双重内存管理: 同时支持4KB普通页和2MB超页
- 智能分配策略: 自动识别何时使用超页
- 页表处理: 正确处理不同级别的页表项
- 兼容性: 保持与现有代码的完全兼容
通过仔细的设计和实现,成功在xv6中添加了超页支持,提高了大内存分配的效率,同时保持了系统的稳定性和兼容性。