关于linux内核fork后cow(写时复制)的代码分析

晨曦之光 发布于 2012/04/10 15:03
阅读 1K+
收藏 1

写时复制是一个众所周知的概念,古老又伟大的unix利用了这一个特性,在当时,内存非常昂贵,cpu计算资源极其昂贵,因此有必要用这种懒惰的方式来节省时间和空间。linux是开放源代码的,它的内部怎么实现cow的呢?看代码吧,我们从fork开始,前面的我就不多说了,从sys_fork一直到 copy_mm,在copy_mm中实现了cow:

static struct mm_struct *dup_mm(struct task_struct *tsk)
{
struct mm_struct *mm, *oldmm = current->mm;
......
mm = allocate_mm();//建议此函数仔细阅读!!
if (!mm)
goto fail_nomem;
...

memcpy(mm, oldmm, sizeof(*mm));//浅拷贝
if (!mm_init(mm))
goto fail_nomem;
......
err = dup_mmap(mm, oldmm);//这是重点!
if (err)
goto free_pt;
......
return mm;
......
}
在dum_mmap中实现了复制,在复制中是按照父进程的页表逐条复制的,最终到了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)) {
swap_duplicate(pte_to_swp_entry(pte));
/* 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);
}
}
goto out_set_pte;
}
......
if (is_cow_mapping(vm_flags)) { //如果是写时复制
ptep_set_wrprotect(src_mm, addr, src_pte);//那么就把页表设置为写保护
pte = *src_pte; //将子进程对应页表也写保护
}
......
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);
rss[!!PageAnon(page)]++;
}
out_set_pte:
set_pte_at(dst_mm, addr, dst_pte, pte);
}
函数内部有一个get_page调用,这是很重要的,但是我们并没有看到对应的put_page调用,对应的put_page在哪里呢?这个问题在宏内核里面很不好回答,借此发挥一下,在宏内核里面,所谓的交互就是函数调用,学过c语言的都知道,函数调用实际上是一种很不好掌控的局面,这可能就是为何面向对象技术目前占上风的原因,面向对象是一种高内聚低耦合的方法,对应到操作系统内核也是这样,高内聚低耦合的系统我们看起来总是舒服些,在那样的系统里面,如果在一处有了get_page那么对称的地方你肯定能找到put_page,而不像linux这里这么混杂的局面,幸运的是,微内核就是这么做的,比如mach内核。但是另一方面,内核的可读性和模块化往往不如其效率更加值得人们关注,毕竟它们不是那种需要经常改动的东西,这也就解释了为何在操作系统领域,面向对象一直没有立足之地。好了,我们继续,寻找put_page。
写时复制制造的局面什么时候被打破的呢?是在页面错误的时候,当程序写一个在上面 ptep_set_wrprotect函数设置成写保
护的页面时,内核就会把执行流导向do_wp_page:
static int do_wp_page(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pte_t *page_table, pmd_t *pmd,
spinlock_t *ptl, pte_t orig_pte)
{
struct page *old_page, *new_page;
pte_t entry;
int ret = VM_FAULT_MINOR;
old_page = vm_normal_page(vma, address, orig_pte);//得到写保护的页面
if (!old_page)
goto gotten;
//以下描述如果是匿名页而且已经没有进程再共享它的时候就会直接拿来使用而不用再分配一个了
if (PageAnon(old_page) && !TestSetPageLocked(old_page)) {
int reuse = can_share_swap_page(old_page);
unlock_page(old_page);
if (reuse) {
flush_cache_page(vma, address, pte_pfn(orig_pte));
entry = pte_mkyoung(orig_pte);
entry = maybe_mkwrite(pte_mkdirty(entry), vma);
ptep_set_access_flags(vma, address, page_table, entry, 1);
update_mmu_cache(vma, address, entry);
lazy_mmu_prot_update(entry);
ret |= VM_FAULT_WRITE;
goto unlock;
}
}
...... //这里又一个get_page,前提是刚才得到了page,无论如何,对应的page一会就put
page_cache_get(old_page);//这个get操作仅仅是为了保护。
gotten:
pte_unmap_unlock(page_table, ptl);
if (unlikely(anon_vma_prepare(vma)))
goto oom;
if (old_page == ZERO_PAGE(address)) {
new_page = alloc_zeroed_user_highpage(vma, address);//实际上分配页面----零页面
if (!new_page)
goto oom;
} else {
new_page = alloc_page_vma(GFP_HIGHUSER, vma, address);//实际上分配页面----非零页面
if (!new_page)
goto oom;
cow_user_page(new_page, old_page, address);//到此old_page可能根本就不在内存,即为null
}
...... //注意,不要忽略了调用do_wp_page的条件,就是write_access,所以访问真正只读页面根本到不了这里!
page_table = pte_offset_map_lock(mm, pmd, address, &ptl);
if (likely(pte_same(*page_table, orig_pte))) {//如果pte在进入页错误到现在没有改的话,这里返回真!
if (old_page) {
page_remove_rmap(old_page);
if (!PageAnon(old_page)) {
dec_mm_counter(mm, file_rss);
inc_mm_counter(mm, anon_rss);
}
} else
inc_mm_counter(mm, anon_rss);
flush_cache_page(vma, address, pte_pfn(orig_pte));
entry = mk_pte(new_page, vma->vm_page_prot);
entry = maybe_mkwrite(pte_mkdirty(entry), vma);//设置为读写
ptep_establish(vma, address, page_table, entry);
update_mmu_cache(vma, address, entry);
lazy_mmu_prot_update(entry);
lru_cache_add_active(new_page);
page_add_new_anon_rmap(new_page, vma, address);
/* Free the old page.. */
new_page = old_page; e赋给new
ret |= VM_FAULT_WRITE;
}
//如果页表发生了更改,那么就不会执行上面的new_page = old_page,所以页面直接被释放,预示有问题。
if (new_page)
page_cache_release(new_page);//如果是cow的话,那么减少引用计数,因为已经有一个脱离cow局面
if (old_page)
page_cache_release(old_page);//这次释放对应前面1472行的get_page
unlock:
pte_unmap_unlock(page_table, ptl);
return ret;
oom:
if (old_page)
page_cache_release(old_page);
return VM_FAULT_OOM;
}
可以看到在1515到1518减了页面引用计数,如果计数为0就当即释放页面,如果页表被修改也是当即释放新飞配的页面,如
果有多个进程同时进入cow局面(比如一个进程连续n次fork或者子进程继续fork)的话,仅凭一个进程脱离cow局面,page的计
数肯定不为0,别忘了在dum_mm中每个进入cow局面的page都增加了页面引用计数 。还需要注意的是,cow局面不仅有个page引
用计数控制着页面释放,还有一个共享计数控制着是否需要真的飞配一个页面,如果就有一个进程在cow局面的话,那么就不分
配新页面了。最后就是如果写了一个只读页面,则根本不会到达上面这个函数,记住,linux对页面的保护是分级的多重的,
页表是一个方面,更上一个方面就是vm_area_struct结构,如果高层检查都通不过,低级检查显然没有必要进行。
从上面可以看出,linux代码的特点是乱中有序,咋一看停乱的,从大局上看,结构清晰。值得好好品味!你,值得拥有...


原文链接:http://blog.csdn.net/dog250/article/details/5303054
加载中
返回顶部
顶部