LWN:2025 疯狂的 mapcount!

关注了就能看到更多这么棒的文章哦~

Looking forward to mapcount madness 2025

By Jonathan Corbet
March 17, 2025
Gemini-1.5-flash translation
https://lwn.net/Articles/1013649/

内核的内存管理子系统必须处理的众多重要任务之一,是跟踪内存页如何映射到系统上运行的进程的地址空间。只要存在到给定页面的映射,就必须保持该页面的存在。事实证明,跟踪这些映射比看起来应该的要困难,并且内存管理子系统内向 folios (页组)的迁移正在增加其自身的复杂性。作为 “mapcount madness” 会议的后续,David Hildenbrand 在 2024 Linux 存储、文件系统、内存管理和 BPF 峰会上发布了 一个补丁系列,旨在改进 folios 的映射计数处理 — 但在某些情况下,精确的跟踪统计仍然难以实现。

理论上,跟踪映射应该相对简单:当添加页面的映射时,递增该页面的映射计数以匹配。删除映射应该导致相关映射计数的递减。但是,巨页(huge page)和大型页组使这种情况变得复杂;它们有自己的映射计数,本质上是它们包含的页面的映射计数的总和。通常重要的是要知道整个页组是否具有映射,因此单独的计数很有用,但它带来了一些复杂性。

例如,内核经常会问一个问题:有多少进程映射了给定的页面或页组?如果知道映射是 独占的 — 页面或页组由单个进程映射,则可以优化许多操作。如果不知道给定映射的专有性,则 copy-on-write (写时复制)页面的处理也很难正确执行;在这方面失败导致了过去一些令人不快的错误。对于单个页面,专有性问题很容易回答:如果映射计数为 1,则映射是独占的,否则是共享的。但是,如果映射计数是在页组级别维护的,则该规则不再适用,因为页组级别的计数几乎不可能是 1。

当前的方案也存在与性能相关的问题,而页组也许可以帮助改进。映射传统的 PMD 大小的巨页相当于映射 512 个基本页面;当前,如果整个巨页都被映射,则必须相应地递增其每个基本页面的映射计数。增加每个基本页面上的映射计数会花费内核开发人员不希望花费的时间;仅跟踪页组级别的单个映射计数会更快。但是,这种优化只会使专有性问题更加难以回答,尤其是在存在部分映射的页组(其中只有部分页面映射到地址空间中)的情况下。

因此,毫不奇怪,随着内存管理复杂性的增加,内核开发人员已经花费了数年时间来弄清楚如何正确管理映射计数。

Make room!

为了更好地跟踪页组级别的映射计数,Hildenbrand 首先需要在 folio 结构中为一些额外的信息腾出更多空间。struct folio 有点复杂且令人困惑。作为在整个内核中促进页组使用过渡的一种方式,此结构覆盖在 struct page 之上,后者描述单个页面。但是,页组通常需要跟踪比可以放入紧密 packed 的 page 结构中的更多信息;对于包含许多页面的大型页组而言,尤其如此。

但是,由于大型页组确实包含许多页面 — 并且是物理上连续的页面,因此可以使用一些技巧。实际上,没有必要为页组中的每个页面维护完整的 page 结构,因为它们是作为一个单元进行管理的;实际上,消除对所有这些 page 结构的管理是页组转换的目标之一。但是这些 page 结构确实存在,它们在系统的内存映射中连续布局。因此,大型页组不仅可以支配一个 page 结构的内存,还可以支配所有组成页面的 page 结构。因此,可以小心地利用“尾页(tail pages)”的 page 结构(第一个页面之后的那些页面)来保存此附加信息。

如果查看 struct folio 的定义,则很快就会清楚它比单个 page 结构大。在覆盖头页面的 page 结构的初始字段之后,您会发现以下内容:

union {     struct {         unsigned long _flags_1;         unsigned long _head_1;         atomic_t _large_mapcount;         atomic_t _entire_mapcount;         atomic_t _nr_pages_mapped;         atomic_t _pincount;#ifdef CONFIG_64BIT         unsigned int _folio_nr_pages;#endif     /* private: the union with struct page is transitional */     };         struct page __page_1;     };

folio 结构的这一部分精确地覆盖了第一个尾页的 page 结构,假设存在这样的页面。它包含旨在帮助维护当前内核中的映射计数以及其他相关字段的信息。还有一个 __page_2 组件(未显示),主要保存 hugetlbfs 子系统使用的信息。因此, folio 结构的长度实际上是三个 page 结构的长度,尽管其中大部分仅对大型(至少四个页面)的页组有效。

尽管这看起来很庞大,但仍然缺少 Hildenbrand 需要的更好地跟踪映射计数的空间。为了能够处理 order-1(两页)的页组,他需要该空间适合上面显示的 page-1 联合中。因此,该系列的最初六个补丁专门用于在 folio 结构中改组字段,并在过程中添加 __page_3 联合。 __page_1 联合增加了一些复杂性,但是工作的核心在于这些新字段:

mm_id_mapcount_t _mm_id_mapcount[2];union {mm_id_t _mm_id[2];unsigned long _mm_ids;};

它们将用于更好地跟踪它们所属的页组的映射。但是,描述如何完成需要更多的背景知识。

One, two, or many

那么,所有这些工作如何帮助改善大型页组的映射计数跟踪,这些大型页组可能在多个进程之间共享,并且可以在任何一个进程中部分映射?起点是表示进程地址空间的 mm_struct 结构。任何时候映射页组,该映射都将属于一个特定的进程,因此也属于一个特定的 mm_struct 结构。因此,页组是否被独占映射的问题归结为它的所有映射是否属于同一个 mm_struct 。这只是跟踪哪些 mm_struct 结构包含页组的映射的问题。

当然,可能存在成千上万个包含此类映射的结构;例如,考虑一下系统中几乎每个进程都将映射 C 库。在不消耗大量时间和内存的情况下跟踪所有这些映射并非易事。但是,实际上并不重要跟踪到 C 库之类的每个映射;此处的目的是掌握独占映射的页组,因此没有所有这些映射。

添加到 folio 结构的 page 1 的 _mm_id 数组旨在用于此目的;它可以跟踪最多两个具有页组映射的 mm_struct 结构。最直接的方法是仅存储指向这些 mm_struct 结构的指针,但是 folio 结构中的空间仍然非常宝贵。因此,使用内核的 ID 分配器子系统,为每个 mm_struct 分配一个较短的 "mm ID" 。

首次创建页组时,两个 _mm_id 条目都设置为 MM_ID_DUMMY ,表示它们未使用。当要添加映射时,内核将搜索 _mm_id 以查找适当的 mm ID,然后递增关联的 _mm_id_mapcount 条目以记录新的映射。因此,例如,如果页组中的八个页面映射到地址空间中,则计数将递增八以匹配。如果 mm ID 在 _mm_id 中没有条目,则内核将查找一个 MM_ID_DUMMY 条目以用于此 mm_struct ,然后开始在此处跟踪映射。

内核现在正在为此页组维护多个映射计数。=folio= 结构的 _large_mapcount 字段继续从任何地址空间计数到页组的所有映射,就像在当前内核中一样。但是,每个 mm_struct 还有一个 _mm_id_mapcount 计数,用于跟踪与该特定结构关联的映射数量。现在很容易回答页组是否被独占映射的问题:如果其中一个 _mm_id_mapcount 计数器等于 _large_mapcount ,则所有映射都属于关联的 mm_struct ,并且内核知道该映射是独占的。否则,映射是共享的。

跟踪两个 mm_struct 结构的能力处理了短期共享映射的最常见情况 — 当进程调用 clone() 以创建新的子进程时。该新进程将使用第二个 _mm_id 插槽,用于父进程和子进程之间现在共享的映射。如果像通常发生的那样,子进程调用 execve() 以运行新程序,则共享映射将被拆除,子进程的 _mm_id 插槽将被释放,并且内核将知道该页组再次被独占映射。

不过,此机制中只有一个很小的差距:当第三个进程出现并映射页组时会发生什么?没有可用的 _mm_id 插槽可供其使用,因此无法跟踪其映射。如果发生这种情况,内核将在 folio 结构中设置一个特殊位,指示它不再控制到页组的所有映射来自何处,并将它视为共享的。这可能会导致内核错误地得出结论,即页组在未被独占映射时被共享映射;后果将是性能下降,但不会缺乏正确性。如果有足够的进程取消映射页组,则可能会出现 _large_mapcount 再次与其中一个 _mm_id_mapcount 计数对齐的情况,并且内核将再次知道该页组被独占映射。

Per-page mapcounts and more

所有这些工作的结果是,内核可以更好地处理任何给定的页组是被独占还是共享映射,尽管它仍然可能偶尔得出结论,即页组在未被独占映射时被共享映射。但这并不是这项工作的唯一目标;Hildenbrand 还希望消除在大型页组中维护 per-page (每页)映射计数的开销。补丁系列的最后一部分是该目标的实现;最后,不再使用或维护 per-page 计数。

放弃 per-page 映射计数的最重要的后果似乎是使内核提供的某些内存管理统计信息(例如,各种 resident-set sizes (常驻集大小))变得有些模糊。Hildenbrand 认为这种不精确性不应该成为问题,但他也承认需要时间来了解真正的影响。为了避免在此期间出现意外,有一个新的配置参数 CONFIG_NO_PAGE_MAPCOUNT ,用于控制这些更改是否有效。这项工作被认为是实验性的,以至于在这一点上,Hildenbrand 不希望默认情况下在生产内核中启用它。

不过,在某个时候会希望这样做;根据补丁 cover letter 中包含的性能结果,对于某些工作负载,放弃 per-page 映射计数可以使 clone() 调用速度提高多达 20%。

同时,这项工作启用了另一种优化,涉及在进程 fork 之后如何使用某些 transparent huge pages (透明巨页)。在当前内核中,如果巨页(页组)在基本页面级别(“PTE 映射”)映射,则在 fork 之后将不会重用它。随着透明巨页的使用(尤其是在 必须 进行 PTE 映射的多大小巨页中)的增长,重用这些巨页将变得越来越重要。现在,借助 per-mm_struct 映射计数,内核可以知道进程何时具有对巨页的独占访问权限,并可以继续将其用作此类。这种重用在某些基准测试结果中产生了显着改进。

预计将来大型页组的使用将会增长;它们是管理任何给定进程使用的大部分内存的更有效方法。因此,尽可能优化这种情况非常重要。Hildenbrand 的补丁集朝着这个方向迈出了一些步伐,同时解决了一个多年来一直难以解决的棘手问题。这些更改目前位于 linux-next 存储库中,因此它们很有可能在 6.15 合并窗口期间进入 mainline (主线,(英文原文: mainline))。如果是这样,2025 Linux 存储、文件系统、内存管理和 BPF 峰会(将与该合并窗口同时进行)可能是最后一个以“mapcount madness”会议为特色的峰会。

全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值