零页面机制在缺页中断中的作用

晨曦之光 发布于 2012/04/10 15:01
阅读 317
收藏 0

在2.6的早期内核以及更早的2.4,2.2以及1.X内核中有一个empty_zero_page的数组,它是一个全局的页面数组,它的作用很大,要比现在2.6.2X/3X内核中empty_zero_page的重要性大,empty_zero_page的主要作用就是只要用户引用一个只读的匿名页面并没有进行写操作,缺页中断处理中内核就不会给用户进程分配新的页面。零页面不加入lru链表,因此它不会被换出,也就是说这些页面根本不参与内存管理,它们没有换入换出的必要,它们中没有数据,它们仅仅使一些桩子;零页面仅仅占用了若干个地址,并且很确定,影响的cacheline也很确定,读页面本身并不会影响cacheline,因为这些页面不允许写,唯一使得零页面影响cacheline的是对于其page结构中引用计数的操作,因为page结构本身也在内存当中,而2.6内核新引入了反向映射,而反向映射必然要操作引用计数,即使零页面也不例外。我们可以看一下2.6.1的缺页处理中的匿名页面部分:

static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma, pte_t *page_table, pmd_t *pmd, int write_access, unsigned long addr)

{

pte_t entry;

struct page * page = ZERO_PAGE(addr); //得到零页面,注意所有的进程缺页中都会引用同样的这个零页面

...

entry = pte_wrprotect(mk_pte(ZERO_PAGE(addr), vma->vm_page_prot)); //写保护,只要有写操作就会分配新页面

if (write_access) {

...

page = alloc_page(GFP_HIGHUSER);

if (!page)

goto no_mem;

clear_user_highpage(page, addr); //安全约定,清除遗留数据

...

mm->rss++;

entry = pte_mkwrite(pte_mkdirty(mk_pte(page, vma->vm_page_prot)));

lru_cache_add_active(page); //加入lru,接受内存管理模块的管理

mark_page_accessed(page);

}

set_pte(page_table, entry); //设置页表项

pte_chain = page_add_rmap(page, page_table, pte_chain); //虽然对于零页其实不进行写操作进而也不会破坏既有的cacheline映射,但是对于零页面的page结构本身却还是需要操作的,page可以视为零页面的元数据,零页面可以避开写,但是零页面毕竟确实被选作了用户页面返给了用户,因此需要接受一定的内存管理,接下来的反向映射就是这么一回事,在2.6.1版本中,反向映射忽略了零页面,但是在后来的版本中为了统一管理还是需要修改零页面page结构的一些字段导致了写内存进一步缓存被更新,最终导致了彻底去除了零页面

pte_unmap(page_table);

update_mmu_cache(vma, addr, entry);

...

}

也许有人会问只读页面就一定是零页面吗?有数据的页面也可能是只读的,这当然是对的,但是对于匿名页面来讲,就不对了,首先由于安全原则,新的匿名页面必须清零,但是由于零页面实际上没有什么用处,那么必然有一个进程对其进行写操作,因此它必须是可写的,最起码对于某一个进程是可写的,但是对于文件映射来讲,只读的的并且有数据的页面是存在的,最简单的例子就是elf文件的代码段内存映射。匿名页面一旦被写入了数据,接下来如果该页面被换出,然后重新引用该页面的时候,do_swap_page将会被调用,以后的操作就是从交换空间换页了,匿名页面摇身一变成了交换空间的文件页面。理解了这一点接下来的问题就是既然零页面实际上没有什么用处,那么为何在用户引用只读页面的时候必须分配页面呢?这是操作系统的要求谁也回避不了,我们能做的就是尽可能的让一切更高效,依据懒惰原理,也即是等到不能再拖的时候在行动的原则,用户引用匿名页面并不一定马上就是写数据,而一旦写了数据它就成了交换空间的文件页面了,因此对于第一次引用的只读页面,它实际上不应该有数据的,因为数据必须是写入的,如果你读它,得到的是全部0,因此没有必要为只读页面分配页面,所有的只读页面在第一次引用的时候只需要返回同一个零页面即可,这样就节省了大量的页面,防止一些进程只引用只读页面而不进行写操作而浪费大量的内存最终导致频繁换页。内核只考虑语义合理,具体实现就是怎么高效安全怎么来,对于只读的,匿名的,第一次引用的页面,映射到同一处没有任何问题,因为都是没有数据的零页面,符合安全规则也更加高效,同时符合用户空间编程语义。

那么这种方式带来的另一个效果是什么呢?试想如果每次都分配新页面,那么每次有很大可能分到不同的页面,这样操作这些页面的元数据的结果就是造成重置大量的cacheline,非常影响效率,零页面位置固定,影响cacheline的位置也固定并且很有限,因此效率可以提高。在只读的匿名页面缺页之后,内核只会给用户一个零页面,并且该零页面是以写保护方式给用户提供的,然后一旦有进程对该页面进行写操作,那么内核会以写保护违规的方式触发缺页中断,在中断处理中会重新分配一个新的页面给用户,这就是所谓的懒惰的方式,直到用户进程最终写页面的时候才会分配页面给用户,对于零页面,其实就是将新分配的页面清零,这在同一进程中常见,进程往往先引用一个页面,然后就行写操作或者什么也不做,对于非零页面就是将老页面的数据拷贝到新的页面,这在fork时比较常见。在2.6.1的代码,调用了一个copy_cow_page来拷贝页面数据,过程就是无论如何先分配一个页面然后再考虑怎么初始化其数据:

static inline void copy_cow_page(struct page * from, struct page * to, unsigned long address)

{

if (from == ZERO_PAGE(address)) {

clear_user_highpage(to, address);

return;

}

copy_user_highpage(to, from, address);

}

2.6.17的代码中去除了copy_cow_page函数,将分配页面的动作放到了判断页面类型之后,因为2.6.17的内核中分配页面更加细化了,这些事情还是自己看代码的好:

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);

}

但是等等,事情发生了,不知道是好事还是坏事,我觉得不怎么好,零页面虽然有很奇妙的功效,但是在最近的内核中被去除了,在缺页中断中无论如何都不会考虑零页面了,而是无论如何都分配新的页面,难道零页面不好吗?Nick觉得不好,正是由于每引用一次零页面就要修改page中的某些字段,而page存于内存,这势必会冲刷cacheline,在机器中,很多的地址将共用一个cacheline,因此即便零页面再好(它比每次重新分配一个页面好在可以控制cacheline的冲刷),它还是会将很多cacheline冲刷,正如一个罪犯随机杀了10个人,另一个罪犯杀了确定的10个人,他们谁的罪更轻些?显然这个问题很可笑,只可惜linux内核的早期版本正是被这种笑话冲昏了头脑,实际上零页面节省的这种cacheline就是类似的一个笑话。虽然固定的冲刷cacheline比随机的冲刷带来的损失可能更小,但是能小多少呢?可以确定的是对于第一次的匿名只读页面分配零页面导致的多了将近一倍的缺页,我们需要做的额外工作是将页面清零,这虽然是个代价,但是却比大量冲刷缓冲要便宜的多。为何呢?注意,每次在缺页中断中引用零页面就意味着一次cacheline的绑定,因为要修改page数据结构的字段,由于cacheling是一个cpu所有进程共用的,在进程分时调度下cacheline不断被冲刷,每次缺页将导致一次cacheline冲刷,虽然是固定地址但是谁也保证不了缺页之前这个cacheline存放的是哪个进程的数据。于是零页面机制在缺页处理中正式下课。

纵观零页面的历史,显示出的是linux内核开发的灵活和当仁不让!只要有用的谁也去不掉,只要没有用,再花哨的东西终将废止。


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