以下文章來源于Linux內核遠航者 ,作者Linux內核遠航者
開場白
環境:
內核源碼:linux-6.6.29
ubuntu版本:20.04.1
代碼閱讀工具:vim+ctags+cscope
本文主要介紹內存管理中的HVO(HugeTLB Vmemmap Optimization)特性,通過HVO可以節省管理HugeTLB 頁面元數據(struct page)的內存占用,甚至在緩存的空間局部性表現上也更好。本文通過圖解結合源代碼分析的方式讓大家徹底理解HVO的實現原理,且本文主要以2M大小的HugeTLB 頁面為例講解。
1.術語解釋
文中會提到三種物理頁面,為了便于闡述,后面統一使用以下幾個概念講解:
例如2M大小的hugetlb頁面,struct page結構大小為64Byte, 則需要 2M/4K = 512個struct page結構來管理hugetlb頁面,那么這些struct page結構占用的物理內存為:512*64 = 32768Byte = 8個4k頁面,即是page0 - page7。
head vmemmap page:hugetlb頁面使用struct page結構占用的第一個物理頁面, 2M大小的hugetlb頁面則head vmemmap page就是page0。
tail vmemmap page:可以優化釋放掉的struct page結構占用的物理頁面,2M大小的hugetlb頁面則tail vmemmap page就是page1 - page7。
new head vmemmap page:如果vmemmap page是連續的物理頁面,假如只釋放掉tail vmemmap page,可能會破壞掉連續性,HVO中會申請新的head vmemmap page,然后將head vmemmap page拷貝到這個頁面,最后同時釋放掉所有的struct page結構占用的物理頁面, 2M大小的hugetlb頁面則釋放掉page0 - page7。
2.HVO優化原理及觸發場景
2.1 HVO優化原理
下面我們從內核源碼角度來看以下HVO優化原理。
//mm/hugetlb_vmemmap.c hugetlb_vmemmap_optimize ->vmemmap_start=(unsignedlong)head//hugetlb頁面的head vmemmap page所在地址。 ->vmemmap_should_optimize//判斷當前的hugetlb頁面大小是否適合做HVO優化, 沒有打開vmemmap_optimize_enabled或者hugetlb頁面使用structpage結構占用的 內存小于一個4k頁面不做優化。 ->vmemmap_end=vmemmap_start+hugetlb_vmemmap_size(h);//獲得優化的tailvmemmappage地址 vmemmap_reuse=vmemmap_start;//重復映射使用headvmemmappage地址 vmemmap_start+=HUGETLB_VMEMMAP_RESERVE_SIZE;//從第一個tailvmemmappage開始優化 ->vmemmap_remap_free(vmemmap_start,vmemmap_end,vmemmap_reuse)//釋放掉hugetlb頁面使用struct page結構冗余的物理頁面。 ->walk.reuse_page=alloc_pages_node(nid,gfp_mask,0); if(walk.reuse_page){ copy_page(page_to_virt(walk.reuse_page), |(void*)walk.reuse_addr); list_add(&walk.reuse_page->lru,&vmemmap_pages); } //優化1:申請一個新的4k頁面,即new head vmemmap page,然后將head vmemmap page拷貝到這個頁面,然后將new head vmemmap page加入vmemmap_pages鏈表 (用于失敗釋放此頁面使用) ->mmap_read_lock(&init_mm)//讀方式獲得init_mm的mmap_lock ->vmemmap_remap_range//遍歷頁表,釋放冗余的物理頁面(由于優化1,這里會釋放掉所有的管理hugetlb頁面使用的struct page結構占用的內存)。 ->mmap_read_unlock(&init_mm)//讀方式釋放init_mm的mmap_lock ->free_vmemmap_page_list(&vmemmap_pages)//釋放調用hugetlb頁面使用structpage結構冗余的物理頁面(例如2M大小的hugetlb頁面,釋放掉8頁(page0-page7)) ->SetHPageVmemmapOptimized(head)//為hugetlb頁面設置HVO優化標記,定義在include/linux/hugetlb.h(HPAGEFLAG(VmemmapOptimized,vmemmap_optimized)) vmemmap_remap_pte的核心代碼如下: staticvoidvmemmap_remap_pte(pte_t*pte,unsignedlongaddr, |structvmemmap_remap_walk*walk) { /* |*Remapthetailpagesasread-onlytocatchillegalwriteoperation |*tothetailpages. |*/ pgprot_tpgprot=PAGE_KERNEL_RO;//映射tailvmemmappage為只讀 structpage*page=pte_page(ptep_get(pte));//通過頁表項獲得structpage指針 pte_tentry; /*Remappingtheheadpagerequiresr/w*/ if(unlikely(addr==walk->reuse_addr)){//如果當前的虛擬地址是reuse_addr pgprot=PAGE_KERNEL;////映射headvmemmappage為可讀可寫 list_del(&walk->reuse_page->lru);//之前在hugetlb_vmemmap_optimize中將這個new head vmemmap page加入了vmemmap_pages,現在刪除,供頁面共享使用。 /* |*Makessurethatprecedingstorestothepagecontentsfrom |*vmemmap_remap_free()becomevisiblebeforetheset_pte_at() |*write. |*/ smp_wmb(); } entry=mk_pte(walk->reuse_page,pgprot);//重要步驟:頁表映射尾頁到頭頁上! list_add_tail(&page->lru,walk->vmemmap_pages);//將需要釋放的頁面加入vmemmap_pages鏈表 set_pte_at(&init_mm,addr,pte,entry);//設置頁表項 }
優化之前圖解:
注意:數據建立頁表映射是在內核初始化階段的start_kernel->setup_arch->paging_init來做的線性頁表映射,而元數據(struct page)建立頁表映射是在內核初始化階段的start_kernel->bootmem_init->sparse_init->sparse_init_nid->__populate_section_memmap來做,通過virt_to_page可以獲得數據的元數據地址,后面HVO優化是改變之前的元數據的映射。
優化之后圖解:
可以看出對于2M大小的hugetlb頁面優化之后節省元數據(struct page)占用內存:7/8= 87.5%, 如果是 1G 的大頁,可以節約的元數據(struct page)內存占用近乎 100%(讀者可自行計算)。
2.2 HVO觸發場景
HVO觸發場景主要為需要申請hugetlb頁面的時候:舉例如下,
場景1:解析cmdline的hugepages=參數
如hugepages=100,啟動階段申請100個2M的hugetlb頁面到大頁池。
mm/hugetlb.c __setup("hugepages=",hugepages_setup) ->hugepages_setup ->hugetlb_hstate_alloc_pages ->alloc_pool_huge_page ->alloc_fresh_hugetlb_folio//分配2M的hugetlb頁面 ->prep_new_hugetlb_folio ->__prep_new_hugetlb_folio ->hugetlb_vmemmap_optimize//觸發HVO優化
場景2:寫相關hugetlb頁面的sys節點,增大相關頁池中hugetlb頁面數量
如:echo 1000 > /sys/kernel/mm/hugepages/hugepages-64B/nr_hugepages
mm/hugetlb.c nr_hugepages_store ->nr_hugepages_store_common ->set_max_huge_pages ->alloc_pool_huge_page ->alloc_fresh_hugetlb_folio//分配2M的hugetlb頁面 ->prep_new_hugetlb_folio ->__prep_new_hugetlb_folio ->hugetlb_vmemmap_optimize//觸發HVO優化
場景3:寫proc節點,增大默認頁池(如2M)中hugetlb頁面數量
如:echo 1000 > /proc/sys/vm/nr_hugepages
mm/hugetlb.c hugetlb_table[] ->hugetlb_sysctl_handler ->hugetlb_sysctl_handler_common ->__nr_hugepages_store_common ->set_max_huge_pages ->alloc_pool_huge_page ->alloc_fresh_hugetlb_folio//分配2M的hugetlb頁面 ->prep_new_hugetlb_folio ->__prep_new_hugetlb_folio ->hugetlb_vmemmap_optimize//觸發HVO優化
3.撤銷HVO優化原理及觸發場景
3.1 撤銷HVO優化原理
有的時候需要撤銷HVO所作的優化,如需要縮小hugetlb頁池中頁面數量。
相關源碼分析如下:
mm/hugetlb_vmemmap.c hugetlb_vmemmap_restore ->首先通過HPageVmemmapOptimized(head)判斷是否hugetlb頁面被HVO優化了,沒有則直接返回 ->vmemmap_end=vmemmap_start+hugetlb_vmemmap_size(h);//獲得優化的tailvmemmappage地址 vmemmap_reuse=vmemmap_start;//重復映射使用的headvmemmappage地址 vmemmap_start+=HUGETLB_VMEMMAP_RESERVE_SIZE;//從第一個tailvmemmappage開始優化 ->vmemmap_remap_alloc(vmemmap_start,vmemmap_end,vmemmap_reuse)//還原HVO之前所作的優化:重新映射vmemmap的虛擬地址范圍到vmemmap_pages頁面 ->alloc_vmemmap_page_list(start,end,&vmemmap_pages)//分配所有的tailvmemmappage,如2M大小的hugetlb頁面,分配page1-page7,共7個頁面,page0已有不需要分配 ->mmap_read_lock(&init_mm)//讀方式獲得init_mm的mmap_lock ->vmemmap_remap_range(reuse,end,&walk)//遍歷頁表,重新映射vmemmap的虛擬地址范圍到vmemmap_pages中分配的頁面 ->vmemmap_restore_pte//對于每個頁表項,調用vmemmap_restore_pte處理 ->mmap_read_unlock(&init_mm)//讀方式釋放init_mm的mmap_lock ->ClearHPageVmemmapOptimized(head)//清除hugetlb頁面的優化標記 vmemmap_remap_pte的核心代碼如下: /* *Howmanystructpagestructsneedtobereset.Whenwereusethehead *structpage,thespecialmetadata(e.g.page->flagsorpage->mapping) *cannotcopytothetailstructpagestructs.Theinvalidvaluewillbe *checkedinthefree_tail_page_prepare().Inordertoavoidthemessage *of"corruptedmappingintailpage".Weneedtoresetatleast3(one *headstructpagestructandtwotailstructpagestructs)structpage *structs. */ #defineNR_RESET_STRUCT_PAGE3 staticinlinevoidreset_struct_pages(structpage*start) { structpage*from=start+NR_RESET_STRUCT_PAGE; BUILD_BUG_ON(NR_RESET_STRUCT_PAGE*2>PAGE_SIZE/sizeof(structpage)); memcpy(start,from,sizeof(*from)*NR_RESET_STRUCT_PAGE); } staticvoidvmemmap_restore_pte(pte_t*pte,unsignedlongaddr, structvmemmap_remap_walk*walk) { pgprot_tpgprot=PAGE_KERNEL;//頁表屬性可讀可寫 structpage*page; void*to; BUG_ON(pte_page(ptep_get(pte))!=walk->reuse_page); page=list_first_entry(walk->vmemmap_pages,structpage,lru);//vmemmap_pages鏈表中獲得一個物理頁面 list_del(&page->lru);//page從vmemmap_pages鏈表中刪除 to=page_to_virt(page); copy_page(to,(void*)walk->reuse_addr);//將headvmemmappage的頁面內容拷貝到這個物理頁面 reset_struct_pages(to);//由于描述hugetlb頁面的structpage結構,只有前3個structpage結構用于描述hugetlb頁面信息, 其他的structpage結構都只是有compound_head是有意義的,為了防止free_tail_page_prepare有錯誤的檢查報告,這里將所有tailvmemmappage的 內容都設置為正常值。 /* |*Makessurethatprecedingstorestothepagecontentsbecomevisible |*beforetheset_pte_at()write. |*/ smp_wmb(); set_pte_at(&init_mm,addr,pte,mk_pte(page,pgprot));//重新映射頁表到這個物理頁面 }
3.2 撤銷HVO觸發場景
場景1:寫相關hugetlb頁面的sys節點,減小相關頁池中hugetlb頁面數量
例如 寫/sys/kernel/mm/hugepages/hugepages-xxxkB/nr_hugepages
echo 500 > /sys/kernel/mm/hugepages/hugepages-64B/nr_hugepages //從之前1000減小到500
mm/hugetlb.c nr_hugepages_store nr_hugepages_store_common set_max_huge_pages ->flush_free_hpage_work(h); ->free_hpage_workfn ->__update_and_free_hugetlb_folio ->hugetlb_vmemmap_restore ->update_and_free_pages_bulk ->__update_and_free_hugetlb_folio ->hugetlb_vmemmap_restore
場景2:寫proc節點,減小默認頁池中hugetlb頁面數量
例如 寫/proc/sys/vm/nr_hugepages
echo 500 > /proc/sys/vm/nr_hugepages //從之前1000減小到500
mm/hugetlb.c hugetlb_table[] ->hugetlb_sysctl_handler ->hugetlb_sysctl_handler_common ->__nr_hugepages_store_common ->set_max_huge_pages ->flush_free_hpage_work(h); ->free_hpage_workfn ->__update_and_free_hugetlb_folio ->hugetlb_vmemmap_restore ->update_and_free_pages_bulk ->__update_and_free_hugetlb_folio ->hugetlb_vmemmap_restore
場景3:寫/sys/kernel/mm/hugepages/hugepages-xxxkB/demote
mm/hugetlb.c demote_store ->demote_pool_huge_page ->demote_free_hugetlb_folio ->hugetlb_vmemmap_restore
4.HVO中頭頁的獲取問題
通過上面的分析,我們知道通過將tail vmemmap page映射head vmemmap page,然后釋放掉tail vmemmap page,從而達到節省vmemmap page占用內存的目的,但是會出現我們獲得的尾頁的struct page指向頭頁的struct page的情況。如圖所示,以2M大小的hugetlb頁面為例:
可以看到需要struct page0 - struct page511,512個struct page來描述hugetlb頁面,那么通過HVO優化后:
[struct page0, struct page63]<----> head vmemmappage
[struct page64, struct page127] <----> tail vmemmap page
... <----> tail vmemmappage
[struct page448, struct page511] <----> tail vmemmappage
都頁表映射到head vmemmap page。
那么struct page0, struct page64,..., struct page448都會指向struct page0,可能會在判斷是否為頭頁的代碼中造成混亂,也可以看出這些struct page地址都是對齊4k的。
這里需要補充說明下:對于復合頁(THP、hugetlb都屬于復合頁),頭頁會設置PG_head標記,而尾頁的compound_head=head_page | 0x1。
像這些描述尾頁的struct page,被成為"偽造的頭頁",內核中處理如下(page_folio為例):
include/linux/page-flags.h page_folio ->_compound_head ->staticinlineunsignedlong_compound_head(conststructpage*page) { unsignedlonghead=READ_ONCE(page->compound_head);//獲取頁面的compound_head成員 if(unlikely(head&1))//是真正的尾頁 returnhead-1;//計算出頭頁地址,返回 return(unsignedlong)page_fixed_fake_head(page);//為真正的頭頁或者為偽造的頭頁。 } page_fixed_fake_head處理如下: /* *Returntherealheadpagestructiffthe@pageisafakeheadpage,otherwise *returnthe@pageitself.SeeDocumentation/mm/vmemmap_dedup.rst. */ static__always_inlineconststructpage*page_fixed_fake_head(conststructpage*page) { if(!static_branch_unlikely(&hugetlb_optimize_vmemmap_key)) returnpage; /* |*OnlyaddressesalignedwithPAGE_SIZEofstructpagemaybefakehead |*structpage.Thealignmentcheckaimstoavoidaccessthefields( |*e.g.compound_head)ofthe@page[1].Itcanavoidtoucha(possibly) |*coldcachelineinsomecases. |*/ if(IS_ALIGNED((unsignedlong)page,PAGE_SIZE)&& |test_bit(PG_head,&page->flags)){//structpage結構地址只有對齊PAGE_SIZE才有可能為偽造的頭頁 /* |*Wecansafelyaccessthefieldofthe@page[1]withPG_head |*becausethe@pageisacompoundpagecomposedwithatleast |*twocontiguouspages. |*/ unsignedlonghead=READ_ONCE(page[1].compound_head);//獲得下一個structpage地址的compound_head成員,實際上就是獲取第一個尾頁的compound_head if(likely(head&1)) return(conststructpage*)(head-1);//計算獲得真正的頭頁地址 } returnpage; }
通過上面計算我們就可以得到真正的頭頁,簡單來說就是通過struct page1->compound_head計算獲得頭頁。
所有打開HVO優化后,可能描述hugetlb頁面的struct page有三種情況:
真正的頭頁,如上例子中的struct page0,計算頭頁的時候直接返回struct page0地址即可。
偽造的頭頁,如上例子中的struct page64,struct pageN(N=n*64, n=1-6) ,struct page448。通過struct page1->compound_head計算獲得頭頁地址。
真正的尾頁,除了1和2的所有情況,直接通過當前struct page->compound_head計算獲得頭頁地址。
5.總結
通過以上的分析,我們知道:HVO主要是并不節省實際用戶數據(如2M大小的HugeTLB 頁面)的內存占用,而是節省管理HugeTLB 頁面元數據(如描述2M大小的HugeTLB 頁面的512個struct page)的內存占用,巧妙的利用了HugeTLB機制的一些特性(如HugeTLB 頁面使用頭三個struct page來描述其頁面狀態,不支持分裂,不支持部分unmap等),使得我們可以共享struct page所在的第一個物理頁面,釋放掉其他冗余的物理頁面,從而達到節省內存的目的。
-
處理器
+關注
關注
68文章
19178瀏覽量
229200 -
內核
+關注
關注
3文章
1366瀏覽量
40235 -
Linux
+關注
關注
87文章
11232瀏覽量
208949 -
內存管理
+關注
關注
0文章
168瀏覽量
14128
原文標題:深入理解Linux內核之HVO(HugeTLB Vmemmap Optimization)
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論