针对 HugeTLB 和 Device DAX 的 vmemmap 精简方案

HugeTLB

本节旨在解释 HugeTLB Vmemmap 优化 (HVO) 的工作原理。

struct page 结构用于描述物理页面帧。默认情况下,页面帧与其对应的 struct page 之间存在一对一的映射。

HugeTLB 页由多个基本页大小的页面组成,并受许多架构的支持。 有关更多详细信息,请参阅 HugeTLB 页面。 在 x86-64 架构上,当前支持大小为 2MB 和 1GB 的 HugeTLB 页。 由于 x86 上的基本页大小为 4KB,因此 2MB HugeTLB 页由 512 个基本页组成,而 1GB HugeTLB 页由 262144 个基本页组成。 对于每个基本页,都有一个对应的 struct page

在 HugeTLB 子系统中,只有前 4 个 struct page 用于包含有关 HugeTLB 页面的唯一信息。__NR_USED_SUBPAGE 提供了这个上限。 剩余的 struct page 中唯一“有用”的信息是 compound_head 字段,并且该字段对于所有尾页都是相同的。

通过删除 HugeTLB 页面的冗余 struct page,可以将内存返回给伙伴分配器以供其他用途。

不同的架构支持不同的 HugeTLB 页面。 例如,下表是 x86 和 arm64 架构支持的 HugeTLB 页面大小。 由于 arm64 支持 4k、16k 和 64k 基本页面并支持连续条目,因此它支持多种大小的 HugeTLB 页面。

架构

页面大小

HugeTLB 页面大小

x86-64

4KB

2MB

1GB

arm64

4KB

64KB

2MB

32MB

1GB

16KB

2MB

32MB

1GB

64KB

2MB

512MB

16GB

当系统启动时,每个 HugeTLB 页面都有多个 struct page 结构,其大小为 (单位: 页)

struct_size = HugeTLB_Size / PAGE_SIZE * sizeof(struct page) / PAGE_SIZE

其中 HugeTLB_Size 是 HugeTLB 页面大小。 我们知道 HugeTLB 页面大小始终是 PAGE_SIZE 的 n 倍。 所以我们可以得到以下关系

HugeTLB_Size = n * PAGE_SIZE

然后

struct_size = n * PAGE_SIZE / PAGE_SIZE * sizeof(struct page) / PAGE_SIZE
            = n * sizeof(struct page) / PAGE_SIZE

我们可以在 pud/pmd 级别对 HugeTLB 页面使用 Huge 映射。

对于 pmd 级别映射的 HugeTLB 页面,则

struct_size = n * sizeof(struct page) / PAGE_SIZE
            = PAGE_SIZE / sizeof(pte_t) * sizeof(struct page) / PAGE_SIZE
            = sizeof(struct page) / sizeof(pte_t)
            = 64 / 8
            = 8 (pages)

其中 n 是一个页面可以包含的 pte 条目的数量。 所以 n 的值是 (PAGE_SIZE / sizeof(pte_t))。

此优化仅支持 64 位系统,因此 sizeof(pte_t) 的值为 8。 并且此优化仅适用于 struct page 的大小是 2 的幂时。 在大多数情况下,struct page 的大小为 64 字节(例如 x86-64 和 arm64)。 因此,如果我们对 HugeTLB 页面使用 pmd 级别映射,则它的 struct page 结构的大小为 8 个页面帧,其大小取决于基本页面大小。

对于 pud 级别映射的 HugeTLB 页面,则

struct_size = PAGE_SIZE / sizeof(pmd_t) * struct_size(pmd)
            = PAGE_SIZE / 8 * 8 (pages)
            = PAGE_SIZE (pages)

其中 struct_size(pmd) 是 pmd 级别映射的 HugeTLB 页面的 struct page 结构的大小。

例如:x86_64 上的 2MB HugeTLB 页面由 8 个页面帧组成,而 1GB HugeTLB 页面由 4096 个页面帧组成。

接下来,我们以 HugeTLB 页面的 pmd 级别映射为例,展示此优化的内部实现。 有 8 个页面 struct page 结构与 pmd 映射的 HugeTLB 页面相关联。

这是优化之前的样子

   HugeTLB                  struct pages(8 pages)         page frame(8 pages)
+-----------+ ---virt_to_page---> +-----------+   mapping to   +-----------+
|           |                     |     0     | -------------> |     0     |
|           |                     +-----------+                +-----------+
|           |                     |     1     | -------------> |     1     |
|           |                     +-----------+                +-----------+
|           |                     |     2     | -------------> |     2     |
|           |                     +-----------+                +-----------+
|           |                     |     3     | -------------> |     3     |
|           |                     +-----------+                +-----------+
|           |                     |     4     | -------------> |     4     |
|    PMD    |                     +-----------+                +-----------+
|   level   |                     |     5     | -------------> |     5     |
|  mapping  |                     +-----------+                +-----------+
|           |                     |     6     | -------------> |     6     |
|           |                     +-----------+                +-----------+
|           |                     |     7     | -------------> |     7     |
|           |                     +-----------+                +-----------+
|           |
|           |
|           |
+-----------+

所有尾页的 page->compound_head 值相同。 与 HugeTLB 页面关联的第一个 struct page 页面(页面 0)包含描述 HugeTLB 所需的 4 个 struct page。 剩余 struct page 页面(页面 1 到页面 7)的唯一用途是指向 page->compound_head。 因此,我们可以将页面 1 到 7 重新映射到页面 0。 每个 HugeTLB 页面仅使用 1 个 struct page 页面。 这将使我们能够将剩余的 7 个页面释放给伙伴分配器。

这是重新映射后的样子

   HugeTLB                  struct pages(8 pages)         page frame(8 pages)
+-----------+ ---virt_to_page---> +-----------+   mapping to   +-----------+
|           |                     |     0     | -------------> |     0     |
|           |                     +-----------+                +-----------+
|           |                     |     1     | ---------------^ ^ ^ ^ ^ ^ ^
|           |                     +-----------+                  | | | | | |
|           |                     |     2     | -----------------+ | | | | |
|           |                     +-----------+                    | | | | |
|           |                     |     3     | -------------------+ | | | |
|           |                     +-----------+                      | | | |
|           |                     |     4     | ---------------------+ | | |
|    PMD    |                     +-----------+                        | | |
|   level   |                     |     5     | -----------------------+ | |
|  mapping  |                     +-----------+                          | |
|           |                     |     6     | -------------------------+ |
|           |                     +-----------+                            |
|           |                     |     7     | ---------------------------+
|           |                     +-----------+
|           |
|           |
|           |
+-----------+

当将 HugeTLB 释放给伙伴系统时,我们应该为 vmemmap 页面分配 7 个页面并恢复之前的映射关系。

对于 pud 级别映射的 HugeTLB 页面。 它与前者类似。 我们也可以使用这种方法来释放 (PAGE_SIZE - 1) 个 vmemmap 页面。

除了 pmd/pud 级别映射的 HugeTLB 页面之外,某些架构(例如 aarch64)还在转换表条目中提供了一个连续位,以提示 MMU 指示它是可以在单个 TLB 条目中缓存的一组连续条目之一。

连续位用于增加 pmd 和 pte(最后)级别的映射大小。 因此,仅当其 struct page 结构的大小大于 1 页面时,才可以优化此类 HugeTLB 页面。

注意:头 vmemmap 页面不会释放给伙伴分配器,并且所有尾 vmemmap 页面都映射到头 vmemmap 页面帧。 因此,我们可以看到多个带有 PG_headstruct page 结构(例如,每个 2 MB HugeTLB 页面 8 个)与每个 HugeTLB 页面关联。compound_head() 可以正确处理此问题。 只有 一个struct page,带有 PG_head 的尾 struct page 是伪头 struct page。 我们需要一种方法来区分这两种不同类型的 struct page,以便当参数是尾 struct page 但带有 PG_head 时,compound_head() 可以返回真正的头 struct page

Device DAX

device-dax 接口使用与上一章中解释的相同的尾部重复数据删除技术,除非与设备中的 vmemmap(altmap)一起使用。

DAX 中支持以下页面大小:PAGE_SIZE(x86_64 上为 4K)、PMD_SIZE(x86_64 上为 2M)和 PUD_SIZE(x86_64 上为 1G)。 有关 powerpc 的等效详细信息,请参阅 Device DAX

与 HugeTLB 的区别相对较小。

它仅使用 3 个 struct page 存储所有信息,而 HugeTLB 页面则使用 4 个。

鉴于 device-dax 内存不是启动时初始化的系统 RAM 范围的一部分,因此没有 vmemmap 的重新映射。 因此,尾页重复数据删除发生在填充节的稍后阶段。 HugeTLB 重用表示头 vmemmap 页面的页面,而 device-dax 重用尾 vmemmap 页面。 与 HugeTLB 相比,这仅节省了一半的成本。

重复数据删除的尾页未映射为只读。

以下是在填充节之后 device-dax 上的外观

+-----------+ ---virt_to_page---> +-----------+   mapping to   +-----------+
|           |                     |     0     | -------------> |     0     |
|           |                     +-----------+                +-----------+
|           |                     |     1     | -------------> |     1     |
|           |                     +-----------+                +-----------+
|           |                     |     2     | ----------------^ ^ ^ ^ ^ ^
|           |                     +-----------+                   | | | | |
|           |                     |     3     | ------------------+ | | | |
|           |                     +-----------+                     | | | |
|           |                     |     4     | --------------------+ | | |
|    PMD    |                     +-----------+                       | | |
|   level   |                     |     5     | ----------------------+ | |
|  mapping  |                     +-----------+                         | |
|           |                     |     6     | ------------------------+ |
|           |                     +-----------+                           |
|           |                     |     7     | --------------------------+
|           |                     +-----------+
|           |
|           |
|           |
+-----------+