proj9.2实现了写时复制(Copy On Write,简称COW)的主要功能,为lab3高效地创建子进程打下了基础。COW有何作用?这里又不得不提前讲讲lab3中的子进程创建。不同的进程应该具有不同的物理内存空间,当用户态进程发出fork( )系统调用来创建子进程时,ucore可复制当前进程(父进程)的整个地址空间,这样就有两块不同的物理地址空间了,新复制的那一块物理地址空间分配给子进程。这种行为是非常耗时和占内存资源的,因为它需要为子进程的页表分配页面,复制父进程的每一个物理内存页。如果子进程加载一个新的程序开始执行(这个过程会释放掉原来申请的全部内存和资源),这样前面的复制工作就白做了,完全没有必要。
为了解决上述问题,ucore采用一种有效的COW机制。其设计思想相对简单:父进程和子进程之间共享(share)页面而不是复制(copy)页面。但只要页面被共享,它们就不能被修改,即是只读的。注意此共享是指父子进程共享一个表示内存空间的mm_struct结构的变量。当父进程或子进程试图写一个共享的页面,就产生一个页访问异常,这时内核就把这个页复制到一个新的页面中并标记为可写。注意,原来的页面仍然是写保护的。当其它进程试图写入时,ucore检查写进程是否是这个页面的唯一属主(通过判断page_ref 和 swap_page_count 即 mem_map 中相关 entry 保存的值的和是否为1。注意区分与share memory的差别,share memory 通过 vma 中的 shmem 实现,这样的 page 是直接标记为共享的,而不是 copy on write,所以也没有任何冲突);如果是,它把这个页面标记为对这个进程是可写的。
在具体实现上,ucore调用dup_mmap函数,并进一步调用copy_range函数来具体完成对页表内容的复制,这样两个页表表示同一个虚拟地址空间(包括对应的物理地址空间),且还需修改两个页表中每一个页对应的页表项属性为只读,但。在这种情况下,两个进程有两个页表,但这两个页表只映射了一块只读的物理内存。同理,对于换出的页,也采用同样的办法来共享一个换出页。综上所述,我们可以总结出:如果一个页的PTE属性是只读的,但此页所属的VMA描述指出其虚地址空间是可写的,则这样的页是COW页。
当对这样的地址空间进行写操作的时候,会触发do_pgfault函数被调用。此函数如果发现是COW页,就会调用alloc_page函数新分配一个物理页,并调用memcpy函数把旧页的内容复制到新页中,并最后调用page_insert函数给当前产生缺页错的进程建立虚拟页地址到新物理页地址的映射关系(即改写PTE,并设置此页为可读写)。
这里还有一个特殊情况,如果产生访问异常的页已经被换出到硬盘上了,则需要把此页通过swap_in_page函数换入到内存中来,如果进一步发现换入的页是一个COW页,则把其属性设置为只读,然后异常处理结束返回。但这样重新执行产生异常的写操作,又会触发一次内存访问异常,则又要执行上一段描述的过程了。
Page结构的ref域用于跟踪共享相应页面的进程数目。只要进程释放一个页面或者在它上面执行写时复制,它的ref域就递减;只有当ref变为0时,这个页面才被释放。