首先切换分支到cow
git checkout cow
make clean
Implement copy-on write
实现写时复制,为了测试方案,以及提供了一个cowtest的xv6程序,位于user/cowtest.c当中
课程给了一个合理的攻克计划:
-
修改
uvmcopy()
将父进程的物理页映射到子进程而不是分配新页面,在子进程和父进程的PTE中清除PTE_W标志 -
修改
usertrap()
以识别页面错误,当COW页面出现页面错误时,使用kalloc()
分配一个新页面,并将旧页面复制到新页面,然后将新页面添加到PTE中并设置PTE_W
-
确保每个物理页在最后一个PTE对它的引用撤销时被释放——而不是在此之前。这样做的一个好方法是为每个物理页保留引用该页面的用户页表数的“引用计数”。当kalloc()分配页时,将页的引用计数设置为1。当fork导致子进程共享页面时,增加页的引用计数;每当任何进程从其页表中删除页面时,减少页的引用计数。kfree()只应在引用计数为零时将页面放回空闲列表
-
修改
copyout()
在遇到COW页面时使用与页面错误相同的方案。
1.首先是每个PTE的布局如下,有两个保留位,可以选取一个保留位用来定义页面是否是COW Fork页面的标志位
kernel/riscv.h
#define PTE_F (1L << 8)
2.构造COW物理结构的引用计数结构,使用计数结构体数组,数组的容量设置成(PHYSTOP-KERNBASE)>>12
#define KERNBASE 0x80000000L //2g
#define PHYSTOP (KERNBASE + 128*1024*1024) //128MB
一个物理页是4096字节,也就是右移12位的事情
为什么是内核大小的物理页数,因为物理内存和vm虚拟内存映射到物理内存的事情都是kernel在管理,故取内核空间大小肯定足够
同时引用计数数组是一个全局数组,那么多进程就有可能对同一个父进程进行fork()等操作,所以需要进行一致性保护,因为操作时间会比较短,此处考虑使用strucct spinlock
3.操作引用计数数组的函数increfcnt()和
decrefcnt() (kernel/kalloc.c)
同时在kernel/def.h中添加函数原型
// cow.c - lab6
void increfcnt(uint64 pa);
uint8 decrefcnt(uint64 pa);
4.修改uvmcopy()函数
mmppages函数进行虚拟地址与物理地址的映射,而uvmunmap函数解除映射同时回收物理页(最后一个参数为1的话)
5.修改usertrap() 和 copyout() 两个函数,对COW的页面进行处理,当需要写时解除共享映射关系,分配新页面并映射同时将PTE_COW设置成0,PTE_W设置成1,考虑到两处处理一致,故封装一个新函数专门处理
walkaddr是用来通过虚拟地址拿映射的物理地址的,那么可以将其更改为若是PTE_COW有效,就分配新页面(这里面引用计数要+1)同时将PTE_COW设置成无效,PTE_W设置成有效
当然上述这个新加的walkcowaddr()需要到defs.h里面放函数原型
uint64 walkcowaddr(pagetable_t, uint64);
6.更改usertrap()(kernel/trap.c)和copyout() (kernel/vm.c)调用walkcowaddr()
多加一个判断条件,当然不像懒分配要判断读或者写,此处只考虑 r_scause()==15
的条件, 因为只有在 store 指令写操作时触发 page fault 才考虑 COW 机制,而懒分配是读写都需要考虑的
copyout()就更容易了,只需要将walkaddr()改成walkcowaddr()即可
7.就像上面第五点说的引用计数相关的要调用到,在kalloc()分配物理页给进程的时候需要增加该物理页的引用计数
8.与kalloc对应的就是kfree,当真正回收之前需要判断引用计数是否为0,若不为0,则只需要--就可以了
这也就是为什么decrefcnt设置了返回值,因为减了一次之后需要判断
9.还有一个隐藏需要修改的就是kernel/kalloc.c
中的 freerange()
函数. 该函数被 kinit()
函数(初始内存分配器)调用, 其主要作用就是对物理内存空间中未使用的部分以物理页划分调用 kfree()
将其添加至 kmem.freelist
中.初始引用计数字段为0,若是都kfree了一下就会变成255,所以需要先加回来
测试