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

简介:

在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内核开发的灵活和当仁不让!只要有用的谁也去不掉,只要没有用,再花哨的东西终将废止。



 本文转自 dog250 51CTO博客,原文链接:http://blog.51cto.com/dog250/1273482

相关文章
|
5月前
|
存储 编译器 C语言
58 C++ - 模板机制剖析
58 C++ - 模板机制剖析
17 0
|
12天前
|
程序员
项目中的全局异常是如何处理的
项目中的全局异常处理通常包括对预期异常(程序员手动抛出)和运行时异常的管理。项目已提供`BaseException`作为基础异常类,用于手动抛出异常,并通过`GlobalExceptionHandler`进行全局处理。`
28 4
|
15天前
|
人工智能 Serverless 对象存储
让你的文档从静态展示到一键部署可操作验证
好的文档应当超越文字的界限,成为知识传递和技能培养的桥梁。阿里云函数计算让我们朝着这一目标迈出了重要一步。我们将文档从传统的静态页面升级为一个动态的、互动性强的工具,用户可以通过一键部署直接在函数计算平台验证文档内容。
148 0
|
9月前
|
缓存 小程序 API
小程序:浅谈小程序更新机制,发版后多久能全覆盖
小程序:浅谈小程序更新机制,发版后多久能全覆盖
251 0
|
10月前
|
敏捷开发 前端开发 Ruby
RailsAdmin如何实现自定义操作
RailsAdmin如何实现自定义操作
67 0
定义一个事件需要单独新建一个文件吗?底层原理是什么?
定义一个事件需要单独新建一个文件吗?底层原理是什么?
|
缓存
读源码长知识 | 动态扩展类并绑定生命周期的新方式
在阅读viewModelScope源码时,发现了一种新的方式。 协程需隶属于某 CoroutineScope ,以实现structured-concurrency,而 CoroutineScope 应
136 0
html+css实战147-定位-作用和使用步骤
html+css实战147-定位-作用和使用步骤
70 0
html+css实战147-定位-作用和使用步骤
|
XML Android开发 数据格式
【PageLayout】非常简单的一键切换加载-空数据-错误页,支持自定义
版权声明:本文为博主原创文章,转载请标明出处。 https://blog.csdn.net/lyhhj/article/details/82594706 项目中我们经常会用到的加载数据,加载完数据后显示内容,如果没有数据显示一个空白页,这是如果网络错误了显示一个网络错误页,自定义一个PageLayout。
1128 0
|
JSON 监控 数据库
TP5_接口开发之全局异常控制
前言: 说到异常控制,也许很多会比较陌生,我身边很少人会去写抛异常的代码。但是异常用好了是非常的方便大家开发。首先我们来回顾下哪里可以看到异常,首先我们用框架开发的时候,我们的代码出错或者别的东西。
1287 0