Lab6: Copy-on-Write Fork for xv6

跟着提示一步一步来

(1).kernel/riscv.h中选取PTE中的保留位定义标记一个页面是否为COW Fork页面的标志位

  1. // 记录应用了COW策略后fork的页面
  2. #define PTE_F (1L << 8)

(2).kalloc.c中进行如下修改

  • 定义引用计数的全局变量ref,其中包含了一个自旋锁和一个引用计数数组,由于ref是全局变量,会被自动初始化为全0。

    这里使用自旋锁是考虑到这种情况:进程P1和P2共用内存M,M引用计数为2,此时CPU1要执行fork产生P1的子进程,CPU2要终止P2,那么假设两个CPU同时读取引用计数为2,执行完成后CPU1中保存的引用计数为3,CPU2保存的计数为1,那么后赋值的语句会覆盖掉先赋值的语句,从而产生错误

  1. struct ref_stru {
  2. struct spinlock lock;
  3. int cnt[PHYSTOP / PGSIZE]; // 引用计数
  4. } ref;
  • kinit中初始化ref的自旋锁
  1. void
  2. kinit()
  3. {
  4. initlock(&kmem.lock, "kmem");
  5. initlock(&ref.lock, "ref");
  6. freerange(end, (void*)PHYSTOP);
  7. }
  • 修改kallockfree函数,在kalloc中初始化内存引用计数为1,在kfree函数中对内存引用计数减1,如果引用计数为0时才真正删除
  1. void
  2. kfree(void *pa)
  3. {
  4. struct run *r;
  5. if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
  6. panic("kfree");
  7. // 只有当引用计数为0了才回收空间
  8. // 否则只是将引用计数减1
  9. acquire(&ref.lock);
  10. if(--ref.cnt[(uint64)pa / PGSIZE] == 0) {
  11. release(&ref.lock);
  12. r = (struct run*)pa;
  13. // Fill with junk to catch dangling refs.
  14. memset(pa, 1, PGSIZE);
  15. acquire(&kmem.lock);
  16. r->next = kmem.freelist;
  17. kmem.freelist = r;
  18. release(&kmem.lock);
  19. } else {
  20. release(&ref.lock);
  21. }
  22. }
  23. void *
  24. kalloc(void)
  25. {
  26. struct run *r;
  27. acquire(&kmem.lock);
  28. r = kmem.freelist;
  29. if(r) {
  30. kmem.freelist = r->next;
  31. acquire(&ref.lock);
  32. ref.cnt[(uint64)r / PGSIZE] = 1; // 将引用计数初始化为1
  33. release(&ref.lock);
  34. }
  35. release(&kmem.lock);
  36. if(r)
  37. memset((char*)r, 5, PGSIZE); // fill with junk
  38. return (void*)r;
  39. }
  • 添加如下四个函数,详细说明已在注释中,这些函数中用到了walk,记得在defs.h中添加声明,最后也需要将这些函数的声明添加到defs.h,在cowalloc中,读取内存引用计数,如果为1,说明只有当前进程引用了该物理内存(其他进程此前已经被分配到了其他物理页面),就只需要改变PTE使能PTE_W;否则就分配物理页面,并将原来的内存引用计数减1。该函数需要返回物理地址,这将在copyout中使用到。
  1. /**
  2. * @brief cowpage 判断一个页面是否为COW页面
  3. * @param pagetable 指定查询的页表
  4. * @param va 虚拟地址
  5. * @return 0 是 -1 不是
  6. */
  7. int cowpage(pagetable_t pagetable, uint64 va) {
  8. if(va >= MAXVA)
  9. return -1;
  10. pte_t* pte = walk(pagetable, va, 0);
  11. if(pte == 0)
  12. return -1;
  13. if((*pte & PTE_V) == 0)
  14. return -1;
  15. return (*pte & PTE_F ? 0 : -1);
  16. }
  17. /**
  18. * @brief cowalloc copy-on-write分配器
  19. * @param pagetable 指定页表
  20. * @param va 指定的虚拟地址,必须页面对齐
  21. * @return 分配后va对应的物理地址,如果返回0则分配失败
  22. */
  23. void* cowalloc(pagetable_t pagetable, uint64 va) {
  24. if(va % PGSIZE != 0)
  25. return 0;
  26. uint64 pa = walkaddr(pagetable, va); // 获取对应的物理地址
  27. if(pa == 0)
  28. return 0;
  29. pte_t* pte = walk(pagetable, va, 0); // 获取对应的PTE
  30. if(krefcnt((char*)pa) == 1) {
  31. // 只剩一个进程对此物理地址存在引用
  32. // 则直接修改对应的PTE即可
  33. *pte |= PTE_W;
  34. *pte &= ~PTE_F;
  35. return (void*)pa;
  36. } else {
  37. // 多个进程对物理内存存在引用
  38. // 需要分配新的页面,并拷贝旧页面的内容
  39. char* mem = kalloc();
  40. if(mem == 0)
  41. return 0;
  42. // 复制旧页面内容到新页
  43. memmove(mem, (char*)pa, PGSIZE);
  44. // 清除PTE_V,否则在mappagges中会判定为remap
  45. *pte &= ~PTE_V;
  46. // 为新页面添加映射
  47. if(mappages(pagetable, va, PGSIZE, (uint64)mem, (PTE_FLAGS(*pte) | PTE_W) & ~PTE_F) != 0) {
  48. kfree(mem);
  49. *pte |= PTE_V;
  50. return 0;
  51. }
  52. // 将原来的物理内存引用计数减1
  53. kfree((char*)PGROUNDDOWN(pa));
  54. return mem;
  55. }
  56. }
  57. /**
  58. * @brief krefcnt 获取内存的引用计数
  59. * @param pa 指定的内存地址
  60. * @return 引用计数
  61. */
  62. int krefcnt(void* pa) {
  63. return ref.cnt[(uint64)pa / PGSIZE];
  64. }
  65. /**
  66. * @brief kaddrefcnt 增加内存的引用计数
  67. * @param pa 指定的内存地址
  68. * @return 0:成功 -1:失败
  69. */
  70. int kaddrefcnt(void* pa) {
  71. if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
  72. return -1;
  73. acquire(&ref.lock);
  74. ++ref.cnt[(uint64)pa / PGSIZE];
  75. release(&ref.lock);
  76. return 0;
  77. }
  • 修改freerange
  1. void
  2. freerange(void *pa_start, void *pa_end)
  3. {
  4. char *p;
  5. p = (char*)PGROUNDUP((uint64)pa_start);
  6. for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE) {
  7. // 在kfree中将会对cnt[]减1,这里要先设为1,否则就会减成负数
  8. ref.cnt[(uint64)p / PGSIZE] = 1;
  9. kfree(p);
  10. }
  11. }

(3). 修改uvmcopy,不为子进程分配内存,而是使父子进程共享内存,但禁用PTE_W,同时标记PTE_F,记得调用kaddrefcnt增加引用计数

  1. int
  2. uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
  3. {
  4. pte_t *pte;
  5. uint64 pa, i;
  6. uint flags;
  7. for(i = 0; i < sz; i += PGSIZE){
  8. if((pte = walk(old, i, 0)) == 0)
  9. panic("uvmcopy: pte should exist");
  10. if((*pte & PTE_V) == 0)
  11. panic("uvmcopy: page not present");
  12. pa = PTE2PA(*pte);
  13. flags = PTE_FLAGS(*pte);
  14. // 仅对可写页面设置COW标记
  15. if(flags & PTE_W) {
  16. // 禁用写并设置COW Fork标记
  17. flags = (flags | PTE_F) & ~PTE_W;
  18. *pte = PA2PTE(pa) | flags;
  19. }
  20. if(mappages(new, i, PGSIZE, pa, flags) != 0) {
  21. uvmunmap(new, 0, i / PGSIZE, 1);
  22. return -1;
  23. }
  24. // 增加内存的引用计数
  25. kaddrefcnt((char*)pa);
  26. }
  27. return 0;
  28. }

(4). 修改usertrap,处理页面错误

  1. uint64 cause = r_scause();
  2. if(cause == 8) {
  3. ...
  4. } else if((which_dev = devintr()) != 0){
  5. // ok
  6. } else if(cause == 13 || cause == 15) {
  7. uint64 fault_va = r_stval(); // 获取出错的虚拟地址
  8. if(fault_va >= p->sz
  9. || cowpage(p->pagetable, fault_va) != 0
  10. || cowalloc(p->pagetable, PGROUNDDOWN(fault_va)) == 0)
  11. p->killed = 1;
  12. } else {
  13. ...
  14. }

(5).copyout中处理相同的情况,如果是COW页面,需要更换pa0指向的物理地址

  1. while(len > 0){
  2. va0 = PGROUNDDOWN(dstva);
  3. pa0 = walkaddr(pagetable, va0);
  4. // 处理COW页面的情况
  5. if(cowpage(pagetable, va0) == 0) {
  6. // 更换目标物理地址
  7. pa0 = (uint64)cowalloc(pagetable, va0);
  8. }
  9. if(pa0 == 0)
  10. return -1;
  11. ...
  12. }