页高速缓存是Linux内核实现的磁盘缓存,它主要的作用是用来减少磁盘IO操作。其实现原理是通过把磁盘中的数据缓存到物理内存中,把对磁盘的访问变为对物理内存的访问
。
磁盘高速缓存之所以在现代操作系统中尤为重要的原因有两个:第一,访问内存的速度要远远高于访问磁盘的速度,ns和ms的差距。第二,根据临时局部原理,数据一旦被访问,就有可能短期内再次被访问,缓存命中会很高。
页高速缓存是由内存中的物理页面组成的,其内容对应物理磁盘上的物理块。页高速缓存的大小能够动态调整,它可以通过占用空闲内存扩大,也可以自我收缩缓解内存使用压力。
当我们发起一个对操作时(例如,进程调用read),它首先会检查需要的数据是否在页缓存中,如果在,则放弃访问磁盘,直接从内存中读取。这个行为称为缓存命中。未命中则需要调度块I/O操作从磁盘读取。然后内核将读取的数据放在页缓存中。
当进程在执行写操作时,比如执行write调用,缓存一般实现为一下三种策略:
(1)不缓存,当对一个缓存中的数据片进行写时,直接跳过缓存,写入到磁盘中,同时缓存失效。
(2)写透缓存,写操作自动更新内存缓存,同时也更新磁盘。
(3)回写,linux采用的策略,程序执行写操作直接写到缓存中,后端存储不会立刻更新,而是将缓存页更新为脏页,并将其加入到脏页链表中,然后由一个进程周期性的将脏页写回磁盘。
缓存算法最后涉及到的重要内容是缓存回收策略
(清除缓存内容,收缩缓存大小),这个工作,决定缓存中什么内容将被清除。linux的缓存回收是通过选择干净页进行简单替换,如果内核中没有足够的干净页,则进行强制回写操作。最难得事情在于决定那些页回收最为合理,理想的回收策略应该是回收那些最不可能使用到的页面。
(1) 最近最少使用
缓存回收策略通过所访问的数据特性,尽量追求预测效率。最成功的算法称作最近最少使用算法,简称LRU,LRU通过将每个页按照访问时间排序,以便能够回收最老时间戳的页面。该策略的良好效果在于缓存的数据越久未被访问,则越不大可能近期在被访问。其弊端在于当遇到对于许多文件只访问一次再不被访问的情景时
,LRU会很失败。
(2)双链策略
Linux实现的是一个修改过的LRU,也称为双链策略。和以前不同,linux会维护两个链表:活跃链表和非活跃链表。处于活跃链表上面的页面被认为是“热”的且不会被换出,而在非活跃链表上的页面则是可以被换出的。两个链表都是从尾部加入,从头部移除。两个链表需要维持平衡,如果活跃链表变得过多而超过了非活跃链表,那么活跃链表的头页面将被重新移回到非活跃链表中,以便能够再被回收。双链表策略解决了传统LRU算法中对仅访问一次的窘境(大量缓存只访问一遍可能会导致将热缓存挤出链表)。而且也更简单实现了伪LRU语义,这种双链表方式称为LUR/2。更普遍的是n个链表,称LRU/n。双链表的目的是提高页面回收的性能。
双链策略详解
综上所示,总结为,通过读和写构建了页缓存,通过回写实现数据同步,通过双链表来做数据回收
。
在页高速缓存中的页可能包含了多个不连续的物理磁盘块,也正是由于页面中映射的磁盘块不一定连续,所以在页面高速缓存中检查特定数据是否已经被缓存颇为困难,因为不能用设备名称和块号来做页高速缓存中的索引。
注:页高速缓存中的信息单位是一个完整的页。一个页包含的磁盘块在物理上不一定相邻,所以不能用设备号和块号标识,而是通过页的所有者和所有者数据中的索引来识别。
同时,因为linux页高速缓存的目标是缓存任何基于页的对象,其中包含各种类型的文件和各种类型的内存映射
,为了维持页高速缓存的普遍性,linux页高速缓存使用address_space结构体来管理页高数缓存项和页I/O操作。内核中任何基于页的对象想要做缓存即可关联该结构体,并实现其方法
。其定义如下:
值得注意的是,当一个文件被10个vm_area_struct(内存章节中提到的内存区域结构体)标识时(10个进程分别使用mmap做了映射),这个文件只会有一个address_space数据结构。也就是文件可以有多个虚拟地址映射,但是物理缓存只有一份。i_mmap字段可以帮助内核高效的查找被关联的映射有哪些。
a_ops域指向被缓存对象的操作函数列表(这与VFS及其相似,都是面向对象的思想,每种被缓存对象都要实现自己的方法),操作函数由address_space_operations结构表示,如下:
这些方法指针指向指定缓存对象
需要实现的页I/O操作。每种基于页的对象都需要实现这些方法来描述自己如何与页高速缓存交互,例如,ex3文件系统在文件fs/ext3/inode.c中就实现了这些函数方法。这些方法提供了管理页高速缓存的各种行为
,包括最常用的读页到缓存、更新缓存数据等等。
页面读操作包含步骤:
页面写操作包含步骤:
所有的页I/O操作必然是通过页高速缓存进行的
。因此,内核也总是试图先通过页高速缓存来满足所有的读请求,如果在高速缓存中未搜索到需要的页,则内核将从磁盘读入需要的页,并将其加入到缓存中。对于写操作,页高速缓存更像是一个存储平台,所有要被写出的页都要加入到高速缓存中。
值得注意的是
,页高速缓存在提供高性能的文件读写,但是可能在计算机故障时可能会丢失数据。如果上层应用(如数据库)对数据刷盘有限制,那么就需要使用直接I/O(Linux设置O_DIRECT标志来禁用Linux页高速缓存),然后上层应用可以根据业务特性自定义高速缓存机制而不是使用操作系统的页高速缓存。
O_DIRECT:
一般如果在Linux内核中读写一个文件,其IO流程都需要经过Kernel内的page cache层次,若想要使用自己开发的缓存系统,那么就可以在打开这个文件的时候,对该文件加以O_DIRECT的标志位,这样一来就可以让程序对该文件的IO直接在磁盘上进行,从而避开了Kernel的page cache,进而对IO流程里的块数据进行拦截,让其流入到自己开发的缓存系统内。
块IO章节中讲到的缓存区,在2.6版本后也被存入页高速缓存了。在早些的内核版本中,有两个独立的磁盘缓存:缓冲区缓存和页高速缓存。前者缓存缓冲区,后者缓存页面,这就导致了一个磁盘块可能同时存于两个缓存中,浪费内存以及影响性能。
值得注意的是
,缓冲区缓存是针对数据块(块IO层章节中提到的)的;而页高速缓存是以页为单位(例如,将文件的内容 缓存到某几个页)。而页面正好包含了块缓冲区,所以在linux最新版本中合并了这个两个缓存。
由于页高速缓存的缓存作用,写操作实际会被延迟。当页高速缓存中的数据比后台存储的数据更新时,该数据就是脏数据。在以下三种情况发生时,脏页被写回磁盘:
(1)当空闲内存低于一个阙值时,内核必须将脏页写回磁盘以便释放内存。
(2)当脏页在内存中驻留时间超过一个特定的阙值时,内核必须将超时脏页写回磁盘。
(3)当用户进程调用sync()和fsync()系统调用时,内核会执行回写操作。
在旧内核中,这是由两个独立的内核线程分别完成的,但是在2.6版本后,改为由一群内核线程(flusher线程)执行这三种工作。