多代LRU
多代LRU是一种替代LRU实现,它优化页面回收并在内存压力下提高性能。页面回收决定了内核的缓存策略和内存过度分配的能力。它直接影响了kswapd CPU的使用率和RAM的效率。
设计概述
目标
设计目标包括:
- 良好的访问时间表现
- 尝试从空间局部性中获益
- 快速路径以做出明显选择
- 简单的自我校正启发式
访问时间表现的表示是所有LRU实现的核心。在多代LRU中,每一代代表一组具有相似访问时间表现的页面。代表性建立了一个(基于时间的)共同参考框架,因此有助于做出更好的选择,例如在计算机上不同的memcgs之间或数据中心中不同的计算机之间(用于作业调度)。
利用空间局部性可以提高在获取访问位时的效率。rmap遍历针对单个页面,不尝试从发现年轻的PTE中获益。页面表遍历可以扫描地址空间中所有年轻的PTE,但地址空间可能过于稀疏而无法获益。关键是优化这两种方法并将它们结合使用。
快速路径可以减少代码复杂性和运行时开销。未映射的页面不需要TLB刷新;干净的页面不需要回写。这些事实只有在其他条件相似时才有帮助,例如访问时间表现。有了代作为共同参考框架,额外的因素就凸显出来。但明显的选择可能不是好选择;因此自我校正是必要的。
简单的自我校正启发式的好处是不言而喻的。同样,有了代作为共同参考框架,这变得可行。具体来说,可以根据额外因素对同一代中的页面进行分类,并且反馈循环可以统计比较这些类别中的重新故障百分比,并推断哪些是更好的选择。
假设
热页面的保护和冷页面的选择基于页面访问通道和模式。有两种访问通道:
- 通过页面表访问
- 通过文件描述符访问
前一通道的保护设计上更强,因为:
- 由于访问位的近似,确定前一通道的访问模式的不确定性更高。
- 由于需要TLB刷新和可能遇到脏位的可能性,淘汰前一通道的成本更高。
- 由于应用程序通常不会为主要页面故障做准备,就像它们为阻塞I/O做的那样,对前一通道的保护不足的惩罚更高。例如,GUI应用程序通常使用专用I/O线程以避免阻塞渲染线程。
还有两种访问模式:
- 具有时间局部性的访问
- 不具有时间局部性的访问
基于上述原因,前一通道被假定遵循前一模式,除非存在VM_SEQ_READ或VM_RAND_READ,而后一通道被假定遵循后一模式,除非观察到异常的重新故障。
工作流概述
可清除页面被划分为每个lruvec的多个代。最年轻的代号存储在lrugen->max_seq中,对于匿名和文件类型,它们的年龄相同。最老的代号分别存储在lrugen->min_seq[]中,对于匿名和文件类型,因为干净的文件页面可以被清除,而不受交换约束的限制。这三个变量是单调递增的。
代号被截断为order_base_2(MAX_NR_GENS+1)位,以适应folio->flags中的代计数器。每个截断的代号是lrugen->folios[]的索引。滑动窗口技术用于跟踪至少MIN_NR_GENS和最多MAX_NR_GENS代。代计数器在页面位于lrugen->folios[]之一时存储在[1, MAX_NR_GENS]范围内的值;否则,它存储为零。
每个代被划分为多个层。通过文件描述符访问N次的页面位于层order_base_2(N)。与代不同,层没有专用的lrugen->folios[]。与跨代移动需要LRU锁不同,跨层移动仅涉及folio->flags上的原子操作,因此成本可以忽略不计。模仿PID控制器的反馈循环监视来自匿名和文件类型的所有层的重新故障,并决定从哪些类型的哪些层清除或保护。期望的效果是在匿名和文件类型之间按照交换级别的比例平衡重新故障百分比。
有两个概念上独立的过程:老化和清除。它们形成一个闭环系统,即页面回收。
老化Aging
老化产生年轻的代。给定一个lruvec,当max_seq-min_seq+1接近MIN_NR_GENS时,它会增加max_seq。当它发现通过页面表访问到热页面时,老化会将其提升到最年轻的代;当它增加max_seq时,冷页面的降级会随之发生。老化使用页面表遍历和rmap遍历来查找年轻的PTE。对于前者,它迭代lruvec_memcg()->mm_list,并对该列表上的每个mm_struct调用walk_page_range()来扫描PTE,然后在每次迭代后增加max_seq。对于后者,当清除遍历rmap并发现年轻的PTE时,老化会扫描相邻的PTE。对于两者,一旦发现年轻的PTE,老化会清除访问位,并将由该PTE映射的页面的代计数器更新为(max_seq%MAX_NR_GENS)+1。
清除Eviction
清除消耗旧的代。给定一个lruvec,当由min_seq%MAX_NR_GENS索引的lrugen->folios[]为空时,它会增加min_seq。为了选择要从中清除的类型和层,它首先比较min_seq[]以选择更老的类型。如果两种类型都一样老,它会选择第一个层的重新故障百分比较低的那个类型。第一个层包含单次使用的未映射干净页面,这是最好的选择。如果老化发现通过页面表访问到该页面并更新了其代计数器,清除会根据其代计数器对页面进行排序。如果该页面通过文件描述符多次访问,并且反馈循环已经检测到该页面所在层的异常重新故障,清除还会将页面移动到下一个代,即min_seq+1。为此,反馈循环使用第一个层作为基准,原因如前所述。
工作集保护(Working set protection)
每个代在出生时都有时间戳。如果设置了lru_gen_min_ttl,则在最老的代出生在lru_gen_min_ttl毫秒内时,lruvec将受到保护,即防止lru_gen_min_ttl毫秒内的工作集被清除。如果无法将此工作集保留在内存中,则会触发OOM killer。
这种基于时间的方法具有以下优点:
- 更容易配置,因为它对应用程序和内存大小是不可知的。
- 更可靠,因为它直接与OOM killer相关联。
mm_struct列表
为每个memcg维护一个mm_struct列表,并且当任务所有者的任务迁移时,mm_struct会跟随其所属的任务到新的memcg。
页面表遍历器迭代lruvec_memcg()->mm_list,并对该列表上的每个mm_struct调用walk_page_range()来扫描PTE。当多个页面表遍历器迭代相同的列表时,它们中的每一个都会获得一个唯一的mm_struct,因此它们可以并行运行。
页面表遍历器会忽略任何错位的页面,例如,如果一个mm_struct被迁移,那么在当前memcg处于回收状态时,留在先前memcg中的页面将被忽略。同样,页面表遍历器将忽略来自除了正在回收的节点之外的其他节点的页面。
该基础设施还会跟踪mm_struct在上下文切换之间的使用情况,以便页面表遍历器可以跳过自上次迭代以来一直处于休眠状态的进程。
Rmap/PT遍历反馈
搜索LRU列表上每个页面映射的PTE的rmap可能很昂贵,因为来自不同VMAs(PA空间)的页面对rmap(VA空间)不友好。对于大部分使用映射页面的工作负载,搜索rmap可能会在回收路径中产生最高的CPU成本。
lru_gen_look_around()利用空间局部性来减少对rmap的访问次数。它扫描年轻PTE的相邻PTE并提升热页面。如果扫描是高效的,它会将指向PTE表的PMD条目添加到Bloom过滤器。这形成了清除和老化之间的反馈循环。
布隆过滤器
布隆过滤器是一种空间和内存高效的数据结构,用于集合成员测试,即测试一个元素是否不在集合中或可能在集合中。
在清除路径中,特别是在lru_gen_look_around()中,如果一个PMD有足够数量的热页面,它的地址将被放入过滤器中。在老化路径中,集合成员意味着将扫描PTE范围以查找年轻页面。
请注意,布隆过滤器在集合成员测试上是概率性的。如果测试是假阳性,那么成本就是额外扫描一段PTE范围,这可能会产生热页面。过滤器本身的参数可以控制极限情况下的假阳性率。
PID控制器
模仿比例-积分-微分(PID)控制器的反馈循环监视匿名和文件类型上的重新故障,并在两种类型都可用时决定要清除哪种类型。
PID控制器使用代而不是壁钟作为时间域,因为在不同的内存压力下,CPU可以以不同的速率扫描页面。它为每个新代计算移动平均值,以避免永久锁定在次优状态。
Memcg LRU
Memcg LRU是memcg的每个节点的LRU。它也是LRU的LRU,因为每个节点和memcg组合都有一个folio的LRU(参见mem_cgroup_lruvec())。它的目标是改善全局回收的可伸缩性,这对于数据中心的系统范围内内存过度分配至关重要。请注意,Memcg LRU仅适用于全局回收。
Memcg LRU的基本结构可以通过与folio的活动/非活动LRU的类比来理解:
- 它有年轻和老的(代),即活动和非活动的对应物;
- 增加max_seq触发提升,即激活的对应物;
- 其他事件触发类似的操作,例如,下线一个memcg触发降级,即非激活的对应物。
在全局回收方面,它具有两个明显的特点:
- 分片,允许每个线程从随机的memcg(在旧代中)开始,并提高并行性;
- 最终公平性,允许直接回收随时退出,并在一段时间内降低延迟而不影响公平性。
在全局回收期间遍历memcg方面,它将最佳情况的复杂度从O(n)改进为O(1),并且不会影响最坏情况的复杂度O(n)。因此,平均而言,它具有次线性的复杂度。
总结
多代LRU(folio)可以分解为以下部分:
- 代
- Rmap遍历
- 通过mm_struct列表的页面表遍历
- 用于rmap/PT遍历反馈的布隆过滤器
- 用于重新故障反馈的PID控制器
老化和清除形成了一个生产者-消费者模型;具体来说,后者通过代的滑动窗口驱动前者。在老化中,rmap遍历通过将热度密集的页面表插入布隆过滤器来驱动页面表遍历。在清除中,PID控制器使用重新故障作为反馈来选择要清除的类型和要保护的层。