Chapter 16 - 页高速缓存和页回写
Created by : Mr Dk.
2019 / 10 / 30 22:13
Nanjing, Jiangsu, China
页高速缓存 (cache) 是 Linux 内核实现的磁盘缓存:
- 减少对磁盘的 I/O 操作
- 把磁盘中的数据缓存到物理内存中
- 把对磁盘的访问变为对物理内存的访问
两个重要因素:
- 访问磁盘的速度远低于访问内存的速度 (差好几个数量级)
- 数据一旦被访问,就可能在短时间内再次被访问到——临时局部原理 (temporal locality)
16.1 缓存手段
页高速缓存的大小能够动态调整,占用空闲内存以扩张大小,自我收缩以缓解内存使用压力。首先检查需要的数据是否在页高速缓存中:
- 如果在,则放弃访问磁盘,直接从内存读取 - 缓存命中
- 未命中则调度块 I/O 操作从磁盘读取数据,将数据放入页缓存,之后的访问就可以命中了
系统不必将整个文件都缓存:可以缓存几个文件的不同页。到底该缓存谁取决于谁被访问到。
16.1.1 写缓存
当进程写磁盘后,缓存如何被使用?
- Nowrite
- 直接跳过缓存写磁盘,同时也使缓存中的数据失效
- 后续读操作进行时,需要重新从磁盘中读取数据
- Write-through cache
- 写操作自动更新缓存,同时更新磁盘文件
- 相当于穿透缓存到磁盘中
- 对保持缓存一致性很有好处
- Write-back
- Linux 中使用
- 写操作直接写到缓存中,后端存储不立刻更新
- 将页高速缓存中的被写入页面标记为 dirty,并加入一个 dirty page 链表中
- 由一个回写进程周期性地写回磁盘,并清理 dirty 标志
通过延迟写磁盘,能够在以后的时间内合并更多的数据并再次刷新。
16.1.2 缓存回收
缓存算法的核心:缓存的数据如何清除。为更重要的缓存项腾出位置,收缩缓存大小,腾出内存。Linux 的缓存回收是通过不脏的页进行简单替换。如果缓存中没有足够的干净页面,则强制进行回写回收。核心在于,如何决定什么页应该回收:
- 最近最少使用 LRU
- 回收最老时间戳的页面
- 缓存的数据越久未被访问,越不大可能近期再被访问
- 对于只被访问一次的文件,LRU 很失败
- 双链策略
- Linux 实现的一种改进版 LRU
- 维护两个链表:活跃链表、非活跃链表
- 活跃链表上的页面比较 hot,不会被换出
- 非活跃链表上的页面可以被换出
- 两个链表都是 FIFO 的
- 两个链表要维护平衡
双链策略解决了 LRU 中仅访问文件一次的窘境。
16.2 Linux 页高速缓存
16.2.1 address_space 对象
Linux 页高速缓存的目标是缓存任何基于页的对象:
- 各种类型的文件
- 各种类型的内存映射
16.2.2 address_space 操作
16.3 缓冲区高速缓存
独立的磁盘块通过块 I/O 缓冲也要被存入页高速缓存。在更早的内核中,有两个独立的磁盘缓存:
- 页高速缓存 - 缓存页面
- 缓冲区高速缓存 - 缓存缓冲区
一个磁盘块可以同时存在于两个缓存中,因此必须同步操作两个缓存中的数据,还浪费内存。在 Linux 2.4 内核中,将两者进行统一。现在只有页高速缓存。
16.4 flusher 线程
在内存中积累起来的脏页最终必须被写回磁盘。时机:
- 空闲内存低于一定阈值时
- 脏页在内存驻留时间超过阈值
- 用户进程调用
sync()
和fsync()
系统调用时
当空闲内存低于阈值时,内核唤醒一个或多个 flusher 线程,该线程开始将脏页写回磁盘,直到满足两个条件:
- 已有指定的最小数目的页被写到磁盘
- 空闲内存数超过了阈值
Flusher 后台线程会被周期性唤醒,将驻留时间过长的脏页写出,确保内存中不会有长期存在的脏页。在系统启动时,内核初始化一个定时器,周期性唤醒 flusher 线程。将脏页写回后,再次初始化定时器。
16.4.1 膝上型计算机模式
特殊的 write-back 策略:将硬盘转动的机械行为最小化,允许硬盘尽可能长时间停滞,延长供电时间。
Flusher 需要找准磁盘运转的时机,完成回写磁盘,保证不会专门为了写磁盘而主动激活磁盘运行。回写延迟较长,保证膝上型计算机模式可以等到磁盘运转时再写入数据。关闭磁盘驱动器是节电的重要手段。这种模式的坏处:发生错误时容易丢失数据。多数 Linux 发布版会在接上电池时自动开启该模式。
16.4.2 历史上的 bdflush、kupdated 和 puflush
16.4.3 避免拥塞的方法:使用多线程
之前的 bdflush 的缺点:仅包含了一个线程。该线程可能堵塞在某个设备的请求队列上,而其它设备的请求队列却无法得到处理。如果系统有多个磁盘,内核应该使每个磁盘都处于忙状态,内核应当有多个回写线程并发执行。Linux 2.6 内核中的每个 flusher 线程可以独立地将脏页刷新回磁盘。如果每个线程在同一个拥塞的队列上挂起了怎么办呢?Flusher 线程模型和具体块设备关联,每个线程从每个给定设备的脏页链表写回磁盘,每个磁盘对应一个线程,在每个磁盘上都维护更高的吞吐量。