通常,用户程序使用 sbrk
进行内存扩容,这里存在两个问题
- 用户频繁调用 sbrk,如果每次都进行内存分配,耗费时间
- 用户可能分配了许多,但是并不一定用这么多,导致浪费
基于这两点,懒分配就变得有必要了,当一个进程进行内存扩容时,内核仅记录其分配的大小,并不实际进行分配,只有当使用的时候,进行内存分配。
懒分配的流程
- 进程请求进行
n
个字节的内存空间扩容 - 内核修改该进程的空间大小标识
p->sz += n
,不进行实际分配 - 进程访问分配的地址
- 页表项不存在,触发中断
- 中断捕获,通过
s_tval
得知异常地址,对该地址所处的页进行内存分配 - 从中断返回,进程重新执行该指令,正常执行
xv6 懒分配实现
-
growproc
在扩容这里,取消扩容的实际实现,但是要加上限定条件,如果扩容大小超过最大虚拟地址,则返回错误,同时,为了保证应用程序的合理性,缩容不采用懒分配策略。
int
growproc(int n)
{
uint sz;
struct proc *p = myproc();
sz = p->sz;
if(n > 0){
if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
return -1;
}
} else if(n < 0){
sz = uvmdealloc(p->pagetable, sz, sz + n);
}
p->sz = sz;
return 0;
}
1 int
239 growproc(int n)
1 {
2 uint sz;
3 struct proc *p = myproc();
4
5 sz = p->sz;
6 if (n > 0) {
7 sz = sz + n;
8 if (sz > MAXVA)
9 return -1;
10 } else if (n < 0) {
11 sz = uvmdealloc(p->pagetable, sz, sz + n);
12 }
13 p->sz = sz;
14 return 0;
15 }
-
usertrap.c
在原来的基础上加上r_scause == 13 || r_scause == 15
的处理逻辑。也就是读写内存地址异常。
36 void
35 usertrap(void)
34 {
33 int which_dev = 0;
32
31 if((r_sstatus() & SSTATUS_SPP) != 0)
30 panic("usertrap: not from user mode");
29
28 // send interrupts and exceptions to kerneltrap(),
27 // since we're now in the kernel.
26 w_stvec((uint64)kernelvec);
25
24 struct proc *p = myproc();
23
22 // save user program counter.
21 p->trapframe->epc = r_sepc();
20
19 if(r_scause() == 8){
18 // system call
17
16 if(p->killed)
15 exit(-1);
14
13 // sepc points to the ecall instruction,
12 // but we want to return to the next instruction.
11 p->trapframe->epc += 4;
10
9 // an interrupt will change sstatus &c registers,
8 // so don't enable until done with those registers.
7 intr_on();
6
5 syscall();
4 } else if((which_dev = devintr()) != 0){
3 // ok
2 } else if(r_scause() == 13 || r_scause() == 15) {
1 uint64 va = PGROUNDDOWN(r_stval());
72 if (va >= p->sz) {
1 //printf("invalid address: %d\n", va);
2 p->killed = 1;
3 goto err;
4 }
5 uint64 pa = (uint64)kalloc();
6 mappages(p->pagetable, va, PGSIZE, pa, PTE_V|PTE_R|PTE_W|PTE_U);
7 } else {
8 printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
9 printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
10 p->killed = 1;
11 }
-
vm.c
关于p->sz
含义更改的一些处理
以前p->sz
代表进程实际占用的地址空间大小,而现在意味着这些地址可能未实际分配,含义的更改导致一些使用该变量的函数需要作出相应的改变。
uvmunmap
将在释放进程时调用,其释放虚拟地址的范围是p->sz
,但是p->sz
存在一些未映射的部分,所以当这些地址未映射时,不进行释放,直接跳过即可。
14 void
13 uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
12 {
11 uint64 a;
10 pte_t *pte;
9
8 if((va % PGSIZE) != 0)
7 panic("uvmunmap: not aligned");
6
5 for(a = va; a < va + npages*PGSIZE; a += PGSIZE){
4 if((pte = walk(pagetable, a, 0)) == 0)
3 continue;
2 if((*pte & PTE_V) == 0)
1 continue;
187 if(PTE_FLAGS(*pte) == PTE_V)
1 panic("uvmunmap: not a leaf");
2 if(do_free){
3 uint64 pa = PTE2PA(*pte);
4 kfree((void*)pa);
5 }
6 *pte = 0;
7 }
8 }
uvmcopy
在进程 fork 时使用,拷贝父进程的地址空间,而父进程的虚拟地址空间为 p->sz
。所以当这些地址空间有一些未实际映射时,直接跳过。
34 int
33 uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
32 {
31 pte_t *pte;
30 uint64 pa, i;
29 uint flags;
28 char *mem;
27
26 for(i = 0; i < sz; i += PGSIZE){
25 if((pte = walk(old, i, 0)) == 0)
24 continue;
23 if((*pte & PTE_V) == 0)
22 continue;
21 pa = PTE2PA(*pte);
20 flags = PTE_FLAGS(*pte);
19 if((mem = kalloc()) == 0)
18 goto err;
17 memmove(mem, (char*)pa, PGSIZE);
16 if(mappages(new, i, PGSIZE, (uint64)mem, flags) != 0){
15 kfree(mem);
14 goto err;
13 }
12 }
11 return 0;
10
9 err:
8 uvmunmap(new, 0, i / PGSIZE, 1);
7 return -1;
6 }
具体代码记录
https://github.com/merore/xv6-labs-2020/commit/a247d13e5287ae71abb708decb6af872b218a5da
网友评论