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