透明大頁支援

本文件描述了透明大頁 (THP) 支援的設計原則及其與記憶體管理系統其他部分的互動。

設計原則

  • “優雅的回退”: 不瞭解透明大頁的 mm 元件會回退到將大的 pmd 對映分解為 pte 表,並在必要時拆分透明大頁。 因此,這些元件可以繼續處理常規頁面或常規 pte 對映。

  • 如果由於記憶體碎片導致大頁分配失敗,則應優雅地分配常規頁面並在同一 vma 中混合,而不會出現任何故障或顯著延遲,並且使用者空間不會注意到

  • 如果某些任務退出並且更多的大頁變得可用(無論是立即在夥伴系統中還是透過 VM),則由常規頁面支援的訪客物理記憶體應自動(使用 khugepaged)重新定位到大頁上

  • 它不需要記憶體預留,而是儘可能使用大頁(這裡唯一可能的預留是 kernelcore=,以避免不可移動的頁面使所有記憶體碎片化,但這種調整並非透明大頁支援所特有,它是一個通用特性,適用於核心中的所有動態高階分配)

get_user_pages 和 pin_user_pages

如果在巨頁上執行 get_user_pages 和 pin_user_pages,它們將像往常一樣返回頭頁或尾頁(與在 hugetlbfs 上完全一樣)。 大多數 GUP 使用者只會關心頁面的實際物理地址及其臨時鎖定以便在 I/O 完成後釋放,因此他們永遠不會注意到頁面是巨大的。 但是,如果任何驅動程式要處理尾頁的頁面結構(例如檢查 page->mapping 或其他與頭頁相關而不是尾頁相關的位),則應更新為跳轉以檢查頭頁。 引用任何頭/尾頁都會阻止該頁被任何人拆分。

注意

這些不是 GUP API 的新約束,它們與適用於 hugetlbfs 的約束相同,因此任何能夠處理 hugetlbfs 上的 GUP 的驅動程式也可以在透明大頁支援的對映上正常工作。

優雅的回退

遍歷頁表但不瞭解大的 pmd 的程式碼可以簡單地呼叫 split_huge_pmd(vma, pmd, addr),其中 pmd 是 pmd_offset 返回的那個。 透過查詢 “pmd_offset” 並在 pmd_offset 返回 pmd 後在缺少的地方新增 split_huge_pmd,可以很容易地使程式碼瞭解透明大頁。 感謝優雅的回退設計,只需一行程式碼的更改,您就可以避免編寫數百甚至數千行復雜的程式碼來使您的程式碼瞭解大頁。

如果您沒有遍歷頁表,但遇到了無法在程式碼中本地處理的物理大頁,則可以透過呼叫 split_huge_page(page) 來拆分它。 這是 Linux VM 在嘗試交換大頁之前所做的事情。 如果該頁面被鎖定,split_huge_page() 可能會失敗,您必須正確處理這種情況。

透過一行程式碼更改使 mremap.c 瞭解透明大頁的示例

diff --git a/mm/mremap.c b/mm/mremap.c
--- a/mm/mremap.c
+++ b/mm/mremap.c
@@ -41,6 +41,7 @@ static pmd_t *get_old_pmd(struct mm_stru
                return NULL;

        pmd = pmd_offset(pud, addr);
+       split_huge_pmd(vma, pmd, addr);
        if (pmd_none_or_clear_bad(pmd))
                return NULL;

巨頁感知程式碼中的鎖定

我們希望儘可能多的程式碼是巨頁感知的,因為呼叫 split_huge_page() 或 split_huge_pmd() 會有成本。

要使頁表遍歷瞭解大的 pmd,您只需在 pmd_offset 返回的 pmd 上呼叫 pmd_trans_huge()。 您必須以讀取(或寫入)模式持有 mmap_lock,以確保 khugepaged 不會從您身下建立大的 pmd(khugepaged collapse_huge_page 除了 anon_vma 鎖之外,還以寫入模式獲取 mmap_lock)。 如果 pmd_trans_huge 返回 false,您只需回退到舊的程式碼路徑。 如果 pmd_trans_huge 返回 true,則必須獲取頁表鎖 (pmd_lock()) 並重新執行 pmd_trans_huge。 獲取頁表鎖將防止大的 pmd 從您身下轉換為常規 pmd(split_huge_pmd 可以與頁表遍歷並行執行)。 如果第二個 pmd_trans_huge 返回 false,您應該只需釋放頁表鎖並像以前一樣回退到舊的程式碼。 否則,您可以繼續本地處理大的 pmd 和大的頁面。 完成後,您可以釋放頁表鎖。

引用計數和透明大頁

THP 上的引用計數與在其他複合頁面上的引用計數基本一致

  • get_page()/put_page() 和 GUP 對 folio->_refcount 進行操作。

  • 尾頁中的 ->_refcount 始終為零:get_page_unless_zero() 永遠不會在尾頁上成功。

  • 整個 THP 的 PMD 條目的 map/unmap 增加/減少 folio->_entire_mapcount 和 folio->_large_mapcount。

    我們還維護兩個用於跟蹤 MM 所有者的槽(MM ID 和相應的 mapcount),以及當前狀態(“可能已對映共享”與“獨佔對映”)。

    使用 CONFIG_PAGE_MAPCOUNT,當 _entire_mapcount 從 -1 變為 0 或 0 變為 -1 時,我們還會將 folio->_nr_pages_mapped 增加/減少 ENTIRELY_MAPPED。

  • 使用 PTE 條目的單個頁面的 map/unmap 增加/減少 folio->_large_mapcount。

    我們還維護兩個用於跟蹤 MM 所有者的槽(MM ID 和相應的 mapcount),以及當前狀態(“可能已對映共享”與“獨佔對映”)。

    使用 CONFIG_PAGE_MAPCOUNT,當 page->_mapcount 從 -1 變為 0 或 0 變為 -1 時,我們還會增加/減少 page->_mapcount 並增加/減少 folio->_nr_pages_mapped,因為這會計算 PTE 對映的頁面數。

split_huge_page 內部必須在從頁面結構中清除所有 PG_head/tail 位之前,將頭頁中的引用計數分配給尾頁。 對於頁面表條目採用的引用計數,可以很容易地完成,但我們沒有足夠的資訊來分配任何額外的鎖定(即來自 get_user_pages)。 split_huge_page() 會拒絕任何拆分鎖定大頁的請求:它期望頁面計數等於所有子頁面的 mapcount 之和加一(split_huge_page 呼叫者必須引用頭頁)。

split_huge_page 使用遷移條目來穩定匿名頁面的 page->_refcount 和 page->_mapcount。 檔案頁面只需取消對映。

我們對物理記憶體掃描器也很安全:掃描器獲取頁面引用的唯一合法方式是 get_page_unless_zero()。

所有尾頁在 atomic_add() 之前都具有零 ->_refcount。 這可以防止掃描器獲取到該點的尾頁的引用。 在 atomic_add() 之後,我們不關心 ->_refcount 值。 我們已經知道在拆分後應該從頭頁中取消多少引用。

對於頭頁,get_page_unless_zero() 將成功,我們不介意。 拆分後引用的去向很明確:它將保留在頭頁上。

請注意,split_huge_pmd() 對引用計數沒有任何限制:pmd 可以在任何時候拆分,永遠不會失敗。

部分取消對映和 deferred_split_folio()(僅限匿名 THP)

取消對映 THP 的一部分(使用 munmap() 或其他方式)不會立即釋放記憶體。 相反,我們在 folio_remove_rmap_*() 中檢測到 THP 的子頁面未使用,並在記憶體壓力到來時將 THP 排隊以進行拆分。 拆分將釋放未使用的子頁面。

由於我們可以在檢測到部分取消對映的地方進行鎖定,因此立即拆分頁面不是一個選項。 如果 THP 跨越 VMA 邊界,它也可能適得其反,因為在 exit(2) 期間會發生許多情況下的部分取消對映。

函式 deferred_split_folio() 用於將 folio 排隊以進行拆分。 拆分本身將在我們透過 shrinker 介面獲得記憶體壓力時發生。

使用 CONFIG_PAGE_MAPCOUNT,我們可以根據 folio->_nr_pages_mapped 可靠地檢測到部分對映。

使用 CONFIG_NO_PAGE_MAPCOUNT,我們根據 THP 中每個頁面的平均 mapcount 來檢測部分對映:如果平均值 < 1,則匿名 THP 肯定是部分對映的。 只要只有一個程序對映 THP,這種檢測就是可靠的。 對於長時間執行的子程序,可能存在當前無法檢測到部分對映的場景,並且將來可能需要在記憶體回收期間進行非同步檢測。