在linux系统中通过系统调用fork/clone来创建一个新的进程,创建进程的过程中根据clone flags会选择复制资源还是公用一份资源,通常资源包括:打开的文件files_struct,文件系统fs_struct,信号处理signal,内存资源mm_struct,其中mm_struct不仅代表虚拟地址空间而且还有关联的物理内存。
早期linux中fork会复制父进程中所有内存的资源:mm_struct,VMA和页表项,物理内存,但是大部分子进程不执行exec加载新的程序,父子进程共享大部分地址空间和数据,另外复制过程的代价相当昂贵,物理内存资源紧张,后来发明了COW机制:fork的时候只复制mm_struct和vma,但是不申请新的物理内存和复制内存内容,也就是他们的页表项中指向同一份物理资源。这样就有个问题,进程中有数据段,如果一直共用一份物理内存这样就不能实现进程间地址空间隔离,所以需要对他们私有的数据进行分离,也就是可能发生写操作的区域,分离的时机取决于写操作发生的时候,这就是Copy On Write操作。为了实现这样COW机制,它利用MMU的缺页异常,将父子进程的页表项都置为只读,当写操作发生的时候因为权限异常触发MMU异常,CPU在尝试修复异常的时候,发现如下特征就会当做COW来修复,此时实现物理内存资源的分离。
- 页表项存在,但是是只读,发生的错误为写:PF_WRITE
- 发生的错误地址位于进程的虚拟地址空间vma中,且区间的权限允许写操作
所以COW机制分为两部分:fork时资源复制时复制页表项并且将其都设置为只读,缺页异常中识别COW引发的错误并实际分配资源实现地址空间隔离。
fork时资源复制
fork过程中复制内存资源,首先分配并且复制mm_struct内容,之后将其中统计字段清零。之后遍历mm_struct->mmap链表,复制其中每一个区域,重点在于虚拟内存区间对应的页表项。复制页表项的过程中可以看到从最顶级的pgd->pud->pmd->pte逐级遍历,如果对应的页表项存在的话就向下逐级遍历直到pte级别。如果页表只有两级pgd和pte时,拷贝pud和pmd的过程基本上是空的。
在拷贝每一项pte时内核会检查当前是否是COW区域:1.vma区域可写,2.vma不能是共享的,3.vma是匿名映射。
do_fork()
copy_process()
copy_mm()
dup_mm()
allocate_mm() /* 分配新的mm_struct */
dup_mmap(mm, oldmm) /* 复制vma和页表 */
dup_mmap(struct mm_struct *mm, struct mm_struct *oldmm) {
for (mpnt = oldmm->mmap; mpnt; mpnt = mpnt->vm_next) { /* 遍历所有的vm_area_struct */
tmp = kmem_cache_alloc(vm_area_cachep, GFP_KERNEL);
*tmp = *mpnt;
copy_page_range(mm, oldmm, mpnt); /* 复制父进程中所有的页表项 */
}
}
copy_page_range()
copy_pud_range()
copy_pmd_range()
copy_pte_range()
copy_one_pte()
copy_one_pte() {
if (is_cow_mapping(vm_flags)) {
ptep_set_wrprotect(src_mm, addr, src_pte); /* 如果需要COW,则将父进程中pte设置写保护位,即~_PAGE_BIT_RW */
pte = pte_wrprotect(pte);
}
set_pte_at(dst_mm, addr, dst_pte, pte); /* 设置子进程中pte写保护位:~_PAGE_BIT_RW*/
}
缺页异常中修复
对pte中标记只读的页进行写操作会触发MMU缺页异常,缺页异常修复中探测触发异常的地址是否是合法的:地址位于vma区间中并且vma区间是可写的,此时就认为是COW的页,下面就是修复异常。缺页异常修复返回修复类型:如果是major/minor类型,统计到进程中的maj_fault,min_fault;如果是异常则发送信号SIGBUS给进程;如果修复失败则返回oom错误并发送SIGKILL信号杀死进程。
- 重新分配物理页并拷贝原始页内容到新的物理页中
- 修正新的页表项属性
- 修正旧的页表项属性
do_page_fault()
handle_mm_fault()
__handle_mm_fault()
handle_pte_fault()
do_wp_page() /* wp:write protect */
do_wp_page() {
new_page = alloc_page_vma(GFP_HIGHUSER, vma, address); //分配新的物理页
cow_user_page(new_page, old_page, address, vma); //拷贝页内容
entry = mk_pte(new_page, vma->vm_page_prot);
entry = maybe_mkwrite(pte_mkdirty(entry), vma); //设置写属性
set_pte_at(mm, address, page_table, entry); //设置新的pte项
}
遗留问题:上面只是第一个进程触发COW机制的下半部,还有另一个的进程如何实现COW的下半部呢?