​Linux 內核透明巨型頁支持

== 目標 ==

處理大內存的性能關鍵計算應用程序工作集已經運行在 libhugetlbfs 之上,然後依次運行 hugetlbfs。透明的巨型頁面支持是另一種使用大頁爲虛擬內存提供大頁支持的方法, 該支持自動提升和降低頁面大小和沒有 hugetlbfs 的缺點。

目前它只適用於匿名內存映射和 tmpfs/shmem。但是將來它可以擴展到其他文件系統。實際上,已經支持了只讀的文件映射

應用程序運行更快的原因有兩個的因素。第一個因素幾乎完全無關緊要,事實並非如此,這很重要,因爲它也有缺點在頁錯誤中需要更大的清除頁拷貝有潛在的負面影響。第一個因素是採取每個 2M 的虛擬區域都有一個頁面錯誤(將內核的進入 / 退出頻率減少 512 倍)。這的生命週期中,一個內存映射只有第一次訪問內存。第二個更持久,也更重要因子將會影響應用程序的運行時整個內存的所有後續訪問 。第二個因素有兩個組件: 1)TLB miss 將運行更快 (特別是使用嵌套分頁的虛擬化,但幾乎總是在沒有虛擬化的裸系統上。2) 單個 TLB 條目將是映射更大數量的虛擬內存,從而減少 TLB miss 次數。使用虛擬化和嵌套分頁只有 KVM 和 Linux 客戶端同時支持映射更大的 TLB 正在使用大頁面,但顯著的速度已經發生了,如果其中一個使用大頁面只是因爲 TLB miss 會跑得更快。

== 設計 ==

 透明大頁支持最大限度地利用空閒內存,如果與 hugetlbfs 的保留方法相比,允許所有 未使用的內存用作緩存或其他可移動 (甚至不可移動的對象)。它不需要預留來防止從用戶空間發現大頁面分配失敗。它允許分頁 和所有其他高級 vm 功能在大頁上。應用程序不需要修改就可以利用它。

然而,應用程序可以進一步優化以利用這個功能,就像他們之前優化過避免每個 malloc(4k) 都需要大量的 mmap 系統調用。優化用戶空間到目前爲止不是強制性的,khugepaged 已經可以照顧長生命週期的頁面分配, 即使對於處理大量內存的不知道大頁的應用程序也是如此。

在某些情況下,當啓用大頁面時,系統範圍內,應用程序可能最終會分配更多的內存資源。一個應用程序可以映射一個 大的區域,但只觸及其中 1 字節,在這種情況下,一個 2M 的頁面可能被分配而不是分配一個 4k 頁面是沒有好處的。這就是爲什麼 可以在系統範圍內禁用大頁面,並且只在內部使用它們 MADV_HUGEPAGE 的 madvise 的區域。

嵌入式系統應該只在 madvise 區域內啓用大頁面爲了消除浪費寶貴內存字節的風險,並且只會跑得更快。

應用程序可以從大頁中獲得很多好處,而不可以冒着丟失內存的風險使用大頁,應該使用 madvise(MADV_HUGEPAGE) 在他們關鍵映射區域。

== sysfs ==

透明大頁支持匿名內存能被完全的禁用(主要是爲了調試)或僅在 MADV_HUGEPAGE 區域內啓用 (避免佔用更多內存資源的風險)或者系統範圍內啓用。這可以通過以下方式實現:

echo never >/sys/kernel/mm/transparent_hugepage/enabled
echo always >/sys/kernel/mm/transparent_hugepage/enabled 
echo madvise >/sys/kernel/mm/transparent_hugepage/enabled

還可以限制 VM 中的碎片整理工作,以生成匿名的巨型頁面,以防它們不能立即自由地使用 madvise 區域, 或者永遠不要嘗試對內存進行碎片整理,而只是回退到常規頁面,除非巨型頁面立即可用。顯然,如果我們花費 CPU 時間對內存進行碎片整理,那麼我們將期望獲得更多的好處, 因爲我們稍後使用了大頁面而不是普通頁面。這不是總能保證的,更可能的情況是分配給一個 MADV_HUGEPAGE 區域。

echo always >/sys/kernel/mm/transparent_hugepage/defrag 
echo defer >/sys/kernel/mm/transparent_hugepage/defrag 
echo defer+madvise >/sys/kernel/mm/transparent_hugepage/defrag 
echo madvise >/sys/kernel/mm/transparent_hugepage/defrag
echo never >/sys/kernel/mm/transparent_hugepage/defrag

“always” 意味着請求 THP 的應用程序將在分配失敗時暫停,並直接回收頁面和規整內存, 以便立即分配 THP。對於那些從 THP 使用中受益頗多並願意延遲虛擬機開始使用它們的虛擬機來說,這可能是可取的。

“defer” 意味着應用程序將在後臺喚醒 kswapd 來回收頁面, 並喚醒 kcompactd 來規整內存,以便在不久的將來 THP 可用。khugepage 負責隨後安裝 THP 頁面。

"defer+madvise" 只對已經使用 madvise(MADV_HUGEPAGE) 的區域,後臺喚醒 kswapd 以回收頁面,並喚醒 kcompactd 以規整內存,以便 THP 在不久的將來可用。

"madvise" 將進入直接回收,像 "always",但只對 madvise(MADV_HUGEPAGE) 的區域。這是默認行爲。

“never” 應該是不言自明的,它不採取任何措施。

默認情況下,內核嘗試在讀取頁面錯誤時使用巨型零頁來進行匿名映射。可以通過寫入 0 來禁用巨型 0 頁,也可以通過寫入 1 來啓用巨型 0 頁:

echo 0 >/sys/kernel/mm/transparent_hugepage/use_zero_page 
echo 1 >/sys/kernel/mm/transparent_hugepage/use_zero_page

一些用戶空間 (比如一個測試程序,或者一個優化的內存分配庫) 可能想知道一個透明大頁的大小(以字節爲單位):

cat /sys/kernel/mm/transparent_hugepage/hpage_pmd_size

 當 transparent_hugepage/enabled 設置爲 “always” 或“madvise”時,khugepaged 將自動啓動,如果設置爲“never”,它將自動關閉。

 khugepaged 的運行頻率通常較低,因此,雖然人們可能不希望在缺頁異常期間同步調用碎片整理算法, 但至少在 khugepaged 中調用碎片整理是值得的。但是,也可以通過寫 0 來禁用 khugepaged 中的碎片整理, 或者通過寫 1 來啓用 khugepaged 中的碎片整理:

echo 0 >/sys/kernel/mm/transparent_hugepage/khugepaged/defrag 
echo 1 >/sys/kernel/mm/transparent_hugepage/khugepaged/defrag

你也可以控制 khugepaged 每次通過時應該掃描多少頁面:

/sys/kernel/mm/transparent_hugepage/khugepaged/pages_to_scan

以及每次通過之間在 khugepaged 中等待毫秒數 (你可以設置爲 0 來運行 khugepaged,在一個核的 100% 利用率):

/sys/kernel/mm/transparent_hugepage/khugepaged/scan_sleep_millisecs

以及在 khugepage 中等待多少毫秒,如果有一個巨大的頁面分配失敗,以阻止下一次分配嘗試。

/sys/kernel/mm/transparent_hugepage/khugepaged/alloc_sleep_millisecs

 khugepaged 的進度可以從坍縮的頁面數中看到:

/sys/kernel/mm/transparent_hugepage/khugepaged/pages_collapsed

每次通過:

/sys/kernel/mm/transparent_hugepage/khugepaged/full_scans

max_ptes_none 指定有多少額外的小頁面(即尚未映射的)可以在踏縮一組小頁到大頁中被分配(查詢到相應的頁表項爲空)。

/sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none

 較高的值會導致程序使用額外的內存。數值越低,獲得的 thp 性能越低。max_ptes_none 值只會浪費很少的 cpu 時間,你可以忽略它。

max_ptes_swap 指定當將一組頁面坍縮(collapse)成一個透明的大頁面時,可以從交換區換入多少頁面(查詢到相應的頁表項爲換出頁標識符)。。

/sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_swap

較高的值會導致過多的交換 IO 並浪費內存。較低的值可以防止 thp 被坍縮, 從而導致更少的頁面坍縮進 thp,內存訪問性能較低。

== 啓動參數 == 

你可以更改透明大頁 sysfs 啓動時的默認值,通過傳遞參數 "transparent_hugepage=always"或"transparent_hugepage=madvise"或"transparent_hugepage=never" 到內核命令行。

 == tmpfs/shmem 中的大頁面 ==

您可以使用掛載選項控制 tmpfs 中的大頁分配策略 "huge="。它可以有以下值:

默認策略爲 “never”。

“mount -o remount,huge= /mountpoint” 在掛載後工作良好: 重新掛載 huge=never 根本不會分解大頁面, 只是停止更多的分配。

還有一個 sysfs 接口可以控制內部 shmem 掛載的大頁分配策略:

/sys/kernel/mm/transparent_hugepage/shmem_enabled。

掛載用於 SysV SHM, memfds,共享匿名映射 (/dev/zero 或 MAP_ANONYMOUS) GPU 驅動的 DRM 對象,Ashmem。

除了上面列出的策略之外,shmem_enabled 還允許另外兩個值:

== 需要重新啓動應用程序 ==

transparent_hugepage/enabled 值和 tmpfs 掛載選項隻影響未來的行爲。因此,爲了使它們有效,您需要重新啓動任何可能使用大頁面的應用程序。這也適用於在 khugepaged 中註冊的區域。

== 監控使用情況 ==

 當前使用的匿名透明大頁面的數量系統可以通過讀取 / proc/meminfo 中的 AnonHugePages 字段來訪問。爲了識別哪些應用程序正在使用匿名透明的大頁面,讀取 / proc/PID/smaps 並統計爲每個映射的 AnonHugePages 字段是必要的。

 映射到用戶空間的文件透明大頁面數量可用通過讀取 / proc/meminfo 中的 ShmemPmdMapped 和 ShmemHugePages 字段。爲了確定哪些應用程序正在映射文件透明的巨大頁面,它讀取 / proc/PID/smaps 並統計爲每個映射 FileHugeMapped 字段是必要的。

注意,讀取 smaps 文件時昂貴的,且經常會產生開銷。

 在 /proc/vmstat 中有許多計數器可以用於監視系統提供大頁面的成功程度。

thp_fault_alloc : 每當處理缺頁異常時,一個大頁面被成功分配,thp_fault_alloc 就會增加。這適用於第一次出現缺頁異常和 COW 錯誤。

thp_collapse_alloc:當它發現一個範圍的頁面坍縮成一個大頁,並有成功分配一個新的巨大頁來存儲數據,thp_collapse_alloc 會被 khugepaged 增加。

thp_fault_fallback: 如果缺頁異常失敗的分配一個大頁,則 thp_fault_fallback 被增加,而回退使用小頁面。

 thp_collapse_alloc_failed: 當它發現一個範圍的頁面應該被坍縮成一個大頁, 但是分配大頁失敗,thp_collapse_alloc_failed 會被 khugepaged 增加。

 thp_file_alloc: 在文件大頁成功分配時遞增。

thp_file_mapped: 每映射到一個文件大頁到用戶地址空間,thp_file_mapped 就增加一次。

thp_split_page:在每次將一個巨大的頁面分裂爲普通頁時遞增。發生這種情況的原因有很多,但都很常見原因是一個巨大的頁面是舊的,正在被回收。這個操作意味着分裂頁面映射的所有 PMD。

thp_split_page_failed:如果內核無法分裂大頁,則增加 thp_split_page_failed 計數。如果頁面被人 pin 住了,就會發生這種情況。

thp_deferred_split_page:當大頁被放到分裂隊列時,thp_deferred_split_page 計數被增加。當一個巨大的頁面部分被 unmap 且分裂它將釋放一些內存就會發生這種情況。分裂隊列上的頁將在內存壓力下分裂。

thp_split_pmd: 每當 pmd 分裂成 pte 表時,thp_split_pmd 就會遞增。例如,當應用程序調用 mprotect() 或 unmap() 在大頁面的一部分。它不會分割大頁面,只是頁表條目。

 thp_zero_page_alloc: thp_zero_page_alloc 在每出現一個巨型零頁被成功地分配時遞增。它包括分配,放棄了與其他分配的競爭。注意,這不算每次巨型零頁的映射,只有它的分配。

thp_zero_page_alloc_failed: 如果內核分配巨型零頁失敗並回退到使用小頁,則 thp_zero_page_alloc_failed 會增加。

 隨着系統老化,分配大頁的開銷可能會很大,因爲系統會使用內存規整在內存周圍來複制數據, 以釋放大頁供使用。在 / proc/vmstat 中有一些計數器可以幫助監視這種開銷。

compact_stall: 每當進程停滯去允許內存規整時,compact_stall 就會增加,以便一個巨大的頁面被釋放供使用。

compact_success: 如果系統規整內存和釋放一個大頁面供使用,則 compact_success 會增加(成功規整的次數)。

 compact_fail: 如果系統試圖規整內存但是失敗了,則 compact_fail 會增加(失敗規整的次數)。

compact_pages_moved: 每次移動頁面時,compact_pages_moved 會增加。如果 這個值是迅速增加的,說明該系統就是複製大量的數據來滿足大頁面分配。複製的成本可能超過任何減少 TLB misse 的節省。

compact_pagemigrate_failed: 在底層機制遞增移動頁面失敗,compact_pagemigrate_failed 會增加(規整時,遷移頁面失敗次數) 。

compact_blocks_moved: 每次內存規整檢查時一個大頁面對齊的頁面範圍,compact_blocks_moved 會增加。

可以使用函數跟蹤器來記錄在__alloc_pages_nodemask 中花費了多長時間, 並使用 mm_page_alloc 跟蹤點來確定哪些分配用於巨大的頁面。

== get_user_pages and follow_page ==

get_user_pages 和 follow_page 如果在一個巨型的頁面上運行,將返回往常一樣的頭頁或尾頁 (就像他們在 hugetlbfs 上做的一樣)。大多數 gup 用戶只關心實際的物理屬性頁的地址和它的臨時固定在 I/O 之後釋放是完整的,所以他們不會注意到頁面是巨型的。但 如果有任何驅動程序會在尾部的頁面結構上損壞 page(用於檢查 page->mapping 或其他相關的位對於頭頁而不是尾頁),應該更新爲跳轉改爲檢查頭頁。在任何頭 / 尾頁上引用都可以防止頁面被任何人分裂。

注意: 這些不是 GUP API 的新約束,它們與 hugetlbfs 上的約束相同, 所以任何能夠在 hugetlbfs 上處理 GUP 的驅動程序也可以很好地處理透明的大頁面支持映射。

如果您不能處理由 follow_page 返回的複合頁面,那麼可以將 FOLL_SPLIT 位指定爲 follow_page 的參數, 這樣它將在返回大頁面之前分裂它們。例如,遷移將 FOLL_SPLIT 作爲參數傳遞給 follow_page,因爲它不知道巨型頁面, 事實上它根本不能在 hugetlbfs 上工作 (但由於 FOLL_SPLIT,它在透明的巨型頁面上工作得很好)。遷移根本無法處理返回的大頁面 (因爲它不僅檢查頁面的 PFN 並在複製期間 pin 住它,而且帶有常規的 pte/pmd 映射)。

== 優化應用程序 ==

爲了保證內核將立即在任何內存區域映射 2M 頁,mmap 區域必須自然對齊。posix_memalign() 可以提供這種保證。

== Hugetlbfs ==

您可以在內核中使用 hugetlbfs,並且始終很好地啓用了透明的超大頁支持。hugetlbfs 中除了整體碎片更少之外,沒有什麼不同。所有屬於 hugetlbfs 的常見特性都被保留且不受影響。libhugetlbfs 也會像往常一樣正常工作。

== 優雅回退 ==

代碼遍歷頁表但不能感知巨型的 pmds,可以簡單地調用 split_huge_pmd(vma, pmd, addr),其中 pmd 是 pmd_offset 返回的那個。通過查詢 “pmd_offset” 並在 pmd_offset 返回 pmd 後丟失的地方添加 split_huge_pmd,使代碼透明地感知大頁是很簡單的。多虧了優雅的回退設計,只需一行代碼的更改,就可以避免編寫數百行 (如果不是數千行的話) 的複雜代碼,從而使代碼具有超大頁面的感知能力。

 如果您沒有遍歷頁表,但是遇到了一個物理的大頁,但是您不能在代碼中原生地處理它, 您可以通過調用 split_huge_page(page) 來分裂它。這就是 Linux VM 在嘗試切換大頁面之前所做的。如果頁面被 pin 住, 那麼 split_huge_page() 可能會失敗,您必須正確處理這個問題。

 讓 mremap.c 透明感知 hugepage 的例子,只需要一行代碼的改變:

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_trans_huge()在由 pmd_offset 返回的 PMD 上。你必須持有 mmap_sem 處於讀 (或寫) 模式,以確保不能出現巨型 PMD 由 khugepaged 創建 (khugepaged 坍縮巨型頁 collapse_huge_page 除 anon_vma 鎖外,還以寫模式持有 mmap_sem)。如果 pmd_trans_huge 返回 false,您只需返回到舊代碼路徑。如果 pmd_trans_huge 返回 true,則必須持有頁表鎖(pmd_lock()),然後重新運行 pmd_trans_huge。持有頁表鎖將防止巨型的 PMD 被轉換成一個常規的 PMD(split_huge_pmd 可以與頁表遍歷並行)。如果第二個 pmd_trans_huge 返回 false,則應該釋放頁表鎖並回退到之前的舊代碼中。否則,您可以繼續處理巨型的 pmd 和 hugepage 本身。一旦完成,您可以釋放頁表鎖。

 == 引用計數和透明大頁 ==

THP 上的引用計數和其他複合頁的引用計數基本一致:

 PageDoubleMap() 表示頁面_可能_映射了 pte。

對於匿名頁面,PageDoubleMap() 還表示 ->_mapcount 在所有子頁面中被抵消了一個。此附加引用是必需的,當子頁面同時被映射到 PMDs 和 PTEs 時,獲得對其子頁面 unmap 的無競爭檢測。

這是降低每個子頁面的 mapcount 跟蹤開銷所需的優化。另一種方法是在整個複合頁面的每個 map/unmap 上的所有子頁面中添加 ->_mapcount。

 對於匿名頁面,當頁面的 PMD 被分裂時,但仍有 PMD 映射,我們設置 PG_double_map 。額外的引用去掉最後一個 compound_mapcount。

文件頁面在帶有 PTE 和的頁面的第一個映射上設置 PG_double_map ,當頁面從頁面緩存中被驅逐時,該頁面就會消失。

 split_huge_page 內部必須在從頭頁到尾頁分配 refcount,然後清除頁面結構中所有的 PG_head / 尾位。它可以很容易地實現頁表條目的引用計數。但我們沒有足夠的信息來分發額外的 pins(即 get_user_pages)。split_huge_page() 請求去分裂 pin 住的大頁面是失敗的: 它期望頁面計數等於所有子頁面的 mapcount 之和加上 1 (split_huge_page 調用者必須有頭頁引用)。

split_huge_page 使用遷移條目來穩定匿名頁面的 page->_refcount 和 page->_mapcount。文件頁面被取消映射。

我們和物理內存掃描器(頁面回收的掃描器)競爭也是安全的: 掃描器來獲取對頁面的引用唯一合法的方式是 get_page_unless_zero()。

在 atomic_add() 之前,所有尾頁的 ->_refcount 都爲 0。這可以防止掃描器獲取到尾頁的引用。在 atomic_add() 之後,我們不關心 ->_refcount 值。我們已經從頭頁上知道有多少引用是取消記賬的。

對於頭頁,get_page_unless_zero() 會成功,我們不介意。它是明確拆分後引用應該去哪裏: 它將停留在首頁。

注意 split_huge_pmd() 對 refcount 沒有任何限制: PMD 可以在任何點被拆分並且永不失敗。

 == 部分 unmap and deferred_split_huge_page() ==

解除 THP 部分映射 (使用 munmap() 或其他方式)不會立即釋放內存。相反,我們在 page_remove_rmap() 中檢測到 THP 的一個子頁面沒有被使用 ,並在內存壓力時,將 THP 排隊以進行拆分。分裂將釋放未使用的子頁面。

由於將上下文鎖住在我們可以檢測到部分 unmap 的地方,所以不能立即拆分頁面。這也可能會適得其反,因爲在許多情況下,如果 THP 跨越 VMA 邊界,在 exit(2) 期間會發生部分 unmap。

用於對頁面進行排隊以進行拆分。當我們通過 shrinker 收縮器接口獲得內存壓力時,分裂本身就會發生。

參考⽂獻

  1. Linux-5.10.50 源碼

  2. Documentation/vm/transhuge.rst

  3. Documentation/admin-guide/mm/transhuge.rst

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/vrSHeFJL3yAbz5sVBlEyLA