文章目录
- 作者
- 环境
- 概述
- 正文
- 父进程执行fork时将可写的页面设置成只读
- copy_mm
- dup_mm
- dup_mmap
- copy_page_range
- 父进程首先写时,触发CoW
- handle_pte_fault
- do_wp_page
- wp_page_copy
- 子进程随后在写时触发缺页
作者
pengdonglin137@163.com
环境
Linux-6.5 + ARM64
概述
前一篇通过实验的方法理解了一下CoW,下面大致分析一下Linux内核源码实现。
我们知道,CoW是父进程在fork时,将自己的私有可写的页面的映射属性设置为只读,然后映射信息跟子进程共享,后面fork完成后,父子进程先发起写操作的一方会触发CoW,而后发起的也会触发缺页,但是不会CoW,而是复用已有的页面。
正文
下面分成几个小题进行分析,为了方便理解,对函数具体实现进行了精简。
父进程执行fork时将可写的页面设置成只读
执行fork时,内核的trace日志,可以点这里查看。
在执行clone的时候,会调用copy_mm复制父进程的地址空间给子进程:
copy_mm
static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
{struct mm_struct *mm, *oldmm;tsk->min_flt = tsk->maj_flt = 0;tsk->nvcsw = tsk->nivcsw = 0;tsk->mm = NULL;tsk->active_mm = NULL;oldmm = current->mm;/* 如何要clone一个内核线程,不需要创建mm_struct,因为内核线程会借用普通进程的mm_struct */if (!oldmm)return 0;/* 如果指定了CLONE_VM标志,表示要创建的时一个普通的用户线程(如通过pthread_create),进程内部的所有线程共享mm_struct,所以这里只需要增加mm_struct的引用计数即可*/if (clone_flags & CLONE_VM) {mmget(oldmm);mm = oldmm;} else {// 要创建的是一个普通的进程(或者理解为普通的用户主线程)mm = dup_mm(tsk, current->mm);}// 对于普通的进程,mm和active_mm的内容相同。对于内核线程,mm是NULL,active_mm指向被借用的哪个进程的mm_structtsk->mm = mm;tsk->active_mm = mm;...return 0;
}
dup_mm
这个函数用来根据传入的task和mm_struct,创建一个新的mm_struct,也就是子进程的地址空间。
static struct mm_struct *dup_mm(struct task_struct *tsk,struct mm_struct *oldmm)
{struct mm_struct *mm;int err;// 从slub中分配一个mm_structmm = allocate_mm();// 先继承父进程的mm_struct,然后再根据实际情况重新初始化部分成员memcpy(mm, oldmm, sizeof(*mm));// 重新初始化子进程的mm_struct的部分成员,如分配pgd等mm_init(mm, tsk, mm->user_ns);// 拷贝父进程的地址空间err = dup_mmap(mm, oldmm);...return mm;
}
dup_mmap
父进程调用这个函数给子进程拷贝自己的地址空间。
static __latent_entropy int dup_mmap(struct mm_struct *mm,struct mm_struct *oldmm)
{struct vm_area_struct *mpnt, *tmp;int retval;unsigned long charge = 0;LIST_HEAD(uf);VMA_ITERATOR(old_vmi, oldmm, 0);VMA_ITERATOR(vmi, mm, 0);mmap_write_lock_killable(oldmm);mmap_write_lock_nested(mm, SINGLE_DEPTH_NESTING);// 继承parent的计数mm->total_vm = oldmm->total_vm;mm->data_vm = oldmm->data_vm;mm->exec_vm = oldmm->exec_vm;mm->stack_vm = oldmm->stack_vm;...// 根据需要的entry数给maple树分配空间retval = vma_iter_bulk_alloc(&vmi, oldmm->map_count);// 遍历parent的mm_struct中的vmafor_each_vma(old_vmi, mpnt) {struct file *file;// VM_DONTCOPY表示在fork时不允许拷贝if (mpnt->vm_flags & VM_DONTCOPY) {// 既然不允许拷贝,那么就从parent那里继承过来的计数减去不允许拷贝的字节数vm_stat_account(mm, mpnt->vm_flags, -vma_pages(mpnt));continue;}// 到这里,说明parent的vma可以拷贝charge = 0;if (mpnt->vm_flags & VM_ACCOUNT) {unsigned long len = vma_pages(mpnt);charge = len;}// 复制parent的vma结构体到新的vma中,然后对部分成员重新初始化,返回给tmptmp = vm_area_dup(mpnt);// 复制parent的vma的内存策略retval = vma_dup_policy(mpnt, tmp);tmp->vm_mm = mm;// VM_WIPEONFORK 表示在fork时清除child的vma信息if (tmp->vm_flags & VM_WIPEONFORK) { /** VM_WIPEONFORK gets a clean slate in the child.* Don't prepare anon_vma until fault since we don't* copy page for current vma.*/tmp->anon_vma = NULL;} else if (anon_vma_fork(tmp, mpnt)) // 建立匿名页反向映射数据结构goto fail_nomem_anon_vma_fork;// 如果是文件映射,file指向被映射的文件的file结构file = tmp->vm_file;if (file