关注了就能看到更多这么棒的文章哦~
Memory-management: tiered memory, huge pages, and EROFS
By Jonathan Corbet
August 15, 2024
Gemini-1.5-flash translation
https://lwn.net/Articles/984839/
内核的内存管理开发者最近一直很繁忙,要想跟上这个核心领域的所有进展可能很困难。为了赶上相关进度,本文将回顾最近影响分层内存系统(tiered-memory systems)、未充分利用的大页(underutilized huge pages)以及增强型只读文件系统 (EROFS) 中重复文件数据的相关工作。
升级和降级
分层内存系统由多种具有不同性能特征的内存类型构成;高层通常更快,而低层则更慢但容量更大。为了最大限度地利用这些内存层级,系统必须能够将每个页面放到最合适的层级。频繁使用的页面通常应该放在快速层级中,而偶尔使用的内存最好放在较慢的层级中。由于使用模式会随着时间推移而发生变化,内存的最佳放置也会随之改变;系统必须能够根据当前使用情况在层级之间移动页面。以这种方式提升和降级页面是分层内存支持中最大的挑战之一。
提升通常是两方面问题中比较容易的一面;系统很容易检测到内存何时被访问并将其移到更快的层级。然而,在当前内核中,这种迁移只适用于已映射到进程地址空间的内存;该机制要求内存被虚拟内存区域 (VMA) 引用才能正常工作。因此,目前未映射的那些频繁使用的内存将不会被提升。
这种情况出现在通过系统调用(例如 read()
和 write()
)访问但未映射到任何地址空间的页面缓存页面中的情况下。然而,内存访问速度对于此类页面同样重要,因此无法提升它们的话就会影响性能。
此补丁系列 来自 Gregory Price,试图解决这个问题。当前内核中的迁移代码(尤其是 migrate_misplaced_folio_prepare()
)需要在迁移之前咨询映射给定 folio(页面集)的 VMA;如果该 folio 是共享的并且以执行权限映射,则迁移将不会发生。然而,完全未映射的页面无法满足该条件,因此缺少 VMA 仅意味着无需执行此检查。有了这种改变(以及其他一些调整),只需在交换代码中添加一个合适的调用,以便在引用 folio 时将其从较低层级迁移到较高层级。
试图适当地放置内存的内核始终会落后于实际情况;它无法在没有观察到新模式的情况下预先检测到访问模式发生改变。然而,有时应用程序会知道它将把注意力从一个内存范围转移到另一个内存范围内。通知内核该事实可能会帮助系统确保内存在需要之前处于最佳位置;至少,这是来自 "BiscuitOS Broiler" 的 此补丁 的目的。
简单来说,此补丁在 madvise()
系统调用中添加了两个新操作。它们被称为 MADV_DEMOTE
和 =MADV_PROMOTE=;它们的作用完全符合预期。应用程序可以使用这些操作,在知道访问模式即将发生变化的情况下显式地请求在层级之间移动内存。
这项工作从技术上来说并不具有挑战性,但也不清楚它是否必要。内核已经提供了一个系统调用,migrate_pages()
,可用于在层级之间移动页面;David Hildenbrand 询问 为什么在这种情况下 migrate_pages()
不够。答案 似乎是 madvise()
存在于 C 库中,但 migrate_pages()
的包装器却存在于额外的 libnuma
库中。正如 Hildenbrand 回答 的,这并不是一个很大的障碍。因此,虽然通过 madvise()
提供此功能对于某些用户来说可能很方便,但这种方便似乎不足以说服要在内核中添加此新功能。
重新获取未充分利用的大页
使用大页(huge pages,巨页)可以提高应用程序性能,因为可以减少系统转换旁路缓冲区 (TLB, translation lookaside buffer) 的使用和内核中的内存管理开销。但大页存在内部碎片问题;如果大页中只有很小一部分内存实际使用,由此产生的浪费可能很大。内存使用的相应增加抑制了许多原本可以从大页中获益的环境中采用大页。
获得两全其美的方案之一可能是主动检测未完全使用的大页,将其拆分为基本页面,然后重新获取未使用的基本页面;这是 此补丁系列 来自 Usama Arif 的目标。它对内存管理子系统进行了两个核心更改,旨在把目前由于内部碎片而浪费的内存恢复出来可供使用。
这些更改中的第一个更改在每次将大页拆分并以基本页面级别映射时生效,就像在当前内核中经常发生的那样。目前,拆分大页会留下完整的基页面(base page)集合,这意味着内存使用量不会改变。但是,如果大页是匿名(用户空间数据)页面,则其中的任何未使用的基页都只包含零。这些基页可以在拥有进程的页表中被替换为对共享零页的引用,从而释放该内存。Arif 的补丁集通过在拆分过程中检查基页面的内容并释放任何被发现仅包含零的页面来实现这一点。
当页面被拆分时,这将释放未充分利用的内存,这是一个开始。然而,如果内核能够主动找到未充分利用的大页并在内存紧张时将其拆分,效果会更好;这是 Arif 的补丁集中第二个更改的目标。
一个大页是由内核中的 folio 表示,有时可以部分映射,这意味着大页中的并非所有基页面都已映射到拥有进程的页表中。当完全映射的 folio 因任何原因被部分取消映射时,folio 会被添加到“延迟拆分列表”中。如果在稍后某个时间点内核需要找到一些空闲内存,它将尝试拆分延迟列表中的 folio,然后努力重新获取每个 folio 中的基页面。
Arif 的补丁集导致内核在每次大页被错误地导入或由 khugepaged
线程从基页面创建时将其添加到延迟列表中。当内存变得紧张且延迟列表被处理时,这些大页(可能仍然完全映射)将被检查是否有零填充的基页面;如果此类页面的数量超过可配置的阈值,则大页将被拆分,所有这些填满 0 的基页面将立即被释放。相反,如果没有达到阈值,则该页面将被视为完全使用并从延迟列表中删除。
值得注意的是,该阈值是一个绝对值;对于封面邮件中提到的测试场景来说,它被设置为 409,大约是 512 页大页的 80%。这种机制意味着,虽然此功能可以拆分处理器实现的未充分利用的 PMD 大小的大页,但它将无法对软件实现的较小的多尺寸大页进行操作。然而,在使用 PMD 大小大页的系统上,封面信中报告的结果表明,这种改变可以提供启用透明大页所带来的性能优势,同时还能收回大部分原本会被浪费的额外内存。
EROFS 的页面缓存去重
令人惊讶的是,系统的内存中经常包含多个包含相同数据的页面。当这种情况发生在匿名页面中时,内核同页合并 功能可以执行去重,恢复一些内存(尽管存在一些安全问题)。然而,针对文件支持的页面的情况则更难。可以导致单个文件以多个名称和 inode 出现的文件系统(例如在 Btrfs 快照中或在提供“reflink”功能的文件系统中)就是一个例子;如果使用了多个名称,文件的多个数据副本可能会出现在页面缓存中。这种情况也可能发生在文件包含相同数据的常见情况下;容器镜像可以通过这种方式复制数据。
对此类页面进行去重,遇到的问题是页面缓存中的每个页面都必须引用它来自的文件;内核中没有关于页面来自多个文件的概念。如果页面被写入,或者如果文件以其他方式发生更改,内核必须在所有级别都做正确的事情。因此,这些重复页面仍然是重复的。
Hongzhen Luo 提出了一种针对 EROFS 文件系统的解决方案,至少在文件级别上是如此。EROFS 是一个只读文件系统,因此由于其文件可能发生的更改而带来的问题不会出现在这里。
EROFS 文件系统可以创建具有特殊扩展属性,名为 trusted.erofs.fingerprint
,附加到每个文件;该属性的内容是文件内容的哈希值。当文件系统中的文件被打开以供读取时,哈希值将被存储在基于 XArray 的数据结构中,与文件的 inode 相关联。每当另一个文件被打开时,它的哈希值就会在该数据结构中被查找;如果存在匹配项,则打开操作将被重定向到首先打开的文件的 inode。
这种机制可能导致多个进程持有指向磁盘上不同文件的多个文件描述符,而这些文件描述符在内核中都引用单个文件。然而,由于这些文件具有相同的内容,因此这种差异对用户空间不可见(尽管 fstat()
调用可能会返回一个令人惊讶的 inode 号)。在内核中,将多个相同文件的多个文件描述符重定向到单个文件意味着只需要在页面缓存中存储该文件内容的一个副本。
该系列中包含的基准测试结果显示,许多不同应用程序的内存使用量显着减少。由于此功能完全包含在 EROFS 文件系统中,因此似乎不太可能遇到核心内存管理补丁经常遇到的各种挑战。页面缓存中文件支持数据的去重在一般情况下仍然是一个难题,但对于这种情况,似乎已经至少部分得到了解决。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~