关注了就能看到更多这么棒的文章哦~
The multi-generational LRU
By Jonathan Corbet
April 2, 2021
DeepL assisted translation
https://lwn.net/Articles/851184/
memory-management 子系统的关键任务之一就是优化系统对可用内存的利用。这意味着需要把包含了不再需要的数据的 page 换出到别的地方去,从而能让这些 page 被更好的利用在其他地方。预测哪些 page 在不久之后会被访问到,这是一项棘手的任务。内核已经开发出一些机制希望提高猜对的几率。但是,内核不仅经常出错,而且很可能还耗费了大量的 CPU 时间来做出了错误的选择。Yu Zhao 发布的 multi-generational LRU patch set 就是希望改善这种情况。
一般来说,内核无法知道哪些 page 在不久的将来会被访问到,所以它必须依靠退而求其次的指标:最近被使用过的 page 的集合。最近被访问过的 page 有可能在未来会再次被使用,但也有例外。例如,假设有一个正在按顺序读取一个文件的应用程序。文件中的每一个 page 在被读取时都会被放入 page cache,其实应用程序已经不会再需要它了。在这种情况下,最近访问过,并不意味着该 page 很快会被再次使用到。
内核使用一对 least-recently-used(LRU,最少最近使用)的 list 来跟踪那些 page。最近被访问过的 page 都被保留在 "active" list 中,刚刚被访问过的 page 被放在 list 的头部。如果 page 最近没有被访问过,就从 list 的尾部移除出来,放在 "inactive" list 的头部。inactive list 中的 page 如果被某个进程访问到了,那么就会被再次加到 active list 中。有些 page,比如上面例子中的来自顺序读取文件所产生的 page,会一开始就在 inactive list 里面,这意味着如果没有后续使用的话,这些 page 会被较快地回收掉。
当然,还有更多的细节需要处理。尤其需要注意的是,这里实际上有两对 list,一对针对匿名页(anonymous page),另一对针对文件内容(file-backed)的 page。如果正在使用 cgroup 的话,那么每个 active cgroup 都有完整的这几个 LRU list。
Zhao 的 patch set 指出了当前方案的一些问题。分为 active/inactive,对于要想得到准确的决策来说,还是太粗糙了,而且 page 经常都被放到错误的 list 上了。尤其是在 cgroup 中使用了全套的独立 list,使得内核很难对各个 cgroup 中 page 的相对新旧程度进行对比。内核由于一些原因,长期以来偏向于优先把针对文件的(file-backed)page 给换出去(evict),这可能导致一些包含有用的文件的 page 被换出而空闲无用的匿名页仍然留在内存中。这个问题在云计算环境中变得更加严重。这种场景下,每个客户的本地存储(local storage)相对较少,因此导致,文件支持(file-backed)的 page 相对来说本来就很少。同时,匿名页的扫描会比较耗时,部分原因是因为它使用了复杂的反向映射(reverse-mapping)机制,当需要进行大量的扫描时,这个机制的表现并不理想。
Closing the generation gap
multi-generational LRU patch 希望试图通过两个底层改动来解决这些问题。
增加更多的 LRU list,目的是不仅只分为 active 和 inactive list,而是将两者之间的 page age 范围也涵盖起来。这些 list 就被成为“generations(代)”
改变 page scanning 的方式来减少其开销。
最近使用过的 page 会被分配到最年轻的一代(尽管有一些例外情况,后面有描述)。随着时间的推移,内存管理子系统会扫描各个进程的 page,以确定每一个 page 自从上次扫描之后是否被使用过。那些一直闲置的 page 就会被移到下一代的 list 去,表示有一段时间没有用过了。任何一代中的 page 如果有发生过访问,就会被移回最年轻的一代的 list 中。
这项工作最后产出的是一个根据 page age 年龄来得到的多组分布,既有那些最近被访问过的 page,又有那些在一段时间内没有被使用的 page。支持多少代,可以通过内核配置来设置。对于手机场景来说,这个数字似乎可以小到 4 个,而对于云服务器来说,数字则应该是这个数字的几倍。
当需要回收 page 时,只需要考虑最老的这一代。对于匿名页和文件内容(file-backed)page 来说,"最老的一代" 可能是不同的。匿名页一般来说更难回收(通常它们总是需要被写入 swap 中),于是这个新功能的代码中会更加偏向于更积极地回收 file-backed page。因此,file-backed page 可能不会像匿名页面那样能经过那么多代的 list 而未被回收掉。不过,目前的 patch 中对 file-backed page 的回收只允许比匿名页回收提前一代。
据称,multi-generational 机制比目前的双链表方法更加准备。当一个页面进入最老的一代时,比起 inactive list 中的页面来说,它更加可能不再被需要了。这反过来意味着这些 page 可以被更积极地回收掉,能拿到更多的内存来用于真正使用它的任务。这种机制允许随时比较匿名页和 file-backed page 的年龄,通过跟踪每一代的创建时间,来比较不同 cgroup 中 page 的年龄。当前的内核中这个信息并没有持续记录下来。反过来,这也使得识别和回收闲置的匿名页变得更加容易。
另一个号称具有的优势是改变了 page 的扫描方式。page 是通过每个进程中的 page-table entries(PTE,页表项)来访问的,这些 PTE 中就含有这个 "最近访问过" 的标志位。不过,目前内核是通过 page 本身进行扫描的,必须使用反向映射来寻找并检查相关的 PTEs,这个操作很耗时。而 multi-generational LRU 代码则直接扫描 PTE,这种方法具有更好的本地性(locality,意味着很少有全局影响)。scheduler 中的一个 hook 则可以帮助跟踪自从上次扫描以来实际运行过的进程,因此可以跳过那些没有执行过的进程。
multi-generational LRU 还受益于跳过了当前内核中用于决定哪些页面应该被回收的许多启发式方法的做法。不过,还是有一些保留了。比如,当一个 page 被首次建立时,就会根据这些规则来决定它应该被安排在哪一个 generation。
当前由于发生 fault 而引入的 page,会被分配到最年轻的一代,这是大家公认的合适做法。
未被映射的 page(也就是驻留在内存中但没有 PTE 指向它,这些 page 包括被选定要回收、但被再次引用之前实际上尚未被回收的 page)如果被再次引用,就会加到第二年轻的这一个 generation。这样做似乎是为了避免使最年轻的一个 generation 包含太多 page 从而延迟后续的 page scan 到创建下一代的时候。
正在被回收(reclaim)的 page,但在其内容完全被写入后备存储之前必须要一直保持可访问,这些就会被添加到第二代。这可以防止在回写(writeback)过程中再次尝试回收它们。
正在被停用(deactivated)的 page 会进入最老的一代。那些由 readahead 机制带来的 page 也会被放到这里,毕竟读取这些 page 其实是内核的一种投机预测行为,并不能保证它们会有用武之地。
有一些提供给 user space 的开关可以控制这个机制,包括可以完全关闭 multi-generational 代码。更多信息请看这个documentation patch。
Generational change
据说,在完成所有这些工作之后,page reclaim 比以前更有效、更有针对性。像安卓这样的系统,在使用这种方案时,可以看到有更少的 low-memory kill(指应用程序进程由于内存不够而被杀死),Chrome OS 也测到更少的 out-of-memory kill,服务器系统也能更好地利用可用内存。这看起来是一个全面的改进。
鉴于此,人们可能会感到疑惑,为什么 multi-generational 算法要与内存管理代码的其他部分分开,并且还是 optional(可选的)。本质上,它是一种完全独立的 page aging and reclaim(内存页老化和回收)方法,跟当前的 LRU list 是并列存在的关系。据推测,分离开来的原因是有很多 workload 可能不会从这种方案中得到好处。我们需要做更多的测试,从而以了解 multi-generational LRU 在哪些方面会出现问题,以及需要做什么来防止这种情况发生。
multi-generational LRU 最终可能会赢得内存管理开发者的支持,他们中的大多数人还没有对这个 patch set 发表意见。不过,它还是需要在大部分 workload 中能表现出更好的性能(或者至少是没有性能降低),才能让大家赞同它可以作为当前 LRU 的替代品,而不是仅仅是 LRU list 的补充。维护两个独立的 LRU 方案,会给内核社区带来不少困难。如果 multi-generational LRU 真的更好,那么完全切换到过去会是更好的选择。
要想回答清楚这个问题,肯定会有一个漫长的过程。memory-management 这边哪怕较小的改动也会需要不少时间才能合入。因为这里的改动非常可能给某些用户带来性能损失。这个变化不是 "relatively small" (改动相对不大)的,所以要想得到合入,标准会更高。但是,如果 multi-generational LRU 真能达到它所说的水平的话,它最终还是应该能达到这个标准的。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~