文章目录
实验五 惰性页面分配
一、代码理解
在开始编码之前,请阅读xv6 书的第 4 章(特别是 4.6)以及可能会修改的相关文件:
内核/trap.c
内核/vm.c
内核/sysproc.c
1.sys_sbrk()进程页面分配
sysproc.c中的sys_sbrk
函数是 sbrk
系统调用的实现,用于增加或减少进程的数据段(堆)的大小。这个系统调用通常用于动态内存分配,允许程序在运行时调整其内存占用。
以下是 sys_sbrk
函数的详细步骤:
-
参数获取:
if(argint(0, &n) < 0) return -1;
这段代码从用户空间获取第一个参数
n
,它表示要增加(正值)或减少(负值)的内存大小。argint
是一个辅助函数,用于从用户空间读取整数参数。如果参数获取失败,函数返回-1
。 -
获取当前进程的内存大小:
addr = myproc()->sz;
这里,
myproc()
返回当前正在执行的进程的struct proc
结构体指针。sz
是该结构体中的一个字段,表示进程的当前内存大小(即数据段的末尾地址)。addr
被赋值为当前进程的内存大小。 -
调整内存大小:
if(growproc(n) < 0) return -1;
growproc
是一个内核函数,用于实际调整进程的内存大小。它根据参数n
的值增加或减少进程的内存。如果growproc
返回负值,表示内存调整失败,函数返回-1
。 -
返回旧的内存末尾地址:
return addr;
如果内存调整成功,函数返回调整前的内存末尾地址
addr
。这个值是用户程序调用sbrk
之前的堆的末尾地址,用户程序可以使用这个地址来管理新分配的内存。
2.usertrap() 用户入内核态
usertrap
函数是在 RISC-V 架构的操作系统中处理从用户空间进入内核的中断、异常或系统调用的核心函数。这个函数通常在用户程序执行系统调用、发生异常或硬件中断时被调用。
-
检查模式:
if((r_sstatus() & SSTATUS_SPP) != 0) panic("usertrap: not from user mode");
这段代码检查当前的中断是否确实来自用户模式。
SSTATUS_SPP
是sstatus
寄存器中的一个位,表示上一个模式(用户模式或监管者模式)。如果不是从用户模式进入的,则触发panic
,表示这是一个错误情况。 -
设置陷阱向量:
w_stvec((uint64)kernelvec);
将陷阱处理向量设置为
kernelvec
,这样后续的中断和异常将由内核的陷阱处理程序处理。 -
获取当前进程:
struct proc *p = myproc();
获取当前正在执行的进程的
proc
结构体指针。 -
保存用户程序计数器:
p->trapframe->epc = r_sepc();
保存异常程序计数器(
sepc
)到进程的陷阱帧中,以便在返回用户空间时恢复程序执行位置。 -
处理系统调用:
if(r_scause() == 8){ // system call if(p->killed) exit(-1); p->trapframe->epc += 4; intr_on(); syscall(); }
如果
scause
寄存器的值为 8,表示这是一个系统调用。首先检查进程是否已被标记为 killed,如果是,则退出。然后,增加epc
以跳过导致系统调用的ecall
指令,确保返回时执行下一条指令。接着,启用中断并调用syscall()
函数来处理系统调用。 -
处理设备中断:
else if((which_dev = devintr()) != 0){ // ok }
调用
devintr()
函数来处理设备中断。如果devintr()
返回非零值,表示成功处理了设备中断。 -
处理意外情况:
else { printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid); printf(" sepc=%p stval=%p\n", r_sepc(), r_stval()); p->killed = 1; }
如果
scause
不是系统调用也不是已知的设备中断,打印错误信息并将进程标记为 killed。 -
检查进程状态:
if(p->killed) exit(-1);
如果进程已被标记为 killed,则退出进程。
-
处理定时器中断:
if(which_dev == 2) yield();
如果中断是由定时器引起的(
which_dev == 2
),则调用yield()
函数让出 CPU。 -
返回用户空间:
usertrapret();
调用
usertrapret()
函数准备返回用户空间,恢复用户模式下的执行状态。
二、Eliminate allocation from sbrk()
1.题目描述
您的首要任务是从 sbrk(n) 系统调用实现中删除页面分配,即 sysproc.c 中的 sys_sbrk() 函数。sbrk(n) 系统调用将进程的内存大小增加 n 个字节,然后返回新分配区域的起始位置(即旧大小)。
您的新 sbrk(n) 应该只是将进程的大小(myproc()->sz)增加 n 并返回旧大小。它不应该分配内存 - 因此您应该删除对 growproc() 的调用(但您仍然需要增加进程的大小!)。
2.解答
通过对上文的sys_sbrk
函数分析不难看出,我们只需要将分配物理内存那一条改成指针指向修改。
// kernel/sysproc.c
uint64
sys_sbrk(void)
{
int addr;
int n;
struct proc *p = myproc();
acquire(&p->lock); // 确保对进程大小的修改是原子的
if(argint(0, &n) < 0)
return -1;
addr = p->sz;
if(n < 0) {
uvmdealloc(p->pagetable, p->sz, p->sz+n); // 缩小就马上释放
}
p->sz += n; // 懒分配
return addr;
}
三、Lazy allocation
1.题目描述
修改 trap.c 中的代码,通过在错误地址映射新分配的物理内存页面,然后返回用户空间以让进程继续执行,来响应来自用户空间的页面错误。
您应该在产生“usertrap(): ...”消息的printf调用之前添加代码。修改您需要的任何其他 xv6 内核代码,以使echo hi正常工作。
2.解答
四、Lazytests and Usertests
1.中断处理
usertrap
函数负责处理从用户空间进入内核的各种情况,包括系统调用、设备中断和异常,确保正确地处理这些事件并安全地返回到用户空间。修改 usertrap 用户态陷阱处理函数,为缺页异常添加检测。
在usertrap
添加条件分支
uint64 va = r_stval();
if((r_scause() == 13 || r_scause() == 15) && uvmshouldtouch(va)){ // 缺页异常,并且发生异常的地址进行过懒分配
uvmlazytouch(va); // 分配物理内存,并在页表创建映射
2.页表处理
uvmlazytouch 函数用于确保一个虚拟地址(va)对应的页面被实际分配并映射到物理内存中。
// 触摸一个惰性分配的页面,使其映射到一个实际的物理页面。
void uvmlazytouch(uint64 va) {
struct proc *p = myproc();
char *mem = kalloc();
if (mem == 0) {
printf("lazy alloc: 内存分配失败,虚拟地址 %p\n", va);
p->killed = 1;
return; // 出现错误时提前退出
} else {
memset(mem, 0, PGSIZE);
int map_result = mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U);
if (map_result != 0) {
printf("lazy alloc: 页面映射失败,虚拟地址 %p, 错误码 %d\n", va, map_result);
kfree(mem); // 映射失败时释放已分配的内存
p->killed = 1;
return; // 出现错误时提前退出
}
}
// 如需调试信息可以取消下行注释:
// printf("lazy alloc: 成功分配并映射虚拟地址 %p\n", PGROUNDDOWN(va));
}
uvmshouldtouch 函数用于判断给定的虚拟地址(va)是否属于那些已经被惰性分配了但还未被实际触及(即分配物理内存并映射)的页面。
// 判断是否一个页面之前已经惰性分配但尚未触摸使用。
int uvmshouldtouch(uint64 va) {
struct proc *p = myproc();
if (va >= p->sz) {
// 地址超出进程的内存大小范围
return 0;
}
if (PGROUNDDOWN(va) == r_sp()) {
// 地址是栈保护页
return 0;
}
pte_t *pte = walk(p->pagetable, va, 0);
if (pte == 0 || (*pte & PTE_V) == 0) {
// 页表项不存在或不有效
return 1;
}
return 0; // 页面存在且有效
}
1.题目描述
我们为您提供了lazytests,这是一个 xv6 用户程序,用于测试可能给您的惰性内存分配器带来压力的一些特定情况。修改您的内核代码,以便惰性测试 和用户测试均能通过。
处理负的 sbrk() 参数。
如果某个进程在高于使用 sbrk() 分配的虚拟内存地址上发生页面错误,则终止该进程。
正确处理 fork() 中父进程到子进程的内存复制。
处理进程将有效地址从 sbrk() 传递给系统调用(例如 read 或 write),但该地址的内存尚未分配的情况。
正确处理内存不足:如果页面错误处理程序中的 kalloc() 失败,则终止当前进程。
处理用户堆栈下无效页面的故障。
如果你的内核通过了惰性测试和用户测试,那么你的解决方案就是可以接受的
2.解答
1. 处理负的 sbrk()
参数
在 sys_sbrk()
系统调用中,检查 n
参数是否为负数。如果是负数,则需要减少进程的大小,并确保不会减少到低于初始堆栈位置。
uint64
sys_sbrk(void)
{
int n;
uint64 addr;
struct proc *p = myproc();
if(argint(0, &n) < 0)
return -1;
addr = p->sz;
if(n < 0) {
if(addr + n < 0) {
return -1; // 防止减少到负地址
}
p->sz = addr + n;
} else {
p->sz = addr + n;
}
return addr;
}
2. 处理高于 sbrk()
分配地址的页面错误
在页面错误处理程序中,检查错误地址是否在进程的合法内存范围内。如果不是,则终止进程。
void
trap(struct trapframe *tf)
{
if(tf->trapno == T_PGFLT) {
uint64 va = r_stval();
struct proc *p = myproc();
if(va >= p->sz || va < PGROUNDDOWN(p->trapframe->sp)) {
p->killed = 1;
return;
}
if(uvmshouldtouch(va)) {
uvmlazytouch(va);
}
}
}
3. 正确处理 fork()
中的内存复制
在 fork()
系统调用中,确保父进程的内存正确复制到子进程。
int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
pte_t *pte;
uint64 pa, i;
uint flags;
char *mem;
for(i = 0; i < sz; i += PGSIZE){
if((pte = walk(old, i, 0)) == 0)
continue; // 如果一个页不存在,则认为是懒加载的页,忽略即可
if((*pte & PTE_V) == 0)
continue; // 如果一个页不存在,则认为是懒加载的页,忽略即可
pa = PTE2PA(*pte);
flags = PTE_FLAGS(*pte);
if((mem = kalloc()) == 0)
goto err;
memmove(mem, (char*)pa, PGSIZE);
if(mappages(new, i, PGSIZE, (uint64)mem, flags) != 0){
kfree(mem);
goto err;
}
}
return 0;
err:
uvmunmap(new, 0, i / PGSIZE, 1);
return -1;
}
4. 处理系统调用中的未分配内存
在内核/用户之间互相拷贝数据处理程序中,如 copyout
和 copyout
,检查传递的地址是否已经分配,在每个函数加上判断条件分支。
if(uvmshouldtouch(dstva))
uvmlazytouch(dstva);
5. 处理内存不足
在页面错误处理程序中,如果 kalloc()
失败,则终止当前进程。
void
uvmlazytouch(uint64 va)
{
struct proc *p = myproc();
char *mem = kalloc();
if (mem == 0) {
printf("lazy alloc: out of memory when allocating for va %p\n", va);
p->killed = 1;
return;
}
memset(mem, 0, PGSIZE);
if(mappages(p->pagetable, PGROUNDDOWN(va), PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0) {
printf("lazy alloc: failed to map page for va %p\n", va);
kfree(mem);
p->killed = 1;
}
}
6. 处理用户堆栈下无效页面的故障
在页面错误处理程序中,检查错误地址是否在用户堆栈保护页之下。
void
trap(struct trapframe *tf)
{
if(tf->trapno == T_PGFLT) {
uint64 va = r_stval();
struct proc *p = myproc();
if(va >= p->sz || va < PGROUNDDOWN(p->trapframe->sp)) {
p->killed = 1;
return;
}
if(uvmshouldtouch(va)) {
uvmlazytouch(va);
}
}
}
五、心得体会
好饿!