Files
xv6-labs/superpage_implementation_summary.md
2025-05-29 20:00:24 +08:00

8.8 KiB
Raw Blame History

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 测试流程

  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中添加了超页支持提高了大内存分配的效率同时保持了系统的稳定性和兼容性。