关注了就能看到更多这么棒的文章哦~
MM medley: huge page allocation, page promotion, KSM, and BPF
By Jonathan Corbet
March 20, 2025
Gemini-1.5-flash translation
https://lwn.net/Articles/1014220/
随着 2025 Linux Storage, Filesystem, Memory-Management, and BPF Summit (LSFMM+BPF) 即将到来,邮件列表中内存管理补丁的密度有所增加。 其中包括旨在提高巨页(huge page)分配的可靠性和性能、在分层内存系统上实现页面晋升(page promotion)、添加一种不同的内存去重方法以及替换 BPF 内存分配器的补丁。 请继续阅读以获得每个补丁的概述。
可靠的巨页分配
使用各种大小的巨页 (huge page) 对于许多(如果不是大多数)系统的整体性能而言,正变得越来越重要。 如果内存碎片阻止内核分配巨页,性能将会受到影响。 虽然多年来在避免和修复碎片方面已经取得了相当大的进展,但是分配巨页的能力仍然会随着系统的生命周期而降低。 Johannes Weiner 几年来一直在缓慢地进行 一个补丁系列 以改善这种情况;所做的更改相对较小,但是它们的影响可能会很大。
内核的页面分配器跟踪每个请求的“迁移类型(migration type)”;该类型描述了如果某个数据页碰巧挡路了,则可以将其移动到物理内存中其他位置的难易程度。migratetype 这个枚举类型 描述了这些类型:例如,页面是否可以移动或容易回收,或者它们是否固定到位。 内核将物理内存划分为称为“页块(page block)”的连续组,通常容纳 512 个页面;每个页块都具有关联的迁移类型,并且仅允许从每个页块进行兼容的分配。 目的是将所有不可移动的页面组合在一起,从而防止一个不可移动的页面阻止大块内存碎片整理的情况。
虽然页面分配器会尽量避免在可移动页块中分配不可移动的页面,但它也会尽量避免在调用者请求内存时执行回收(reclaim)和压缩(compaction)动作。 启动直接回收或压缩的分配请求会在返回所请求的内存之前执行大量工作,从而在可能不需要的地方产生延迟。 在当前的内核中,页面分配器会放弃并从具有错误迁移类型的页块中进行分配,然后再尝试更昂贵的补救措施。
Weiner 得出的结论是,这些补救措施只是在短期内会更加昂贵,因为错放的分配会产生碎片,这种碎片可能会持续到系统重新启动。 因此,他的系列 patch 首先引入了一种新的“碎片整理模式(defrag mode)”,可以通过一个 sysctl 开关来启用。 启用此模式后,将尝试进行回收和压缩,然后再回退到从具有错误迁移类型的页块中进行分配。 此补丁 中显示的基准测试结果表明,启用此模式后,巨页分配的成功率不再随时间而降低。
但是,数据也表明,当前的成功率比当前内核低得多。 还需要进行两项更改,才能使成功率达到高于新启动的当前内核可以达到的水平,并使其保持在该水平。 第一个更改 是一个小的更改,指示 kswapd 和 kcompactd 内核线程尝试创建页块大小的空闲内存块,而不是满足于较小的成功;这使得更多的完整页块可用于分配为巨页。 另一个更改 提高了这些内核线程旨在创建的空闲页块数量的门槛。
最终结果是透明巨页(Transparent Huge Page, THP)的分配成功率大大提高,并且分配延迟也降低了。 结果看起来不错,但是像这样的更改可能会在其他类型的工作负载中造成性能下降。 因此,在内存管理社区对合并它充满信心之前,可能需要对这项工作进行更广泛的测试。
热页晋升
在分层内存系统(具有不同性能特征的内存)中,内核必须不断努力将正确的页面保存在正确的内存类型中。 通常,这意味着将频繁访问的(“热(hot)”)数据放置在最快且最靠近使用它的工作负载的内存中,而将不太频繁访问的(“冷(cold)”)数据降级到较慢、更远的内存中。
这种方法的一个问题是,很难确定给定页面有多热。 检测一段时间未被访问的内存相对容易,因此可以将冷页面降级到较慢的内存中,使其工作得相当好。 晋升问题需要确定慢速内存中的哪些数据被足够积极地使用,值得迁移到更快的内存,这有点困难。
解决晋升问题的最新尝试是来自 Bharata B Rao 的 这个系列,它建立在以下思想之上:关于数据热度的信息来源的数量正在随着时间的推移而增长。 经典方法(例如扫描内存并检查硬件维护的“已访问(accessed)”位)仍然存在。 但是,包括 AMD 的 基于指令的采样(Instruction Based Sampling, IBS) 和 CXL 内存提供的监控等较新的技术也正在出现。 所需要的是一种收集和使用所有这些信息以将最热门的页面晋升到更快内存的方法。
Rao 的补丁集添加了一个新函数,任何了解内存访问的子系统都可以使用它:
int kpromoted_record_access(u64 pfn, int nid, int src, unsigned long time);此调用告诉内核,页面帧编号 pfn 指示的内存刚刚从 nid 指示的 NUMA 节点访问。 信息来源由 src 提供,而 time 是访问的时间,以节拍为单位。 来源由宏表示,例如 KPROMOTED_HW_HINTS 表示硬件提供的访问信息,或 KPROMOTED_PGTABLE_SCAN 表示通过扫描页表获得的访问信息。 返回值是零或错误代码,指示调用是否能够记录访问信息。 调用者应该如何处理该信息尚不清楚;补丁集中包含的 IBS 驱动程序 忽略了它。
访问数据存储在哈希表中,该哈希表以页面帧编号作为键值。 这种存储看起来很昂贵,因为系统中每个页面都维护着相当多的数据。 此数据的使用者是一个名为 kpromoted 的新内核线程;它使用 Rao 描述为“非常原始”的算法来晋升看起来很热门的页面 — 在最近 5 毫秒内已被访问最小次数(当前补丁中为两次)的页面。
Rao 承认,此系列中的大部分代码都相当基础。 此时的目的不是提供一个用于合并的完善的子系统;相反,它是为了尝试了解总体方法是否可行。 到目前为止,大多数评论都集中在细节上,但是 Jonathan Cameron 指出,CXL 内存提供商将能够通过聚合单个事件来提供此补丁集正在努力创建的大部分信息。 他建议,使用该数据可能会给出足够好的结果,而无需 Rao 的方法所需的所有存储。
更简单的 KSM
内核同页合并(kernel samepage merging, KSM)机制 旨在通过检测具有相同内容的内存页面来提高内存利用率,然后确保使用共享的单个页面,同时释放重复页面。 这些页面可以在完全不知道彼此的进程之间共享;共享内存(如果可写)被标记为写时复制(copy-on-write),如果一个进程写入共享页面,则会导致共享被破坏。 KSM 可以释放相当多的内存供其他用途使用,但是它的使用从未像人们想象的那么频繁。 它的页面扫描需要 CPU 时间,它需要进行一些繁琐的调整才能匹配工作负载,并且它会引发安全问题,因为它可用于确定系统中是否存在具有给定内容的页面。
Mathieu Desnoyers 最近发布了 一种简化的 KSM 式功能,他称之为“同步 KSM(Synchronous KSM, SKSM)”。 它的目标是特定的、看似小众的用例,但是它应该可以更广泛地适用。 Desnoyers 正在努力使运行时代码修补在用户空间中更加常见。 代码修补在内核中被大量使用,以利用检测到的 CPU 的最佳指令,启用或禁用功能而无需运行时测试,启用或禁用跟踪点等等。 Desnoyers 希望更多地将这些技术引入用户空间。
运行时代码修补的问题在于,它破坏了可执行代码通常发生的共享。 如果许多进程正在运行同一程序,它们通常会共享其文本的单个副本;代码修补会破坏该共享,即使每个进程都以相同的方式修补其代码也是如此。 这会增加内存使用量并降低缓存局部性,从而夺走这些技术旨在带来的性能。
SKSM 旨在允许进程通过显式选择加入来恢复在修补这些页面后共享这些页面。 调用 madvise() 并使用 MADV_MERGE 操作将为指示的内存范围设置此共享。 重要的是,该范围会立即被扫描,并且在 madvise() 调用本身中建立任何共享。 没有内核线程在事后扫描内存寻找共享机会。 因此,SKSM 产生的开销完全发生在初始化时;之后,它的工作就完成了。
Linus Torvalds 回应 了该帖子,称他对允许第二个 KSM 实现进入内核不感兴趣,但是“如果感觉这可以 取代 当前 KSM 的恐怖(hoerror),我就会更感兴趣”。 他 承认 他不知道现在谁在使用 KSM,并担心它可能在特定的云环境中运行良好,因此无法删除。 David Hildenbrand 说 KSM 主要用于私有云中,在私有云中它可以“非常有效”,但是由于相关的安全问题,强烈建议不要在公共云中使用它。
因此,尚不清楚 SKSM 将走向何方。 现有的 KSM 功能虽然有其缺点,但似乎确实对某些用户有效,因此删除它不会受到普遍欢迎。 除非 SKSM 能够以某种方式取代 KSM,否则可能根本没有办法合入主线(mainline)内核。
淘汰 BPF 分配器
BPF 程序几乎可以在任何上下文中运行,包括在中断处理程序中,甚至在不可屏蔽中断(non-maskable interrupt, NMI)的处理程序中,在这些处理程序中,获取锁的能力受到严重限制。 这使得在这些程序中分配内存具有挑战性。 BPF 内存分配器 在 6.1 开发周期中被引入,作为 BPF 程序在任何可能的执行上下文中(尝试)分配内存的一种方式。 它的工作方式是维护自己的内存池,可以在需要时使用该内存池,而无需获取锁。 此分配器可以工作,但是它独立于内存管理子系统运行,并且与减少内核中内存分配器数量的持续努力背道而驰。 在 2024 LSFMM+BPF 会议上 讨论 的目标之一是希望将 BPF 程序的分配重新放回内存管理核心中。
Alexei Starovoitov 一直在从事该项目;到目前为止,他的工作已于 2 月下旬 以第九次修订版发布 。 它添加了一个新的分配函数:
struct page *try_alloc_pages(int nid, unsigned int order);此函数可以在任何上下文中安全地调用。 在更多受限的上下文中,此函数将尝试从为给定 NUMA 节点 ID (nid) 维护的 per-CPU 空闲列表中获取所需数量的页面(由 order 指示)。 如果失败,它将尝试获取合适的 per-zone 锁,并直接从伙伴分配器中获取内存。 但是,这只是一个尝试;如果该锁不可用,则分配器将拒绝该请求,而不是等待它。 因此, try_alloc_pages() 具有相对较高的失败可能性,并且调用者必须做好没有所请求内存的准备。
还有一个新的 free_pages_nolock() 函数,可以在不可能获取锁的上下文中将页面释放回系统,而无需获取任何锁。 此补丁集并未完全删除 BPF 特定的分配器,尽管它确实提供了完成该步骤所需的大部分基础架构。
Andrew Morton 最初 抵制 了这项工作,担心它会给核心页面分配器带来额外的维护负担,而其所带来的好处却无法证明其合理性。 Starovoitov 提醒了他 去年的讨论,并补充说,将此功能移回内存管理子系统将消除 BPF 分配器中浪费的内存。 Slab 维护者 Vlastimil Babka 补充了他的支持 。 Morton 没有进一步抵制,并且这些更改目前位于 linux-next 中,可能会合并到 6.15 版本的主线(mainline)中。 请注意,linux-next 包含尚未合并到任何稳定内核版本中的更改,包括那些尚未被接受进入主线(mainline)开发的“非合并变更集(non-merge changeset)”。
敬请关注
2025 LSFMM+BPF 会议于 3 月 24 日在加拿大蒙特利尔开始。 上述许多(如果不是全部)主题可能会在那里得到进一步的讨论。 当然,LWN 将在那里;请密切关注这些页面,以获取我们从会议发回的报道。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~

250

被折叠的 条评论
为什么被折叠?



