深入探究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);
}

上面的代碼判斷如果父進程的頁支持寫時復制,就將父子進程的頁都置為寫保護。
講到這里,寫時拷貝技術就基本分析完了。

注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2020 ITdaan.com