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