page cache的淘汰策略和组织形式

page cache和buffer cache的区别

free -m 看到的cached列表示当前的页缓存(page cache)占用量,buffers列表示当前的块缓存(buffer cache)占用量。用一句话来解释:page cache用于缓存文件的页数据,buffer cache用于缓存块设备(如磁盘)的块数据。页是逻辑上的概念,因此page cache是与文件系统同级的;块是物理上的概念,因此buffer cache是与块设备驱动程序同级的。

page cache与buffer cache的共同目的都是加速数据I/O:写数据时首先写到缓存,将写入的页标记为dirty,然后向外部存储flush,也就是缓存写机制中的write-back(另一种是write-through,Linux未采用);读数据时首先读取缓存,如果未命中,再去外部存储读取,并且将读取来的数据也加入缓存。操作系统总是积极地将所有空闲内存都用作page cache和buffer cache,当内存不够用时也会用LRU等算法淘汰缓存页。

在Linux 2.4版本的内核之前,page cache与buffer cache是完全分离的。但是,块设备大多是磁盘,磁盘上的数据又大多通过文件系统来组织,这种设计导致很多数据被缓存了两次,浪费内存。所以在2.4版本内核之后,两块缓存近似融合在了一起:如果一个文件的页加载到了page cache,那么同时buffer cache只需要维护块指向页的指针就可以了。只有那些没有文件表示的块,或者绕过了文件系统直接操作(如dd命令)的块,才会真正放到buffer cache里。因此,我们现在提起page cache,基本上都同时指page cache和buffer cache两者,本文之后也不再区分,直接统称为page cache。

下图近似地示出32-bit Linux系统中可能的一种page cache结构,其中block size大小为1KB,page size大小为4KB。

作者:LittleMagic

page cache的淘汰算法

1 LRU 和 two-list LRU
将磁盘的内容放到内存固然可以提高后来的访问性能,但因为内存是有限的,肯定会又内存不足的情况。 这时就应该将一些在将来被用到可能性小的数据赶出内存(如果这个数据已经被修改了,则要先写回磁盘,再挪位)。那么哪些数据是将来可能不被访问的呢? 早起采取的算法就是LRU(Least Recently Used), 就是按照最后访问时间进行排序,将越长时间没有访问的数据 就越有可能被敢出去。但LRU 的不足时, 有些情况,一些大的文件只被访问了一次, 例如编译了一个大的项目, 后面就不会被访问了, 太慢占用了大量的内存,但却需要较长的时间才被赶出去。 为了解决这个问题, 就引入了two-list LRU 机制。 任何文件第一次访问的时候,都会被放到一个 inactive LRU 上, 只有第二次访问的时候才会被放到acitve LRU 上。 而清除内存的时候总是先在Inactive LRU 上进行的。

2 writeback 和 flusher threads
一般情况下,写操作都是写到内存。然后有linux 进程将脏数据写回磁盘。 以下三种情况会触发writeback 操作:
2.1 当free memory 下降的一定程度, 就会触发写回操作
2.2 当脏页的数目增加到一定程度,那些够老的脏页就会被写回
2.3 应用程序要求写回。通常是为了考虑到数据安全性的问题。 可以通过sync, fsync, fdatasync 等方式来实现。

2.1 的需求是受vm.dirty_background_ratio 控制,显示的是一个百分比。 如果脏块的数量的百分比超过这个值,就会唤醒flusher 去写回脏数据。
2.2 的需求是受vm.dirty_writeback_centisecs 和 vm.dirty_expire_centisecs 控制,单位都是1/100 秒。 前者控制flusher 被唤醒的周期, 后者表示 唤醒后,多老的数据会被写回。

另外 vm.dirty_ratio 总内存的百分比, 但是是只一个进程的脏页如果高于这个值,就会被写回。

在调整这些参数之前,先要了解程序的写是否是因为2.3 的需求。 如果是这样,修改其他参数也没什么用了。 例如在Mysql 中有一系列这种参数, 控制一次写入后是否需要fsync。 如果要提高性能,先要关闭这些fsync, 然后再考虑调节这几个vm 参数。

在调整这些参数以后,再通过sar 或者pidstat 来查看io 的状况是否如预期。

3 flusher thread 和 bdflush, kupdated , pdfush
简单的说 flusher 是2.6.32的内核才引用的。主要优点是 flusher 可以启动多个线程, 不同的线程负责不同的disk spindles. 从而产生更好的性能。 这个特性经常被称为Per-backing-device based writeback。
其他的几个以后应该看不到了。也就不谈了。 它们都对device 不敏感的。

如果想清空page cache 的脏页,可以:
echo 1 > /proc/sys/vm/drop_caches

更多的vm 参数含义可以访问:
https://www.kernel.org/doc/Documentation/sysctl/vm.txt

page cache的组织形式

基于raix tree的管理形式:
https://en.wikipedia.org/wiki/File:Patricia_trie.svg
page cache的淘汰策略和组织形式「参考https://en.wikipedia.org/wiki/Radix_tree」

page cache中那么多的page frames,怎么管理和查找呢?这就要说到之前的文章提到的address_space结构体,一个address_space管理了一个文件在内存中缓存的所有pages。这个address_space可不是进程虚拟地址空间的address space,但是两者之间也是由很多联系的。

这篇文章讲到,mmap映射可以将文件的一部分区域映射到虚拟地址空间的一个VMA,如果有5个进程,每个进程mmap同一个文件两次(文件的两个不同部分),那么就有10个VMAs,但address_space只有一个。

每个进程打开一个文件的时候,都会生成一个表示这个文件的struct file,但是文件的struct inode只有一个,inode才是文件的唯一标识,指向address_space的指针就是内嵌在inode结构体中的。在page cache中,每个page都有对应的文件,这个文件就是这个page的owner,address_space将属于同一owner的pages联系起来,将这些pages的操作方法与文件所属的文件系统联系起来。

来看下address_space结构体具体是怎样构成的:

struct address_space {
struct inode host; / Owner, either the inode or the block_device /
struct radix_tree_root page_tree; /
Cached pages /
spinlock_t tree_lock; /
page_tree lock /
struct prio_tree_root i_mmap; /
Tree of private and shared mappings /
struct spinlock_t i_mmap_lock; /
Protects @i_mmap /
unsigned long nrpages; /
total number of pages /
struct address_space_operations
a_ops; / operations table /
...
}
host指向address_space对应文件的inode。
address_space中的page cache之前一直是用radix tree的数据结构组织的,tree_lock是访问这个radix tree的spinlcok(现在已换成xarray)。
i_mmap是管理address_space所属文件的多个VMAs映射的,用priority search tree的数据结构组织,i_mmap_lock是访问这个priority search tree的spinlcok。
nrpages是address_space中含有的page frames的总数。
a_ops是关于page cache如何与磁盘(backing store)交互的一系列operations。
从Radix Tree到XArray

Radix tree的每个节点可以存放64个slots(由RADIX_TREE_MAP_SHIFT设定,小型系统为了节省内存可以配置为16),每个slot的指针指向下一层节点,最后一层slot的指针指向struct page(关于struct page请参考这篇文章),因此一个高度为2的radix tree可以容纳64个pages,高度为3则可以容纳4096个pages。

如何在radix tree中找到一个指定的page呢?那就要回顾下struct page中的mapping和index域了,mapping指向page所属文件对应的address_space,进而可以找到address_space的radix tree,index既是page在文件内的offset,也可作为查找这个radix tree的索引,因为radix tree就是按page的index来组织struct page的。

具体的查找方法和使用***做索引的page table(参考这篇文章)以及使用PPN做索引的sparse section查找(参考这篇文章)都是类似的。这里是用page index中的一部分bits作为radix tree第一层的索引,另一部分bits作为第二层的索引,以此类推。因为一个radix tree节点存放64个slots,因此一层索引需要6个bits,如果radix tree高度为2,则需要12个bits。

内核中具体的查找函数是find_get_page(mapping, offset),如果在page cache中没有找到,就会触发page fault,调用__page_cache_alloc()在内存中分配若干物理页面,然后将数据从磁盘对应位置copy过来,通过add_to_page_cache()-->radix_tree_insert()放入radix tree中。在将一个page添加到page cache和从page cache移除时,需要将page和对应的radix tree都上锁。

Linux中radix tree的每个slot除了存放指针,还存放着标志page和磁盘文件同步状态的tag。如果page cache中一个page在内存中被修改后没有同步到磁盘,就说这个page是dirty的,此时tag就是PAGE_CACHE_DIRTY。如果正在同步,tag就是PAGE_CACHE_WRITEBACK。只要下一层中有一个slot指向的page是dirty的,那么上一层的这个slot的tag就是PAGE_CACHE_DIRTY的,就像一滴墨水一样,放入清水后,清水也就不再完全清澈了。

前面介绍struct page中的flags时提到,flags可以是PG_dirty或PG_writeback,既然struct page中已经有了标识同步状态的信息,为什么这里radix tree还要再加上tag来标记呢?这是为了管理的方便,内核可以据此快速判断某个区域中是否有dirty page或正在write back的page,而无须扫描该区域中的所有pages。

想想进程虚拟地址空间中管理VMA的red black tree(参考这篇文章),这个叫radix tree长的跟我们平时见到的树好像不太一样啊,它的每一个节点更像是一个指针数组吧。所以啊,现在address_space中radix tree已经被xarray取代了(参考这篇文章)。

Reverse Mapping

如果要回收page cache中一个页面,可不仅仅是释放掉那么简单,别忘了Linux中进程和内核都是使用虚拟地址的,多少个PTE页表项还指向这个page呢,回收之前,需要将这些PTE中P标志位设为0(not present),同时将page的物理页面号PFN也全部设成0,要不然下次PTE指向的位置存放的就是无效的数据了。可是struct page中好像并没有一个维护所有指向这个page的PTEs组成的链表。

前面的文章说过,struct page数量极其庞大,如果每个page都有这样一个链表,那将显著增加内存占用,而且PTE中的内容是在不断变化的,维护这一链表的开销也是很大的。那如何找到这些PTE呢?从虚拟地址映射到物理地址是正向映射,而通过物理页面寻找映射它的虚拟地址,则是reverse mapping(逆向映射)。page的确没有直接指向PTE的反向指针,但是page所属的文件是和VMA有mmap线性映射关系的啊,通过page在文件中的offset/index,就可以知道VMA中的哪个虚拟地址映射了这个page。

在代码中的实现是这样的:

__vma_address(struct page page, struct vm_area_struct vma)
{
pgoff_t pgoff = page_to_pgoff(page);
return vma->vm_start + ((pgoff - vma->vm_pgoff) << PAGE_SHIFT);
}
映射了某个address_space中至少一个page的所有进程的所有VMAs,就共同构成了这个address_space的priority search tree(PST)。

PST是一种糅合了radix tree和heap的数据结构,其实现较为复杂,现在已经被基于augmented rbtree的interval tree所取代,详情请参考这篇文章。

对比一下,一个进程所含有的所有VMAs是通过链表和红黑树组织起来的,一个文件所对应的所有VMA是通过基于红黑树的interval tree组织起来的。因此,一个VMA被创建之后,需要通过vma_link()插入到这3种数据结构中。

__vma_link_list(mm, vma, prev, rb_parent);
vma_link_rb(mm, vma, rb_link, rb_parent);
vma_link_file(vma);
下文将主要介绍page cache的同步问题。

原创文章,转载请注明出处。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章