Linux内核-页高速缓存与页回写

Posted by keys961 on April 10, 2019

1. 缓存手段

页高速缓存由内存物理页面组成,内容为磁盘上的物理块。

其大小可动态调整。页高速缓存可减少I/O操作次数。

1.1. 写缓存

写磁盘时,有3种策略:

  • 不缓存:直接写入磁盘,并使缓存失效
  • 自动更新缓存,并写入磁盘文件(即write-through cache)
  • 回写(Linux采用的):写操作先写入缓存,但不会立即写入磁盘,而会将页面标记为“脏”,并添加到链表中,回写进程会周期性将其写入磁盘,并消除“脏”标志

1.2. 缓存回收/替换

Linux对非脏页面进行回收/替换,增加可用页数。替换的策略有多种,如:

  • LRU
  • 双链策略(LRU/2):维护2个链表——活跃链表和非活跃链表,前者不能被换出,后者可被换出。2个链表会动态平衡,如活跃链表的页面可被移到非活跃链表中。2个链表由伪LRU构造。
  • 多链策略(LRU/n),类似于LRU/2

2. Linux页高速缓存

2.1. address_space

它和vm_area_struct类似,但是表示的是物理地址(VMA表示的是虚拟地址),其定义在<linux/fs.h>中。

其中i_mmap字段是一个优先搜索树,包含了在该结构体中所有共享的与私有的映射页面。

通常address_space会和一个inode或者swapper关联。

2.2. 操作

address_space_operations结构体表示,如:

  • 写回页面:writepage(struct page*, struct writeback_control*)
  • 读取页面:readpage(struct file*, struct page*)
  • 设置脏页:set_page_dirty(struct page*)

对于读缓存页面

  • 使用find_get_page(mapping, index)找到需要的数据,传入adress_space和一个offset
    • 如果找不到会返回一个NULL,并且内核新分配一个页面,并将之前搜索的页面加载到页高速缓存

对于写缓存页面

  • 首先搜索需要的页
    • 如果需要的页不在,则新分配一个空闲项;
  • 然后创建一个写请求
  • 接着数据被从用户空间拷贝到缓冲中,标记为脏页
  • 最后写入磁盘(被回写进程)

2.3. Radix树

每个address_space都有一个唯一的Radix树(page_tree域)。

它是一个二叉树,是一种压缩前缀树(前缀树是Trie,它比Trie更节省空间),只要指定文件偏移,就能迅速找到所需的页。

它定义在<linux/radix_tree.h>中。

3. 缓冲区高速缓存

之前的Linux有,这样会有2个缓冲区,造成浪费,且同步开销变大,因此现在被统一了,只有页高速缓存。

4. flusher线程

它将脏页写回磁盘。

在下面3种情况下,发生页面写回:

  • 空闲内存低于一个阈值(低于阈值后,调用flusher_threads()通知并唤醒)
  • 脏页驻留时间高于一个阈值(通过周期性唤醒实现)
  • 显式调用sync()fsync()

内核通过多个flusher线程解决回写拥塞,每个线程可独立将脏页写回磁盘,且不同flusher线程处理不同的设备队列。