HugeTLB 和设备 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 页面使用巨大的映射。

对于 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 个页面帧组成。

接下来,我们以 pmd 级别映射的 HugeTLB 页面为例,说明此优化的内部实现。有 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 (page 0) 包含描述 HugeTLB 所需的 4 个 struct page。剩余的 struct page (page 1 到 page 7) 的唯一用途是指向 page->compound_head。因此,我们可以将 page 1 到 page 7 重新映射到 page 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 页面帧。因此,我们可以看到多个 struct page 结构与每个 HugeTLB 页面关联,其中包含 PG_head(例如,每个 2 MB HugeTLB 页面 8 个)。compound_head() 可以正确处理此问题。只有 一个struct page,带有 PG_head 的尾 struct page 是伪头 struct page。我们需要一种方法来区分这两种不同类型的 struct page,以便当参数是尾 struct page 但具有 PG_head 时,compound_head() 可以返回真正的头 struct page

设备 DAX

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

DAX 支持以下页面大小:PAGE_SIZE(x86_64 上为 4K)、PMD_SIZE(x86_64 上为 2M)和 PUD_SIZE(x86_64 上为 1G)。有关 powerpc 的等效详细信息,请参阅 设备 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     | --------------------------+
|           |                     +-----------+
|           |
|           |
|           |
+-----------+