易知x86-64都实现了lazy allocation
例如malloc一块很大的内存,直到对这个内存进行写/读之前,都不会
为xv6实现懒分配lazy allocation
- lazy allocation
- 目的 : 为防止sbrk大量内存但实际上并没有使用的情况
- 实现 : 通过 虚拟内存 和 page fault handler
- 阶段1. 设定va为合法
- sbrk系统调用 : 仅仅设定user的这段va是合法的,而不分配内存建立映射。
- 阶段2. 使用va触发pagefault, 进行响应(handle)
- pagefault handelr : 为va 分配一块physical page 并 建立映射
- 阶段2中对于va
- user使用无效 userva
- MMU –检测–> trap –> pagefault handler
- kernel使用无效 userva
- argaddr 中检测 –> pagefault handelr
- user使用无效 userva
- pgfault handelr之后 返回到造成pgfault的指令继续执行.
- 阶段1. 设定va为合法
背景
- 背景:实现lazy allocation之前:也即 eager allocation
- 通过sbrk(bytes) 向上扩展heap空间
- 这意味着,当sbrk实际发生或者被调用的时候,内核会分配一些物理内存,并将这些内存映射到用户应用程序的地址空间,然后将内存内容初始化为0,再返回sbrk系统调用。这样,应用程序可以通过多次sbrk系统调用来增加它所需要的内存。类似的,应用程序还可以通过给sbrk传入负数作为参数,来减少或者压缩它的地址空间。我们暂且只关注内存增加
- 在XV6中,sbrk的实现默认是eager allocation。这表示了,一旦调用了sbrk,内核会立即分配应用程序所需要的物理内存。但是实际上,对于应用程序来说很难预测自己需要多少内存,所以通常来说,应用程序倾向于申请多于自己所需要的内存。这意味着,进程的内存消耗会增加许多,但是有部分内存永远也不会被应用程序所使用到。
- 你或许会认为这里很蠢,怎么可以这样呢?你可以设想自己写了一个应用程序,读取了一些输入然后通过一个矩阵进行一些运算。你需要为最坏的情况做准备,比如说为最大可能的矩阵分配内存,但是应用程序可能永远也用不上这些内存,通常情况下,应用程序会在一个小得多的矩阵上进行运算。所以,程序员过多的申请内存但是过少的使用内存,这种情况还挺常见的。
- 原则上来说,这不是一个大问题。但是使用虚拟内存和page fault handler,我们完全可以用某种更聪明的方法来解决这里的问题,这里就是利用lazy allocation。
下面 称 未建立映射的va 为
只设定为合法,但是 在pgtbl中没有相应pte,或者相应的pte并没有指向物理内存
part1 and part2
- mit教授课上做的就是
lazy alloc 核心思想
lazy allocation的核心思想很简单,如下。可分为两个阶段。
- 阶段1. 设定va为合法
- 通过sbrk系统调用
- 设定user的这段va是合法的。通过将p->trapframe->sz增加来达成这一目的。仅仅做了这一件事。根本就没在user pgtbl上登记有关va的任何信息,也没分配physical page。
- 通过sbrk系统调用
- 阶段2. 使用va,触发pagefault 进行响应(handle)
- 关于如何对page fault响应,大致来说就是
- 为va 分配一块physical page
- 将该page与触发page fault的va 在 user pgtbl中建立映射。
- 重新执行造成 page fault的指令(如load/store)
- 具体来讲,其阶段二也可分为两部分
- user使用无效 userva
- kernel使用无效 userva
- 关于如何对page fault响应,大致来说就是
- 阶段1. 设定va为合法
kernel识别page fault 并进行响应 ((pagefault handler)lazy allocation的所需 )的有效信息
- 1. 触发page fault的va
- ($STVAL)
- 2. 进入trap(引起page fault的)原因类型
- ($SCAUSE)
- 比如从user进入usertrap可能有多种原因,而属于pagefault造成的进入kernel的只有三种:instruction page fault(12) , load page fault (13), store page fault(15)
- 3. 触发page fault的指令
- ($SEPC) ,保存在trapframe->epc中
- 1. 触发page fault的va
code
- 更改code如下:
- usertrap中增加handle page fault的分支(通过检测r_scause)
- growproc:
- sys_sbrk -> growproc。原先是立刻分配sz大小的物理内存,现在改成只增长heap顶部的上界,也即将va设定为”合法”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// 只有sbrk会调用到这里
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;
// }
// 只设定合法但不kalloc 也不建立映射
sz += n;
} else if(n < 0){
sz = uvmdealloc(p->pagetable, sz, sz + n);
}
p->sz = sz;
return 0;
}
- sys_sbrk -> growproc。原先是立刻分配sz大小的物理内存,现在改成只增长heap顶部的上界,也即将va设定为”合法”。
- isvalidva_proc
- 检验va是否是应当进行lazy allocation的va(即是否是合法的va)(va应当是heap段的va)
1
2
3
4
5
6
7
8
9uint64 isvalidva_proc(uint64 va)
{
// Handle faults on the invalid page below the user stack.
// Kill a process if it page-faults on a virtual memory address higher than any allocated with sbrk().
struct proc *p = myproc();
if(va <= p->trapframe->sp || va >= p->sz) return 0;
// p->trapframe->sp trap时保留的用户栈顶
return 1;
}
- 检验va是否是应当进行lazy allocation的va(即是否是合法的va)(va应当是heap段的va)
- uvmlazyalloc
- 对于合法但未分配物理内存和建立映射的虚拟地址va,进行lazyalloc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// 对于合法但未分配物理内存和建立映射的虚拟地址va,进行lazyalloc
void uvmlazyalloc(uint64 va)
{
struct proc *p = myproc();
va = PGROUNDDOWN(va);
// 为va申请对应的physical mem
uint64 pa = (uint64) kalloc();
// oom
if(pa == 0){
p->killed = 1;
}
// 申请成功 建立映射
else{
memset((void*)pa,0,PGSIZE);
// 映射
if(mappages(p->pagetable,va,PGSIZE,pa,PTE_W|PTE_R|PTE_U)!=0){
// 映射失败 释放pm
kfree((void*)pa);
p->killed = 1;
}
}
}
- 对于合法但未分配物理内存和建立映射的虚拟地址va,进行lazyalloc
- uvmunmap
- 防止释放合法但还没分配物理内存建立映射的va
part3
- uvmcopy
- fork时会调用uvmcopy,将parent process的heap段虚拟内存对应的pte拷贝给child的pte,并且还有分配物理内存去映射。显然引入lazy之后就不能这么干了。因为很可能parent的这些虚拟内存还只不过是还未建立映射的地址。自然不应该拷贝。
bug 及 解决
以上这些函数都没花多长时间就改完了。。下面这个改了好长时间:
一直卡在sbrkarg test。
由lazy alloc 核心思想中所述,lazy alloc可分为两个阶段:1. 设定va为合法 和 2. 触发page fault并进行响应。
可具体来说,第二阶段触发page fault并进行响应可分为两种情况 — 根据在user态还是kernel态触发page fault 来分类。具体如下所示
page fault 的触发!
思考问题:user态触发page fault和kernel触发有什么区别?触发后会跳转到哪里?user会进入usertrap,那么kernel会进入kernel trap吗?找到造成缺页的指令是哪条?是哪里检测出了page fault ? 当时这里没搞清晰,改bug改了一上午。见下。
- 一开始误认为kernel 使用user传入的无效va,也会进入kernel trap(实际上不会)。结果在kernel trap里写了个page fault分支,没有反应。一直卡在sbrkarg。当时debug的时候发现会进入filewrite。改了蛮久才找出来。
user态和kernel态使用无效的user va所发生的反应不同,一个触发 fault,一个没有。关键是在二者使用user va的方式不同,一个是MMU硬件查找,一个是walk模拟。
user 态触发 page fault
- user态使用了一个user 无效的va , 会导致进入usertrap 。原因见下
- user态发生的page fault (我认为)应当是由硬件MMU检测到的
- 为什么?因为此时cpu的$satp 是 user的pgtbl。此时是MMU来查找user pgtbl并返回pa的
- 比如user先p = sbrk(PGSIZE); 然后赋值 *p = 100 ;
- p是一个还没建立映射的虚拟地址
- MMU根据va查看user的pgtbl后,发现pte是个空或者无效则会爆出pagefault,设置scause。而后进入trampoline,进入usertrap。
- 所以我们在usertrap处写一个page fault分支,是用于处理user态造成的page fault的。对于kernel态的,无能为力。
- user态发生的page fault (我认为)应当是由硬件MMU检测到的
kernel 态触发page fault(实际上并没有page fault)
- kernel态使用了一个user 无效的va ,并不会进入kerneltrap。原因见下。
- kernel使用user态无效的虚拟地址 发生在如下情况(简易取一种情况)
- user将user va 通过syscall传给kernel和kernel对user va的使用流程:举例write
- user va = sbrk(xxxx);
- write –user va–> trampoline -> usertrap —> syscall -> sys_write —-取出user va—>filewrite —> either_copyin –> copyin –user va—> walkaddr
- user va有效 —> 使用user va,读取
- user va无效(未建立映射的、等待lazyalloc的地址)–> 不读取,放弃copyin,return -1。 而后一层层return -1给sys_write。回到sycall,将-1作为系统调用返回值返回给user。
- 由此观之,也即,当kernel使用未建立映射的user va时,即userpgtbl中user va对应的pte无效或不存在时,放弃使用(读取/写入)改user va,并return -1(fail)
解决
当kenerl拿到user传入给kernel的user va时,就使得va进行lazyalloc的第二步:分配物理内存并建立映射。
argaddr
- 检验user va是否是heap上的user va,是没建立映射(防止重复建立映射)
- uvmlazyalloc。为user va进行lazyalloc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// Retrieve an argument as a pointer.
// Doesn't check for legality, since
// copyin/copyout will do that.
int
argaddr(int n, uint64 *ip)
{
// *ip为user传入的user空间的虚拟addr
*ip = argraw(n);
// 防止不是heap上懒分配的page
if(!isvalidva_proc(*ip)) return 0;
// 防止重复分配physical memory、重复mappage建立映射
pte_t *pte = walk(myproc()->pagetable,*ip,1);
// 当当前要使用的user的虚拟地址是一个未建立映射的虚拟地址(等待懒分配的),那么就lazyalloc它
if((pte == 0 || (*pte & PTE_V) == 0)){
printf("shc syscall leads to kernel page fault!\n");
uvmlazyalloc(*ip);
}
return 0;
}
成功pass sbrkarg
之前没改这里之前,一直卡在sbrkarg
- 由上述解释可知,在没改之前,write会返回-1,故sbrktest会出现write sbrk failed。符合预期。
改完!结束!
补充:write使用va流程图 不看也tm ok
唠叨文字版:user sbrk了一个地址 然后user使用系统调用write 并传入这个地址,比如write(fd, buf, sizeof(buf))(可以看sbrkarg,就是test这个的)。buf即为一个user态没建立映射的虚拟地址,kernel会使用(读取)这个addr。
从trampoline 进入 usertrap 进入syscall分支 进入sys_write ,在这里通过argaddr取出user传入的地址,之后调用filewrite使用了取出的user的va 。filewrite里又调用writei,wirtei调用either_copyin,either_copyin调用copyin(user的va作为srcva传入该函数)。copyin中通过walkaddr软件模拟来使用user的va,如果这个va有效,那么得到pa,然后使用pa进行一个写入。如果va无效,那么walkaddr查找pgtbl 就会没找到有效pte,就会返回0。
copyin发现pa为0,则return -1代表写入失败。顺着刚才描述的栈帧,一层层向上return,最后return -1给filewrite,然后return-1给write,回到sycall。存到trapframe,然后作为系统调用返回值返回给user层。
- 下面这没啥p用,就是印证下理论
- 可以看到如果不处理,就会死循环
- 不断缺页 不断进入usertrap.c这里的 pagefault分支
- 因为这个pagefault分支并没有对缺页做任何处理 只是直接返回到造成缺页的指令 不断执行这条指令 不断造成缺页。
理论比较
- 和csapp上的理论上的page fault比较一下(linux也是csapp这么实现的)(图中也并没有画出kenerl如何使用无效va,只有user的pagefault 不过也正常,因为那造成的也不是page fault)
- 相同之处 在于 当user引用为建立映射的va时,都会在dram中分配一个physical page ,去给userpgtbl建立va到pa的映射。
- 不同之处在于并没有实现和磁盘的交换操作,只是将dram中的physical page映射给了user va。
- 缺憾在于这样在dram满了的时候就无法再给user va 分配映射的物理内存了