针对 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_head
的 struct 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 | --------------------------+
| | +-----------+
| |
| |
| |
+-----------+