概念
磁盘高速缓存是一种Linux内核将磁盘中的一些数据保留在RAM中的软件机制,这种方式不用再访问磁盘,直接读写缓存即可。磁盘高速缓存有多种方式,比如目录项高速缓存和索引节点高速缓存。前者存放的是描述文件系统路径名的目录项对象,后者存放的是描述磁盘索引节点的索引节点对象。此外,页高速缓存是一种对完整数据页进行操作的磁盘高速缓存。
页高速缓存(Page Cache)是在Linux内核中实现磁盘缓存。其通过把磁盘中的数据缓存在物理内存中,把对磁盘的访问变成对内存的访问,从而减少了磁盘IO操作,提高了磁盘数据访问效率。页高速缓存通过占用空闲内存扩张大小,也可以自我缩放以缓解内存使用压力,从而实现缓存大小的动态调整。
文件访问和预读
应用程序对文件的访问常用的访问方式有两种:一种是通过系统调用mmap()创建直接访问的虚拟地址空间映射;另一种是利用系统调用read()和write()进行寻址访问。
文件通过mmap()映射到虚拟内存空间后的第一次访问,由于页表尚未建立,必然会出现一个内存访问的缺页错误。内核在处理缺页错误时,会通过预读函数来分配Page Cache,之后将对应的文件块读入。再次访问该文件的块时,由于已经存在于Page Cache中,就不需要再次访问文件了。
通过系统调用read()访问文件也会通过页面预读函数分配Page Cache。对文件缓存的写操作,使用写时复制(Copy on Write),等到要同步或者清理缓存时再把文件同步回去。
Linux的预读架构如下图所示,预读算法负责填充Page Cache。应用程序的读缓存一般都比较小,比如文件拷贝命令cp的读写粒度就是4KB;内核的预读算法则会以它认为更合适的大小进行预读 I/O,比如16-128KB。

写缓存策略
写缓存策略一般有三种:不缓存,写穿(Write Through)和写回(WriteBack)。
不缓存就是不使用缓存,直接读写磁盘;
写穿是指写操作穿透缓存,立刻写到磁盘中,这样能很好地保证缓存一致性,但效率较低。
写回是指写操作写在缓存,并将缓存中的页面标记为“脏”页,并添加到脏页链表中,随后由回写进程周期性的将脏页写回到磁盘。写回方式保证了最终一致性,其减少了磁盘IO,提高了效率,但实现复杂度相对较高,断电会丢失缓存数据。
缓存替换和回收策略
Linux内核以最好的可行方式使用空闲的RAM。当系统负载低时,RAM的大部分被磁盘高速缓存占用;当负载增加时,高速缓存就会缩小对RAM的占用,从而为负载进程让出空间。无论是为更重要的缓存项让出空间,还是收缩缓存大小、腾出更多内存,都需要缓存回收策略起作用。Linux采用双链表、伪LRU算法进行缓存替换和回收。Linux内核需要维护两个链表:active链表和inactive链表。active链表存放最近被访问的页;inactive链表存放最近未被访问的页,缓存剔除只能发生在非活动链表。在该回收策略中,有两个重要的标志位,PG_active
和PG_reference
。PG_active
表示页面是否活跃,active链表中的页的PG_active
都是1,inactive链表的都是0;PG_reference
表示最近是否被访问过。
页面移动策略

如果页面被认为是活跃的,则将该页的
PG_active
置位,页被放到active链表上;否则,不置位,页被放在inactive链表上。当页面被访问时,检查该页的
PG_referenced
位,若未被置位,则置位;此时,如果页面在inactive链表上,则不动;
如果页面在active链表上,则移动到链表头部。
若发现该页的
PG_referenced
已经被置位过了,则意味着该页经常被访问,此时,若该页在 inactive 链表上,则置位其
PG_active
位,将其移动到 active 链表上去,并清除其PG_referenced
位的设置。如果页面的
PG_referenced
位被置位了一段时间后,该页面没有被再次访问,那么 Linux 操作系统会清除该页面的PG_referenced
位,因为这意味着这个页面最近这段时间都没有被访问。对于某个在 active 链表上的页面来说,如果
PG_referenced
位未被置位,一段时间后,该页面如果还是没有被访问,那么该页面会被清除其PG_active
位,挪到 inactive 链表上去。对于某个在inactive链表上的页面来说,如果
PG_referenced
位未被置位,一段时间后,该页面如果还是没有被访问,则根据LRU算法,逐渐移出缓存。
场景优化和参数调节示例
常用的参数可以通过以下命令查看:
$ sysctl -a | grep dirty
vm.dirty_background_ratio = 10
vm.dirty_background_bytes = 0
vm.dirty_ratio = 20
vm.dirty_bytes = 0
vm.dirty_writeback_centisecs = 500
vm.dirty_expire_centisecs = 3000
vm.dirty_background_ratio
和vm.dirty_ratio
成对使用。达到vm.dirty_background_ratio
,脏页在刷写回磁盘,不阻塞新的IO请求;如果IO流量很大并且仍旧持续,脏页可以最大达到vm.dirty_ratio
,就必须刷回磁盘,此时新的IO请求会被阻塞。vm.dirty_background_bytes
和vm.dirty_bytes
成对使用。与 ratio 相同。vm.dirty_expire_centisecs
脏页在内存中最长可以待如此长时间,脏页总在内存中会有丢失的风险,超时刷写到磁盘是安全保障。flush相关线程会根据该值刷写磁盘。vm.dirty_writeback_centisecs
定时唤醒flush线程
通过以下命令可以查看脏页和回写页面数量:
$ cat /proc/vmstat | egrep "dirty|writeback"
nr_dirty 878
nr_writeback 0
nr_writeback_temp 0
参数的调整取决于需要执行的操作。在存在快速磁盘子系统的情况下,由于它们具有可供电的NVRAM缓存,这时候再将内容保存在page cache中就不再必要。尝试以更及时的方式将I / O发送到阵列。为此,我们通过修改/etc/sysctl.conf
中的配置并使用sysctl –p
重新加载来降低vm.dirty_background_ratio
和vm.dirty_ratio
。
vm.dirty_background_ratio = 5
vm.dirty_ratio = 10
在某些情况下,大幅提高缓存会对性能产生积极影响。在这些情况下,Linux guest虚拟机上包含的数据不是很关键,可以丢失,通常是应用程序可重复写入相同的文件。从理论上讲,通过允许更多脏页存在于内存中,应用程序反复重写相同的块,但只需要对磁盘进行一次写操作。为此,我们提高参数。有时,还会增加vm.dirty_expire_centisecs
参数,以在缓存中驻留更长时间。这种方式,除了增加数据丢失的风险外,如果高速缓存已满并且需要降级,还有长时间I / O暂停的风险。
vm.dirty_background_ratio = 50
vm.dirty_ratio = 80
在某些情况下,系统必须处理写入磁盘的不频繁的突发流量(比如半夜的批处理作业,在Raspberry Pi上写入SD卡等)。在那种情况下,一种方法可能是允许所有写I / O都存储在缓存中,以便后台刷新操作可以随时间异步处理它。以下配置中,当达到5%的上限时,后台进程将立即开始写入,但是系统不会强制同步I / O,直到达到80%的满载。从那里,您只需调整系统RAM和vm.dirty_ratio
的大小即可使用所有写入的数据。
vm.dirty_background_ratio = 5
vm.dirty_ratio = 80
参考文献
《深入理解Linux内核(第三版)》
https://docs.oracle.com/en/database/other-databases/nosql-database/19.5/admin/linux-page-cache-tuning.html




