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

这几天在看《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);
}

上面的代码判断如果父进程的页支持写时复制,就将父子进程的页都置为写保护。
讲到这里,写时拷贝技术就基本分析完了。

你可能感兴趣的:(Linux内核)