Lab: Copy-on-Write Fork for xv6
2021的lab没有了lazy lab作为COW的前置,虽然实验内容里提到了lazy lab。而且提示里少了一两条非常关键的信息,似乎默认了做过前面的lab。
在做这个lab之前先读一下fork的流程:
- 分配一个新进程
np = allocproc()
- 把整个内存空间复制给新进程
uvncopy(p->pagetable, np->pagetable, p->sz)
- 复制寄存器,设置函数返回值
*(np->trapframe) = *(p->trapframe); np->trapframe->a0=0
- 所有进程已经打开的文件,链接数+1
- 设置基础数据,名称状态父级等
这里要优化的就是第二步:复制整个内存空间的操作太大。改为懒惰机制:只在有必要的时候复制物理内存。
改写uvmcopy
uvmcopy目前的实现是直接分配一个物理内存mem=kalloc()
,然后将其映射在新进程的内存空间里。现在,不直接分配一个物理内存而是将手上的物理内存pa
映射给新进程。
两个进程前后走到的物理页的标志位里,write
位需要清除,写入RSW
位表示是因为COW产生的页面。还需要一个refcount来记录当前这个物理页被复制的次数。
改写usertrap
在上一步修改了uvmcopy的实现之后,如果试图对COW的页面写,就会触发一个pagefault exception。这个异常需要在usertrap里捕获,查表得知错误码是15或者13。
触发了异常的指令虚拟地址存放在stval
寄存器里,可以直接获得,然后通过虚拟地址获得对应的物理地址和pte。拿到之后复制对应物理页,如果物理页申请不到,直接panic。在实验里可以忽略这种情形。注意这里重新map之前需要unmap,否则会被panic掉。
接下来将原有的权限取出,加入写权限,然后将新的物理页映射给触发异常的虚拟地址。
假设只有一次fork,那么如果是父(p)进程触发了写异常,则p进程对该物理页的权限现在是RW,子(c)进程对该物理页的权限还是R。可以想到这里可以做一个简单的优化:如果触发了写异常且对应物理页的refcount=1,那么可以直接将其写权限打开,因为没有其他人看到该物理页了。不难想到这个优化不限制单次或多次fork。
物理内存refcount
需要记录每个物理地址当前被引用的次数来确保释放物理地址的时机正确:只有在一个物理地址不再有任何进程引用时可以释放。整个物理空间可以看作是一个大数组,其大小是PHYSTOP-KERNBASE
,内核加载在KERNBASE
的位置。但是出于简单考虑,可以直接用pa / PGSIZE
来定位数组,数组大小即PHYSTOP / PGSIZE
。
修改refcount的情形有:
- kalloc分配物理内存时
- kfree释放物理内存时
- uvmcopy复制内存空间时
- 初始化物理内存时
修改copyout
如果copyout中的虚拟地址是复制出来的页,则将其复制。因为copyout等价写操作。
这里需要额外判断对应页是否是用户页,是否合法等。
usertests
- 注意内核里很多时候不能简单的panic掉,需要正确的处理异常情况:例如kalloc失败时需要将用户进程kill掉(
p->killed=1
)。 - 物理内存初始化时也需要注意refcount,因为调用了kfree来初始化内存里的数据。一种做法是在kinit里先给所有物理地址的引用数置为1,然后kfree结束时就是正确的0。
- 小心修改refcount,因为多线程存在race,考虑用kmem的锁。
- 在试图复制页之前需要判断空指针,是否合法和是否是用户页。如果不满足,说明错误的捕获了一个不属于cow页的异常,需要补齐对异常的处理(kill process)。
对于第四点,需要理解代码本来的结构是:
1 | if (r_cause() == 8) {...} |
在这之前除了=8的scause都会导致用户进程被杀掉。所以是否需要复制cow的页面还要看是否空指针,是否有效页和是否用户页,否则会把其他情况带进来导致panic。