xv6 lab6 cow

📅 2026/7/6 13:36:34
xv6 lab6 cow
0.前置cow核心思想fork时不拷贝物理页父子共享一份内存当其中一方尝试写入共享页的时候再临时分配新页拷贝数据让双方重新拥有独立副本。cow实现要点fork出的子进程页表映射的是父进程物理地址且俩方页表项都设置不可写并分配一个权限位表示是否位于cow页面当子进程需要对物理页进行修改时会触发异常usertrap分支处理为子进程分配真实物理页并修改子进程页表项为可写异常返回可修改物理内容。释放物理页由于父子进程共享物理内存需加个对物理页面引用量为0时才真正释放物理地址。[ 用户态写入只读共享页 ]硬件触发 Trap[ trampoline.S 中的 uservec ] (保存用户寄存器切页表/栈)[ trap.c 中的 usertrap() ] (识别到 cause 13/15缺页异常)[ kalloc.c 中的 cowpage() ] (验证该地址是不是合法的 COW 页判断该进程是否fork)[ kalloc.c 中的 cowalloc() ] (核心处理分配新物理页、拷贝数据、改页表)物理内存管理 (kalloc.c)- kalloc() (申请一块新 4KB 物理页)- memmove() (将旧物理页的数据完全复制到新物理页)- walk() / mappages() (在当前进程页表里将该 va 映射到新物理页)- kfree() / mem_ref 递减 (将旧物理页的引用计数减 1[ 用户态执行系统调用 read(fd, buf, len) ]1. 正常的系统调用陷入内核[ usertrap() - syscall() - sys_read() ][ sys_read() ] (读取磁盘/管道数据准备写回用户缓冲区 buf)[ copyout(pagetable, buf, data, len) ] (修改发生在这里)[ 用户态调用 fork() ]系统调用陷入内核态 (ecall - usertrap - syscall)[ sys_fork() ] (kernel/sysproc.c - 系统调用入口)[ fork() ] (kernel/proc.c - 进程创建核心函数)-- [ allocproc() ] (分配一个新的 struct proc pcb以及 trapframe)-- [ proc_pagetable() ] (为子进程创建空的新页表目录)-- [ uvmcopy() ] (重点修改点建立共享而不是拷贝物理内存)uvmcopy() 内部的精细物理操作 (kernel/vm.c)- walk() (查找父进程页表中的物理地址 pa 和原始标志位 flags)- mappages() (将子进程新页表也指向这个相同的 pa 物理页)- add_ref(pa) (重点修改点物理页引用计数加 1)- 修改父子进程页表项 (PTE) 的权限清除 PTE_W开启 PTE_COW实验步骤修改uvmcopy()以将父进程的物理页面映射到子进程的页面而不是为子进程分配新物理页面。同时在子进程和父进程的PTE中清除PTE_W修改usertrap()以识别页面错误当需要写时复制的页面发生错误的时候使用kalloc()为其分配一个新页面并将旧页面复制到新页面中并设置到pte修改pte的映射地址为新页面同时PTE_W位设置为1确保在对每个物理页面的最后一个PTE引用消失的时候释放物理页面。所以需要为每一个物理页面设置一个计数器。分配物理页面kalloc的时候将页面的引用计数设置为1。但fork导致父子进程共享页面的时候增加该物理页面的计数进程消亡的时候减少页面的计数只有当页面的引用计数为零的时候kfree()才真正地释放物理页面。可用采用一个固定大小的数组来保存每个物理页面的引用数修改copyout()由于该函数把内核物理地址中的内容复制到用户物理地址中对于COW页面需要为该用户分配新物理页面并建立映射关系和设置权限位将内核物理地址的内容写入到新分配的物理页面中。1.定义cow标志位在riscv.h#define PTE_COW (1L 8) //PTE的倒数第8位设置为cow标志位2.定义结构体用于标记物理页面引用数kalloc.c//记录每个物理页的引用次数 struct { struct spinlock lock; //自旋锁互斥 int cnt[PHYSTOP/PGSIZE]; //数组大小为最大内存/每页大小 }mem_ref;3.kalloc函数生成物理页面需将引用数1kfree需-1为0时才能释放物理页freerange是初始化里的调用需提前将引用数置1再调用kfreevoid kinit() { initlock(kmem.lock, kmem); initlock(mem_ref.lock,mem_ref);//初始化mem_ref自旋锁 freerange(end, (void*)PHYSTOP); } void //作用释放起始物理地址到终止地址的页面 freerange(void *pa_start, void *pa_end) { char *p; p (char*)PGROUNDUP((uint64)pa_start); for(; p PGSIZE (char*)pa_end; p PGSIZE){ acquire(mem_ref.lock); mem_ref.cnt[(uint64)p/PGSIZE] 1;//初始化每个物理页的引用次数为1 release(mem_ref.lock); kfree(p); } } void kfree(void *pa) { struct run *r; int should_free 0; if(((uint64)pa % PGSIZE) ! 0 || (char*)pa end || (uint64)pa PHYSTOP) panic(kfree); // 锁内完成递减计数 判断是否真正释放 acquire(mem_ref.lock); --mem_ref.cnt[(uint64)pa / PGSIZE]; if(mem_ref.cnt[(uint64)pa / PGSIZE] 0){ should_free 1; } release(mem_ref.lock); if(should_free 1){ // 计数真的为0才回收物理页 memset(pa, 1, PGSIZE); r (struct run*)pa; acquire(kmem.lock); r-next kmem.freelist; kmem.freelist r; release(kmem.lock); } } void * kalloc(void) { struct run *r; acquire(kmem.lock); r kmem.freelist; if(r){ kmem.freelist r-next; } release(kmem.lock); // 释放kmem锁后再设置引用计数 if(r){ acquire(mem_ref.lock); mem_ref.cnt[(uint64)r / PGSIZE] 1; release(mem_ref.lock); memset((char*)r, 5, PGSIZE); } return (void*)r; }4.添加辅助函数用于在trap分支下的判断是否cow页及为cow页分配新物理页并将其添加到defs.h/辅助函数判断此页面是否为cow页是则返回0 int cowpage(pagetable_t pagetable, uint64 va){ if (va MAXVA) return -1; pte_t* pte walk(pagetable, va, 0); if (pte 0) return -1; if ((*pte PTE_V) 0) return -1; return(*pte PTE_COW ? 0 : -1); } //为cow页面分配新的物理页并将原来的内容复制到新的物理页中 void* cowalloc(pagetable_t pagetable, uint64 va){ if(va % PGSIZE ! 0) return 0; uint64 pa walkaddr(pagetable, (uint64)va); //cow的物理页面 if (pa 0) return 0; pte_t* pte walk(pagetable, (uint64)va, 0); //cow页的pte页表项 //查看该物理页面引用数 acquire(mem_ref.lock); uint16 mem_ref_cnt mem_ref.cnt[(uint64)pa / PGSIZE]; release(mem_ref.lock); //如果该物理页面引用数为1说明并没有fork进程共享了将pte的PTE_W设置为1PTE_COW设为0 if(mem_ref_cnt 1){ *pte (*pte | PTE_W) ~PTE_COW; //按位或将写位置1按位与非将cow位置0 return (void*)pa; }else{ uint64 new_pa (uint64)kalloc();//分配新物理页 if(new_pa 0) return 0; // 内存耗尽保护 memmove((void*)new_pa, (void*)pa, PGSIZE);//旧物理页内容拷贝新物理页 // 1. 保留原本的标志位同时清除 PTE_COW开启 PTE_W 和 PTE_V uint flags PTE_FLAGS(*pte); flags ~PTE_COW; flags | PTE_W; flags | PTE_V; // 确保有效位开启 // 2. 将新的物理地址 new_pa 写入 pte *pte PA2PTE(new_pa) | flags; kfree((void*)pa);//原物理页引用数量-1 return (void*)new_pa; } } //返回现有pa对应物理页面引用数 int krefcnt(void* pa){ return mem_ref.cnt[(uint64)pa / PGSIZE]; } //增加物理页面引用数 int kaddrefcnt(void* pa){ if(((uint64)pa % PGSIZE) ! 0 || (char*)pa end || (uint64)pa PHYSTOP) //物理页地址不合法 panic(kaddrefcnt); acquire(mem_ref.lock); mem_ref.cnt[(uint64)pa / PGSIZE]; release(mem_ref.lock); return 0; }5.修改uvmcopy在创建子进程调用其函数父子PTE都改为只读cow页不实际拷贝物理页面int uvmcopy(pagetable_t old, pagetable_t new, uint64 sz) { pte_t *pte; uint64 pa, i; uint flags; //char *mem; //pte_t *new_pte; for(i 0; i sz; i PGSIZE){ if((pte walk(old, i, 0)) 0) panic(uvmcopy: pte should exist); if((*pte PTE_V) 0) panic(uvmcopy: page not present); pa PTE2PA(*pte); flags PTE_FLAGS(*pte); //父子都要改成只读 COW标记 if(flags PTE_W){ flags (flags ~PTE_W) | PTE_COW; *pte PA2PTE(pa) | flags; // 关键更新父进程页表权限 } if(mappages(new, i, PGSIZE, (uint64)pa, flags) ! 0){ // kfree(mem); goto err; } if (kaddrefcnt((void*)pa) -1) //页计数指针 { goto err; } } return 0;6.copyout是内核态下的内核数据拷贝到用户态页面增加判断条件int copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len) { uint64 n, va0, pa0; while(len 0){ va0 PGROUNDDOWN(dstva); pa0 walkaddr(pagetable, va0); // 如果va0是cow页面分配新的物理页并将原来的内容复制到新的物理页中 if(cowpage(pagetable, va0) 0){ if ((pa0 (uint64)cowalloc(pagetable, va0)) 0) return -1; } if(pa0 0) return -1; n PGSIZE - (dstva - va0); if(n len) n len; memmove((void *)(pa0 (dstva - va0)), src, n); len - n; src n; dstva va0 PGSIZE; } return 0; }6.修改usrtrap下的缺页异常判断syscall(); }else if(r_scause() 15 || r_scause() 13){ uint64 va r_stval(); //获取缺页异常的虚拟地址 if(va p-sz || cowpage(p-pagetable, va) ! 0 || cowalloc(p-pagetable, PGROUNDDOWN(va)) 0) //如果va不在用户空间或者va不是cow页或者cowalloc失败 p-killed 1; }else if((which_dev devintr()) ! 0){锁问题总结kfree之前写法将「引用计数递减」和「判断计数是否为 0、是否释放物理页」拆成了两步中间释放了自旋锁acquire(mem_ref.lock); --mem_ref.cnt[(uint64)pa/PGSIZE]; // 1. 锁内递减 release(mem_ref.lock); // 中间释放锁 if(mem_ref.cnt[(uint64)pa/PGSIZE] 0){ // 2. 锁外读取判断 // 真正释放物理页 }引用计数的「读 - 减 - 判断」是一个复合原子操作必须全程在锁的保护下完成。中间释放锁后其他 CPU 可以同时修改同一个计数导致判断结果完全错误。最初在持有kmem.lock空闲链表锁的同时又去申请mem_ref.lock引用计数锁形成锁嵌套。存在死锁风险。acquire(kmem.lock); r kmem.freelist; if(r){ acquire(mem_ref.lock); // 持有A锁时申请B锁 mem_ref.cnt[(uint64)r/PGSIZE] 1; release(mem_ref.lock); kmem.freelist r-next; } release(kmem.lock);如果某段代码先拿mem_ref.lock再拿kmem.lock和这里的顺序相反两个 CPU 各持一把锁等对方就会永久死锁。操作系统中锁嵌套必须严格遵循统一的加锁顺序。