lab5

21年后的lab删除了该lab

内存分配

  • 急分配:当进程请求内存时,操作系统立即为其分配内存。
  • 懒分配:当进程请求内存时,操作系统不会立即为其分配内存,而是等待内存被释放后再为其分配。

  • COPY-ON-WRITE(COW) —FORK 牛叉:当一个进程创建另一个进程的副本时,新进程会开始时共享父进程的内存页。只有在这些页被写入时,才会复制这些页。

  • Demand page:当进程请求一个不存在的页面时,操作系统会将其加载到内存中。

  • least-recently-used(LRU):LRU是一种页面替换算法,它根据页面最近被访问的时间来决定哪个页面应该被替换。

  • PTE_A 的作用:可用于时钟算法

  • memory-mapped-file:mmap系统调用

Eliminate allocation from sbrk() (easy)

老师已经在lecture上讲了,所以完成的比较轻松。

首先我们修改sys_sbrk()函数,只修改sz大小,将内存分配交给中断!

1
2
3
4
5
6
7
8
9
10
11
uint64
sys_sbrk(void)
{
int addr;
int n;
if(argint(0, &n) < 0)
return -1;
addr = myproc()->sz;
myproc()->sz = myproc()->sz + n;
return addr;
}

我们修改中断函数,在中断时,为r_stval寄存器(存储要访问内存的地址)分配页。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
else if(r_scause() == 13 || r_scause() == 15) 
{
uint64 va = r_stval();
char *mem ;
va = PGROUNDDOWN(va);

printf("page fault: %p\n",va);
if((mem = kalloc()) == 0) //懒分配,每次只分配一次页面即可
{
p->killed = 1;
}else{ //防止执行下面代码
memset(mem, 0, PGSIZE);
if(mappages(p->pagetable, va, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0)
{
kfree(mem);
p->killed = 1;
}
}
}

由于该内存可能未经使用,而我们已经修改了sz,当释放堆内存时,由于是逐页释放的,当访问到未使用的页(未分配内存),检测到有效位PTE_V未设置,触发panic,因此我们需要修改uvmunmap

1
2
3
4
5
6
7
8
void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
...
if((*pte & PTE_V) == 0)
continue;
...
}

Lazytests and Usertests

sys_sbrk我们需要处理当n为负数的情况,参考growproc(),当空出一页时要及时释放该页。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
uint64
sys_sbrk(void)
{
int addr;
int n;
struct proc *p = myproc();
if(argint(0, &n) < 0)
return -1;
addr = p->sz;

if(n >= 0){
p->sz = p->sz + n;
return addr;
}else if( addr + n > 0){
p->sz = uvmdealloc(p->pagetable, addr, p->sz + n);
}
else {
return -1;
}

trap.c里,添加对va的限制(限制在堆区)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
else if(r_scause() == 13 || r_scause() == 15) 
{
uint64 va = r_stval();
char *mem ;
va = PGROUNDDOWN(va);

// printf("page fault: %p\n",va);

if(va > p->sz ) //高于sbrk()则终止进程
{
p->killed = 1;
}
else
if (va >= p->sz || va <= PGROUNDDOWN(p->trapframe->sp)) {
p->killed = 1;
}else
if((mem = kalloc()) == 0) //懒分配,每次只分配一次页面即可
{
p->killed = 1;
}else{ //防止执行下面代码
memset(mem, 0, PGSIZE);
if(mappages(p->pagetable, va, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0)
{
kfree(mem);
p->killed = 1;
}
}
}


修改vm.cuvmunmapuvmcopy,防止在folk()中出现panic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
...
if((pte = walk(pagetable, a, 0)) == 0)
continue;
//panic("uvmunmap: walk");
if((*pte & PTE_V) == 0)
continue;
//panic("uvmunmap: not mapped");
...
}

int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
...
if((pte = walk(old, i, 0)) == 0)
continue;
//panic("uvmcopy: pte should exist");
if((*pte & PTE_V) == 0)
continue;
//panic("uvmcopy: page not present");
...
}

readwrite等系统调用中,可能会用到未实际分配内存的地址。

此时由于进入了内核,无法触发用户中断,因此我们要让该地址所在的页已分配才能正常执行这些系统调用。显然,一个个修改系统调用是不现实的。

我们知道系统调用实际是通过argaddrargint间接获得参数,且地址传入都通过argaddr函数。

因此我们可以在argaddr检测该地址所在的页是否被分配,如果未分配,则先分配再进入系统调用执行阶段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int
argaddr(int n, uint64 *ip)
{
*ip = argraw(n);
struct proc* p = myproc();
if(walkaddr(p->pagetable, *ip) == 0)
{
//地址在堆空间
if(PGROUNDUP(p->trapframe->sp) <= *ip && *ip < p->sz) {
char* pa = kalloc();
if(pa == 0)
return -1;
memset(pa, 0, PGSIZE);

if(mappages(p->pagetable, PGROUNDDOWN(*ip), PGSIZE, (uint64)pa, PTE_R | PTE_W | PTE_X | PTE_U) != 0) {
kfree(pa);
return -1;
}
} else {
return -1;
}
}
return 0;
}