隨著硬件能力的提升,系統(tǒng)內(nèi)存容量變得越來越大。尤其是在服務(wù)器上,過T級別的內(nèi)存容量也已經(jīng)不罕見了。
如此海量內(nèi)存給內(nèi)核帶來了很多挑戰(zhàn),其中之一就是page struct存放在哪里。
page struct的三種存放方式
在內(nèi)核中,我們將物理內(nèi)存按照頁大小進行管理。這樣每個頁就對應(yīng)一個page struct作為這個頁的管理數(shù)據(jù)結(jié)構(gòu)。
隨著內(nèi)存容量的增加,相對應(yīng)的page struct也就增加。而這部分內(nèi)存和其他的內(nèi)存略有不同,因為這部分內(nèi)存不能給到頁分配器。也就是必須在系統(tǒng)能夠正常運行起來之前就分配好。
在內(nèi)核中我們可以看到,為了應(yīng)對這樣的變化進化出了幾個不同的版本。有幸的是,這部分內(nèi)容我們現(xiàn)在還能在代碼中直接看到,因為這個實現(xiàn)是通過內(nèi)核配置來區(qū)分的。我們通過查找_pfn_to_page的定義就能發(fā)現(xiàn)一下幾種memory model:
CONFIG_FLATMEM
CONFIG_SPARSEMEM
CONFIGSPARSEMEMVMEMMAP
接下來讓小編給各位看官一一道來。
1) FLATMEM
在這種情況下,宏_pfn_to_page的定義是:
#define__pfn_to_page(pfn)(mem_map+((pfn)-ARCH_PFN_OFFSET))
而這個mem_map的定義是
structpage*mem_map;
所以在這種情況下,page struct就是一個大數(shù)組,所有的人都按照自己的物理地址有序得挨著。
2) SPARSEMEM
雖然第一種方式非常簡單直觀,但是有幾個非常大的缺點:
內(nèi)存如果有空洞,那么中間可能會有巨大的page struct空間浪費
所有的page struct內(nèi)存都在一個NUMA節(jié)點上,會耗盡某一個節(jié)點內(nèi)存,甚至是分配失敗
且會產(chǎn)生夸NUMA訪問導(dǎo)致性能下降
所以第二種方式就是將內(nèi)存按照一定粒度,如128M,劃分了section,每個section中有個成員指定了對應(yīng)的page struct的存儲空間。
這樣就解決了上述的幾個問題:
如果有空洞,那么對應(yīng)的 page struct就不會占用空間
每個section對應(yīng)的page struct是屬于本地NUMA的
怎么樣,是不是覺得很完美。這一部分具體的實現(xiàn)可以可以看函數(shù)sparse_init()函數(shù)。
有了這個基礎(chǔ)知識,我們再來看這種情況下_pfn_to_page的定義:
#define __pfn_to_page(pfn) ({ unsigned long __pfn = (pfn); struct mem_section *__sec = __pfn_to_section(__pfn); __section_mem_map_addr(__sec) + __pfn; })
就是先找到pfn對應(yīng)的section,然后在section中保存的地址上翻譯出對應(yīng)pfn的page struct。
既然講到了這里,我們就要對sparsemem中重要的組成部分mem_section多說兩句。
先來一張mem_section的整體圖解:
這是一個 NRSECTIONROOTS x SECTIONSPERROOT的二維數(shù)組。其中每一個成員就代表了我們剛才提到的128M內(nèi)存。
當然最開始它不是這個樣子的。
其實最開始這個數(shù)組是一個靜態(tài)數(shù)組。很明顯這么做帶來的問題是這個數(shù)組定義太大太小都不合適。所以后來引進了CONFIGSPARSEMEMEXTREME編譯選項,當設(shè)置為y時,這個數(shù)組就變成了動態(tài)的。
如果上面這個算作是空間上的限制的話,那么接下來就是一個時間上的限制了。
在系統(tǒng)初始化時,每個mem_section都要和相應(yīng)的內(nèi)存空間關(guān)聯(lián)。在老版本上,這個步驟通過對整個數(shù)組接待完成。原來的版本上問題不大,因為整個數(shù)組的大小還沒有很大。但隨著內(nèi)存容量的增加,這個數(shù)值就變得對系統(tǒng)有影響了。如果系統(tǒng)上確實有這么多內(nèi)存,那么確實需要初始化也就忍了。但是在內(nèi)存較小的系統(tǒng)上,哪怕沒有這么多內(nèi)存,還是要挨個初始化,那就浪費了太多的時間。
commit c4e1be9ec1130fff4d691cdc0e0f9d666009f9aeAuthor: Dave Hansen
Dave在這個提交中增加了對系統(tǒng)最大存在內(nèi)存的跟蹤,來減少不必要的初始化時間。
瞧,內(nèi)核代碼一開始其實也沒有這么高大上不是。
3) SPARSEMEM_VMEMMAP
最后要講的,也是當前x86系統(tǒng)默認配置的內(nèi)存模型是SPARSEMEM_VMEMMAP。那為什么要引入這么一個新的模型呢?那自然是sparsemem依然有不足。
細心的朋友可能已經(jīng)注意到了,前兩種內(nèi)存模型在做pfn到page struct轉(zhuǎn)換是有著一些些的差異。為了看得清,我們把這兩個定義再拿過來對比一下:
先看看FLATMEM時的定義:
#define__pfn_to_page(pfn)(mem_map+((pfn)-ARCH_PFN_OFFSET))
再來看看使用SPASEMEM后的定義:
#define __pfn_to_page(pfn) ({ unsigned long __pfn = (pfn); struct mem_section *__sec = __pfn_to_section(__pfn); __section_mem_map_addr(__sec) + __pfn; })
更改后,需要先找到section,然后再從section->memmap的內(nèi)容中換算出page的地址。
不僅計算的內(nèi)容多了,更重要的是還有一次訪問內(nèi)存的操作
可以想象,訪問內(nèi)存和單純計算之間的速度差異那是巨大的差距。
既然產(chǎn)生了這樣的問題,那有沒有辦法解決呢?其實說來簡單,內(nèi)核開發(fā)者利用了我們常見的一個內(nèi)存單元來解決這個問題。
頁表
是不是很簡單粗暴?如果我們能夠通過某種方式將page struct線性映射到頁表,這樣我們不就能又通過簡單的計算來換算物理地址和page struct了么?
內(nèi)核開發(fā)者就是這么做的,我們先來看一眼最后那簡潔的代碼:
#define__pfn_to_page(pfn)(vmemmap+(pfn))
經(jīng)過內(nèi)核開發(fā)這的努力,物理地址到page struct的轉(zhuǎn)換又變成如此的簡潔。不需要訪問內(nèi)存,所以速度的問題得到了解決。
但是天下沒有免費的午餐,世界哪有這么美好,魚和熊掌可以兼得的情況或許只有在夢境之中。為了達到如此簡潔的轉(zhuǎn)化,我們是要付出代價的。為了實現(xiàn)速度上的提升,我們付出了空間的代價。
至此引出了計算機界一個經(jīng)典的話題:
時間和空間的轉(zhuǎn)換
話不多說,也不矯情了,我們來看看內(nèi)核中實現(xiàn)的流程。
既然是利用了頁表進行轉(zhuǎn)換,那么自然是要構(gòu)建頁表在做這樣的映射。這個步驟主要由函數(shù)vmemmap_populate()來完成,其中還區(qū)分了有沒有大頁的情況。我們以普通頁的映射為例,看看這個實現(xiàn)。
int __meminit vmemmap_populate_basepages(unsigned long start, unsigned long end, int node){ unsigned long addr = start; pgd_t *pgd; p4d_t *p4d; pud_t *pud; pmd_t *pmd; pte_t *pte; for (; addr < end; addr += PAGE_SIZE) { pgd = vmemmap_pgd_populate(addr, node); if (!pgd) return -ENOMEM; p4d = vmemmap_p4d_populate(pgd, addr, node); if (!p4d) return -ENOMEM; pud = vmemmap_pud_populate(p4d, addr, node); if (!pud) return -ENOMEM; pmd = vmemmap_pmd_populate(pud, addr, node); if (!pmd) return -ENOMEM; pte = vmemmap_pte_populate(pmd, addr, node); if (!pte) return -ENOMEM; vmemmap_verify(pte, node, addr, addr + PAGE_SIZE); } return 0;}
內(nèi)核代碼的優(yōu)美之處就在于,你可能不一定看懂了所有細節(jié),但是從優(yōu)美的結(jié)構(gòu)上能猜到究竟做了些什么。上面這段代碼的工作就是對每一個頁,按照層級去填充頁表內(nèi)容。其中具體的細節(jié)就不在這里展開了,相信有興趣的同學(xué)會自行去探索。
那這么做的代價究竟是多少呢?
以x86為例,每個section是128M,那么每個section的page struct正好是2M,也就是一個大頁。
(128M / 4K) * 64 = (128 * (1 < 20) / (1 < 12)) * 64 = 2M
假如使用大頁做頁表映射,那么每64G才用掉一個4K頁表做映射。
128M * 512 = 64G
所以在使用大頁映射的情況下,這個損耗的級別在百萬分之一。還是能夠容忍的。
好了,我們終于沿著內(nèi)核發(fā)展的歷史重走了一遍安放page struct之路。相信大家在這一路上領(lǐng)略了代碼演進的樂趣,也會對以后自己代碼的設(shè)計有了更深的思考。
-
服務(wù)器
+關(guān)注
關(guān)注
12文章
9294瀏覽量
85855 -
數(shù)據(jù)結(jié)構(gòu)
+關(guān)注
關(guān)注
3文章
573瀏覽量
40190 -
PAGE
+關(guān)注
關(guān)注
0文章
11瀏覽量
20197
原文標題:page結(jié)構(gòu)體,何處安放你的靈魂?
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論