這幾天在看《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的源碼,找了半天沒找到
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); }
本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。