xv6 Lab: page tables - Use superpages(巨页)
题目分析
修改xv6内核使能巨页,需要实现巨页物理内存的分配与释放,以及巨页虚拟内存的管理。结合xv6已给提示,从sbrk入手,进行分析。
由上图,我们首先需要为巨页实现物理内存的分配(super_alloc()/super_free())以及虚拟地址映射(super_walk)。
物理内存分配/释放
完全可以仿照xv6的链式管理方式。
struct super_run
{
struct super_run *next;
};
struct {
struct spinlock lock;
struct super_run *freelist;
} skmem;
void
kinit()
{
initlock(&kmem.lock, "kmem");
initlock(&skmem.lock, "skem");
freerange(end, (void*)PHYSTOP);
}
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) //留5个巨页
kfree(p);
p = (char*)SUPERPGROUNDUP((uint64)p);
for (; p + SUPERPGSIZE <= (char *)pa_end; p += SUPERPGSIZE) {
superfree(p);
}
}
void *
superalloc(void)
{
struct super_run *r;
acquire(&skmem.lock);
r = skmem.freelist;
if (r)
skmem.freelist = r->next;
release(&skmem.lock);
if (r)
memset((void *)r, 5, SUPERPGSIZE);
return (void *)r;
}
虚拟内存管理
内存分配
添加PTE_S(页表项巨页标识位)
添加在riscv.h中
#define PTE_S (1L << 8) // 是否是巨页
修改uvmalloc()
uint64
uvmalloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz, int xperm)
{
char *mem;
uint64 a;
int sz;
uint64 soldsz = 0;
if(newsz < oldsz)
return oldsz;
oldsz = PGROUNDUP(oldsz);
if (newsz - oldsz < SUPERPGSIZE) {
oldsz = PGROUNDUP(oldsz);
for(a = oldsz; a < newsz; a += sz){
sz = PGSIZE;
mem = kalloc();
if(mem == 0){
uvmdealloc(pagetable, a, oldsz);
return 0;
}
#ifndef LAB_SYSCALL
memset(mem, 0, sz);
#endif
if(mappages(pagetable, a, sz, (uint64)mem, PTE_R|PTE_U|xperm) != 0){
kfree(mem);
uvmdealloc(pagetable, a, oldsz);
return 0;
}
}
} else {
soldsz = SUPERPGROUNDUP(oldsz);
for(a = oldsz; a < soldsz; a += sz){ /* 分配并映射空页 */
sz = PGSIZE;
mem = kalloc();
if(mem == 0){
uvmdealloc(pagetable, a, oldsz);
return 0;
}
#ifndef LAB_SYSCALL
memset(mem, 0, sz);
#endif
if(mappages(pagetable, a, sz, (uint64)mem, PTE_R|PTE_U|xperm) != 0){
kfree(mem);
uvmdealloc(pagetable, a, oldsz);
return 0;
}
}
for (a = soldsz; a < newsz; a+= sz) {
sz = SUPERPGSIZE;
mem = superalloc();
if (mem == 0) {
return 0;
}
if (mappages(pagetable, a, sz, (uint64)mem, PTE_R | PTE_U | PTE_S | xperm) != 0) {
superfree(mem);
return 0;
}
}
}
return newsz;
}
当需要分配的内存大于2M时,分配巨页。
注意:
1. 分配巨页前,一定要按SUPERPGSIZE对齐。若按PGSIZE对齐,会出现巨页覆盖先前正常页的情况(后文分析)。
2. 向上SUPERPGSIZE对齐后,在巨页首地址和对齐前的地址之间,会有很多未分配和映射的正常页,在释放内存遍历到这些空页时,会报错,所以需要将这些空页分配物理内存并映射。
修改mappages()
由于巨页使用level-1页表作为叶子页表,而原walk函数使用level-0页表作叶子页表,所以需要新的walk函数(super_walk())。应该是可以通过扩展walk()的第三个参数的值来实现两种walk()的复用,时间关系我也没再修改了。
int
mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
{
uint64 a, last;
pte_t *pte;
if((va % PGSIZE) != 0)
panic("mappages: va not aligned");
if((size % PGSIZE) != 0)
panic("mappages: size not aligned");
if(size == 0)
panic("mappages: size");
a = va;
if ((perm & PTE_S) == 0) { /*不使用巨页*/
last = va + size - PGSIZE;
for(;;){
if((pte = walk(pagetable, a, 1)) == 0)
return -1;
if(*pte & PTE_V)
panic("mappages: remap");
*pte = PA2PTE(pa) | perm | PTE_V;
if(a == last)
break;
a += PGSIZE;
pa += PGSIZE;
}
} else { /* 使用巨页 */
last = va + size - SUPERPGSIZE;
for (;;) {
if ((pte = super_walk(pagetable, a, 1)) == 0)
return -1;
if (*pte & PTE_V)
panic("super mappages: remap");
*pte = PA2PTE(pa) | perm | PTE_V;
if (a == last)
break;
a += SUPERPGSIZE;
pa += SUPERPGSIZE;
}
}
return 0;
}
新增super_walk()
依然是直接仿照xv6原有walk(),由于只循环一次,删掉了walk原有的for循环。
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)];
}
修改walk()
在代码的其他地方,会使用walk()进行寻址,而walk()本身是无法满足巨页需要的两级页表寻址的,所以需要修改。
其实也很简单,在遍历level-1页表时,遇到PTE_S直接return就行。
pte_t *
walk(pagetable_t pagetable, uint64 va, int alloc)
{
if(va >= MAXVA)
panic("walk");
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_S) {
return pte;
}
#endif
} else {
if(!alloc || (pagetable = (pde_t*)kalloc()) == 0)
return 0;
memset(pagetable, 0, PGSIZE);
*pte = PA2PTE(pagetable) | PTE_V;
}
}
return &pagetable[PX(0, va)];
}
修改uvmcopy()
在fork的时候,子进程需要从父进程复制所有内存,原有uvmcopy()无法满足巨页的复制要求,所以需要修改。
int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
pte_t *pte;
uint64 pa, i;
uint flags;
char *mem;
int szinc;
for(i = 0; i < sz; i += szinc){
szinc = PGSIZE;
if((pte = walk(old, i, 0)) == 0)
panic("uvmcopy: pte should exist");
if((*pte & PTE_V) == 0)
panic("uvmcopy: page not present");
pa = PTE2PA(*pte);
flags = PTE_FLAGS(*pte);
if ((flags & PTE_S) == 0) {
if((mem = kalloc()) == 0)
goto err;
memmove(mem, (char*)pa, PGSIZE);
if(mappages(new, i, PGSIZE, (uint64)mem, flags) != 0){
kfree(mem);
goto err;
}
} else {
if ((mem = superalloc()) == 0)
goto err;
if (mappages(new, i, SUPERPGSIZE, (uint64)mem, flags) != 0) {
superfree(mem);
goto err;
}
memmove(mem, (char*)pa, SUPERPGSIZE);
i += SUPERPGSIZE - szinc; /* 跳过巨页空间 */
}
}
return 0;
err:
uvmunmap(new, 0, i / PGSIZE, 1);
return -1;
}
改动不大,就是按正常页遍历并copy的过程中,当walk返回巨页的页表项时,直接copy后面一个巨页的内存,并将这段虚拟地址跳过。
行文至此,巨页的物理内存分配、虚拟内存映射、fork()内存复制均以实现。接下来还需实现巨页的释放。
内存释放
只需修改uvmunmap(),和uvmcopy的改动相似,也是只需在正常页遍历并free的过程中,当walk返回巨页的页表项时,直接free后面一个巨页的内存,并将这段虚拟地址跳过。
void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
uint64 a;
pte_t *pte;
int sz;
if((va % PGSIZE) != 0)
panic("uvmunmap: not aligned");
for(a = va; a < va + npages*PGSIZE; a += sz){
sz = PGSIZE;
if((pte = walk(pagetable, a, 0)) == 0)
panic("uvmunmap: walk");
if((*pte & PTE_V) == 0) {
printf("va=%ld pte=%ld\n", a, *pte);
panic("uvmunmap: not mapped");
}
if(PTE_FLAGS(*pte) == PTE_V)
panic("uvmunmap: not a leaf");
if ((*pte & PTE_S)) { /* 释放巨页 */
a += SUPERPGSIZE - sz;
}
if(do_free){
uint64 pa = PTE2PA(*pte);
kfree((void*)pa);
}
*pte = 0;
}
}
总结
代码本身不难,难的是厘清xv6的物理内存链式管理方式,以及基于三级页表的虚拟内存管理方式。
本实验从sbrk()入手,可谓是虽管中窥豹,但也可见一斑。
本log行文简略,我也并无太多时间精心编辑,只求作个记录,各位看客见谅。