Linux页高速缓存和页回写
页高速缓存(cache)是Linux内核实现磁盘缓存。它主要用来减少对磁盘I/O操作。是通过把磁盘中的数据缓存到 物理内存中,把对磁盘的访问变为对物理内存的访问。
磁盘高速缓存有两个重要因素:第一,访问磁盘的速度要远低于访问内存的速度,若从处理器L1和L2高速缓存访问则速度更快。第二,数据一旦被访问,就很有可能短时间内再次访问。正是由于基于访问内存比磁盘快的多,所以磁盘的内存缓存将给系统存储性能带来质的飞越。
缓存手段
页高速缓存是由内存中的物理页面组成的,其内容UI应磁盘上的物理块。页高速缓存大小能动态调整--它可以通过占用空闲内存以扩张大小,也可以自我收缩以缓解内存使用压力。我们称正被缓存的存储设备为后备存储,因为缓存背后的磁盘无疑才是所有缓存数据的归属。当内核开始一个读操作,它首先会检查需要的数据是否在页高速缓存中。如果在则放弃访问磁盘,而直接从内存中读取。这个行文称为缓存命中。如果数据没有在缓存中,称为缓存未命中,那么内核必须调度块I/O操作从磁盘去读取数据。然后内核将读来的数据放入页缓存中,于是任何后续相同的数据读取都可以命中缓存了。系统不一定要将整个文件都缓存,可以缓存文件的一页或几页,到底缓存谁取决于谁被访问到。
写缓存
缓存一般被实现成下面三种策略之一:
第一种策略称为不缓存(nowrite),也就是说高速缓存不去缓存任何写操作。当对一个缓存中的数据片进行写时,将直接跳过缓存,写到磁盘,同时也使缓存中的数据失效。那么如果后续读操作进行时,需要再重新从磁盘中读取数据。这种策略很少使用。
第二种策略,写操作将自动更新内存缓存,同时也更新磁盘文件。这种方式,通常称为写透缓存(write-through cache),因为写操作会立刻穿透缓存到磁盘中。这种策略对保持缓存一直性很有好处---缓存数据时刻与后备存储保持同步,所以不需要让缓存失效,同时它的实现也最简单。
第三种策略,称为“回写”。在这种策略下,程序执行写操作直接写到缓存中,后端存储不会立刻直接更新,而是将页高速缓存中被写入的页面标记成“脏”,并且被加入到脏页链表中。然后由一个进程周期性将脏页链表中的页写回到磁盘,从而让磁盘中的数据和内存中最终一致。最后清理“脏”页标识。实际上脏的并不是高速缓存中的数据而是磁盘上的数据(过时了)。更好的描述方式是未同步。通常认为回写策略要好于写透策略。
缓存回收
缓存算法最后涉及的最重要内容是缓存中的数据如何清除,或者是为更重要的缓存项腾出位置,或者是收缩缓存大小。这个工作,也就是决定缓存中什么内容将被清除的策略,称为缓存回收策略。Linux的缓存回收是通过选择干净页进行简单替换。如果缓存中没有足够的干净页面,内核将强制地进行回写操作,以腾出更多的干净可用也。最难的事情在于决定什么页应该回收。理想的回收策略应该是回收那些以后最不可能使用的页面。
最近最少用
缓存回收策略通过所访问的数据特性,尽量最求预测效率。最成功的算法(特别是对于通用目的的页高速缓存)称作最近最少使用算法,简称LRU。LRU回收策略需要跟踪每个页面的访问踪迹(或者至少按照访问时间为序的页链表),以便能回收最老时间戳的页面(或者回收排序链表头所指的页面)。该策略的良好效果源自于缓存的数据越久未被访问,则越不大可能近期再访问,而最近被访问的最有可能被再次访问。但是LRU策略并非是放之四海皆准的法则,对于许多文件被访问一次,再不被访问的情景,LRU尤其失败。将这些页面放在LRU链表的顶端显然不是最优,当然,内核并没办法知道一个文件只会被访问一次,但是它却知道过去访问了多少次。
双链表策略
Linux实现的是一个修改过的LRU,也称为双链策略。和以前不同,Linux维护的不再是一个LRU链表,而是维护两个链表:活跃链表和非活跃链表。处于活跃链表上的页面被认为是“热”的且不会被换出,而在非活跃链表上的页面则是可以被换出的。在活跃链表中的页面必须在其被访问时就处于非活跃链表中。两个链表都被伪LRU规则维护:页面从尾部加入,从头部移除,如同队列。两个链表需要维持平衡--如果活跃链表变得过多而超过了非活跃链表,那么活跃链表的头页面将被重新移回到非活跃链表中,一遍能再被回收。双链表策略解决了传统LRU算法中对仅一次访问的窘境。而且也更加简单的实现了伪LRU语义。这种双链表方式也称作LRU/2。更普遍的是n个链表,故称LRU /n。
假设你正在开发一个很大的软件工程那么你将有大量的源文件被打开,只要你打开读取源文件,这些文件就将被存储在页高速缓存中。只要数据被缓存,那么从一个文件跳到另一个文件将瞬间完成。当编辑文件时,存储文件也会瞬间完成,因为写操作只需要写到内存,而不是磁盘。编译项目时,缓存的文件将使得编译过程更少访问磁盘,所以编译速度也就更快了。如果整个源码树太大了,无法一次性放入内存,那么其中一部分必须被回收--由于双链表策略,任何回收的文件都将处于非活跃链表,而且不大可能是正在编译的文件。在没编译的时候,内核会执行页回写,刷新所修改的文件的磁盘副本。由此可见,缓存将极大地提高系统性能。
Linux页高速缓存
页高速缓存缓存的是内存页面。缓存中的页来自对正规文件、块设备文件和内存映射文件的读写。如果依赖,页高速缓存就包含了最近被访问过的文件的数据块。在执行一个I/O操作前,内核会检查数据是否已经在页高速缓存中了,如果所需要的数据确实在高速缓存中,那么内核可以从内存中迅速地访问需要的页,而不再需要从相对较慢的磁盘上读取数据。
在页高速缓存中的页可能包含了多个不连续的物理磁盘块。也正是由于页面中映射的磁盘块不一定连续,所以在页面高速缓存中检查特定的数据是否已经缓存是件很困难的工作。
基树
任何I/O操作前内核都要检查页是否已经在页高速缓存中了,所以这种频繁进行检查必须迅速、高效,否则搜索和检查页的开销足以抵消高速缓存带来的好处。
Linux使用基树(radix tree)检索页是否已经存在,基树是一个二叉树,只要指定了文件的偏移量,就可以在基树中迅速检索到希望的页。
缓冲区高速缓存
独立的磁盘块通过I/O缓冲池要被存入页高速缓存。一个缓存是一个物理磁盘块在内存里的表示。缓冲的作用就是映射内存中的页面到磁盘块,这样一来页高速缓存在块I/O操作时也减少了磁盘访问,因为它缓存磁盘块和减少块I/O操作。这个缓存通常称为缓冲区高速缓存,虽然实现上它没有作为独立缓存,而是作为页高速缓存的一部分。
缓冲和页高速缓存并非天生就是统一的,2.4内核的主要工作就是统一他们。在更早的内核中,有两个独立的磁盘缓存:页高速缓存和缓冲区缓存。前者缓存页,后者缓存缓冲区。
flusher线程
由于页高速缓存的缓存作用,写操作实际上会被延迟。当页高速缓存中的数据比后台存储的数据更新时,该数据就称作脏数据。在内存中累积起来的脏页最终被写回磁盘。在以下三种情况发生时,脏页被写回磁盘:
- 当空闲内存低于一个特定阈值时,内核必须将脏页写回磁盘以便释放内存,因为只有干净内存才可以被回收。当内存干净后,内核就可以从缓存清理数据,然后收缩缓存,最终释放出更多内存。
- 当脏页的内存中驻留时间超过一个特定的阈值时,内核必须将超时的脏页回写磁盘,以确保脏页不会无限期地驻留在内存中。
- 当用户进程调用sync()和fsync()方法时,内核会按照要求执行回写动作。
在2.6内核中,由一个内核线程flusher执行这三项工作,首先,flusher线程在系统中的空闲内存低于一个特定的阈值时,将脏页刷新回写磁盘。flusher会被周期的唤醒处理过久的脏页数据。如果系统崩溃,没有来得及写回磁盘的脏页就会丢。
系统管理员可以在/proc/sys/vm中设置回写相关参数,也可以通过sysctl系统调用设置他们,下图给出了与pdflush相关的所有可设置变量。