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 | --------------------------+
| | +-----------+
| |
| |
| |
+-----------+