您的位置:首页 > 运维架构 > Linux

深入探究fork函数写时拷贝技术的实现

2017-03-25 21:03 274 查看
这几天在看《Linux内核设计与实现》,看到fork函数写时拷贝(copy on write)那一节,突然发现以前学习写时拷贝技术的时候只是大概理解了它的原理,并没有深入理解,本来想在网上找找有没有分析写时拷贝技术实现原理的博客,找了半天发现全是些介绍理论的,balabala一大堆,于是决定自己去看Linux的源码。

我用的Linux内核源码是2.6.26版本。要学习copy on write,肯定得先找到fork函数的系统调用——sys_fork函数

/* r12-r8 are dummy parameters to force the compiler to use the stack */
asmlinkage int sys_fork(struct pt_regs *regs)
{
return do_fork(SIGCHLD, regs->sp, regs, 0, NULL, NULL);
}
最开始我看到这段代码的时候很奇怪,因为按照《Linux内核设计与实现》上讲的——“内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝”,子进程的地址空间应该指向父进程的地址空间,那就应该传入一个CLONE_VM标志啊,所以当时觉得glibc中的fork调用的肯定不是sys_fork系统调用,于是去翻glibc关于fork的源码,找了半天没找到

,所以再去网上查阅关于fork写时拷贝技术的原理,过了一段时间我确信内核的sys_fork确实是在内部实现了写时拷贝技术,于是我顺藤摸瓜,再次查阅资料,发现了一篇博客http://blog.csdn.net/evenness/article/details/7656812,里面写的验证了我之前的想法,看完之后茅舍顿开。

Linux通过一系列函数最终实现写时拷贝的过程如下:

sys_fork->do_fork->copy_process->copy_mm->dup_mm->dup_mmap->copy_page_range->copy_pud_range->copy_pmd_range->copy_pte_range->copy_one_pte


看到这么多函数调用是不是有种要崩溃的感觉

,没关系,咱们一点一点地的分析,说重点

之前提到了为什么sys_fork在调用do_fork的时候没有传CLONE_VM,首先我们看传了会怎样

static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)
{
struct mm_struct * mm, *oldmm;
int retval;

tsk->min_flt = tsk->maj_flt = 0;
tsk->nvcsw = tsk->nivcsw = 0;

tsk->mm = NULL;
tsk->active_mm = NULL;

/*
* Are we cloning a kernel thread?
*
* We need to steal a active VM for that..
*/
oldmm = current->mm;
if (!oldmm)
return 0;
//如果标志中有 CLONE_VM
if (clone_flags & CLONE_VM) {
atomic_inc(&oldmm->mm_users);
//子进程的地址空间并不分配单独的,而是直接指向父进程的地址空间
mm = oldmm;
goto good_mm;
}

retval = -ENOMEM;
mm = dup_mm(tsk);
if (!mm)
goto fail_nomem;

good_mm:
/* Initializing for Swap token stuff */
mm->token_priority = 0;
mm->last_interval = 0;

tsk->mm = mm;
tsk->active_mm = mm;
return 0;

fail_nomem:
return retval;
}


在copy_mm函数中可以清楚地看到如果设置了CLONE_VM标志,父进程不会调用dup_mm为子进程分配地址空间,问题就出在这,写时拷贝技术是有自己的地址空间的,并不会和父进程共享,写时拷贝技术真正共享的是物理空间,所以我觉得这本书上讲得有点问题,也可能是翻译得问题,再来看dup_mm这个函数

/*
* Allocate a new mm structure and copy contents from the
* mm structure of the passed in task structure.
*/
struct mm_struct *dup_mm(struct task_struct *tsk)
{
struct mm_struct *mm, *oldmm = current->mm;
int err;

if (!oldmm)
return NULL;

mm = allocate_mm();
if (!mm)
goto fail_nomem;

memcpy(mm, oldmm, sizeof(*mm));

/* Initializing for Swap token stuff */
mm->token_priority = 0;
mm->last_interval = 0;

if (!mm_init(mm, tsk))
goto fail_nomem;

if (init_new_context(tsk, mm))
goto fail_nocontext;

dup_mm_exe_file(oldmm, mm);

err = dup_mmap(mm, oldmm);
if (err)
goto free_pt;

mm->hiwater_rss = get_mm_rss(mm);
mm->hiwater_vm = mm->total_vm;

return mm;

free_pt:
mmput(mm);

fail_nomem:
return NULL;

fail_nocontext:
/*
* If init_new_context() failed, we cannot use mmput() to free the mm
* because it calls destroy_context()
*/
mm_free_pgd(mm);
free_mm(mm);
return NULL;
}
dup_mm先给子进程分配了一个新的结构体,然后调用dup_mmap拷贝父进程地址空间,所以我们再进入 dup_mmap看看拷贝了什么东西,因为dup_mmap函数代码太长就不贴出来了,直接看copy_page_range函数,这个函数负责页表得拷贝,我们知道Linux从2.6.11开始采用四级分页模型,分别是pgd、pud、pmd、pte,所以从copy_page_range一直调用到copy_pte_range都是拷贝相应的页表条目,最后我们再来看看copy_pte_range调用的copy_one_pte函数

static inline void
copy_one_pte(struct mm_struct *dst_mm, struct mm_struct *src_mm,
pte_t *dst_pte, pte_t *src_pte, struct vm_area_struct *vma,
unsigned long addr, int *rss)
{
unsigned long vm_flags = vma->vm_flags;
pte_t pte = *src_pte;
struct page *page;

/* pte contains position in swap or file, so copy. */
if (unlikely(!pte_present(pte))) {
if (!pte_file(pte)) {
swp_entry_t entry = pte_to_swp_entry(pte);

swap_duplicate(entry);
/* make sure dst_mm is on swapoff's mmlist. */
if (unlikely(list_empty(&dst_mm->mmlist))) {
spin_lock(&mmlist_lock);
if (list_empty(&dst_mm->mmlist))
list_add(&dst_mm->mmlist,
&src_mm->mmlist);
spin_unlock(&mmlist_lock);
}
if (is_write_migration_entry(entry) &&
is_cow_mapping(vm_flags)) {
/*
* COW mappings require pages in both parent
* and child to be set to read.
*/
make_migration_entry_read(&entry);
pte = swp_entry_to_pte(entry);
set_pte_at(src_mm, addr, src_pte, pte);
}
}
goto out_set_pte;
}

/*
* If it's a COW mapping, write protect it both
* in the parent and the child
*/
if (is_cow_mapping(vm_flags)) {
ptep_set_wrprotect(src_mm, addr, src_pte);
pte = pte_wrprotect(pte);
}

/*
* If it's a shared mapping, mark it clean in
* the child
*/
if (vm_flags & VM_SHARED)
pte = pte_mkclean(pte);
pte = pte_mkold(pte);

page = vm_normal_page(vma, addr, pte);
if (page) {
get_page(page);
page_dup_rmap(page, vma, addr);
rss[!!PageAnon(page)]++;
}

out_set_pte:
set_pte_at(dst_mm, addr, dst_pte, pte);
}
上面的这段函数便是写时拷贝技术的核心之所在

if (is_cow_mapping(vm_flags)) {
ptep_set_wrprotect(src_mm, addr, src_pte);
pte = pte_wrprotect(pte);
}

上面的代码判断如果父进程的页支持写时复制,就将父子进程的页都置为写保护。

讲到这里,写时拷贝技术就基本分析完了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息