0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

一步一圖帶你深入理解Linux物理內(nèi)存管理

小林coding ? 來源:圖解學習網(wǎng)站 ? 作者:圖解學習網(wǎng)站 ? 2022-12-06 15:13 ? 次閱讀

這篇依然很硬核,深入 Linux 源碼講解物理內(nèi)存的管理,大家一次性看不完,可以收藏起來慢慢看。 文章也已經(jīng)同步到了我的圖解網(wǎng)站,習慣在網(wǎng)站閱讀的同學,可以直接去網(wǎng)站閱讀。 具體多少字,我就不說了,看圖:

091edf2c-751d-11ed-8abf-dac502259ad0.png

1. 前文回顧

在上篇文章 《3.5 萬字 + 60 張圖 |一步一圖帶你深入理解 Linux 虛擬內(nèi)存管理》 中,我分別從進程用戶態(tài)和內(nèi)核態(tài)的角度詳細深入地為大家介紹了 Linux 內(nèi)核如何對進程虛擬內(nèi)存空間進行布局以及管理的相關(guān)實現(xiàn)。

在我們深入理解了虛擬內(nèi)存之后,那么何不順帶著也探秘一下物理內(nèi)存的管理呢?

所以本文的目的是在深入理解虛擬內(nèi)存管理的基礎(chǔ)之上繼續(xù)帶大家向前奮進,一舉擊破物理內(nèi)存管理的知識盲區(qū),使大家能夠俯瞰整個 Linux 內(nèi)存管理子系統(tǒng)的整體全貌。

而在正式開始物理內(nèi)存管理的主題之前,筆者覺得有必須在帶大家回顧下上篇文章中介紹的虛擬內(nèi)存管理的相關(guān)知識,方便大家來回對比虛擬內(nèi)存和物理內(nèi)存,從而可以全面整體地掌握 Linux 內(nèi)存管理子系統(tǒng)。

在上篇文章的一開始,筆者首先為大家展現(xiàn)了我們應用程序頻繁接觸到的虛擬內(nèi)存地址,清晰地為大家介紹了到底什么是虛擬內(nèi)存地址,以及虛擬內(nèi)存地址分別在 32 位系統(tǒng)和 64 位系統(tǒng)中的具體表現(xiàn)形式:

09310026-751d-11ed-8abf-dac502259ad0.pngimage.png09613b7e-751d-11ed-8abf-dac502259ad0.pngimage.png

在我們清楚了虛擬內(nèi)存地址這個基本概念之后,隨后筆者又拋出了一個問題:為什么我們要通過虛擬內(nèi)存地址訪問內(nèi)存而不是直接通過物理地址訪問?

原來是在多進程系統(tǒng)中直接操作物理內(nèi)存地址的話,我們需要精確地知道每一個變量的位置都被安排在了哪里,而且還要注意當前進程在和多個進程同時運行的時候,不能共用同一個地址,否則就會造成地址沖突。

09b79474-751d-11ed-8abf-dac502259ad0.pngimage.png

而虛擬內(nèi)存空間的引入正是為了解決多進程地址沖突的問題,使得進程與進程之間的虛擬內(nèi)存地址空間相互隔離,互不干擾。每個進程都認為自己獨占所有內(nèi)存空間,將多進程之間的協(xié)同相關(guān)細節(jié)統(tǒng)統(tǒng)交給內(nèi)核中的內(nèi)存管理模塊來處理,極大地解放了程序員的心智負擔。這一切都是因為虛擬內(nèi)存能夠為進程提供內(nèi)存地址空間隔離的功勞。

09d2e60c-751d-11ed-8abf-dac502259ad0.pngimage.png

在我們清楚了虛擬內(nèi)存空間引入的意義之后,筆者緊接著為大家介紹了進程用戶態(tài)虛擬內(nèi)存空間分別在 32 位機器和 64 位機器上的布局情況:

0a2eaad2-751d-11ed-8abf-dac502259ad0.png32 位機器.png0a3d04ec-751d-11ed-8abf-dac502259ad0.png64 位機器.png

在了解了用戶態(tài)虛擬內(nèi)存空間的布局之后,緊接著我們又介紹了 Linux 內(nèi)核如何對用戶態(tài)虛擬內(nèi)存空間進行管理以及相應的管理數(shù)據(jù)結(jié)構(gòu):

0a4e01fc-751d-11ed-8abf-dac502259ad0.pngimage.png

在介紹完用戶態(tài)虛擬內(nèi)存空間的布局以及管理之后,我們隨后又介紹了內(nèi)核態(tài)虛擬內(nèi)存空間的布局情況,并結(jié)合之前介紹的用戶態(tài)虛擬內(nèi)存空間,得到了 Linux 虛擬內(nèi)存空間分別在 32 位和 64 位系統(tǒng)中的整體布局情況:

0a609cd6-751d-11ed-8abf-dac502259ad0.png32位系統(tǒng)中虛擬內(nèi)存空間整體布局.png0aa5d210-751d-11ed-8abf-dac502259ad0.png64位系統(tǒng)中虛擬內(nèi)存空間整體布局.png

在虛擬內(nèi)存全部介紹完畢之后,為了能夠承上啟下,于是筆者繼續(xù)在上篇文章的最后一個小節(jié)從計算機組成原理的角度介紹了物理內(nèi)存的物理組織結(jié)構(gòu),方便讓大家理解到底什么是真正的物理內(nèi)存 ?物理內(nèi)存地址到底是什么 ?由此為本文的主題 —— 物理內(nèi)存的管理 ,埋下伏筆~~~

0abeeb1a-751d-11ed-8abf-dac502259ad0.png內(nèi)存IO單位.png

最后筆者介紹了 CPU 如何通過物理內(nèi)存地址向物理內(nèi)存讀寫數(shù)據(jù)的完整過程:

0acb5daa-751d-11ed-8abf-dac502259ad0.pngCPU讀取內(nèi)存.png

在我們回顧完上篇文章介紹的用戶態(tài)和內(nèi)核態(tài)虛擬內(nèi)存空間的管理,以及物理內(nèi)存在計算機中的真實組成結(jié)構(gòu)之后,下面筆者就來正式地為大家介紹本文的主題 —— Linux 內(nèi)核如何對物理內(nèi)存進行管理

0adab8ea-751d-11ed-8abf-dac502259ad0.png

2. 從 CPU 角度看物理內(nèi)存模型

在前邊的文章中,筆者曾多次提到內(nèi)核是以頁為基本單位對物理內(nèi)存進行管理的,通過將物理內(nèi)存劃分為一頁一頁的內(nèi)存塊,每頁大小為 4K。一頁大小的內(nèi)存塊在內(nèi)核中用 struct page 結(jié)構(gòu)體來進行管理,struct page 中封裝了每頁內(nèi)存塊的狀態(tài)信息,比如:組織結(jié)構(gòu),使用信息,統(tǒng)計信息,以及與其他結(jié)構(gòu)的關(guān)聯(lián)映射信息等。

而為了快速索引到具體的物理內(nèi)存頁,內(nèi)核為每個物理頁 struct page 結(jié)構(gòu)體定義了一個索引編號:PFN(Page Frame Number)。PFN 與 struct page 是一一對應的關(guān)系。

內(nèi)核提供了兩個宏來完成 PFN 與 物理頁結(jié)構(gòu)體 struct page 之間的相互轉(zhuǎn)換。它們分別是 page_to_pfn 與 pfn_to_page。

內(nèi)核中如何組織管理這些物理內(nèi)存頁 struct page 的方式我們稱之為做物理內(nèi)存模型,不同的物理內(nèi)存模型,應對的場景以及 page_to_pfn 與 pfn_to_page 的計算邏輯都是不一樣的。

2.1 FLATMEM 平坦內(nèi)存模型

我們先把物理內(nèi)存想象成一片地址連續(xù)的存儲空間,在這一大片地址連續(xù)的內(nèi)存空間中,內(nèi)核將這塊內(nèi)存空間分為一頁一頁的內(nèi)存塊 struct page 。

由于這塊物理內(nèi)存是連續(xù)的,物理地址也是連續(xù)的,劃分出來的這一頁一頁的物理頁必然也是連續(xù)的,并且每頁的大小都是固定的,所以我們很容易想到用一個數(shù)組來組織這些連續(xù)的物理內(nèi)存頁 struct page 結(jié)構(gòu),其在數(shù)組中對應的下標即為 PFN 。這種內(nèi)存模型就叫做平坦內(nèi)存模型 FLATMEM 。

0afddf00-751d-11ed-8abf-dac502259ad0.pngimage.png

內(nèi)核中使用了一個 mem_map 的全局數(shù)組用來組織所有劃分出來的物理內(nèi)存頁。mem_map 全局數(shù)組的下標就是相應物理頁對應的 PFN 。

在平坦內(nèi)存模型下 ,page_to_pfn 與 pfn_to_page 的計算邏輯就非常簡單,本質(zhì)就是基于 mem_map 數(shù)組進行偏移操作。

#ifdefined(CONFIG_FLATMEM)
#define__pfn_to_page(pfn)(mem_map+((pfn)-ARCH_PFN_OFFSET))
#define__page_to_pfn(page)((unsignedlong)((page)-mem_map)+ARCH_PFN_OFFSET)
#endif

ARCH_PFN_OFFSET 是 PFN 的起始偏移量。

Linux 早期使用的就是這種內(nèi)存模型,因為在 Linux 發(fā)展的早期所需要管理的物理內(nèi)存通常不大(比如幾十 MB),那時的 Linux 使用平坦內(nèi)存模型 FLATMEM 來管理物理內(nèi)存就足夠高效了。

內(nèi)核中的默認配置是使用 FLATMEM 平坦內(nèi)存模型。

2.2 DISCONTIGMEM 非連續(xù)內(nèi)存模型

FLATMEM 平坦內(nèi)存模型只適合管理一整塊連續(xù)的物理內(nèi)存,而對于多塊非連續(xù)的物理內(nèi)存來說使用 FLATMEM 平坦內(nèi)存模型進行管理則會造成很大的內(nèi)存空間浪費。

因為 FLATMEM 平坦內(nèi)存模型是利用 mem_map 這樣一個全局數(shù)組來組織這些被劃分出來的物理頁 page 的,而對于物理內(nèi)存存在大量不連續(xù)的內(nèi)存地址區(qū)間這種情況時,這些不連續(xù)的內(nèi)存地址區(qū)間就形成了內(nèi)存空洞。

由于用于組織物理頁的底層數(shù)據(jù)結(jié)構(gòu)是 mem_map 數(shù)組,數(shù)組的特性又要求這些物理頁是連續(xù)的,所以只能為這些內(nèi)存地址空洞也分配 struct page 結(jié)構(gòu)用來填充數(shù)組使其連續(xù)。

而每個 struct page 結(jié)構(gòu)大部分情況下需要占用 40 字節(jié)(struct page 結(jié)構(gòu)在不同場景下內(nèi)存占用會有所不同,這一點我們后面再說),如果物理內(nèi)存中存在的大塊的地址空洞,那么為這些空洞而分配的 struct page 將會占用大量的內(nèi)存空間,導致巨大的浪費。

0b161976-751d-11ed-8abf-dac502259ad0.pngimage.png

為了組織和管理這些不連續(xù)的物理內(nèi)存,內(nèi)核于是引入了 DISCONTIGMEM 非連續(xù)內(nèi)存模型,用來消除這些不連續(xù)的內(nèi)存地址空洞對 mem_map 的空間浪費。

在 DISCONTIGMEM 非連續(xù)內(nèi)存模型中,內(nèi)核將物理內(nèi)存從宏觀上劃分成了一個一個的節(jié)點 node (微觀上還是一頁一頁的物理頁),每個 node 節(jié)點管理一塊連續(xù)的物理內(nèi)存。這樣一來這些連續(xù)的物理內(nèi)存頁均被劃歸到了對應的 node 節(jié)點中管理,就避免了內(nèi)存空洞造成的空間浪費。

0b33f75c-751d-11ed-8abf-dac502259ad0.pngimage.png

內(nèi)核中使用 struct pglist_data 表示用于管理連續(xù)物理內(nèi)存的 node 節(jié)點(內(nèi)核假設(shè) node 中的物理內(nèi)存是連續(xù)的),既然每個 node 節(jié)點中的物理內(nèi)存是連續(xù)的,于是在每個 node 節(jié)點中還是采用 FLATMEM 平坦內(nèi)存模型的方式來組織管理物理內(nèi)存頁。每個 node 節(jié)點中包含一個 struct page *node_mem_map 數(shù)組,用來組織管理 node 中的連續(xù)物理內(nèi)存頁。

typedefstructpglist_data{
#ifdefCONFIG_FLATMEM
structpage*node_mem_map;
#endif
}

我們可以看出 DISCONTIGMEM 非連續(xù)內(nèi)存模型其實就是 FLATMEM 平坦內(nèi)存模型的一種擴展,在面對大塊不連續(xù)的物理內(nèi)存管理時,通過將每段連續(xù)的物理內(nèi)存區(qū)間劃歸到 node 節(jié)點中進行管理,避免了為內(nèi)存地址空洞分配 struct page 結(jié)構(gòu),從而節(jié)省了內(nèi)存資源的開銷。

由于引入了 node 節(jié)點這個概念,所以在 DISCONTIGMEM 非連續(xù)內(nèi)存模型下 page_to_pfn 與 pfn_to_page 的計算邏輯就比 FLATMEM 內(nèi)存模型下的計算邏輯多了一步定位 page 所在 node 的操作。

  • 通過 arch_pfn_to_nid 可以根據(jù)物理頁的 PFN 定位到物理頁所在 node。

  • 通過 page_to_nid 可以根據(jù)物理頁結(jié)構(gòu) struct page 定義到 page 所在 node。

當定位到物理頁 struct page 所在 node 之后,剩下的邏輯就和 FLATMEM 內(nèi)存模型一模一樣了。

#ifdefined(CONFIG_DISCONTIGMEM)

#define__pfn_to_page(pfn)
({unsignedlong__pfn=(pfn);
unsignedlong__nid=arch_pfn_to_nid(__pfn);
NODE_DATA(__nid)->node_mem_map+arch_local_page_offset(__pfn,__nid);
})

#define__page_to_pfn(pg)
({conststructpage*__pg=(pg);
structpglist_data*__pgdat=NODE_DATA(page_to_nid(__pg));
(unsignedlong)(__pg-__pgdat->node_mem_map)+
__pgdat->node_start_pfn;
})

2.3 SPARSEMEM 稀疏內(nèi)存模型

隨著內(nèi)存技術(shù)的發(fā)展,內(nèi)核可以支持物理內(nèi)存的熱插拔了(后面筆者會介紹),這樣一來物理內(nèi)存的不連續(xù)就變?yōu)槌B(tài)了,在上小節(jié)介紹的 DISCONTIGMEM 內(nèi)存模型中,其實每個 node 中的物理內(nèi)存也不一定都是連續(xù)的。

0b4794a6-751d-11ed-8abf-dac502259ad0.pngimage.png

而且每個 node 中都有一套完整的內(nèi)存管理系統(tǒng),如果 node 數(shù)目多的話,那這個開銷就大了,于是就有了對連續(xù)物理內(nèi)存更細粒度的管理需求,為了能夠更靈活地管理粒度更小的連續(xù)物理內(nèi)存,SPARSEMEM 稀疏內(nèi)存模型就此登場了。

SPARSEMEM 稀疏內(nèi)存模型的核心思想就是對粒度更小的連續(xù)內(nèi)存塊進行精細的管理,用于管理連續(xù)內(nèi)存塊的單元被稱作 section 。物理頁大小為 4k 的情況下, section 的大小為 128M ,物理頁大小為 16k 的情況下, section 的大小為 512M。

在內(nèi)核中用 struct mem_section 結(jié)構(gòu)體表示 SPARSEMEM 模型中的 section。

structmem_section{
unsignedlongsection_mem_map;
...
}

由于 section 被用作管理小粒度的連續(xù)內(nèi)存塊,這些小的連續(xù)物理內(nèi)存在 section 中也是通過數(shù)組的方式被組織管理,每個 struct mem_section 結(jié)構(gòu)體中有一個 section_mem_map 指針用于指向 section 中管理連續(xù)內(nèi)存的 page 數(shù)組。

SPARSEMEM 內(nèi)存模型中的這些所有的 mem_section 會被存放在一個全局的數(shù)組中,并且每個 mem_section 都可以在系統(tǒng)運行時改變 offline / online (下線 / 上線)狀態(tài),以便支持內(nèi)存的熱插拔(hotplug)功能。

#ifdefCONFIG_SPARSEMEM_EXTREME
externstructmem_section*mem_section[NR_SECTION_ROOTS];
0b82df5c-751d-11ed-8abf-dac502259ad0.pngimage.png

在 SPARSEMEM 稀疏內(nèi)存模型下 page_to_pfn 與 pfn_to_page 的計算邏輯又發(fā)生了變化。

  • 在 page_to_pfn 的轉(zhuǎn)換中,首先需要通過 page_to_section 根據(jù) struct page 結(jié)構(gòu)定位到 mem_section 數(shù)組中具體的 section 結(jié)構(gòu)。然后在通過 section_mem_map 定位到具體的 PFN。

在 struct page 結(jié)構(gòu)中有一個 unsigned long flags 屬性,在 flag 的高位 bit 中存儲著 page 所在 mem_section 數(shù)組中的索引,從而可以定位到所屬 section。

  • 在 pfn_to_page 的轉(zhuǎn)換中,首先需要通過 __pfn_to_section 根據(jù) PFN 定位到 mem_section 數(shù)組中具體的 section 結(jié)構(gòu)。然后在通過 PFN 在 section_mem_map 數(shù)組中定位到具體的物理頁 Page 。

PFN 的高位 bit 存儲的是全局數(shù)組 mem_section 中的 section 索引,PFN 的低位 bit 存儲的是 section_mem_map 數(shù)組中具體物理頁 page 的索引。

#ifdefined(CONFIG_SPARSEMEM)
/*
*Note:section'smem_mapisencodedtoreflectitsstart_pfn.
*section[i].section_mem_map==mem_map'saddress-start_pfn;
*/
#define__page_to_pfn(pg)
({conststructpage*__pg=(pg);
int__sec=page_to_section(__pg);
(unsignedlong)(__pg-__section_mem_map_addr(__nr_to_section(__sec)));
})

#define__pfn_to_page(pfn)
({unsignedlong__pfn=(pfn);
structmem_section*__sec=__pfn_to_section(__pfn);
__section_mem_map_addr(__sec)+__pfn;
})
#endif

從以上的內(nèi)容介紹中,我們可以看出 SPARSEMEM 稀疏內(nèi)存模型已經(jīng)完全覆蓋了前兩個內(nèi)存模型的所有功能,因此稀疏內(nèi)存模型可被用于所有內(nèi)存布局的情況。

2.3.1 物理內(nèi)存熱插拔

前面提到隨著內(nèi)存技術(shù)的發(fā)展,物理內(nèi)存的熱插拔 hotplug 在內(nèi)核中得到了支持,由于物理內(nèi)存可以動態(tài)的從主板中插入以及拔出,所以導致了物理內(nèi)存的不連續(xù)已經(jīng)成為常態(tài),因此內(nèi)核引入了 SPARSEMEM 稀疏內(nèi)存模型以便應對這種情況,提供對更小粒度的連續(xù)物理內(nèi)存的靈活管理能力。

本小節(jié)筆者就為大家介紹一下物理內(nèi)存熱插拔 hotplug 功能在內(nèi)核中的實現(xiàn)原理,作為 SPARSEMEM 稀疏內(nèi)存模型的擴展內(nèi)容補充。

在大規(guī)模的集群中,尤其是現(xiàn)在我們處于云原生的時代,為了實現(xiàn)集群資源的動態(tài)均衡,可以通過物理內(nèi)存熱插拔的功能實現(xiàn)集群機器物理內(nèi)存容量的動態(tài)增減。

集群的規(guī)模一大,那么物理內(nèi)存出故障的幾率也會大大增加,物理內(nèi)存的熱插拔對提供集群高可用性也是至關(guān)重要的。

從總體上來講,內(nèi)存的熱插拔分為兩個階段:

  • 物理熱插拔階段:這個階段主要是從物理上將內(nèi)存硬件插入(hot-add),拔出(hot-remove)主板的過程,其中涉及到硬件和內(nèi)核的支持。

  • 邏輯熱插拔階段:這一階段主要是由內(nèi)核中的內(nèi)存管理子系統(tǒng)來負責,涉及到的主要工作為:如何動態(tài)的上線啟用(online)剛剛 hot-add 的內(nèi)存,如何動態(tài)下線(offline)剛剛 hot-remove 的內(nèi)存。

物理內(nèi)存拔出的過程需要關(guān)注的事情比插入的過程要多的多,實現(xiàn)起來也更加的困難, 這就好比在《Java 技術(shù)棧中間件優(yōu)雅停機方案設(shè)計與實現(xiàn)全景圖》 一文中我們討論服務優(yōu)雅啟動,停機時提到的:優(yōu)雅停機永遠比優(yōu)雅啟動要考慮的場景要復雜的多,因為停機的時候,線上的服務正在承載著生產(chǎn)的流量需要確保做到業(yè)務無損。

同樣的道理,物理內(nèi)存插入比較好說,困難的是物理內(nèi)存的動態(tài)拔出,因為此時即將要被拔出的物理內(nèi)存中可能已經(jīng)為進程分配了物理頁,如何妥善安置這些已經(jīng)被分配的物理頁是一個棘手的問題。

前邊我們介紹 SPARSEMEM 內(nèi)存模型的時候提到,每個 mem_section 都可以在系統(tǒng)運行時改變 offline ,online 狀態(tài),以便支持內(nèi)存的熱插拔(hotplug)功能。 當 mem_section offline 時, 內(nèi)核會把這部分內(nèi)存隔離開, 使得該部分內(nèi)存不可再被使用, 然后再把 mem_section 中已經(jīng)分配的內(nèi)存頁遷移到其他 mem_section 的內(nèi)存上. 。

0bafef4c-751d-11ed-8abf-dac502259ad0.pngimage.png

但是這里會有一個問題,就是并非所有的物理頁都可以遷移,因為遷移意味著物理內(nèi)存地址的變化,而內(nèi)存的熱插拔應該對進程來說是透明的,所以這些遷移后的物理頁映射的虛擬內(nèi)存地址是不能變化的。

這一點在進程的用戶空間是沒有問題的,因為進程在用戶空間訪問內(nèi)存都是根據(jù)虛擬內(nèi)存地址通過頁表找到對應的物理內(nèi)存地址,這些遷移之后的物理頁,雖然物理內(nèi)存地址發(fā)生變化,但是內(nèi)核通過修改相應頁表中虛擬內(nèi)存地址與物理內(nèi)存地址之間的映射關(guān)系,可以保證虛擬內(nèi)存地址不會改變。

0be5a704-751d-11ed-8abf-dac502259ad0.pngimage.png

但是在內(nèi)核態(tài)的虛擬地址空間中,有一段直接映射區(qū),在這段虛擬內(nèi)存區(qū)域中虛擬地址與物理地址是直接映射的關(guān)系,虛擬內(nèi)存地址直接減去一個固定的偏移量(0xC000 0000 ) 就得到了物理內(nèi)存地址。

直接映射區(qū)中的物理頁的虛擬地址會隨著物理內(nèi)存地址變動而變動, 因此這部分物理頁是無法輕易遷移的,然而不可遷移的頁會導致內(nèi)存無法被拔除,因為無法妥善安置被拔出內(nèi)存中已經(jīng)為進程分配的物理頁。那么內(nèi)核是如何解決這個頭疼的問題呢?

既然是這些不可遷移的物理頁導致內(nèi)存無法拔出,那么我們可以把內(nèi)存分一下類,將內(nèi)存按照物理頁是否可遷移,劃分為不可遷移頁,可回收頁,可遷移頁。

大家這里需要記住一點,內(nèi)核會將物理內(nèi)存按照頁面是否可遷移的特性進行分類,筆者后面在介紹內(nèi)核如何避免內(nèi)存碎片的時候還會在提到

然后在這些可能會被拔出的內(nèi)存中只分配那些可遷移的內(nèi)存頁,這些信息會在內(nèi)存初始化的時候被設(shè)置,這樣一來那些不可遷移的頁就不會包含在可能會拔出的內(nèi)存中,當我們需要將這塊內(nèi)存熱拔出時, 因為里邊的內(nèi)存頁全部是可遷移的, 從而使內(nèi)存可以被拔除。

3. 從 CPU 角度看物理內(nèi)存架構(gòu)

在上小節(jié)中筆者為大家介紹了三種物理內(nèi)存模型,這三種物理內(nèi)存模型是從 CPU 的視角來看待物理內(nèi)存內(nèi)部是如何布局,組織以及管理的,主角是物理內(nèi)存。

在本小節(jié)中筆者為大家提供一個新的視角,這一次我們把物理內(nèi)存看成一個整體,從 CPU 訪問物理內(nèi)存的角度來看一下物理內(nèi)存的架構(gòu),并從 CPU 與物理內(nèi)存的相對位置變化來看一下不同物理內(nèi)存架構(gòu)下對性能的影響。

3.1 一致性內(nèi)存訪問 UMA 架構(gòu)

我們在上篇文章 《深入理解 Linux 虛擬內(nèi)存管理》的 “ 8.2 CPU 如何讀寫主存” 小節(jié)中提到 CPU 與內(nèi)存之間的交互是通過總線完成的。

0cbc6514-751d-11ed-8abf-dac502259ad0.pngCPU與內(nèi)存之間的總線結(jié)構(gòu).png
  • 首先 CPU 將物理內(nèi)存地址作為地址信號放到系統(tǒng)總線上傳輸。隨后 IO bridge 將系統(tǒng)總線上的地址信號轉(zhuǎn)換為存儲總線上的電子信號。

  • 主存感受到存儲總線上的地址信號并通過存儲控制器將存儲總線上的物理內(nèi)存地址 A 讀取出來。

  • 存儲控制器通過物理內(nèi)存地址定位到具體的存儲器模塊,從 DRAM 芯片中取出物理內(nèi)存地址對應的數(shù)據(jù)。

  • 存儲控制器將讀取到的數(shù)據(jù)放到存儲總線上,隨后 IO bridge 將存儲總線上的數(shù)據(jù)信號轉(zhuǎn)換為系統(tǒng)總線上的數(shù)據(jù)信號,然后繼續(xù)沿著系統(tǒng)總線傳遞。

  • CPU 芯片感受到系統(tǒng)總線上的數(shù)據(jù)信號,將數(shù)據(jù)從系統(tǒng)總線上讀取出來并拷貝到寄存器中。

上圖展示的是單核 CPU 訪問內(nèi)存的架構(gòu)圖,那么在多核服務器中多個 CPU 與內(nèi)存之間的架構(gòu)關(guān)系又是什么樣子的呢?

0cc91c3c-751d-11ed-8abf-dac502259ad0.pngimage.png

在 UMA 架構(gòu)下,多核服務器中的多個 CPU 位于總線的一側(cè),所有的內(nèi)存條組成一大片內(nèi)存位于總線的另一側(cè),所有的 CPU 訪問內(nèi)存都要過總線,而且距離都是一樣的,由于所有 CPU 對內(nèi)存的訪問距離都是一樣的,所以在 UMA 架構(gòu)下所有 CPU 訪問內(nèi)存的速度都是一樣的。這種訪問模式稱為 SMP(Symmetric multiprocessing),即對稱多處理器。

這里的一致性是指同一個 CPU 對所有內(nèi)存的訪問的速度是一樣的。即一致性內(nèi)存訪問 UMA(Uniform Memory Access)。

但是隨著多核技術(shù)的發(fā)展,服務器上的 CPU 個數(shù)會越來越多,而 UMA 架構(gòu)下所有 CPU 都是需要通過總線來訪問內(nèi)存的,這樣總線很快就會成為性能瓶頸,主要體現(xiàn)在以下兩個方面:

  1. 總線的帶寬壓力會越來越大,隨著 CPU 個數(shù)的增多導致每個 CPU 可用帶寬會減少

  2. 總線的長度也會因此而增加,進而增加訪問延遲

UMA 架構(gòu)的優(yōu)點很明顯就是結(jié)構(gòu)簡單,所有的 CPU 訪問內(nèi)存速度都是一致的,都必須經(jīng)過總線。然而它的缺點筆者剛剛也提到了,就是隨著處理器核數(shù)的增多,總線的帶寬壓力會越來越大。解決辦法就只能擴寬總線,然而成本十分高昂,未來可能仍然面臨帶寬壓力。

為了解決以上問題,提高 CPU 訪問內(nèi)存的性能和擴展性,于是引入了一種新的架構(gòu):非一致性內(nèi)存訪問 NUMA(Non-uniform memory access)。

3.2 非一致性內(nèi)存訪問 NUMA 架構(gòu)

在 NUMA 架構(gòu)下,內(nèi)存就不是一整片的了,而是被劃分成了一個一個的內(nèi)存節(jié)點 (NUMA 節(jié)點),每個 CPU 都有屬于自己的本地內(nèi)存節(jié)點,CPU 訪問自己的本地內(nèi)存不需要經(jīng)過總線,因此訪問速度是最快的。當 CPU 自己的本地內(nèi)存不足時,CPU 就需要跨節(jié)點去訪問其他內(nèi)存節(jié)點,這種情況下 CPU 訪問內(nèi)存就會慢很多。

在 NUMA 架構(gòu)下,任意一個 CPU 都可以訪問全部的內(nèi)存節(jié)點,訪問自己的本地內(nèi)存節(jié)點是最快的,但訪問其他內(nèi)存節(jié)點就會慢很多,這就導致了 CPU 訪問內(nèi)存的速度不一致,所以叫做非一致性內(nèi)存訪問架構(gòu)。

0d011966-751d-11ed-8abf-dac502259ad0.pngimage.png

如上圖所示,CPU 和它的本地內(nèi)存組成了 NUMA 節(jié)點,CPU 與 CPU 之間通過 QPI(Intel QuickPath Interconnect)點對點完成互聯(lián),在 CPU 的本地內(nèi)存不足的情況下,CPU 需要通過 QPI 訪問遠程 NUMA 節(jié)點上的內(nèi)存控制器從而在遠程內(nèi)存節(jié)點上分配內(nèi)存,這就導致了遠程訪問比本地訪問多了額外的延遲開銷(需要通過 QPI 遍歷遠程 NUMA 節(jié)點)。

在 NUMA 架構(gòu)下,只有 DISCONTIGMEM 非連續(xù)內(nèi)存模型和 SPARSEMEM 稀疏內(nèi)存模型是可用的。而 UMA 架構(gòu)下,前面介紹的三種內(nèi)存模型都可以配置使用。

3.2.1 NUMA 的內(nèi)存分配策略

NUMA 的內(nèi)存分配策略是指在 NUMA 架構(gòu)下 CPU 如何請求內(nèi)存分配的相關(guān)策略,比如:是優(yōu)先請求本地內(nèi)存節(jié)點分配內(nèi)存呢 ?還是優(yōu)先請求指定的 NUMA 節(jié)點分配內(nèi)存 ?是只能在本地內(nèi)存節(jié)點分配呢 ?還是允許當本地內(nèi)存不足的情況下可以請求遠程 NUMA 節(jié)點分配內(nèi)存 ?

內(nèi)存分配策略 策略描述
MPOL_BIND 必須在綁定的節(jié)點進行內(nèi)存分配,如果內(nèi)存不足,則進行 swap
MPOL_INTERLEAVE 本地節(jié)點和遠程節(jié)點均可允許分配內(nèi)存
MPOL_PREFERRED 優(yōu)先在指定節(jié)點分配內(nèi)存,當指定節(jié)點內(nèi)存不足時,選擇離指定節(jié)點最近的節(jié)點分配內(nèi)存
MPOL_LOCAL (默認) 優(yōu)先在本地節(jié)點分配,當本地節(jié)點內(nèi)存不足時,可以在遠程節(jié)點分配內(nèi)存

我們可以在應用程序中通過 libnuma 共享庫中的 API 調(diào)用 set_mempolicy 接口設(shè)置進程的內(nèi)存分配策略。

#include

longset_mempolicy(intmode,constunsignedlong*nodemask,
unsignedlongmaxnode);
  • mode : 指定 NUMA 內(nèi)存分配策略。

  • nodemask:指定 NUMA 節(jié)點 Id。

  • maxnode:指定最大 NUMA 節(jié)點 Id,用于遍歷遠程節(jié)點,實現(xiàn)跨 NUMA 節(jié)點分配內(nèi)存。

libnuma 共享庫 API 文檔:https://man7.org/linux/man-pages/man3/numa.3.html#top_of_page

set_mempolicy 接口文檔:https://man7.org/linux/man-pages/man2/set_mempolicy.2.html

3.2.2 NUMA 的使用簡介

在我們理解了物理內(nèi)存的 NUMA 架構(gòu),以及在 NUMA 架構(gòu)下的內(nèi)存分配策略之后,本小節(jié)筆者來為大家介紹下如何正確的利用 NUMA 提升我們應用程序的性能。

前邊我們介紹了這么多的理論知識,但是理論的東西總是很虛,正所謂眼見為實,大家一定想親眼看一下 NUMA 架構(gòu)在計算機中的具體表現(xiàn)形式,比如:在支持 NUMA 架構(gòu)的機器上到底有多少個 NUMA 節(jié)點?每個 NUMA 節(jié)點包含哪些 CPU 核,具體是怎樣的一個分布情況?

前面也提到 CPU 在訪問本地 NUMA 節(jié)點中的內(nèi)存時,速度是最快的。但是當訪問遠程 NUMA 節(jié)點,速度就會相對很慢,那么到底有多慢?本地節(jié)點與遠程節(jié)點之間的訪問速度差異具體是多少 ?

3.2.2.1 查看 NUMA 相關(guān)信息

numactl 文檔:https://man7.org/linux/man-pages/man8/numactl.8.html

針對以上具體問題,numactl -H 命令可以給出我們想要的答案:

available: 4 nodes (0-3)
node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 
node 0 size: 64794 MB
node 0 free: 55404 MB

node 1 cpus: 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
node 1 size: 65404 MB
node 1 free: 58642 MB

node 2 cpus: 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
node 2 size: 65404 MB
node 2 free: 61181 MB

node 3 cpus:  48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
node 3 size: 65402 MB
node 3 free: 55592 MB

node distances:
node   0   1   2   3
  0:  10  16  32  33
  1:  16  10  25  32
  2:  32  25  10  16
  3:  33  32  16  10

numactl -H 命令可以查看服務器的 NUMA 配置,上圖中的服務器配置共包含 4 個 NUMA 節(jié)點(0 - 3),每個 NUMA 節(jié)點中包含 16個 CPU 核心,本地內(nèi)存大小約為 64G。

大家可以關(guān)注下最后 node distances: 這一欄,node distances 給出了不同 NUMA 節(jié)點之間的訪問距離,對角線上的值均為本地節(jié)點的訪問距離 10 。比如 [0,0] 表示 NUMA 節(jié)點 0 的本地內(nèi)存訪問距離。

我們可以很明顯的看到當出現(xiàn)跨 NUMA 節(jié)點訪問的時候,訪問距離就會明顯增加,比如節(jié)點 0 訪問節(jié)點 1 的距離 [0,1] 是16,節(jié)點 0 訪問節(jié)點 3 的距離 [0,3] 是 33。距離越遠,跨 NUMA 節(jié)點內(nèi)存訪問的延時越大。應用程序運行時應減少跨 NUMA 節(jié)點訪問內(nèi)存。

此外我們還可以通過 numactl -s 來查看 NUMA 的內(nèi)存分配策略設(shè)置:

policy: default
preferred node: current

通過 numastat 還可以查看各個 NUMA 節(jié)點的內(nèi)存訪問命中率:

node0node1node2node3
numa_hit12965542579180184441296574252828018454
numa_miss854175840297198754475141267108
numa_foreign402885958550361414885858450375
interleave_hit45651459184665449718
local_node12318970318353441221141898045915354158
other_node646572268267432259465772582675425

  • numa_hit :內(nèi)存分配在該節(jié)點中成功的次數(shù)。

  • numa_miss : 內(nèi)存分配在該節(jié)點中失敗的次數(shù)。

  • numa_foreign:表示其他 NUMA 節(jié)點本地內(nèi)存分配失敗,跨節(jié)點(numa_miss)來到本節(jié)點分配內(nèi)存的次數(shù)。

  • interleave_hit : 在 MPOL_INTERLEAVE 策略下,在本地節(jié)點分配內(nèi)存的次數(shù)。

  • local_node:進程在本地節(jié)點分配內(nèi)存成功的次數(shù)。

  • other_node:運行在本節(jié)點的進程跨節(jié)點在其他節(jié)點上分配內(nèi)存的次數(shù)。

numastat 文檔:https://man7.org/linux/man-pages/man8/numastat.8.html

3.2.2.2 綁定 NUMA 節(jié)點

numactl 工具可以讓我們應用程序指定運行在哪些 CPU 核心上,同時也可以指定我們的應用程序可以在哪些 NUMA 節(jié)點上分配內(nèi)存。通過將應用程序與具體的 CPU 核心和 NUMA 節(jié)點綁定,從而可以提升程序的性能。

numactl --membind=nodes  --cpunodebind=nodes  command
  • 通過 --membind 可以指定我們的應用程序只能在哪些具體的 NUMA 節(jié)點上分配內(nèi)存,如果這些節(jié)點內(nèi)存不足,則分配失敗。

  • 通過 --cpunodebind 可以指定我們的應用程序只能運行在哪些 NUMA 節(jié)點上。

numactl --physcpubind=cpus  command

另外我們還可以通過 --physcpubind 將我們的應用程序綁定到具體的物理 CPU 上。這個選項后邊指定的參數(shù)我們可以通過 cat /proc/cpuinfo 輸出信息中的 processor 這一欄查看。例如:通過 numactl --physcpubind= 0-15 ./numatest.out 命令將進程 numatest 綁定到 0~15 CPU 上執(zhí)行。

我們可以通過 numactl 命令將 numatest 進程分別綁定在相同的 NUMA 節(jié)點上和不同的 NUMA 節(jié)點上,運行觀察。

numactl --membind=0 --cpunodebind=0 ./numatest.out
numactl --membind=0 --cpunodebind=1 ./numatest.out

大家肯定一眼就能看出綁定在相同 NUMA 節(jié)點的進程運行會更快,因為通過前邊對 NUMA 架構(gòu)的介紹,我們知道 CPU 訪問本地 NUMA 節(jié)點的內(nèi)存是最快的。

除了 numactl 這個工具外,我們還可以通過共享庫 libnuma 在程序中進行 NUMA 相關(guān)的操作。這里筆者就不演示了,感興趣可以查看下 libnuma 的 API 文檔:https://man7.org/linux/man-pages/man3/numa.3.html#top_of_page

4. 內(nèi)核如何管理 NUMA 節(jié)點

在前邊我們介紹物理內(nèi)存模型和物理內(nèi)存架構(gòu)的時候提到過:在 NUMA 架構(gòu)下,只有 DISCONTIGMEM 非連續(xù)內(nèi)存模型和 SPARSEMEM 稀疏內(nèi)存模型是可用的。而 UMA 架構(gòu)下,前面介紹的三種內(nèi)存模型均可以配置使用。

無論是 NUMA 架構(gòu)還是 UMA 架構(gòu)在內(nèi)核中都是使用相同的數(shù)據(jù)結(jié)構(gòu)來組織管理的,在內(nèi)核的內(nèi)存管理模塊中會把 UMA 架構(gòu)當做只有一個 NUMA 節(jié)點的偽 NUMA 架構(gòu)。這樣一來這兩種架構(gòu)模式就在內(nèi)核中被統(tǒng)一管理起來。

下面筆者先從最頂層的設(shè)計開始為大家介紹一下內(nèi)核是如何管理這些 NUMA 節(jié)點的~~

0d011966-751d-11ed-8abf-dac502259ad0.pngimage.png

NUMA 節(jié)點中可能會包含多個 CPU,這些 CPU 均是物理 CPU,這點大家需要注意一下。

4.1 內(nèi)核如何統(tǒng)一組織 NUMA 節(jié)點

首先我們來看第一個問題,在內(nèi)核中是如何將這些 NUMA 節(jié)點統(tǒng)一管理起來的?

內(nèi)核中使用了 struct pglist_data 這樣的一個數(shù)據(jù)結(jié)構(gòu)來描述 NUMA 節(jié)點,在內(nèi)核 2.4 版本之前,內(nèi)核是使用一個 pgdat_list 單鏈表將這些 NUMA 節(jié)點串聯(lián)起來的,單鏈表定義在 /include/linux/mmzone.h 文件中:

externpg_data_t*pgdat_list;

每個 NUMA 節(jié)點的數(shù)據(jù)結(jié)構(gòu) struct pglist_data 中有一個 next 指針,用于將這些 NUMA 節(jié)點串聯(lián)起來形成 pgdat_list 單鏈表,鏈表的末尾節(jié)點 next 指針指向 NULL。

typedefstructpglist_data{
structpglist_data*pgdat_next;
}

在內(nèi)核 2.4 之后的版本中,內(nèi)核移除了 struct pglist_data 結(jié)構(gòu)中的 pgdat_next 之指針, 同時也刪除了 pgdat_list 單鏈表。取而代之的是,內(nèi)核使用了一個大小為 MAX_NUMNODES ,類型為 struct pglist_data 的全局數(shù)組 node_data[] 來管理所有的 NUMA 節(jié)點。

0d7ae93a-751d-11ed-8abf-dac502259ad0.png

全局數(shù)組 node_data[] 定義在文件 /arch/arm64/include/asm/mmzone.h中:

#ifdefCONFIG_NUMA
externstructpglist_data*node_data[];
#defineNODE_DATA(nid)(node_data[(nid)])

NODE_DATA(nid) 宏可以通過 NUMA 節(jié)點的 nodeId,找到對應的 struct pglist_data 結(jié)構(gòu)。

node_data[] 數(shù)組大小 MAX_NUMNODES 定義在 /include/linux/numa.h文件中:

#ifdefCONFIG_NODES_SHIFT
#defineNODES_SHIFTCONFIG_NODES_SHIFT
#else
#defineNODES_SHIFT0
#endif
#defineMAX_NUMNODES(1<

UMA 架構(gòu)下 NODES_SHIFT 為 0 ,所以內(nèi)核中只用一個 NUMA 節(jié)點來管理所有物理內(nèi)存。

4.2 NUMA 節(jié)點描述符 pglist_data 結(jié)構(gòu)

typedefstructpglist_data{
//NUMA節(jié)點id
intnode_id;
//指向NUMA節(jié)點內(nèi)管理所有物理頁page的數(shù)組
structpage*node_mem_map;
//NUMA節(jié)點內(nèi)第一個物理頁的pfn
unsignedlongnode_start_pfn;
//NUMA節(jié)點內(nèi)所有可用的物理頁個數(shù)(不包含內(nèi)存空洞)
unsignedlongnode_present_pages;
//NUMA節(jié)點內(nèi)所有的物理頁個數(shù)(包含內(nèi)存空洞)
unsignedlongnode_spanned_pages;
//保證多進程可以并發(fā)安全的訪問NUMA節(jié)點
spinlock_tnode_size_lock;
.............
}

node_id 表示 NUMA 節(jié)點的 id,我們可以通過 numactl -H 命令的輸出結(jié)果查看節(jié)點 id。從 0 開始依次對 NUMA 節(jié)點進行編號。

struct page 類型的數(shù)組 node_mem_map 中包含了 NUMA節(jié)點內(nèi)的所有的物理內(nèi)存頁。

0da6250a-751d-11ed-8abf-dac502259ad0.pngimage.png

node_start_pfn 指向 NUMA 節(jié)點內(nèi)第一個物理頁的 PFN,系統(tǒng)中所有 NUMA 節(jié)點中的物理頁都是依次編號的,每個物理頁的 PFN 都是全局唯一的(不只是其所在 NUMA 節(jié)點內(nèi)唯一)

0e1a2d9c-751d-11ed-8abf-dac502259ad0.pngimage.png

node_present_pages 用于統(tǒng)計 NUMA 節(jié)點內(nèi)所有真正可用的物理頁面數(shù)量(不包含內(nèi)存空洞)。

由于 NUMA 節(jié)點內(nèi)包含的物理內(nèi)存并不總是連續(xù)的,可能會包含一些內(nèi)存空洞,node_spanned_pages 則是用于統(tǒng)計 NUMA 節(jié)點內(nèi)所有的內(nèi)存頁,包含不連續(xù)的物理內(nèi)存地址(內(nèi)存空洞)的頁面數(shù)。

0e2d217c-751d-11ed-8abf-dac502259ad0.pngimage.png

以上內(nèi)容是筆者從整體上為大家介紹的 NUMA 節(jié)點如何管理節(jié)點內(nèi)部的本地內(nèi)存。事實上內(nèi)核還會將 NUMA 節(jié)點中的本地內(nèi)存做近一步的劃分。那么為什么要近一步劃分呢?

4.3 NUMA 節(jié)點物理內(nèi)存區(qū)域的劃分

我們都知道內(nèi)核對物理內(nèi)存的管理都是以頁為最小單位來管理的,每頁默認 4K 大小,理想狀況下任何種類的數(shù)據(jù)都可以存放在任何頁框中,沒有什么限制。比如:存放內(nèi)核數(shù)據(jù),用戶數(shù)據(jù),磁盤緩沖數(shù)據(jù)等。

但是實際的計算機體系結(jié)構(gòu)受到硬件方面的制約,間接導致限制了頁框的使用方式。

比如在 X86 體系結(jié)構(gòu)下,ISA 總線的 DMA (直接內(nèi)存存?。┛刂破?,只能對內(nèi)存的前16M 進行尋址,這就導致了 ISA 設(shè)備不能在整個 32 位地址空間中執(zhí)行 DMA,只能使用物理內(nèi)存的前 16M 進行 DMA 操作。

因此直接映射區(qū)的前 16M 專門讓內(nèi)核用來為 DMA 分配內(nèi)存,這塊 16M 大小的內(nèi)存區(qū)域我們稱之為 ZONE_DMA。

用于 DMA 的內(nèi)存必須從 ZONE_DMA 區(qū)域中分配。

0be5a704-751d-11ed-8abf-dac502259ad0.pngimage.png

而直接映射區(qū)中剩下的部分也就是從 16M 到 896M(不包含 896M)這段區(qū)域,我們稱之為 ZONE_NORMAL。從字面意義上我們可以了解到,這塊區(qū)域包含的就是正常的頁框(沒有任何使用限制)。

ZONE_NORMAL 由于也是屬于直接映射區(qū)的一部分,對應的物理內(nèi)存 16M 到 896M 這段區(qū)域也是被直接映射至內(nèi)核態(tài)虛擬內(nèi)存空間中的 3G + 16M 到 3G + 896M 這段虛擬內(nèi)存上。

而物理內(nèi)存 896M 以上的區(qū)域被內(nèi)核劃分為 ZONE_HIGHMEM 區(qū)域,我們稱之為高端內(nèi)存。

由于內(nèi)核虛擬內(nèi)存空間中的前 896M 虛擬內(nèi)存已經(jīng)被直接映射區(qū)所占用,而在 32 體系結(jié)構(gòu)下內(nèi)核虛擬內(nèi)存空間總共也就 1G 的大小,這樣一來內(nèi)核剩余可用的虛擬內(nèi)存空間就變?yōu)榱?1G - 896M = 128M。

顯然物理內(nèi)存中剩下的這 3200M 大小的 ZONE_HIGHMEM 區(qū)域無法繼續(xù)通過直接映射的方式映射到這 128M 大小的虛擬內(nèi)存空間中。

這樣一來物理內(nèi)存中的 ZONE_HIGHMEM 區(qū)域就只能采用動態(tài)映射的方式映射到 128M 大小的內(nèi)核虛擬內(nèi)存空間中,也就是說只能動態(tài)的一部分一部分的分批映射,先映射正在使用的這部分,使用完畢解除映射,接著映射其他部分。

所以內(nèi)核會根據(jù)各個物理內(nèi)存區(qū)域的功能不同,將 NUMA 節(jié)點內(nèi)的物理內(nèi)存主要劃分為以下四個物理內(nèi)存區(qū)域:

  1. ZONE_DMA:用于那些無法對全部物理內(nèi)存進行尋址的硬件設(shè)備,進行 DMA 時的內(nèi)存分配。例如前邊介紹的 ISA 設(shè)備只能對物理內(nèi)存的前 16M 進行尋址。該區(qū)域的長度依賴于具體的處理器類型。

  2. ZONE_DMA32:與 ZONE_DMA 區(qū)域類似,該區(qū)域內(nèi)的物理頁面可用于執(zhí)行 DMA 操作,不同之處在于該區(qū)域是提供給 32 位設(shè)備(只能尋址 4G 物理內(nèi)存)執(zhí)行 DMA 操作時使用的。該區(qū)域只在 64 位系統(tǒng)中起作用,因為只有在 64 位系統(tǒng)中才會專門為 32 位設(shè)備提供專門的 DMA 區(qū)域。

  3. ZONE_NORMAL:這個區(qū)域的物理頁都可以直接映射到內(nèi)核中的虛擬內(nèi)存,由于是線性映射,內(nèi)核可以直接進行訪問。

  4. ZONE_HIGHMEM:這個區(qū)域包含的物理頁就是我們說的高端內(nèi)存,內(nèi)核不能直接訪問這些物理頁,這些物理頁需要動態(tài)映射進內(nèi)核虛擬內(nèi)存空間中(非線性映射)。該區(qū)域只在 32 位系統(tǒng)中才會存在,因為 64 位系統(tǒng)中的內(nèi)核虛擬內(nèi)存空間太大了(128T),都可以進行直接映射。

以上這些物理內(nèi)存區(qū)域的劃分定義在 /include/linux/mmzone.h 文件中:

enumzone_type{
#ifdefCONFIG_ZONE_DMA
ZONE_DMA,
#endif
#ifdefCONFIG_ZONE_DMA32
ZONE_DMA32,
#endif
ZONE_NORMAL,
#ifdefCONFIG_HIGHMEM
ZONE_HIGHMEM,
#endif
ZONE_MOVABLE,
#ifdefCONFIG_ZONE_DEVICE
ZONE_DEVICE,
#endif
//充當結(jié)束標記,在內(nèi)核中想要迭代系統(tǒng)中所有內(nèi)存域時,會用到該常量
__MAX_NR_ZONES

};

大家可能注意到內(nèi)核中定義的 zone_type 除了上邊為大家介紹的四個物理內(nèi)存區(qū)域,又多出了兩個區(qū)域:ZONE_MOVABLE 和 ZONE_DEVICE。

ZONE_DEVICE 是為支持熱插拔設(shè)備而分配的非易失性內(nèi)存( Non Volatile Memory ),也可用于內(nèi)核崩潰時保存相關(guān)的調(diào)試信息。

ZONE_MOVABLE 是內(nèi)核定義的一個虛擬內(nèi)存區(qū)域,該區(qū)域中的物理頁可以來自于上邊介紹的幾種真實的物理區(qū)域。該區(qū)域中的頁全部都是可以遷移的,主要是為了防止內(nèi)存碎片和支持內(nèi)存的熱插拔。

既然有了這些實際的物理內(nèi)存區(qū)域,那么內(nèi)核為什么又要劃分出一個 ZONE_MOVABLE 這樣的虛擬內(nèi)存區(qū)域呢 ?

因為隨著系統(tǒng)的運行會伴隨著不同大小的物理內(nèi)存頁的分配和釋放,這種內(nèi)存不規(guī)則的分配釋放隨著系統(tǒng)的長時間運行就會導致內(nèi)存碎片,內(nèi)存碎片會使得系統(tǒng)在明明有足夠內(nèi)存的情況下,依然無法為進程分配合適的內(nèi)存。

0e88c6c6-751d-11ed-8abf-dac502259ad0.pngimage.png

如上圖所示,假如現(xiàn)在系統(tǒng)一共有 16 個物理內(nèi)存頁,當前系統(tǒng)只是分配了 3 個物理頁,那么在當前系統(tǒng)中還剩余 13 個物理內(nèi)存頁的情況下,如果內(nèi)核想要分配 8 個連續(xù)的物理頁的話,就會由于內(nèi)存碎片的存在導致分配失敗。(只能分配最多 4 個連續(xù)的物理頁)

內(nèi)核中請求分配的物理頁面數(shù)只能是 2 的次冪??!

如果這些物理頁處于 ZONE_MOVABLE 區(qū)域,它們就可以被遷移,內(nèi)核可以通過遷移頁面來避免內(nèi)存碎片的問題:

0e9a699e-751d-11ed-8abf-dac502259ad0.pngimage.png

內(nèi)核通過遷移頁面來規(guī)整內(nèi)存,這樣就可以避免內(nèi)存碎片,從而得到一大片連續(xù)的物理內(nèi)存,以滿足內(nèi)核對大塊連續(xù)內(nèi)存分配的請求。所以這就是內(nèi)核需要根據(jù)物理頁面是否能夠遷移的特性,而劃分出 ZONE_MOVABLE 區(qū)域的目的

到這里,我們已經(jīng)清楚了 NUMA 節(jié)點中物理內(nèi)存區(qū)域的劃分,下面我們繼續(xù)回到 struct pglist_data 結(jié)構(gòu)中看下內(nèi)核如何在 NUMA 節(jié)點中組織這些劃分出來的內(nèi)存區(qū)域:

typedefstructpglist_data{
//NUMA節(jié)點中的物理內(nèi)存區(qū)域個數(shù)
intnr_zones;
//NUMA節(jié)點中的物理內(nèi)存區(qū)域
structzonenode_zones[MAX_NR_ZONES];
//NUMA節(jié)點的備用列表
structzonelistnode_zonelists[MAX_ZONELISTS];
}pg_data_t;

nr_zones 用于統(tǒng)計 NUMA 節(jié)點內(nèi)包含的物理內(nèi)存區(qū)域個數(shù),不是每個 NUMA 節(jié)點都會包含以上介紹的所有物理內(nèi)存區(qū)域,NUMA 節(jié)點之間所包含的物理內(nèi)存區(qū)域個數(shù)是不一樣的。

事實上只有第一個 NUMA 節(jié)點可以包含所有的物理內(nèi)存區(qū)域,其它的節(jié)點并不能包含所有的區(qū)域類型,因為有些內(nèi)存區(qū)域比如:ZONE_DMA,ZONE_DMA32 必須從物理內(nèi)存的起點開始。這些在物理內(nèi)存開始的區(qū)域可能已經(jīng)被劃分到第一個 NUMA 節(jié)點了,后面的物理內(nèi)存才會被依次劃分給接下來的 NUMA 節(jié)點。因此后面的 NUMA 節(jié)點并不會包含 ZONE_DMA,ZONE_DMA32 區(qū)域。

0be5a704-751d-11ed-8abf-dac502259ad0.pngimage.png

ZONE_NORMAL、ZONE_HIGHMEM 和 ZONE_MOVABLE 是可以出現(xiàn)在所有 NUMA 節(jié)點上的。

0ec0e3ee-751d-11ed-8abf-dac502259ad0.pngimage.png

node_zones[MAX_NR_ZONES] 數(shù)組包含了 NUMA 節(jié)點中的所有物理內(nèi)存區(qū)域,物理內(nèi)存區(qū)域在內(nèi)核中的數(shù)據(jù)結(jié)構(gòu)是 struct zone 。

node_zonelists[MAX_ZONELISTS] 是 struct zonelist 類型的數(shù)組,它包含了備用 NUMA 節(jié)點和這些備用節(jié)點中的物理內(nèi)存區(qū)域。備用節(jié)點是按照訪問距離的遠近,依次排列在 node_zonelists 數(shù)組中,數(shù)組第一個備用節(jié)點是訪問距離最近的,這樣當本節(jié)點內(nèi)存不足時,可以從備用 NUMA 節(jié)點中分配內(nèi)存。

各個 NUMA 節(jié)點之間的內(nèi)存分配情況我們可以通過前邊介紹的 numastat 命令查看。

4.4 NUMA 節(jié)點中的內(nèi)存規(guī)整與回收

內(nèi)存可以說是計算機系統(tǒng)中最為寶貴的資源了,再怎么多也不夠用,當系統(tǒng)運行時間長了之后,難免會遇到內(nèi)存緊張的時候,這時候就需要內(nèi)核將那些不經(jīng)常使用的內(nèi)存頁面回收起來,或者將那些可以遷移的頁面進行內(nèi)存規(guī)整,從而可以騰出連續(xù)的物理內(nèi)存頁面供內(nèi)核分配。

內(nèi)核會為每個 NUMA 節(jié)點分配一個 kswapd 進程用于回收不經(jīng)常使用的頁面,還會為每個 NUMA 節(jié)點分配一個 kcompactd 進程用于內(nèi)存的規(guī)整避免內(nèi)存碎片。

typedefstructpglist_data{
.........
//頁面回收進程
structtask_struct*kswapd;
wait_queue_head_tkswapd_wait;
//內(nèi)存規(guī)整進程
structtask_struct*kcompactd;
wait_queue_head_tkcompactd_wait;

..........
}pg_data_t;

NUMA 節(jié)點描述符 struct pglist_data 結(jié)構(gòu)中的 struct task_struct *kswapd 屬性用于指向內(nèi)核為 NUMA 節(jié)點分配的 kswapd 進程。

kswapd_wait 用于 kswapd 進程周期性回收頁面時使用到的等待隊列。

同理 struct task_struct *kcompactd 用于指向內(nèi)核為 NUMA 節(jié)點分配的 kcompactd 進程。

kcompactd_wait 用于 kcompactd 進程周期性規(guī)整內(nèi)存時使用到的等待隊列。

本小節(jié)筆者主要為大家介紹 NUMA 節(jié)點的數(shù)據(jù)結(jié)構(gòu) struct pglist_data。詳細的內(nèi)存回收會在本文后面的章節(jié)單獨介紹。

4.5 NUMA 節(jié)點的狀態(tài) node_states

如果系統(tǒng)中的 NUMA 節(jié)點多于一個,內(nèi)核會維護一個位圖 node_states,用于維護各個 NUMA 節(jié)點的狀態(tài)信息。

如果系統(tǒng)中只有一個 NUMA 節(jié)點,則沒有節(jié)點位圖。

節(jié)點位圖以及節(jié)點的狀態(tài)掩碼值定義在 /include/linux/nodemask.h 文件中:

typedefstruct{DECLARE_BITMAP(bits,MAX_NUMNODES);}nodemask_t;
externnodemask_tnode_states[NR_NODE_STATES];

節(jié)點的狀態(tài)可通過以下掩碼表示:

enumnode_states{
N_POSSIBLE,/*Thenodecouldbecomeonlineatsomepoint*/
N_ONLINE,/*Thenodeisonline*/
N_NORMAL_MEMORY,/*Thenodehasregularmemory*/
#ifdefCONFIG_HIGHMEM
N_HIGH_MEMORY,/*Thenodehasregularorhighmemory*/
#else
N_HIGH_MEMORY=N_NORMAL_MEMORY,
#endif
#ifdefCONFIG_MOVABLE_NODE
N_MEMORY,/*Thenodehasmemory(regular,high,movable)*/
#else
N_MEMORY=N_HIGH_MEMORY,
#endif
N_CPU,/*Thenodehasoneormorecpus*/
NR_NODE_STATES
};

N_POSSIBLE 表示 NUMA 節(jié)點在某個時刻可以變?yōu)?online 狀態(tài),N_ONLINE 表示 NUMA 節(jié)點當前的狀態(tài)為 online 狀態(tài)。

我們在本文《2.3.1 物理內(nèi)存熱插拔》小節(jié)中提到,在稀疏內(nèi)存模型中,NUMA 節(jié)點的狀態(tài)可以在系統(tǒng)運行的過程中隨時切換 online ,offline 的狀態(tài),用來支持內(nèi)存的熱插拔。

0ee05850-751d-11ed-8abf-dac502259ad0.pngimage.png

N_NORMAL_MEMORY 表示節(jié)點沒有高端內(nèi)存,只有 ZONE_NORMAL 內(nèi)存區(qū)域。

N_HIGH_MEMORY 表示節(jié)點有 ZONE_NORMAL 內(nèi)存區(qū)域或者有 ZONE_HIGHMEM 內(nèi)存區(qū)域。

N_MEMORY 表示節(jié)點有 ZONE_NORMAL,ZONE_HIGHMEM,ZONE_MOVABLE 內(nèi)存區(qū)域。

N_CPU 表示節(jié)點包含一個或多個 CPU。

此外內(nèi)核還提供了兩個輔助函數(shù)用于設(shè)置或者清除指定節(jié)點的特定狀態(tài):

staticinlinevoidnode_set_state(intnode,enumnode_statesstate)
staticinlinevoidnode_clear_state(intnode,enumnode_statesstate)

內(nèi)核提供了 for_each_node_state 宏用于迭代處于特定狀態(tài)的所有 NUMA 節(jié)點。

#definefor_each_node_state(__node,__state)
for_each_node_mask((__node),node_states[__state])

比如:for_each_online_node 用于迭代所有 online 的 NUMA 節(jié)點:

#definefor_each_online_node(node)for_each_node_state(node,N_ONLINE)

5. 內(nèi)核如何管理 NUMA 節(jié)點中的物理內(nèi)存區(qū)域

0ec0e3ee-751d-11ed-8abf-dac502259ad0.pngimage.png

在前邊《4.3 NUMA 節(jié)點物理內(nèi)存區(qū)域的劃分》小節(jié)的介紹中,由于實際的計算機體系結(jié)構(gòu)受到硬件方面的制約,間接限制了頁框的使用方式。于是內(nèi)核會根據(jù)各個物理內(nèi)存區(qū)域的功能不同,將 NUMA 節(jié)點內(nèi)的物理內(nèi)存劃分為:ZONE_DMA,ZONE_DMA32,ZONE_NORMAL,ZONE_HIGHMEM 這幾個物理內(nèi)存區(qū)域。

ZONE_MOVABLE 區(qū)域是內(nèi)核從邏輯上的劃分,區(qū)域中的物理頁面來自于上述幾個內(nèi)存區(qū)域,目的是避免內(nèi)存碎片和支持內(nèi)存熱插拔(前邊筆者已經(jīng)介紹過了)。

我們可以通過 cat /proc/zoneinfo | grep Node 命令來查看 NUMA 節(jié)點中內(nèi)存區(qū)域的分布情況:

0f130570-751d-11ed-8abf-dac502259ad0.pngimage.png

筆者使用的服務器是 64 位,所以不包含 ZONE_HIGHMEM 區(qū)域。

通過 cat /proc/zoneinfo 命令來查看系統(tǒng)中各個 NUMA 節(jié)點中的各個內(nèi)存區(qū)域的內(nèi)存使用情況:

下圖中我們以 NUMA Node 0 中的 ZONE_NORMAL 區(qū)域為例說明,大家只需要瀏覽一個大概,圖中每個字段的含義筆者會在本小節(jié)的后面一一為大家介紹~~~

0f33cbde-751d-11ed-8abf-dac502259ad0.pngimage.png

內(nèi)核中用于描述和管理 NUMA 節(jié)點中的物理內(nèi)存區(qū)域的結(jié)構(gòu)體是 struct zone,上圖中顯示的 ZONE_NORMAL 區(qū)域中,物理內(nèi)存使用統(tǒng)計的相關(guān)數(shù)據(jù)均來自于 struct zone 結(jié)構(gòu)體,我們先來看一下內(nèi)核對 struct zone 結(jié)構(gòu)體的整體布局情況:

structzone{

.............省略..............

ZONE_PADDING(_pad1_)

.............省略..............

ZONE_PADDING(_pad2_)

.............省略..............

ZONE_PADDING(_pad3_)

.............省略..............

}____cacheline_internodealigned_in_smp;

由于 struct zone 結(jié)構(gòu)體在內(nèi)核中是一個訪問非常頻繁的結(jié)構(gòu)體,在多處理器系統(tǒng)中,會有不同的 CPU 同時大量頻繁的訪問 struct zone 結(jié)構(gòu)體中的不同字段。

因此內(nèi)核對 struct zone 結(jié)構(gòu)體的設(shè)計是相當考究的,將這些頻繁訪問的字段信息歸類為 4 個部分,并通過 ZONE_PADDING 來分割。

目的是通過 ZONE_PADDING 來填充字節(jié),將這四個部分,分別填充到不同的 CPU 高速緩存行(cache line)中,使得它們各自獨占 cache line,提高訪問性能。

根據(jù)前邊物理內(nèi)存區(qū)域劃分的相關(guān)內(nèi)容介紹,我們知道內(nèi)核會把 NUMA 節(jié)點中的物理內(nèi)存區(qū)域頂多劃分為 ZONE_DMA,ZONE_DMA32,ZONE_NORMAL,ZONE_HIGHMEM 這幾個物理內(nèi)存區(qū)域。因此 struct zone 的實例在內(nèi)核中會相對比較少,通過 ZONE_PADDING 填充字節(jié),帶來的 struct zone 結(jié)構(gòu)體實例內(nèi)存占用增加是可以忽略不計的。

在結(jié)構(gòu)體的最后內(nèi)核還是用了 ____cacheline_internodealigned_in_smp 編譯器關(guān)鍵字來實現(xiàn)最優(yōu)的高速緩存行對齊方式。

關(guān)于 CPU 高速緩存行對齊的詳細內(nèi)容,感興趣的同學可以回看下筆者之前的文章 《一文聊透對象在JVM中的內(nèi)存布局,以及內(nèi)存對齊和壓縮指針的原理及應用》 。

筆者為了使大家能夠更好地理解內(nèi)核如何使用 struct zone 結(jié)構(gòu)體來描述內(nèi)存區(qū)域,從而把結(jié)構(gòu)體中的字段按照一定的層次結(jié)構(gòu)重新排列介紹,這并不是原生的字段對齊方式,這一點需要大家注意!?。?/p>

structzone{
//防止并發(fā)訪問該內(nèi)存區(qū)域
spinlock_tlock;
//內(nèi)存區(qū)域名稱:Normal,DMA,HighMem
constchar*name;
//指向該內(nèi)存區(qū)域所屬的NUMA節(jié)點
structpglist_data*zone_pgdat;
//屬于該內(nèi)存區(qū)域中的第一個物理頁PFN
unsignedlongzone_start_pfn;
//該內(nèi)存區(qū)域中所有的物理頁個數(shù)(包含內(nèi)存空洞)
unsignedlongspanned_pages;
//該內(nèi)存區(qū)域所有可用的物理頁個數(shù)(不包含內(nèi)存空洞)
unsignedlongpresent_pages;
//被伙伴系統(tǒng)所管理的物理頁數(shù)
atomic_long_tmanaged_pages;
//伙伴系統(tǒng)的核心數(shù)據(jù)結(jié)構(gòu)
structfree_areafree_area[MAX_ORDER];
//該內(nèi)存區(qū)域內(nèi)存使用的統(tǒng)計信息
atomic_long_tvm_stat[NR_VM_ZONE_STAT_ITEMS];
}____cacheline_internodealigned_in_smp;

struct zone 是會被內(nèi)核頻繁訪問的一個結(jié)構(gòu)體,在多核處理器中,多個 CPU 會并發(fā)訪問 struct zone,為了防止并發(fā)訪問,內(nèi)核使用了一把 spinlock_t lock 自旋鎖來防止并發(fā)錯誤以及不一致。

name 屬性會根據(jù)該內(nèi)存區(qū)域的類型不同保存內(nèi)存區(qū)域的名稱,比如:Normal ,DMA,HighMem 等。

前邊我們介紹 NUMA 節(jié)點的描述符 struct pglist_data 的時候提到,pglist_data 通過 struct zone 類型的數(shù)組 node_zones 將 NUMA 節(jié)點中劃分的物理內(nèi)存區(qū)域連接起來。

typedefstructpglist_data{
//NUMA節(jié)點中的物理內(nèi)存區(qū)域個數(shù)
intnr_zones;
//NUMA節(jié)點中的物理內(nèi)存區(qū)域
structzonenode_zones[MAX_NR_ZONES];
}

這些物理內(nèi)存區(qū)域也會通過 struct zone 中的 zone_pgdat 指向自己所屬的 NUMA 節(jié)點。

0f55002e-751d-11ed-8abf-dac502259ad0.pngimage.png

NUMA 節(jié)點 struct pglist_data 結(jié)構(gòu)中的 node_start_pfn 指向 NUMA 節(jié)點內(nèi)第一個物理頁的 PFN。同理物理內(nèi)存區(qū)域 struct zone 結(jié)構(gòu)中的 zone_start_pfn 指向的是該內(nèi)存區(qū)域內(nèi)所管理的第一個物理頁面 PFN 。

后面的屬性也和 NUMA 節(jié)點對應的字段含義一樣,比如:spanned_pages 表示該內(nèi)存區(qū)域內(nèi)所有的物理頁總數(shù)(包含內(nèi)存空洞),通過 spanned_pages = zone_end_pfn - zone_start_pfn 計算得到。

present_pages 則表示該內(nèi)存區(qū)域內(nèi)所有實際可用的物理頁面總數(shù)(不包含內(nèi)存空洞),通過 present_pages = spanned_pages - absent_pages(pages in holes) 計算得到。

在 NUMA 架構(gòu)下,物理內(nèi)存被劃分成了一個一個的內(nèi)存節(jié)點(NUMA 節(jié)點),在每個 NUMA 節(jié)點內(nèi)部又將其所管理的物理內(nèi)存按照功能不同劃分成了不同的內(nèi)存區(qū)域,每個內(nèi)存區(qū)域管理一片用于具體功能的物理內(nèi)存,而內(nèi)核會為每一個內(nèi)存區(qū)域分配一個伙伴系統(tǒng)用于管理該內(nèi)存區(qū)域下物理內(nèi)存的分配和釋放。

物理內(nèi)存在內(nèi)核中管理的層級關(guān)系為:None -> Zone -> page

0f61f536-751d-11ed-8abf-dac502259ad0.pngimage.png

struct zone 結(jié)構(gòu)中的 managed_pages 用于表示該內(nèi)存區(qū)域內(nèi)被伙伴系統(tǒng)所管理的物理頁數(shù)量。

數(shù)組 free_area[MAX_ORDER] 是伙伴系統(tǒng)的核心數(shù)據(jù)結(jié)構(gòu),筆者會在后面的系列文章中詳細為大家介紹伙伴系統(tǒng)的實現(xiàn)。

vm_stat 維護了該內(nèi)存區(qū)域物理內(nèi)存的使用統(tǒng)計信息,前邊介紹的 cat /proc/zoneinfo命令的輸出數(shù)據(jù)就來源于這個 vm_stat。

0f33cbde-751d-11ed-8abf-dac502259ad0.pngimage.png

5.1 物理內(nèi)存區(qū)域中的預留內(nèi)存

除了前邊介紹的關(guān)于物理內(nèi)存區(qū)域的這些基本信息之外,每個物理內(nèi)存區(qū)域 struct zone 還為操作系統(tǒng)預留了一部分內(nèi)存,這部分預留的物理內(nèi)存用于內(nèi)核的一些核心操作,這些操作無論如何是不允許內(nèi)存分配失敗的。

什么意思呢?內(nèi)核中關(guān)于內(nèi)存分配的場景無外乎有兩種方式:

  1. 當進程請求內(nèi)核分配內(nèi)存時,如果此時內(nèi)存比較充裕,那么進程的請求會被立刻滿足,如果此時內(nèi)存已經(jīng)比較緊張,內(nèi)核就需要將一部分不經(jīng)常使用的內(nèi)存進行回收,從而騰出一部分內(nèi)存滿足進程的內(nèi)存分配的請求,在這個回收內(nèi)存的過程中,進程會一直阻塞等待。

  2. 另一種內(nèi)存分配場景,進程是不允許阻塞的,內(nèi)存分配的請求必須馬上得到滿足,比如執(zhí)行中斷處理程序或者執(zhí)行持有自旋鎖等臨界區(qū)內(nèi)的代碼時,進程就不允許睡眠,因為中斷程序無法被重新調(diào)度。這時就需要內(nèi)核提前為這些核心操作預留一部分內(nèi)存,當內(nèi)存緊張時,可以使用這部分預留的內(nèi)存給這些操作分配。

structzone{
...........

unsignedlongnr_reserved_highatomic;
longlowmem_reserve[MAX_NR_ZONES];

...........
}

nr_reserved_highatomic 表示的是該內(nèi)存區(qū)域內(nèi)預留內(nèi)存的大小,范圍為 128 到 65536 KB 之間。

lowmem_reserve 數(shù)組則是用于規(guī)定每個內(nèi)存區(qū)域必須為自己保留的物理頁數(shù)量,防止更高位的內(nèi)存區(qū)域?qū)ψ约旱膬?nèi)存空間進行過多的侵占擠壓。

那么什么是高位內(nèi)存區(qū)域 ?什么是低位內(nèi)存區(qū)域 ? 高位內(nèi)存區(qū)域為什么會對低位內(nèi)存區(qū)域進行侵占擠壓呢 ?

因為物理內(nèi)存區(qū)域比如前邊介紹的 ZONE_DMA,ZONE_DMA32,ZONE_NORMAL,ZONE_HIGHMEM 這些都是針對物理內(nèi)存進行的劃分,所謂的低位內(nèi)存區(qū)域和高位內(nèi)存區(qū)域其實還是按照物理內(nèi)存地址從低到高進行排列布局:

0be5a704-751d-11ed-8abf-dac502259ad0.pngimage.png

根據(jù)物理內(nèi)存地址的高低,低位內(nèi)存區(qū)域到高位內(nèi)存區(qū)域的順序依次是:ZONE_DMA,ZONE_DMA32,ZONE_NORMAL,ZONE_HIGHMEM。

高位內(nèi)存區(qū)域為什么會對低位內(nèi)存區(qū)域進行擠壓呢

一些用于特定功能的物理內(nèi)存必須從特定的內(nèi)存區(qū)域中進行分配,比如外設(shè)的 DMA 控制器就必須從 ZONE_DMA 或者 ZONE_DMA32 中分配內(nèi)存。

但是一些用于常規(guī)用途的物理內(nèi)存則可以從多個物理內(nèi)存區(qū)域中進行分配,當 ZONE_HIGHMEM 區(qū)域中的內(nèi)存不足時,內(nèi)核可以從 ZONE_NORMAL 進行內(nèi)存分配,ZONE_NORMAL 區(qū)域內(nèi)存不足時可以進一步降級到 ZONE_DMA 區(qū)域進行分配。

而低位內(nèi)存區(qū)域中的內(nèi)存總是寶貴的,內(nèi)核肯定希望這些用于常規(guī)用途的物理內(nèi)存從常規(guī)內(nèi)存區(qū)域中進行分配,這樣能夠節(jié)省 ZONE_DMA 區(qū)域中的物理內(nèi)存保證 DMA 操作的內(nèi)存使用需求,但是如果內(nèi)存很緊張了,高位內(nèi)存區(qū)域中的物理內(nèi)存不夠用了,那么內(nèi)核就會去占用擠壓其他內(nèi)存區(qū)域中的物理內(nèi)存從而滿足內(nèi)存分配的需求。

但是內(nèi)核又不會允許高位內(nèi)存區(qū)域?qū)Φ臀粌?nèi)存區(qū)域的無限制擠壓占用,因為畢竟低位內(nèi)存區(qū)域有它特定的用途,所以每個內(nèi)存區(qū)域會給自己預留一定的內(nèi)存,防止被高位內(nèi)存區(qū)域擠壓占用。而每個內(nèi)存區(qū)域為自己預留的這部分內(nèi)存就存儲在 lowmem_reserve 數(shù)組中。

每個內(nèi)存區(qū)域是按照一定的比例來計算自己的預留內(nèi)存的,這個比例我們可以通過 cat /proc/sys/vm/lowmem_reserve_ratio 命令查看:

0fc36fdc-751d-11ed-8abf-dac502259ad0.pngimage.png

從左到右分別代表了 ZONE_DMA,ZONE_DMA32,ZONE_NORMAL,ZONE_MOVABLE,ZONE_DEVICE 物理內(nèi)存區(qū)域的預留內(nèi)存比例。

筆者使用的服務器是 64 位,所以沒有 ZONE_HIGHMEM 區(qū)域。

那么每個內(nèi)存區(qū)域如何根據(jù)各自的 lowmem_reserve_ratio 來計算各自區(qū)域中的預留內(nèi)存大小呢?

為了讓大家更好的理解,下面我們以 ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM 這三個物理內(nèi)存區(qū)域舉例,它們的 lowmem_reserve_ratio 分別為 256,32,0。它們的大小分別是:8M,64M,256M,按照每頁大小 4K 計算它們區(qū)域里包含的物理頁個數(shù)分別為:2048, 16384, 65536。

lowmem_reserve_ratio 內(nèi)存區(qū)域大小 物理內(nèi)存頁個數(shù)
ZONE_DMA 256 8M 2048
ZONE_NORMAL 32 64M 16384
ZONE_HIGHMEM 0 256M 65536
  • ZONE_DMA 為防止被 ZONE_NORMAL 擠壓侵占,而為自己預留的物理內(nèi)存頁為:16384 / 256 = 64。

  • ZONE_DMA 為防止被 ZONE_HIGHMEM 擠壓侵占而為自己預留的物理內(nèi)存頁為:(65536 + 16384) / 256 = 320

  • ZONE_NORMAL 為防止被 ZONE_HIGHMEM 擠壓侵占而為自己預留的物理內(nèi)存頁為:65536 / 32 = 2048。

各個內(nèi)存區(qū)域為防止被高位內(nèi)存區(qū)域過度擠壓占用,而為自己預留的內(nèi)存大小,我們可以通過前邊 cat /proc/zoneinfo 命令來查看,輸出信息的 protection:則表示各個內(nèi)存區(qū)域預留內(nèi)存大小。

0fd0d7ee-751d-11ed-8abf-dac502259ad0.pngimage.png

此外我們還可以通過 sysctl 對內(nèi)核參數(shù) lowmem_reserve_ratio 進行動態(tài)調(diào)整,這樣內(nèi)核會根據(jù)新的 lowmem_reserve_ratio 動態(tài)重新計算各個內(nèi)存區(qū)域的預留內(nèi)存大小。

前面介紹的物理內(nèi)存區(qū)域內(nèi)被伙伴系統(tǒng)所管理的物理頁數(shù)量 managed_pages 的計算方式就通過 present_pages 減去這些預留的物理內(nèi)存頁 reserved_pages 得到的。

調(diào)整內(nèi)核參數(shù)的多種方法,筆者在《從 Linux 內(nèi)核角度探秘 JDK NIO 文件讀寫本質(zhì)》 一文中的 "13.6 臟頁回寫參數(shù)的相關(guān)配置方式" 小節(jié)中已經(jīng)詳細介紹過了,感興趣的同學可以在回看下。

5.2 物理內(nèi)存區(qū)域中的水位線

內(nèi)存資源是系統(tǒng)中最寶貴的系統(tǒng)資源,是有限的。當內(nèi)存資源緊張的時候,系統(tǒng)的應對方法無非就是三種:

  1. 產(chǎn)生 OOM,內(nèi)核直接將系統(tǒng)中占用大量內(nèi)存的進程,將 OOM 優(yōu)先級最高的進程干掉,釋放出這個進程占用的內(nèi)存供其他更需要的進程分配使用。

  2. 內(nèi)存回收,將不經(jīng)常使用到的內(nèi)存回收,騰挪出來的內(nèi)存供更需要的進程分配使用。

  3. 內(nèi)存規(guī)整,將可遷移的物理頁面進行遷移規(guī)整,消除內(nèi)存碎片。從而獲得更大的一片連續(xù)物理內(nèi)存空間供進程分配。

我們都知道,內(nèi)核將物理內(nèi)存劃分成一頁一頁的單位進行管理(每頁 4K 大?。?。內(nèi)存回收的單位也是按頁來的。在內(nèi)核中,物理內(nèi)存頁有兩種類型,針對這兩種類型的物理內(nèi)存頁,內(nèi)核會有不同的回收機制。

第一種就是文件頁,所謂文件頁就是其物理內(nèi)存頁中的數(shù)據(jù)來自于磁盤中的文件,當我們進行文件讀取的時候,內(nèi)核會根據(jù)局部性原理將讀取的磁盤數(shù)據(jù)緩存在 page cache 中,page cache 里存放的就是文件頁。當進程再次讀取讀文件頁中的數(shù)據(jù)時,內(nèi)核直接會從 page cache 中獲取并拷貝給進程,省去了讀取磁盤的開銷。

對于文件頁的回收通常會比較簡單,因為文件頁中的數(shù)據(jù)來自于磁盤,所以當回收文件頁的時候直接回收就可以了,當進程再次讀取文件頁時,大不了再從磁盤中重新讀取就是了。

但是當進程已經(jīng)對文件頁進行修改過但還沒來得及同步回磁盤,此時文件頁就是臟頁,不能直接進行回收,需要先將臟頁回寫到磁盤中才能進行回收。

我們可以在進程中通過 fsync() 系統(tǒng)調(diào)用將指定文件的所有臟頁同步回寫到磁盤,同時內(nèi)核也會根據(jù)一定的條件喚醒專門用于回寫臟頁的 pflush 內(nèi)核線程。

關(guān)于文件頁相關(guān)的詳細內(nèi)容,感興趣的同學可以回看下筆者的這篇文章 《從 Linux 內(nèi)核角度探秘 JDK NIO 文件讀寫本質(zhì)》 。

而另外一種物理頁類型是匿名頁,所謂匿名頁就是它背后并沒有一個磁盤中的文件作為數(shù)據(jù)來源,匿名頁中的數(shù)據(jù)都是通過進程運行過程中產(chǎn)生的,比如我們應用程序中動態(tài)分配的堆內(nèi)存。

當內(nèi)存資源緊張需要對不經(jīng)常使用的那些匿名頁進行回收時,因為匿名頁的背后沒有一個磁盤中的文件做依托,所以匿名頁不能像文件頁那樣直接回收,無論匿名頁是不是臟頁,都需要先將匿名頁中的數(shù)據(jù)先保存在磁盤空間中,然后在對匿名頁進行回收。

并把釋放出來的這部分內(nèi)存分配給更需要的進程使用,當進程再次訪問這塊內(nèi)存時,在重新把之前匿名頁中的數(shù)據(jù)從磁盤空間中讀取到內(nèi)存就可以了,而這塊磁盤空間可以是單獨的一片磁盤分區(qū)(Swap 分區(qū))或者是一個特殊的文件(Swap 文件)。匿名頁的回收機制就是我們經(jīng)??吹降?Swap 機制。

所謂的頁面換出就是在 Swap 機制下,當內(nèi)存資源緊張時,內(nèi)核就會把不經(jīng)常使用的這些匿名頁中的數(shù)據(jù)寫入到 Swap 分區(qū)或者 Swap 文件中。從而釋放這些數(shù)據(jù)所占用的內(nèi)存空間。

所謂的頁面換入就是當進程再次訪問那些被換出的數(shù)據(jù)時,內(nèi)核會重新將這些數(shù)據(jù)從 Swap 分區(qū)或者 Swap 文件中讀取到內(nèi)存中來。

綜上所述,物理內(nèi)存區(qū)域中的內(nèi)存回收分為文件頁回收(通過 pflush 內(nèi)核線程)和匿名頁回收(通過 kswapd 內(nèi)核進程)。Swap 機制主要針對的是匿名頁回收。

那么當內(nèi)存緊張的時候,內(nèi)核到底是該回收文件頁呢?還是該回收匿名頁呢?

事實上 Linux 提供了一個 swappiness 的內(nèi)核選項,我們可以通過 cat /proc/sys/vm/swappiness 命令查看,swappiness 選項的取值范圍為 0 到 100,默認為 60。

swappiness 用于表示 Swap 機制的積極程度,數(shù)值越大,Swap 的積極程度越高,內(nèi)核越傾向于回收匿名頁。數(shù)值越小,Swap 的積極程度越低。內(nèi)核就越傾向于回收文件頁。

注意: swappiness 只是表示 Swap 積極的程度,當內(nèi)存非常緊張的時候,即使將 swappiness 設(shè)置為 0 ,也還是會發(fā)生 Swap 的。

那么到底什么時候內(nèi)存才算是緊張的?緊張到什么程度才開始 Swap 呢?這一切都需要一個量化的標準,于是就有了本小節(jié)的主題 —— 物理內(nèi)存區(qū)域中的水位線。

內(nèi)核會為每個 NUMA 節(jié)點中的每個物理內(nèi)存區(qū)域定制三條用于指示內(nèi)存容量的水位線,分別是:WMARK_MIN(頁最小閾值), WMARK_LOW (頁低閾值),WMARK_HIGH(頁高閾值)。

0f61f536-751d-11ed-8abf-dac502259ad0.pngimage.png

這三條水位線定義在 /include/linux/mmzone.h 文件中:

enumzone_watermarks{
WMARK_MIN,
WMARK_LOW,
WMARK_HIGH,
NR_WMARK
};

#definemin_wmark_pages(z)(z->_watermark[WMARK_MIN]+z->watermark_boost)
#definelow_wmark_pages(z)(z->_watermark[WMARK_LOW]+z->watermark_boost)
#definehigh_wmark_pages(z)(z->_watermark[WMARK_HIGH]+z->watermark_boost)

這三條水位線對應的 watermark 數(shù)值存儲在每個物理內(nèi)存區(qū)域 struct zone 結(jié)構(gòu)中的 _watermark[NR_WMARK] 數(shù)組中。

structzone{
//物理內(nèi)存區(qū)域中的水位線
unsignedlong_watermark[NR_WMARK];
//優(yōu)化內(nèi)存碎片對內(nèi)存分配的影響,可以動態(tài)改變內(nèi)存區(qū)域的基準水位線。
unsignedlongwatermark_boost;

}____cacheline_internodealigned_in_smp;

注意:下面提到的物理內(nèi)存區(qū)域的剩余內(nèi)存是需要刨去上小節(jié)介紹的 lowmem_reserve 預留內(nèi)存大小。

10096f0a-751d-11ed-8abf-dac502259ad0.pngimage.png
  • 當該物理內(nèi)存區(qū)域的剩余內(nèi)存容量高于 _watermark[WMARK_HIGH] 時,說明此時該物理內(nèi)存區(qū)域中的內(nèi)存容量非常充足,內(nèi)存分配完全沒有壓力。

  • 當剩余內(nèi)存容量在 _watermark[WMARK_LOW] 與_watermark[WMARK_HIGH] 之間時,說明此時內(nèi)存有一定的消耗但是還可以接受,能夠繼續(xù)滿足進程的內(nèi)存分配需求。

  • 當剩余內(nèi)容容量在 _watermark[WMARK_MIN] 與 _watermark[WMARK_LOW] 之間時,說明此時內(nèi)存容量已經(jīng)有點危險了,內(nèi)存分配面臨一定的壓力,但是還可以滿足進程的內(nèi)存分配要求,當給進程分配完內(nèi)存之后,就會喚醒 kswapd 進程開始內(nèi)存回收,直到剩余內(nèi)存高于 _watermark[WMARK_HIGH] 為止。

在這種情況下,進程的內(nèi)存分配會觸發(fā)內(nèi)存回收,但請求進程本身不會被阻塞,由內(nèi)核的 kswapd 進程異步回收內(nèi)存。

  • 當剩余內(nèi)容容量低于 _watermark[WMARK_MIN] 時,說明此時的內(nèi)容容量已經(jīng)非常危險了,如果進程在這時請求內(nèi)存分配,內(nèi)核就會進行直接內(nèi)存回收,這時請求進程會同步阻塞等待,直到內(nèi)存回收完畢。

位于 _watermark[WMARK_MIN] 以下的內(nèi)存容量是預留給內(nèi)核在緊急情況下使用的,這部分內(nèi)存就是我們在 《5.1 物理內(nèi)存區(qū)域中的預留內(nèi)存》小節(jié)中介紹的預留內(nèi)存 nr_reserved_highatomic。

我們可以通過 cat /proc/zoneinfo 命令來查看不同 NUMA 節(jié)點中不同內(nèi)存區(qū)域中的水位線:

104a4232-751d-11ed-8abf-dac502259ad0.pngimage.png

其中大部分字段的含義筆者已經(jīng)在前面的章節(jié)中為大家介紹過了,下面我們只介紹和本小節(jié)內(nèi)容相關(guān)的字段含義:

  • free 就是該物理內(nèi)存區(qū)域內(nèi)剩余的內(nèi)存頁數(shù),它的值和后面的 nr_free_pages 相同。

  • min、low、high 就是上面提到的三條內(nèi)存水位線:_watermark[WMARK_MIN],_watermark[WMARK_LOW] ,_watermark[WMARK_HIGH]。

  • nr_zone_active_anon 和 nr_zone_inactive_anon 分別是該內(nèi)存區(qū)域內(nèi)活躍和非活躍的匿名頁數(shù)量。

  • nr_zone_active_file 和 nr_zone_inactive_file 分別是該內(nèi)存區(qū)域內(nèi)活躍和非活躍的文件頁數(shù)量。

5.3 水位線的計算

在上小節(jié)中我們介紹了內(nèi)核通過對物理內(nèi)存區(qū)域設(shè)置內(nèi)存水位線來決定內(nèi)存回收的時機,那么這三條內(nèi)存水位線的值具體是多少,內(nèi)核中是根據(jù)什么計算出來的呢?

事實上 WMARK_MIN,WMARK_LOW ,WMARK_HIGH 這三個水位線的數(shù)值是通過內(nèi)核參數(shù) /proc/sys/vm/min_free_kbytes 為基準分別計算出來的,用戶也可以通過 sysctl 來動態(tài)設(shè)置這個內(nèi)核參數(shù)。

內(nèi)核參數(shù) min_free_kbytes 的單位為 KB 。

10566346-751d-11ed-8abf-dac502259ad0.pngimage.png

通常情況下 WMARK_LOW 的值是 WMARK_MIN 的 1.25 倍,WMARK_HIGH 的值是 WMARK_LOW 的 1.5 倍。而 WMARK_MIN 的數(shù)值就是由這個內(nèi)核參數(shù) min_free_kbytes 來決定的。

下面我們就來看下內(nèi)核中關(guān)于 min_free_kbytes 的計算方式:

5.4 min_free_kbytes 的計算邏輯

以下計算邏輯是針對 64 位系統(tǒng)中內(nèi)存區(qū)域水位線的計算,在 64 位系統(tǒng)中沒有高端內(nèi)存 ZONE_HIGHMEM 區(qū)域。

min_free_kbytes 的計算邏輯定義在內(nèi)核文件 /mm/page_alloc.cinit_per_zone_wmark_min 方法中,用于計算最小水位線 WMARK_MIN 的數(shù)值也就是這里的 min_free_kbytes (單位為 KB)。 水位線的單位是物理內(nèi)存頁的數(shù)量。

int__meminitinit_per_zone_wmark_min(void)
{
//低位內(nèi)存區(qū)域(除高端內(nèi)存之外)的總和
unsignedlonglowmem_kbytes;
//待計算的min_free_kbytes
intnew_min_free_kbytes;

//將低位內(nèi)存區(qū)域內(nèi)存容量總的頁數(shù)轉(zhuǎn)換為KB
lowmem_kbytes=nr_free_buffer_pages()*(PAGE_SIZE>>10);
//min_free_kbytes計算邏輯:對lowmem_kbytes*16進行開平方
new_min_free_kbytes=int_sqrt(lowmem_kbytes*16);
//min_free_kbytes的范圍為128到65536KB之間
if(new_min_free_kbytes>user_min_free_kbytes){
min_free_kbytes=new_min_free_kbytes;
if(min_free_kbytes128)
min_free_kbytes=128;
if(min_free_kbytes>65536)
min_free_kbytes=65536;
}else{
pr_warn("min_free_kbytesisnotupdatedto%dbecauseuserdefinedvalue%dispreferred
",
new_min_free_kbytes,user_min_free_kbytes);
}
//計算內(nèi)存區(qū)域內(nèi)的三條水位線
setup_per_zone_wmarks();
//計算內(nèi)存區(qū)域的預留內(nèi)存大小,防止被高位內(nèi)存區(qū)域過度擠壓占用
setup_per_zone_lowmem_reserve();
.............省略................
return0;
}
core_initcall(init_per_zone_wmark_min)

首先我們需要先計算出當前 NUMA 節(jié)點中所有低位內(nèi)存區(qū)域(除高端內(nèi)存之外)中內(nèi)存總?cè)萘恐?。也即是說 lowmem_kbytes 的值為: ZONE_DMA 區(qū)域中 managed_pages + ZONE_DMA32 區(qū)域中 managed_pages + ZONE_NORMAL 區(qū)域中 managed_pages 。

lowmem_kbytes 的計算邏輯在 nr_free_zone_pages 方法中:

/**
*nr_free_zone_pages-countnumberofpagesbeyondhighwatermark
*@offset:Thezoneindexofthehighestzone
*
*nr_free_zone_pages()countsthenumberofcountspageswhicharebeyondthe
*highwatermarkwithinallzonesatorbelowagivenzoneindex.Foreach
*zone,thenumberofpagesiscalculatedas:
*managed_pages-high_pages
*/
staticunsignedlongnr_free_zone_pages(intoffset)
{
structzoneref*z;
structzone*zone;

unsignedlongsum=0;
//獲取當前NUMA節(jié)點中的所有物理內(nèi)存區(qū)域zone
structzonelist*zonelist=node_zonelist(numa_node_id(),GFP_KERNEL);
//計算所有物理內(nèi)存區(qū)域內(nèi)managed_pages-high_pages的總和
for_each_zone_zonelist(zone,z,zonelist,offset){
unsignedlongsize=zone->managed_pages;
unsignedlonghigh=high_wmark_pages(zone);
if(size>high)
sum+=size-high;
}
//lowmem_kbytes的值
returnsum;
}

nr_free_zone_pages 方法上面的注釋大家可能看的有點蒙,這里需要為大家解釋一下,nr_free_zone_pages 方法的計算邏輯本意是給定一個 zone index (方法參數(shù) offset),計算范圍為:這個給定 zone 下面的所有低位內(nèi)存區(qū)域。

nr_free_zone_pages 方法會計算這些低位內(nèi)存區(qū)域內(nèi)在 high watermark 水位線之上的內(nèi)存容量( managed_pages - high_pages )之和。作為該方法的返回值。

但此時我們正準備計算這些水位線,水位線還沒有值,所以此時這個方法的語義就是計算低位內(nèi)存區(qū)域內(nèi)被伙伴系統(tǒng)所管理的內(nèi)存容量( managed_pages )之和。也就是我們想要的 lowmem_kbytes。

接下來在 init_per_zone_wmark_min 方法中會對 lowmem_kbytes * 16 進行開平方得到 new_min_free_kbytes。

1073a320-751d-11ed-8abf-dac502259ad0.pngimage.png

如果計算出的 new_min_free_kbytes 大于用戶設(shè)置的內(nèi)核參數(shù)值 /proc/sys/vm/min_free_kbytes ,那么最終 min_free_kbytes 就是 new_min_free_kbytes。如果小于用戶設(shè)定的值,那么就采用用戶指定的 min_free_kbytes 。

min_free_kbytes 的取值范圍限定在 128 到 65536 KB 之間。

隨后內(nèi)核會根據(jù)這個 min_free_kbytes 在 setup_per_zone_wmarks() 方法中計算出該物理內(nèi)存區(qū)域的三條水位線。

最后在 setup_per_zone_lowmem_reserve() 方法中計算內(nèi)存區(qū)域的預留內(nèi)存大小,防止被高位內(nèi)存區(qū)域過度擠壓占用。該方法的邏輯就是我們在《5.1 物理內(nèi)存區(qū)域中的預留內(nèi)存》小節(jié)中提到的內(nèi)容。

5.5 setup_per_zone_wmarks 計算水位線

這里我們依然不會考慮高端內(nèi)存區(qū)域 ZONE_HIGHMEM。

物理內(nèi)存區(qū)域內(nèi)的三條水位線:WMARK_MIN,WMARK_LOW,WMARK_HIGH 的最終計算邏輯是在 __setup_per_zone_wmarks 方法中完成的:

staticvoid__setup_per_zone_wmarks(void)
{
//將min_free_kbytes轉(zhuǎn)換為頁
unsignedlongpages_min=min_free_kbytes>>(PAGE_SHIFT-10);
//所有低位內(nèi)存區(qū)域managed_pages之和
unsignedlonglowmem_pages=0;
structzone*zone;
unsignedlongflags;

/*Calculatetotalnumberof!ZONE_HIGHMEMpages*/
for_each_zone(zone){
if(!is_highmem(zone))
lowmem_pages+=zone->managed_pages;
}

//循環(huán)計算各個內(nèi)存區(qū)域中的水位線
for_each_zone(zone){
u64tmp;
tmp=(u64)pages_min*zone->managed_pages;
//計算WMARK_MIN水位線的核心方法
do_div(tmp,lowmem_pages);
if(is_highmem(zone)){
...........省略高端內(nèi)存區(qū)域............
}else{
//WMARK_MIN水位線
zone->watermark[WMARK_MIN]=tmp;
}
//這里可暫時忽略
tmp=max_t(u64,tmp>>2,
mult_frac(zone->managed_pages,
watermark_scale_factor,10000));

zone->watermark[WMARK_LOW]=min_wmark_pages(zone)+tmp;
zone->watermark[WMARK_HIGH]=min_wmark_pages(zone)+tmp*2;
}
}

在 for_each_zone 循環(huán)內(nèi)依次遍歷 NUMA 節(jié)點中的所有內(nèi)存區(qū)域 zone,計算每個內(nèi)存區(qū)域 zone 里的內(nèi)存水位線。其中計算 WMARK_MIN 水位線的核心邏輯封裝在 do_div 方法中,在 do_div 方法中會先計算每個 zone 內(nèi)存容量之間的比例,然后根據(jù)這個比例去從 min_free_kbytes 中劃分出對應 zone 的 WMARK_MIN 水位線來。

比如:當前 NUMA 節(jié)點中有兩個 zone :ZONE_DMA 和 ZONE_NORMAL,內(nèi)存容量大小分別是:100 M 和 800 M。那么 ZONE_DMA 與 ZONE_NORMAL 之間的比例就是 1 :8。

根據(jù)這個比例,ZONE_DMA 區(qū)域里的 WMARK_MIN 水位線就是:min_free_kbytes * 1 / 8 。ZONE_NORMAL 區(qū)域里的 WMARK_MIN 水位線就是:min_free_kbytes * 7 / 8 。

計算出了 WMARK_MIN 的值,那么接下來 WMARK_LOW, WMARK_HIGH 的值也就好辦了,它們都是基于 WMARK_MIN 計算出來的。

WMARK_LOW 的值是 WMARK_MIN 的 1.25 倍,WMARK_HIGH 的值是 WMARK_LOW 的 1.5 倍。

此外,大家可能對下面這段代碼比較有疑問?

/*
*Setthekswapdwatermarksdistanceaccordingtothe
*scalefactorinproportiontoavailablememory,but
*ensureaminimumsizeonsmallsystems.
*/
tmp=max_t(u64,tmp>>2,
mult_frac(zone->managed_pages,
watermark_scale_factor,10000));

這段代碼主要是通過內(nèi)核參數(shù) watermark_scale_factor 來調(diào)節(jié)水位線:WMARK_MIN,WMARK_LOW,WMARK_HIGH 之間的間距,那么為什么要調(diào)整水位線之間的間距大小呢?

5.6 watermark_scale_factor 調(diào)整水位線的間距

10096f0a-751d-11ed-8abf-dac502259ad0.pngimage.png

為了避免內(nèi)核的直接內(nèi)存回收 direct reclaim 阻塞進程影響系統(tǒng)的性能,所以我們需要盡量保持內(nèi)存區(qū)域中的剩余內(nèi)存容量盡量在 WMARK_MIN 水位線之上,但是有一些極端情況,比如突然遇到網(wǎng)絡(luò)流量增大,需要短時間內(nèi)申請大量的內(nèi)存來存放網(wǎng)絡(luò)請求數(shù)據(jù),此時 kswapd 回收內(nèi)存的速度可能趕不上內(nèi)存分配的速度,從而造成直接內(nèi)存回收 direct reclaim,影響系統(tǒng)性能。

在內(nèi)存分配過程中,剩余內(nèi)存容量處于 WMARK_MIN 與 WMARK_LOW 水位線之間會喚醒 kswapd 進程來回收內(nèi)存,直到內(nèi)存容量恢復到 WMARK_HIGH 水位線之上。

剩余內(nèi)存容量低于 WMARK_MIN 水位線時就會觸發(fā)直接內(nèi)存回收 direct reclaim。

而剩余內(nèi)存容量高于 WMARK_LOW 水位線又不會喚醒 kswapd 進程,因此 kswapd 進程活動的關(guān)鍵范圍在 WMARK_MIN 與 WMARK_LOW 之間,而為了應對這種突發(fā)的網(wǎng)絡(luò)流量暴增,我們需要保證 kswapd 進程活動的范圍大一些,這樣內(nèi)核就能夠時刻進行內(nèi)存回收使得剩余內(nèi)存容量較長時間的保持在 WMARK_HIGH 水位線之上。

這樣一來就要求 WMARK_MIN 與 WMARK_LOW 水位線之間的間距不能太小,因為 WMARK_LOW 水位線之上就不會喚醒 kswapd 進程了。

因此內(nèi)核引入了 /proc/sys/vm/watermark_scale_factor 參數(shù)來調(diào)節(jié)水位線之間的間距。該內(nèi)核參數(shù)默認值為 10,最大值為 3000。

10c8fca8-751d-11ed-8abf-dac502259ad0.pngimage.png

那么如何使用 watermark_scale_factor 參數(shù)調(diào)整水位線之間的間距呢?

水位線間距計算公式:(watermark_scale_factor / 10000) * managed_pages 。

zone->watermark[WMARK_MIN]=tmp;
//水位線間距的計算邏輯
tmp=max_t(u64,tmp>>2,
mult_frac(zone->managed_pages,
watermark_scale_factor,10000));

zone->watermark[WMARK_LOW]=min_wmark_pages(zone)+tmp;
zone->watermark[WMARK_HIGH]=min_wmark_pages(zone)+tmp*2;

在內(nèi)核中水位線間距計算邏輯是:(WMARK_MIN / 4) 與 (zone_managed_pages * watermark_scale_factor / 10000) 之間較大的那個值。

用戶可以通過 sysctl 來動態(tài)調(diào)整 watermark_scale_factor 參數(shù),內(nèi)核會動態(tài)重新計算水位線之間的間距,使得 WMARK_MIN 與 WMARK_LOW 之間留有足夠的緩沖余地,使得 kswapd 能夠有時間回收足夠的內(nèi)存,從而解決直接內(nèi)存回收導致的性能抖動問題

5.7 物理內(nèi)存區(qū)域中的冷熱頁

之前筆者在《一文聊透對象在JVM中的內(nèi)存布局,以及內(nèi)存對齊和壓縮指針的原理及應用》 一文中為大家介紹 CPU 的高速緩存時曾提到過,根據(jù)摩爾定律:芯片中的晶體管數(shù)量每隔 18 個月就會翻一番。導致 CPU 的性能和處理速度變得越來越快,而提升 CPU 的運行速度比提升內(nèi)存的運行速度要容易和便宜的多,所以就導致了 CPU 與內(nèi)存之間的速度差距越來越大。

CPU 與 內(nèi)存之間的速度差異到底有多大呢? 我們知道寄存器是離 CPU 最近的,CPU 在訪問寄存器的時候速度近乎于 0 個時鐘周期,訪問速度最快,基本沒有時延。而訪問內(nèi)存則需要 50 - 200 個時鐘周期。

所以為了彌補 CPU 與內(nèi)存之間巨大的速度差異,提高CPU的處理效率和吞吐,于是我們引入了 L1 , L2 , L3 高速緩存集成到 CPU 中。CPU 訪問高速緩存僅需要用到 1 - 30 個時鐘周期,CPU 中的高速緩存是對內(nèi)存熱點數(shù)據(jù)的一個緩存。

10d9c7b8-751d-11ed-8abf-dac502259ad0.pngCPU緩存結(jié)構(gòu).png

CPU 訪問高速緩存的速度比訪問內(nèi)存的速度快大約10倍,引入高速緩存的目的在于消除CPU與內(nèi)存之間的速度差距,CPU 用高速緩存來用來存放內(nèi)存中的熱點數(shù)據(jù)。

另外我們根據(jù)程序的時間局部性原理可以知道,內(nèi)存的數(shù)據(jù)一旦被訪問,那么它很有可能在短期內(nèi)被再次訪問,如果我們把經(jīng)常訪問的物理內(nèi)存頁緩存在 CPU 的高速緩存中,那么當進程再次訪問的時候就會直接命中 CPU 的高速緩存,避免了進一步對內(nèi)存的訪問,極大提升了應用程序的性能。

程序局部性原理表現(xiàn)為:時間局部性和空間局部性。時間局部性是指如果程序中的某條指令一旦執(zhí)行,則不久之后該指令可能再次被執(zhí)行;如果某塊數(shù)據(jù)被訪問,則不久之后該數(shù)據(jù)可能再次被訪問??臻g局部性是指一旦程序訪問了某個存儲單元,則不久之后,其附近的存儲單元也將被訪問。

本文我們的主題是 Linux 物理內(nèi)存的管理,那么在 NUMA 內(nèi)存架構(gòu)下,這些 NUMA 節(jié)點中的物理內(nèi)存區(qū)域 zone 管理的這些物理內(nèi)存頁,哪些是在 CPU 的高速緩存中?哪些又不在 CPU 的高速緩存中呢?內(nèi)核如何來管理這些加載進 CPU 高速緩存中的物理內(nèi)存頁呢?

0ec0e3ee-751d-11ed-8abf-dac502259ad0.pngimage.png

本小節(jié)標題中所謂的熱頁就是已經(jīng)加載進 CPU 高速緩存中的物理內(nèi)存頁,所謂的冷頁就是還未加載進 CPU 高速緩存中的物理內(nèi)存頁,冷頁是熱頁的后備選項。

筆者先以內(nèi)核版本 2.6.25 之前的冷熱頁相關(guān)的管理邏輯為大家講解,因為這個版本的邏輯比較直觀,大家更容易理解。在這個基礎(chǔ)之上,筆者會在介紹內(nèi)核 5.0 版本對于冷熱頁管理的邏輯,差別不是很大。

structzone{
structper_cpu_pagesetpageset[NR_CPUS];
}

在 2.6.25 版本之前的內(nèi)核源碼中,物理內(nèi)存區(qū)域 struct zone 包含了一個 struct per_cpu_pageset 類型的數(shù)組 pageset。其中內(nèi)核關(guān)于冷熱頁的管理全部封裝在 struct per_cpu_pageset 結(jié)構(gòu)中。

因為每個 CPU 都有自己獨立的高速緩存,所以每個 CPU 對應一個 per_cpu_pageset 結(jié)構(gòu),pageset 數(shù)組容量 NR_CPUS 是一個可以在編譯期間配置的宏常數(shù),表示內(nèi)核可以支持的最大 CPU個數(shù),注意該值并不是系統(tǒng)實際存在的 CPU 數(shù)量。

在 NUMA 內(nèi)存架構(gòu)下,每個物理內(nèi)存區(qū)域都是屬于一個特定的 NUMA 節(jié)點,NUMA 節(jié)點中包含了一個或者多個 CPU,NUMA 節(jié)點中的每個內(nèi)存區(qū)域會關(guān)聯(lián)到一個特定的 CPU 上,但 struct zone 結(jié)構(gòu)中的 pageset 數(shù)組包含的是系統(tǒng)中所有 CPU 的高速緩存頁。

因為雖然一個內(nèi)存區(qū)域關(guān)聯(lián)到了 NUMA 節(jié)點中的一個特定 CPU 上,但是其他CPU 依然可以訪問該內(nèi)存區(qū)域中的物理內(nèi)存頁,因此其他 CPU 上的高速緩存仍然可以包含該內(nèi)存區(qū)域中的物理內(nèi)存頁。

每個 CPU 都可以訪問系統(tǒng)中的所有物理內(nèi)存頁,盡管訪問速度不同(這在前邊我們介紹 NUMA 架構(gòu)的時候已經(jīng)介紹過),因此特定的物理內(nèi)存區(qū)域 struct zone 不僅要考慮到所屬 NUMA 節(jié)點中相關(guān)的 CPU,還需要照顧到系統(tǒng)中的其他 CPU。

在表示每個 CPU 高速緩存結(jié)構(gòu) struct per_cpu_pageset 中有一個 struct per_cpu_pages 類型的數(shù)組 pcp,容量為 2。 數(shù)組 pcp 索引 0 表示該內(nèi)存區(qū)域加載進 CPU 高速緩存的熱頁集合,索引 1 表示該內(nèi)存區(qū)域中還未加載進 CPU 高速緩存的冷頁集合。

structper_cpu_pageset{
structper_cpu_pagespcp[2];/*0:hot.1:cold*/
}

struct per_cpu_pages 結(jié)構(gòu)則是最終用于管理 CPU 高速緩存中的熱頁,冷頁集合的數(shù)據(jù)結(jié)構(gòu):

structper_cpu_pages{
intcount;/*numberofpagesinthelist*/
inthigh;/*highwatermark,emptyingneeded*/
intbatch;/*chunksizeforbuddyadd/remove*/
structlist_headlist;/*thelistofpages*/
};
  • int count :表示集合中包含的物理頁數(shù)量,如果該結(jié)構(gòu)是熱頁集合,則表示加載進 CPU 高速緩存中的物理頁面?zhèn)€數(shù)。

  • struct list_head list :該 list 是一個雙向鏈表,保存了當前 CPU 的熱頁或者冷頁。

  • int batch:每次批量向 CPU 高速緩存填充或者釋放的物理頁面?zhèn)€數(shù)。

  • int high:如果集合中頁面的數(shù)量 count 值超過了 high 的值,那么表示 list 中的頁面太多了,內(nèi)核會從高速緩存中釋放 batch 個頁面到物理內(nèi)存區(qū)域中的伙伴系統(tǒng)中。

  • int low : 在之前更老的版本中,per_cpu_pages 結(jié)構(gòu)還定義了一個 low 下限值,如果 count 低于 low 的值,那么內(nèi)核會從伙伴系統(tǒng)中申請 batch 個頁面填充至當前 CPU 的高速緩存中。之后的版本中取消了 low ,內(nèi)核對容量過低的頁面集合并沒有顯示的使用水位值 low,當列表中沒有其他成員時,內(nèi)核會重新填充高速緩存。

以上則是內(nèi)核版本 2.6.25 之前管理 CPU 高速緩存冷熱頁的相關(guān)數(shù)據(jù)結(jié)構(gòu),我們看到在 2.6.25 之前,內(nèi)核是使用兩個 per_cpu_pages 結(jié)構(gòu)來分別管理冷頁和熱頁集合的

后來內(nèi)核開發(fā)人員通過測試發(fā)現(xiàn),用兩個列表來管理冷熱頁,并不會比用一個列表集中管理冷熱頁帶來任何的實質(zhì)性好處,因此在內(nèi)核版本 2.6.25 之后,將冷頁和熱頁的管理合并在了一個列表中,熱頁放在列表的頭部,冷頁放在列表的尾部。

在內(nèi)核 5.0 的版本中, struct zone 結(jié)構(gòu)中去掉了原來使用 struct per_cpu_pageset 數(shù),因為 struct per_cpu_pageset 結(jié)構(gòu)中分別管理了冷頁和熱頁。

structzone{
structper_cpu_pages__percpu*per_cpu_pageset;

intpageset_high;
intpageset_batch;

}____cacheline_internodealigned_in_smp;

直接使用 struct per_cpu_pages 結(jié)構(gòu)的鏈表來集中管理系統(tǒng)中所有 CPU 高速緩存冷熱頁。

structper_cpu_pages{
intcount;/*numberofpagesinthelist*/
inthigh;/*highwatermark,emptyingneeded*/
intbatch;/*chunksizeforbuddyadd/remove*/

.............省略............

/*Listsofpages,onepermigratetypestoredonthepcp-lists*/
structlist_headlists[NR_PCP_LISTS];
};

前面我們提到,內(nèi)核為了最大程度的防止內(nèi)存碎片,將物理內(nèi)存頁面按照是否可遷移的特性分為了多種遷移類型:可遷移,可回收,不可遷移。在 struct per_cpu_pages 結(jié)構(gòu)中,每一種遷移類型都會對應一個冷熱頁鏈表。

6. 內(nèi)核如何描述物理內(nèi)存頁

0f61f536-751d-11ed-8abf-dac502259ad0.pngimage.png

經(jīng)過前邊幾個小節(jié)的介紹,我想大家現(xiàn)在應該對 Linux 內(nèi)核整個內(nèi)存管理框架有了一個總體上的認識。

如上圖所示,在 NUMA 架構(gòu)下內(nèi)存被劃分成了一個一個的內(nèi)存節(jié)點(NUMA Node),在每個 NUMA 節(jié)點中,內(nèi)核又根據(jù)節(jié)點內(nèi)物理內(nèi)存的功能用途不同,將 NUMA 節(jié)點內(nèi)的物理內(nèi)存劃分為四個物理內(nèi)存區(qū)域分別是:ZONE_DMA,ZONE_DMA32,ZONE_NORMAL,ZONE_HIGHMEM。其中 ZONE_MOVABLE 區(qū)域是邏輯上的劃分,主要是為了防止內(nèi)存碎片和支持內(nèi)存的熱插拔。

物理內(nèi)存區(qū)域中管理的就是物理內(nèi)存頁( Linux 內(nèi)存管理的最小單位),前面我們介紹的內(nèi)核對物理內(nèi)存的換入,換出,回收,內(nèi)存映射等操作的單位就是頁。內(nèi)核為每一個物理內(nèi)存區(qū)域分配了一個伙伴系統(tǒng),用于管理該物理內(nèi)存區(qū)域下所有物理內(nèi)存頁面的分配和釋放。

Linux 默認支持的物理內(nèi)存頁大小為 4KB,在 64 位體系結(jié)構(gòu)中還可以支持 8KB,有的處理器還可以支持 4MB,支持物理地址擴展 PAE 機制的處理器上還可以支持 2MB。

那么 Linux 為什么會默認采用 4KB 作為標準物理內(nèi)存頁的大小呢 ?

首先關(guān)于物理頁面的大小,Linux 規(guī)定必須是 2 的整數(shù)次冪,因為 2 的整數(shù)次冪可以將一些數(shù)學運算轉(zhuǎn)換為移位操作,比如乘除運算可以通過移位操作來實現(xiàn),這樣效率更高。

那么系統(tǒng)支持 4KB,8KB,2MB,4MB 等大小的物理頁面,它們都是 2 的整數(shù)次冪,為啥偏偏要選 4KB 呢?

因為前面提到,在內(nèi)存緊張的時候,內(nèi)核會將不經(jīng)常使用到的物理頁面進行換入換出等操作,還有在內(nèi)存與文件映射的場景下,都會涉及到與磁盤的交互,數(shù)據(jù)在磁盤中組織形式也是根據(jù)一個磁盤塊一個磁盤塊來管理的,4kB 和 4MB 都是磁盤塊大小的整數(shù)倍,但在大多數(shù)情況下,內(nèi)存與磁盤之間傳輸小塊數(shù)據(jù)時會更加的高效,所以綜上所述內(nèi)核會采用 4KB 作為默認物理內(nèi)存頁大小。


假設(shè)我們有 4G 大小的物理內(nèi)存,每個物理內(nèi)存頁大小為 4K,那么這 4G 的物理內(nèi)存會被內(nèi)核劃分為 1M 個物理內(nèi)存頁,內(nèi)核使用一個 struct page 的結(jié)構(gòu)體來描述物理內(nèi)存頁,而每個 struct page 結(jié)構(gòu)體占用內(nèi)存大小為 40 字節(jié),那么內(nèi)核就需要用額外的 40 * 1M = 40M 的內(nèi)存大小來描述物理內(nèi)存頁。

對于 4G 物理內(nèi)存而言,這額外的 40M 內(nèi)存占比相對較小,這個代價勉強可以接受,但是對內(nèi)存錙銖必較的內(nèi)核來說,還是會盡最大努力想盡一切辦法來控制 struct page 結(jié)構(gòu)體的大小。

因為對于 4G 的物理內(nèi)存來說,內(nèi)核就需要使用 1M 個物理頁面來管理,1M 個物理頁的數(shù)量已經(jīng)是非常龐大的了,因此在后續(xù)的內(nèi)核迭代中,對于 struct page 結(jié)構(gòu)的任何微小改動,都可能導致用于管理物理內(nèi)存頁的 struct page 實例所需要的內(nèi)存暴漲。

回想一下我們經(jīng)歷過的很多復雜業(yè)務系統(tǒng),由于業(yè)務邏輯已經(jīng)非常復雜,在加上業(yè)務版本日積月累的迭代,整個業(yè)務系統(tǒng)已經(jīng)變得異常復雜,在這種類型的業(yè)務系統(tǒng)中,我們經(jīng)常會使用一個非常龐大的類來包裝全量的業(yè)務響應信息用以應對各種復雜的場景,但是這個類已經(jīng)包含了太多太多的業(yè)務字段了,而且這些業(yè)務字段在有的場景中會用到,在有的場景中又不會用到,后面還可能繼續(xù)臨時增加很多字段。系統(tǒng)的維護就這樣變得越來越困難。

相比上面業(yè)務系統(tǒng)開發(fā)中隨意地增加改動類中的字段,在內(nèi)核中肯定是不會允許這樣的行為發(fā)生的。struct page 結(jié)構(gòu)是內(nèi)核中訪問最為頻繁的一個結(jié)構(gòu)體,就好比是 Linux 世界里最繁華的地段,在這個最繁華的地段租間房子,那租金可謂是相當?shù)母撸瑯拥牡览?,?nèi)核在 struct page 結(jié)構(gòu)體中增加一個字段的代價也是非常之大,該結(jié)構(gòu)體中每個字段中的每個比特,內(nèi)核用的都是淋漓盡致。

但是 struct page 結(jié)構(gòu)同樣會面臨很多復雜的場景,結(jié)構(gòu)體中的某些字段在某些場景下有用,而在另外的場景下卻沒有用,而內(nèi)核又不可能像業(yè)務系統(tǒng)開發(fā)那樣隨意地為 struct page 結(jié)構(gòu)增加字段,那么內(nèi)核該如何應對這種情況呢?

下面我們即將會看到 struct page 結(jié)構(gòu)體里包含了大量的 union 結(jié)構(gòu),而 union 結(jié)構(gòu)在 C 語言中被用于同一塊內(nèi)存根據(jù)不同場景保存不同類型數(shù)據(jù)的一種方式。內(nèi)核之所以在 struct page 結(jié)構(gòu)中使用 union,是因為一個物理內(nèi)存頁面在內(nèi)核中的使用場景和使用方式是多種多樣的。在這多種場景下,利用 union 盡最大可能使 struct page 的內(nèi)存占用保持在一個較低的水平。

struct page 結(jié)構(gòu)可謂是內(nèi)核中最為繁雜的一個結(jié)構(gòu)體,應用在內(nèi)核中的各種功能場景下,在本小節(jié)中一一解釋清楚各個字段的含義是不現(xiàn)實的,下面筆者只會列舉 struct page 中最為常用的幾個字段,剩下的字段筆者會在后續(xù)相關(guān)文章中專門介紹。

structpage{
//存儲page的定位信息以及相關(guān)標志位
unsignedlongflags;

union{
struct{/*Pagecacheandanonymouspages*/
//用來指向物理頁page被放置在了哪個lru鏈表上
structlist_headlru;
//如果page為文件頁的話,低位為0,指向page所在的pagecache
//如果page為匿名頁的話,低位為1,指向其對應虛擬地址空間的匿名映射區(qū)anon_vma
structaddress_space*mapping;
//如果page為文件頁的話,index為page在pagecache中的索引
//如果page為匿名頁的話,表示匿名頁在對應進程虛擬內(nèi)存區(qū)域VMA中的偏移
pgoff_tindex;
//在不同場景下,private指向的場景信息不同
unsignedlongprivate;
};

struct{/*slab,slobandslub*/
union{
//用于指定當前page位于slab中的哪個具體管理鏈表上。
structlist_headslab_list;
struct{
//當page位于slab結(jié)構(gòu)中的某個管理鏈表上時,next指針用于指向鏈表中的下一個page
structpage*next;
#ifdefCONFIG_64BIT
//表示slab中總共擁有的page個數(shù)
intpages;
//表示slab中擁有的特定類型的對象個數(shù)
intpobjects;
#else
shortintpages;
shortintpobjects;
#endif
};
};
//用于指向當前page所屬的slab管理結(jié)構(gòu)
structkmem_cache*slab_cache;

//指向page中的第一個未分配出去的空閑對象
void*freelist;
union{
//指向page中的第一個對象
void*s_mem;
struct{/*SLUB*/
//表示slab中已經(jīng)被分配出去的對象個數(shù)
unsignedinuse:16;
//slab中所有的對象個數(shù)
unsignedobjects:15;
//當前內(nèi)存頁page被slab放置在CPU本地緩存列表中,frozen=1,否則frozen=0
unsignedfrozen:1;
};
};
};
struct{/*復合頁compoundpage相關(guān)*/
//復合頁的尾頁指向首頁
unsignedlongcompound_head;
//用于釋放復合頁的析構(gòu)函數(shù),保存在首頁中
unsignedcharcompound_dtor;
//該復合頁有多少個page組成
unsignedcharcompound_order;
//該復合頁被多少個進程使用,內(nèi)存頁反向映射的概念,首頁中保存
atomic_tcompound_mapcount;
};

//表示slab中需要釋放回收的對象鏈表
structrcu_headrcu_head;
};

union{/*Thisunionis4bytesinsize.*/
//表示該page映射了多少個進程的虛擬內(nèi)存空間,一個page可以被多個進程映射
atomic_t_mapcount;

};

//內(nèi)核中引用該物理頁的次數(shù),表示該物理頁的活躍程度。
atomic_t_refcount;

#ifdefined(WANT_PAGE_VIRTUAL)
void*virtual;//內(nèi)存頁對應的虛擬內(nèi)存地址
#endif/*WANT_PAGE_VIRTUAL*/

}_struct_page_alignment;

下面筆者就來為大家介紹下 struct page 結(jié)構(gòu)在不同場景下的使用方式:

第一種使用方式是內(nèi)核直接分配使用一整頁的物理內(nèi)存,在《5.2 物理內(nèi)存區(qū)域中的水位線》小節(jié)中我們提到,內(nèi)核中的物理內(nèi)存頁有兩種類型,分別用于不同的場景:

  1. 一種是匿名頁,匿名頁背后并沒有一個磁盤中的文件作為數(shù)據(jù)來源,匿名頁中的數(shù)據(jù)都是通過進程運行過程中產(chǎn)生的,匿名頁直接和進程虛擬地址空間建立映射供進程使用。

  2. 另外一種是文件頁,文件頁中的數(shù)據(jù)來自于磁盤中的文件,文件頁需要先關(guān)聯(lián)一個磁盤中的文件,然后再和進程虛擬地址空間建立映射供進程使用,使得進程可以通過操作虛擬內(nèi)存實現(xiàn)對文件的操作,這就是我們常說的內(nèi)存文件映射。

structpage{
//如果page為文件頁的話,低位為0,指向page所在的pagecache
//如果page為匿名頁的話,低位為1,指向其對應虛擬地址空間的匿名映射區(qū)anon_vma
structaddress_space*mapping;
//如果page為文件頁的話,index為page在pagecache中的索引
//如果page為匿名頁的話,表示匿名頁在對應進程虛擬內(nèi)存區(qū)域VMA中的偏移
pgoff_tindex;
}

我們首先來介紹下 struct page 結(jié)構(gòu)中的 struct address_space *mapping 字段。提到 struct address_space 結(jié)構(gòu),如果大家之前看過筆者 《從 Linux 內(nèi)核角度探秘 JDK NIO 文件讀寫本質(zhì)》 這篇文章的話,一定不會對 struct address_space 感到陌生。

11356fc8-751d-11ed-8abf-dac502259ad0.pngimage.png

在內(nèi)核中每個文件都會有一個屬于自己的 page cache(頁高速緩存),頁高速緩存在內(nèi)核中的結(jié)構(gòu)體就是這個 struct address_space。它被文件的 inode 所持有。

如果當前物理內(nèi)存頁 struct page 是一個文件頁的話,那么 mapping 指針的最低位會被設(shè)置為 0 ,指向該內(nèi)存頁關(guān)聯(lián)文件的 struct address_space(頁高速緩存),pgoff_t index 字段表示該內(nèi)存頁 page 在頁高速緩存 page cache 中的 index 索引。內(nèi)核會利用這個 index 字段從 page cache 中查找該物理內(nèi)存頁,

1246107a-751d-11ed-8abf-dac502259ad0.pngimage.png

同時該 pgoff_t index 字段也表示該內(nèi)存頁中的文件數(shù)據(jù)在文件內(nèi)部的偏移 offset。偏移單位為 page size。

對相關(guān)查找細節(jié)感興趣的同學可以在回看下筆者 《從 Linux 內(nèi)核角度探秘 JDK NIO 文件讀寫本質(zhì)》 文章中的《8. page cache 中查找緩存頁》小節(jié)。

如果當前物理內(nèi)存頁 struct page 是一個匿名頁的話,那么 mapping 指針的最低位會被設(shè)置為 1 , 指向該匿名頁在進程虛擬內(nèi)存空間中的匿名映射區(qū)域 struct anon_vma 結(jié)構(gòu)(每個匿名頁對應唯一的 anon_vma 結(jié)構(gòu)),用于物理內(nèi)存到虛擬內(nèi)存的反向映射。

6.1 匿名頁的反向映射

我們通常所說的內(nèi)存映射是正向映射,即從虛擬內(nèi)存到物理內(nèi)存的映射。而反向映射則是從物理內(nèi)存到虛擬內(nèi)存的映射,用于當某個物理內(nèi)存頁需要進行回收或遷移時,此時需要去找到這個物理頁被映射到了哪些進程的虛擬地址空間中,并斷開它們之間的映射。

在沒有反向映射的機制前,需要去遍歷所有進程的虛擬地址空間中的映射頁表,這個效率顯然是很低下的。有了反向映射機制之后內(nèi)核就可以直接找到該物理內(nèi)存頁到所有進程映射的虛擬地址空間 VMA ,并從 VMA 使用的進程頁表中取消映射,

談到 VMA 大家一定不會感到陌生,VMA 相關(guān)的內(nèi)容筆者在 《深入理解 Linux 虛擬內(nèi)存管理》 這篇文章中詳細的介紹過。

如下圖所示,進程的虛擬內(nèi)存空間在內(nèi)核中使用 struct mm_struct 結(jié)構(gòu)表示,進程的虛擬內(nèi)存空間包含了一段一段的虛擬內(nèi)存區(qū)域 VMA,比如我們經(jīng)常接觸到的堆,棧。內(nèi)核中使用 struct vm_area_struct 結(jié)構(gòu)來描述這些虛擬內(nèi)存區(qū)域。

0a4e01fc-751d-11ed-8abf-dac502259ad0.pngimage.png

這里筆者只列舉出 struct vm_area_struct 結(jié)構(gòu)中與匿名頁反向映射相關(guān)的字段屬性:

structvm_area_struct{

structlist_headanon_vma_chain;
structanon_vma*anon_vma;
}

這里大家可能會感到好奇,既然內(nèi)核中有了 struct vm_area_struct 結(jié)構(gòu)來描述虛擬內(nèi)存區(qū)域,那不管是文件頁也好,還是匿名頁也好,都可以使用 struct vm_area_struct 結(jié)構(gòu)體來進行描述,這里為什么有會出現(xiàn) struct anon_vma 結(jié)構(gòu)和 struct anon_vma_chain 結(jié)構(gòu)?這兩個結(jié)構(gòu)到底是干嘛的?如何利用它倆來完成匿名內(nèi)存頁的反向映射呢?

根據(jù)前幾篇文章的內(nèi)容我們知道,進程利用 fork 系統(tǒng)調(diào)用創(chuàng)建子進程的時候,內(nèi)核會將父進程的虛擬內(nèi)存空間相關(guān)的內(nèi)容拷貝到子進程的虛擬內(nèi)存空間中,此時子進程的虛擬內(nèi)存空間和父進程的虛擬內(nèi)存空間是一模一樣的,其中虛擬內(nèi)存空間中映射的物理內(nèi)存頁也是一樣的,在內(nèi)核中都是同一份,在父進程和子進程之間共享(包括 anon_vma 和 anon_vma_chain)。

當進程在向內(nèi)核申請內(nèi)存的時候,內(nèi)核首先會為進程申請的這塊內(nèi)存創(chuàng)建初始化一段虛擬內(nèi)存區(qū)域 struct vm_area_struct 結(jié)構(gòu),但是并不會為其分配真正的物理內(nèi)存。

當進程開始訪問這段虛擬內(nèi)存時,內(nèi)核會產(chǎn)生缺頁中斷,在缺頁中斷處理函數(shù)中才會去真正的分配物理內(nèi)存(這時才會為子進程創(chuàng)建自己的 anon_vma 和 anon_vma_chain),并建立虛擬內(nèi)存與物理內(nèi)存之間的映射關(guān)系(正向映射)。

staticvm_fault_thandle_pte_fault(structvm_fault*vmf)
{
.............

if(!vmf->pte){
if(vma_is_anonymous(vmf->vma))
//處理匿名頁缺頁
returndo_anonymous_page(vmf);
else
//處理文件頁缺頁
returndo_fault(vmf);
}

.............

if(vmf->flags&(FAULT_FLAG_WRITE|FAULT_FLAG_UNSHARE)){
if(!pte_write(entry))
//子進程缺頁處理
returndo_wp_page(vmf);
}

這里我們主要關(guān)注 do_anonymous_page 函數(shù),正是在這里內(nèi)核完成了 struct anon_vma 結(jié)構(gòu)和 struct anon_vma_chain 結(jié)構(gòu)的創(chuàng)建以及相關(guān)匿名頁反向映射數(shù)據(jù)結(jié)構(gòu)的相互關(guān)聯(lián)。

staticvm_fault_tdo_anonymous_page(structvm_fault*vmf)
{
structvm_area_struct*vma=vmf->vma;
structpage*page;

........省略虛擬內(nèi)存到物理內(nèi)存正向映射相關(guān)邏輯.........

if(unlikely(anon_vma_prepare(vma)))
gotooom;

page=alloc_zeroed_user_highpage_movable(vma,vmf->address);

if(!page)
gotooom;
//建立反向映射關(guān)系
page_add_new_anon_rmap(page,vma,vmf->address);

........省略虛擬內(nèi)存到物理內(nèi)存正向映射相關(guān)邏輯.........
}

在 do_anonymous_page 匿名頁缺頁處理函數(shù)中會為 struct vm_area_struct 結(jié)構(gòu)創(chuàng)建匿名頁相關(guān)的 struct anon_vma 結(jié)構(gòu)和 struct anon_vma_chain 結(jié)構(gòu)。

并在 anon_vma_prepare 函數(shù)中實現(xiàn) anon_vma 和 anon_vma_chain 之間的關(guān)聯(lián) ,隨后調(diào)用 alloc_zeroed_user_highpage_movable 從伙伴系統(tǒng)中獲取物理內(nèi)存頁 struct page,并在 page_add_new_anon_rmap 函數(shù)中完成 struct page 與 anon_vma 的關(guān)聯(lián)(這里正是反向映射關(guān)系建立的關(guān)鍵)

在介紹匿名頁反向映射源碼實現(xiàn)之前,筆者先來為大家介紹一下相關(guān)的兩個重要數(shù)據(jù)結(jié)構(gòu) struct anon_vma 和 struct anon_vma_chain,方便大家理解為何 struct page 與 anon_vma 關(guān)聯(lián)起來就能實現(xiàn)反向映射?

前面我們提到,匿名頁的反向映射關(guān)鍵就是建立物理內(nèi)存頁 struct page 與進程虛擬內(nèi)存空間 VMA 之間的映射關(guān)系。

匿名頁的 struct page 中的 mapping 指針指向的是 struct anon_vma 結(jié)構(gòu)。

structpage{
structaddress_space*mapping;
pgoff_tindex;
}

只要我們實現(xiàn)了 anon_vma 與 vm_area_struct 之間的關(guān)聯(lián),那么 page 到 vm_area_struct 之間的映射就建立起來了,struct anon_vma_chain 結(jié)構(gòu)做的事情就是建立 anon_vma 與 vm_area_struct 之間的關(guān)聯(lián)關(guān)系。

structanon_vma_chain{
//匿名頁關(guān)聯(lián)的進程虛擬內(nèi)存空間(vma屬于一個特定的進程,多個進程多個vma)
structvm_area_struct*vma;
//匿名頁page指向的anon_vma
structanon_vma*anon_vma;
structlist_headsame_vma;
structrb_noderb;
unsignedlongrb_subtree_last;
#ifdefCONFIG_DEBUG_VM_RB
unsignedlongcached_vma_start,cached_vma_last;
#endif
};

struct anon_vma_chain 結(jié)構(gòu)通過其中的 vma 指針和 anon_vma 指針將相關(guān)的匿名頁與其映射的進程虛擬內(nèi)存空間關(guān)聯(lián)了起來。

1264dd98-751d-11ed-8abf-dac502259ad0.pngimage.png

從目前來看匿名頁 struct page 算是與 anon_vma 建立了關(guān)系,又通過 anon_vma_chain 將 anon_vma 與 vm_area_struct 建立了關(guān)系。那么就剩下最后一道關(guān)系需要打通了,就是如何通過 anon_vma 找到 anon_vma_chain 進而找到 vm_area_struct 呢?這就需要我們將 anon_vma 與 anon_vma_chain 之間的關(guān)系也打通。

我們知道每個匿名頁對應唯一的 anon_vma 結(jié)構(gòu),但是一個匿名物理頁可以映射到不同進程的虛擬內(nèi)存空間中,每個進程的虛擬內(nèi)存空間都是獨立的,也就是說不同的進程就會有不同的 VMA。

09d2e60c-751d-11ed-8abf-dac502259ad0.pngimage.png

不同的 VMA 意味著同一個匿名頁 anon_vma 就會對應多個 anon_vma_chain。那么如何通過一個 anon_vma 找到和他關(guān)聯(lián)的所有 anon_vma_chain 呢?找到了這些 anon_vma_chain 也就意味著 struct page 找到了與它關(guān)聯(lián)的所有進程虛擬內(nèi)存空間 VMA。

我們看看能不能從 struct anon_vma 結(jié)構(gòu)中尋找一下線索:

structanon_vma{
structanon_vma*root;/*Rootofthisanon_vmatree*/
structrw_semaphorerwsem;
atomic_trefcount;
unsigneddegree;
structanon_vma*parent;/*Parentofthisanon_vma*/
structrb_rootrb_root;/*Intervaltreeofprivate"related"vmas*/
};

我們重點來看 struct anon_vma 結(jié)構(gòu)中的 rb_root 字段,struct anon_vma 結(jié)構(gòu)中管理了一顆紅黑樹,這顆紅黑樹上管理的全部都是與該 anon_vma 關(guān)聯(lián)的 anon_vma_chain。我們可以通過 struct page 中的 mapping 指針找到 anon_vma,然后遍歷 anon_vma 中的這顆紅黑樹 rb_root ,從而找到與其關(guān)聯(lián)的所有 anon_vma_chain。

structanon_vma_chain{
//匿名頁關(guān)聯(lián)的進程虛擬內(nèi)存空間(vma屬于一個特定的進程,多個進程多個vma)
structvm_area_struct*vma;
//匿名頁page指向的anon_vma
structanon_vma*anon_vma;
//指向vm_area_struct中的anon_vma_chain列表
structlist_headsame_vma;
//anon_vma管理的紅黑樹中該anon_vma_chain對應的紅黑樹節(jié)點
structrb_noderb;
};

struct anon_vma_chain 結(jié)構(gòu)中的 rb 字段表示其在對應 anon_vma 管理的紅黑樹中的節(jié)點。

1282f3dc-751d-11ed-8abf-dac502259ad0.pngimage.png

到目前為止,物理內(nèi)存頁 page 到與其映射的進程虛擬內(nèi)存空間 VMA,這樣一種一對多的映射關(guān)系現(xiàn)在就算建立起來了。

而 vm_area_struct 表示的只是進程虛擬內(nèi)存空間中的一段虛擬內(nèi)存區(qū)域,這塊虛擬內(nèi)存區(qū)域中可能會包含多個匿名頁,所以 VMA 與物理內(nèi)存頁 page 也是有一對多的映射關(guān)系存在。而這個映射關(guān)系在哪里保存呢?

大家注意 struct anon_vma_chain 結(jié)構(gòu)中還有一個列表結(jié)構(gòu) same_vma,從這個名字上我們很容易就能猜到這個列表 same_vma 中存儲的 anon_vma_chain 對應的 VMA 全都是一樣的,而列表元素 anon_vma_chain 中的 anon_vma 卻是不一樣的。內(nèi)核用這樣一個鏈表結(jié)構(gòu) same_vma 存儲了進程相應虛擬內(nèi)存區(qū)域 VMA 中所包含的所有匿名頁。

struct vm_area_struct 結(jié)構(gòu)中的 struct list_head anon_vma_chain 指向的也是這個列表 same_vma。

structvm_area_struct{
//存儲該VMA中所包含的所有匿名頁anon_vma
structlist_headanon_vma_chain;
//用于快速判斷VMA有沒有對應的匿名page
//一個VMA可以包含多個page,但是該區(qū)域內(nèi)的所有page只需要一個anon_vma來反向映射即可。
structanon_vma*anon_vma;
}
12962268-751d-11ed-8abf-dac502259ad0.pngimage.png

現(xiàn)在整個匿名頁到進程虛擬內(nèi)存空間的反向映射鏈路關(guān)系,筆者就為大家梳理清楚了,下面我們接著回到 do_anonymous_page 函數(shù)中,來一一驗證上述映射邏輯:

staticvm_fault_tdo_anonymous_page(structvm_fault*vmf)
{
structvm_area_struct*vma=vmf->vma;
structpage*page;

........省略虛擬內(nèi)存到物理內(nèi)存正向映射相關(guān)邏輯.........

if(unlikely(anon_vma_prepare(vma)))
gotooom;

page=alloc_zeroed_user_highpage_movable(vma,vmf->address);

if(!page)
gotooom;

page_add_new_anon_rmap(page,vma,vmf->address);

........省略虛擬內(nèi)存到物理內(nèi)存正向映射相關(guān)邏輯.........
}

在 do_anonymous_page 中首先會調(diào)用 anon_vma_prepare 方法來為匿名頁創(chuàng)建 anon_vma 實例和 anon_vma_chain 實例,并建立它們之間的關(guān)聯(lián)關(guān)系。

int__anon_vma_prepare(structvm_area_struct*vma)
{
//獲取進程虛擬內(nèi)存空間
structmm_struct*mm=vma->vm_mm;
//準備為匿名頁分配anon_vma以及anon_vma_chain
structanon_vma*anon_vma,*allocated;
structanon_vma_chain*avc;
//分配anon_vma_chain實例
avc=anon_vma_chain_alloc(GFP_KERNEL);
if(!avc)
gotoout_enomem;
//在相鄰的虛擬內(nèi)存區(qū)域VMA中查找可復用的anon_vma
anon_vma=find_mergeable_anon_vma(vma);
allocated=NULL;
if(!anon_vma){
//沒有可復用的anon_vma則創(chuàng)建一個新的實例
anon_vma=anon_vma_alloc();
if(unlikely(!anon_vma))
gotoout_enomem_free_avc;
allocated=anon_vma;
}

anon_vma_lock_write(anon_vma);
/*page_table_locktoprotectagainstthreads*/
spin_lock(&mm->page_table_lock);
if(likely(!vma->anon_vma)){
//VMA中的anon_vma屬性就是在這里賦值的
vma->anon_vma=anon_vma;
//建立反向映射關(guān)聯(lián)
anon_vma_chain_link(vma,avc,anon_vma);
/*vmareferenceorself-parentlinkfornewroot*/
anon_vma->degree++;
allocated=NULL;
avc=NULL;
}
.................
}

anon_vma_prepare 方法中調(diào)用 anon_vma_chain_link 方法來建立 anon_vma,anon_vma_chain,vm_area_struct 三者之間的關(guān)聯(lián)關(guān)系:

staticvoidanon_vma_chain_link(structvm_area_struct*vma,
structanon_vma_chain*avc,
structanon_vma*anon_vma)
{
//通過anon_vma_chain關(guān)聯(lián)anon_vma和對應的vm_area_struct
avc->vma=vma;
avc->anon_vma=anon_vma;
//將vm_area_struct中的anon_vma_chain鏈表加入到anon_vma_chain中的same_vma鏈表中
list_add(&avc->same_vma,&vma->anon_vma_chain);
//將初始化好的anon_vma_chain加入到anon_vma管理的紅黑樹rb_root中
anon_vma_interval_tree_insert(avc,&anon_vma->rb_root);
}
12962268-751d-11ed-8abf-dac502259ad0.pngimage.png

到現(xiàn)在為止還缺關(guān)鍵的最后一步,就是打通匿名內(nèi)存頁 page 到 vm_area_struct 之間的關(guān)系,首先我們就需要調(diào)用 alloc_zeroed_user_highpage_movable 方法從伙伴系統(tǒng)中申請一個匿名頁。當獲取到 page 實例之后,通過 page_add_new_anon_rmap 最終建立起 page 到 vm_area_struct 的整條反向映射鏈路。

staticvoid__page_set_anon_rmap(structpage*page,
structvm_area_struct*vma,unsignedlongaddress,intexclusive)
{
structanon_vma*anon_vma=vma->anon_vma;
.........省略..............
//低位置1
anon_vma=(void*)anon_vma+PAGE_MAPPING_ANON;
//轉(zhuǎn)換為address_space指針賦值給page結(jié)構(gòu)中的mapping字段
page->mapping=(structaddress_space*)anon_vma;
//page結(jié)構(gòu)中的index表示該匿名頁在虛擬內(nèi)存區(qū)域vma中的偏移
page->index=linear_page_index(vma,address);
}

現(xiàn)在讓我們再次回到本小節(jié) 《6.1 匿名頁的反向映射》的開始,再來看這段話,是不是感到非常清晰了呢~~

如果當前物理內(nèi)存頁 struct page 是一個匿名頁的話,那么 mapping 指針的最低位會被設(shè)置為 1 , 指向該匿名頁在進程虛擬內(nèi)存空間中的匿名映射區(qū)域 struct anon_vma 結(jié)構(gòu)(每個匿名頁對應唯一的 anon_vma 結(jié)構(gòu)),用于物理內(nèi)存到虛擬內(nèi)存的反向映射。

如果當前物理內(nèi)存頁 struct page 是一個文件頁的話,那么 mapping 指針的最低位會被設(shè)置為 0 ,指向該內(nèi)存頁關(guān)聯(lián)文件的 struct address_space(頁高速緩存)。pgoff_t index 字段表示該內(nèi)存頁 page 在頁高速緩存中的 index 索引,也表示該內(nèi)存頁中的文件數(shù)據(jù)在文件內(nèi)部的偏移 offset。偏移單位為 page size。

struct page 結(jié)構(gòu)中的 struct address_space *mapping 指針的最低位如何置 1 ,又如何置 0 呢?關(guān)鍵在下面這條語句:

structanon_vma*anon_vma=vma->anon_vma;
//低位置1
anon_vma=(void*)anon_vma+PAGE_MAPPING_ANON;

anon_vma 指針加上 PAGE_MAPPING_ANON ,并轉(zhuǎn)換為 address_space 指針,這樣可確保 address_space 指針的低位為 1 表示匿名頁。

address_space 指針在轉(zhuǎn)換為 anon_vma 指針的時候可通過如下語句實現(xiàn):

anon_vma=(structanon_vma*)(mapping-PAGE_MAPPING_ANON)

PAGE_MAPPING_ANON 常量定義在內(nèi)核 /include/linux/page-flags.h 文件中:

#definePAGE_MAPPING_ANON0x1

而對于文件頁來說,page 結(jié)構(gòu)的 mapping 指針最低位本來就是 0 ,因為 address_space 類型的指針實現(xiàn)總是對齊至 sizeof(long),因此在 Linux 支持的所有計算機上,指向 address_space 實例的指針最低位總是為 0 。

內(nèi)核可以通過這個技巧直接檢查 page 結(jié)構(gòu)中的 mapping 指針的最低位來判斷該物理內(nèi)存頁到底是匿名頁還是文件頁。

前面說了文件頁的 page 結(jié)構(gòu)的 index 屬性表示該內(nèi)存頁 page 在磁盤文件中的偏移 offset ,偏移單位為 page size 。

那匿名頁的 page 結(jié)構(gòu)中的 index 屬性表示什么呢?我們接著來看 linear_page_index 函數(shù):

staticinlinepgoff_tlinear_page_index(structvm_area_struct*vma,
unsignedlongaddress)
{
pgoff_tpgoff;
if(unlikely(is_vm_hugetlb_page(vma)))
returnlinear_hugepage_index(vma,address);
pgoff=(address-vma->vm_start)>>PAGE_SHIFT;
pgoff+=vma->vm_pgoff;
returnpgoff;
}

邏輯很簡單,就是表示匿名頁在對應進程虛擬內(nèi)存區(qū)域 VMA 中的偏移。

在本小節(jié)最后,還有一個與反向映射相關(guān)的重要屬性就是 page 結(jié)構(gòu)中的 _mapcount。

structpage{
structaddress_space*mapping;
pgoff_tindex;
//表示該page映射了多少個進程的虛擬內(nèi)存空間,一個page可以被多個進程映射
atomic_t_mapcount
}

經(jīng)過本小節(jié)詳細的介紹,我想大家現(xiàn)在已經(jīng)猜到 _mapcount 字段的含義了,我們知道一個物理內(nèi)存頁可以映射到多個進程的虛擬內(nèi)存空間中,比如:共享內(nèi)存映射,父子進程的創(chuàng)建等。page 與 VMA 是一對多的關(guān)系,這里的 _mapcount 就表示該物理頁映射到了多少個進程的虛擬內(nèi)存空間中。

6.2 內(nèi)存頁回收相關(guān)屬性

我們接著來看 struct page 中剩下的其他屬性,我們知道物理內(nèi)存頁在內(nèi)核中分為匿名頁和文件頁,在《5.2 物理內(nèi)存區(qū)域中的水位線》小節(jié)中,筆者還提到過兩個重要的鏈表分別為:active 鏈表和 inactive 鏈表。

其中 active 鏈表用來存放訪問非常頻繁的內(nèi)存頁(熱頁), inactive 鏈表用來存放訪問不怎么頻繁的內(nèi)存頁(冷頁),當內(nèi)存緊張的時候,內(nèi)核就會優(yōu)先將 inactive 鏈表中的內(nèi)存頁置換出去。

內(nèi)核在回收內(nèi)存的時候,這兩個列表中的回收優(yōu)先級為:inactive 鏈表尾部 > inactive 鏈表頭部 > active 鏈表尾部 > active 鏈表頭部。

我們可以通過 cat /proc/zoneinfo 命令來查看不同 NUMA 節(jié)點中不同內(nèi)存區(qū)域中的 active 鏈表和 inactive 鏈表中物理內(nèi)存頁的個數(shù):

104a4232-751d-11ed-8abf-dac502259ad0.pngimage.png
  • nr_zone_active_anon 和 nr_zone_inactive_anon 分別是該內(nèi)存區(qū)域內(nèi)活躍和非活躍的匿名頁數(shù)量。

  • nr_zone_active_file 和 nr_zone_inactive_file 分別是該內(nèi)存區(qū)域內(nèi)活躍和非活躍的文件頁數(shù)量。

為什么會有 active 鏈表和 inactive 鏈表?

內(nèi)存回收的關(guān)鍵是如何實現(xiàn)一個高效的頁面替換算法 PFRA (Page Frame Replacement Algorithm) ,提到頁面替換算法大家可能立馬會想到 LRU (Least-Recently-Used) 算法。LRU 算法的核心思想就是那些最近最少使用的頁面,在未來的一段時間內(nèi)可能也不會再次被使用,所以在內(nèi)存緊張的時候,會優(yōu)先將這些最近最少使用的頁面置換出去。在這種情況下其實一個 active 鏈表就可以滿足我們的需求。

但是這里會有一個嚴重的問題,LRU 算法更多的是在時間維度上的考量,突出最近最少使用,但是它并沒有考量到使用頻率的影響,假設(shè)有這樣一種狀況,就是一個頁面被瘋狂頻繁的使用,毫無疑問它肯定是一個熱頁,但是這個頁面最近的一次訪問時間離現(xiàn)在稍微久了一點點,此時進來大量的頁面,這些頁面的特點是只會使用一兩次,以后將再也不會用到。

在這種情況下,根據(jù) LRU 的語義這個之前頻繁地被瘋狂訪問的頁面就會被置換出去了(本來應該將這些大量一次性訪問的頁面置換出去的),當這個頁面在不久之后要被訪問時,此時已經(jīng)不在內(nèi)存中了,還需要在重新置換進來,造成性能的損耗。這種現(xiàn)象也叫 Page Thrashing(頁面顛簸)。

因此,內(nèi)核為了將頁面使用頻率這個重要的考量因素加入進來,于是就引入了 active 鏈表和 inactive 鏈表。工作原理如下:

  1. 首先 inactive 鏈表的尾部存放的是訪問頻率最低并且最少訪問的頁面,在內(nèi)存緊張的時候,這些頁面被置換出去的優(yōu)先級是最大的。

  2. 對于文件頁來說,當它被第一次讀取的時候,內(nèi)核會將它放置在 inactive 鏈表的頭部,如果它繼續(xù)被訪問,則會提升至 active 鏈表的尾部。如果它沒有繼續(xù)被訪問,則會隨著新文件頁的進入,內(nèi)核會將它慢慢的推到 inactive 鏈表的尾部,如果此時再次被訪問則會直接被提升到 active 鏈表的頭部。大家可以看出此時頁面的使用頻率這個因素已經(jīng)被考量了進來。

  3. 對于匿名頁來說,當它被第一次讀取的時候,內(nèi)核會直接將它放置在 active 鏈表的尾部,注意不是 inactive 鏈表的頭部,這里和文件頁不同。因為匿名頁的換出 Swap Out 成本會更大,內(nèi)核會對匿名頁更加優(yōu)待。當匿名頁再次被訪問的時候就會被被提升到 active 鏈表的頭部。

  4. 當遇到內(nèi)存緊張的情況需要換頁時,內(nèi)核會從 active 鏈表的尾部開始掃描,將一定量的頁面降級到 inactive 鏈表頭部,這樣一來原來位于 inactive 鏈表尾部的頁面就會被置換出去。

內(nèi)核在回收內(nèi)存的時候,這兩個列表中的回收優(yōu)先級為:inactive 鏈表尾部 > inactive 鏈表頭部 > active 鏈表尾部 > active 鏈表頭部。

為什么會把 active 鏈表和 inactive 鏈表分成兩類,一類是匿名頁,一類是文件頁?

在本文 《5.2 物理內(nèi)存區(qū)域中的水位線》小節(jié)中,筆者為大家介紹了一個叫做 swappiness 的內(nèi)核參數(shù), 我們可以通過 cat /proc/sys/vm/swappiness 命令查看,swappiness 選項的取值范圍為 0 到 100,默認為 60。

swappiness 用于表示 Swap 機制的積極程度,數(shù)值越大,Swap 的積極程度,越高越傾向于回收匿名頁。數(shù)值越小,Swap 的積極程度越低,越傾向于回收文件頁。

因為回收匿名頁和回收文件頁的代價是不一樣的,回收匿名頁代價會更高一點,所以引入 swappiness 來控制內(nèi)核回收的傾向。

注意: swappiness 只是表示 Swap 積極的程度,當內(nèi)存非常緊張的時候,即使將 swappiness 設(shè)置為 0 ,也還是會發(fā)生 Swap 的。

假設(shè)我們現(xiàn)在只有 active 鏈表和 inactive 鏈表,不對這兩個鏈表進行匿名頁和文件頁的歸類,在需要頁面置換的時候,內(nèi)核會先從 active 鏈表尾部開始掃描,當 swappiness 被設(shè)置為 0 時,內(nèi)核只會置換文件頁,不會置換匿名頁。

由于 active 鏈表和 inactive 鏈表沒有進行物理頁面類型的歸類,所以鏈表中既會有匿名頁也會有文件頁,如果鏈表中有大量的匿名頁的話,內(nèi)核就會不斷的跳過這些匿名頁去尋找文件頁,并將文件頁替換出去,這樣從性能上來說肯定是低效的。

因此內(nèi)核將 active 鏈表和 inactive 鏈表按照匿名頁和文件頁進行了歸類,當 swappiness 被設(shè)置為 0 時,內(nèi)核只需要去 nr_zone_active_file 和 nr_zone_inactive_file 鏈表中掃描即可,提升了性能。

其實除了以上筆者介紹的四種 LRU 鏈表(匿名頁的 active 鏈表,inactive 鏈表和文件頁的active 鏈表, inactive 鏈表)之外,內(nèi)核還有一種鏈表,比如進程可以通過 mlock() 等系統(tǒng)調(diào)用把內(nèi)存頁鎖定在內(nèi)存里,保證該內(nèi)存頁無論如何不會被置換出去,比如出于安全或者性能的考慮,頁面中可能會包含一些敏感的信息不想被 swap 到磁盤上導致泄密,或者一些頻繁訪問的內(nèi)存頁必須一直貯存在內(nèi)存中。

當這些被鎖定在內(nèi)存中的頁面很多時,內(nèi)核在掃描 active 鏈表的時候也不得不跳過這些頁面,所以內(nèi)核又將這些被鎖定的頁面單獨拎出來放在一個獨立的鏈表中。

現(xiàn)在筆者為大家介紹五種用于存放 page 的鏈表,內(nèi)核會根據(jù)不同的情況將一個物理頁放置在這五種鏈表其中一個上。那么對于物理頁的 struct page 結(jié)構(gòu)中就需要有一個屬性用來標識該物理頁究竟被內(nèi)核放置在哪個鏈表上。

structpage{
structlist_headlru;
atomic_t_refcount;
}

struct list_head lru 屬性就是用來指向物理頁被放置在了哪個鏈表上。

atomic_t _refcount 屬性用來記錄內(nèi)核中引用該物理頁的次數(shù),表示該物理頁的活躍程度。

6.3 物理內(nèi)存頁屬性和狀態(tài)的標志位 flag

structpage{
unsignedlongflags;
}

在本文 《2.3 SPARSEMEM 稀疏內(nèi)存模型》小節(jié)中,我們提到,內(nèi)核為了能夠更靈活地管理粒度更小的連續(xù)物理內(nèi)存,于是就此引入了 SPARSEMEM 稀疏內(nèi)存模型。

12d8a75a-751d-11ed-8abf-dac502259ad0.pngimage.png

SPARSEMEM 稀疏內(nèi)存模型的核心思想就是提供對粒度更小的連續(xù)內(nèi)存塊進行精細的管理,用于管理連續(xù)內(nèi)存塊的單元被稱作 section 。內(nèi)核中用于描述 section 的數(shù)據(jù)結(jié)構(gòu)是 struct mem_section。

由于 section 被用作管理小粒度的連續(xù)內(nèi)存塊,這些小的連續(xù)物理內(nèi)存在 section 中也是通過數(shù)組的方式被組織管理(圖中 struct page 類型的數(shù)組)。

每個 struct mem_section 結(jié)構(gòu)體中有一個 section_mem_map 指針用于指向連續(xù)內(nèi)存的 page 數(shù)組。而所有的 mem_section 也會被存放在一個全局的數(shù)組 mem_section 中。

那么給定一個具體的 struct page,在稀疏內(nèi)存模型中內(nèi)核如何定位到這個物理內(nèi)存頁到底屬于哪個 mem_section 呢 ?這是第一個問題~~

筆者在《5. 內(nèi)核如何管理 NUMA 節(jié)點中的物理內(nèi)存區(qū)域》小節(jié)中講到了內(nèi)存的架構(gòu),在 NUMA 架構(gòu)下,物理內(nèi)存被劃分成了一個一個的內(nèi)存節(jié)點(NUMA 節(jié)點),在每個 NUMA 節(jié)點內(nèi)部又將其所管理的物理內(nèi)存按照功能不同劃分成了不同的內(nèi)存區(qū)域 zone,每個內(nèi)存區(qū)域管理一片用于特定具體功能的物理內(nèi)存 page。

物理內(nèi)存在內(nèi)核中管理的層級關(guān)系為:None -> Zone -> page

0f61f536-751d-11ed-8abf-dac502259ad0.pngimage.png

那么在 NUMA 架構(gòu)下,給定一個具體的 struct page,內(nèi)核又該如何確定該物理內(nèi)存頁究竟屬于哪個 NUMA 節(jié)點,屬于哪塊內(nèi)存區(qū)域 zone 呢? 這是第二個問題。

關(guān)于以上筆者提出的兩個問題所需要的定位信息全部存儲在 struct page 結(jié)構(gòu)中的 flags 字段中。前邊我們提到,struct page 是 Linux 世界里最繁華的地段,這里的地價非常昂貴,所以 page 結(jié)構(gòu)中這些字段里的每一個比特內(nèi)核都會物盡其用。

structpage{
unsignedlongflags;
}

因此這個 unsigned long 類型的 flags 字段中不僅包含上面提到的定位信息還會包括物理內(nèi)存頁的一些屬性和標志位。flags 字段的高 8 位用來表示 struct page 的定位信息,剩余低位表示特定的標志位。

138b6610-751d-11ed-8abf-dac502259ad0.pngimage.png

struct page 與其所屬上層結(jié)構(gòu)轉(zhuǎn)換的相應函數(shù)定義在 /include/linux/mm.h 文件中:

staticinlineunsignedlongpage_to_section(conststructpage*page)
{
return(page->flags>>SECTIONS_PGSHIFT)&SECTIONS_MASK;
}

staticinlinepg_data_t*page_pgdat(conststructpage*page)
{
returnNODE_DATA(page_to_nid(page));
}

staticinlinestructzone*page_zone(conststructpage*page)
{
return&NODE_DATA(page_to_nid(page))->node_zones[page_zonenum(page)];
}

在我們介紹完了 flags 字段中高位存儲的位置定位信息之后,接下來就該來介紹下在低位比特中表示的物理內(nèi)存頁的那些標志位~~

物理內(nèi)存頁的這些標志位定義在內(nèi)核 /include/linux/page-flags.h文件中:

enumpageflags{
PG_locked,/*Pageislocked.Don'ttouch.*/
PG_referenced,
PG_uptodate,
PG_dirty,
PG_lru,
PG_active,
PG_slab,
PG_reserved,
PG_compound,
PG_private,
PG_writeback,
PG_reclaim,
#ifdefCONFIG_MMU
PG_mlocked,/*Pageisvmamlocked*/
PG_swapcache=PG_owner_priv_1,

................
};
  • PG_locked 表示該物理頁面已經(jīng)被鎖定,如果該標志位置位,說明有使用者正在操作該 page , 則內(nèi)核的其他部分不允許訪問該頁, 這可以防止內(nèi)存管理出現(xiàn)競態(tài)條件,例如:在從硬盤讀取數(shù)據(jù)到 page 時。

  • PG_mlocked 表示該物理內(nèi)存頁被進程通過 mlock 系統(tǒng)調(diào)用鎖定常駐在內(nèi)存中,不會被置換出去。

  • PG_referenced 表示該物理頁面剛剛被訪問過。

  • PG_active 表示該物理頁位于 active list 鏈表中。PG_referenced 和 PG_active 共同控制了系統(tǒng)使用該內(nèi)存頁的活躍程度,在內(nèi)存回收的時候這兩個信息非常重要。

  • PG_uptodate 表示該物理頁的數(shù)據(jù)已經(jīng)從塊設(shè)備中讀取到內(nèi)存中,并且期間沒有出錯。

  • PG_readahead 當進程在順序訪問文件的時候,內(nèi)核會預讀若干相鄰的文件頁數(shù)據(jù)到 page 中,物理頁 page 結(jié)構(gòu)設(shè)置了該標志位,表示它是一個正在被內(nèi)核預讀的頁。相關(guān)詳細內(nèi)容可回看筆者之前的這篇文章 《從 Linux 內(nèi)核角度探秘 JDK NIO 文件讀寫本質(zhì)》

  • PG_dirty 物理內(nèi)存頁的臟頁標識,表示該物理內(nèi)存頁中的數(shù)據(jù)已經(jīng)被進程修改,但還沒有同步會磁盤中。筆者在 《從 Linux 內(nèi)核角度探秘 JDK NIO 文件讀寫本質(zhì)》 一文中也詳細介紹過。

  • PG_lru 表示該物理內(nèi)存頁現(xiàn)在被放置在哪個 lru 鏈表上,比如:是在 active list 鏈表中 ? 還是在 inactive list 鏈表中 ?

  • PG_highmem 表示該物理內(nèi)存頁是在高端內(nèi)存中。

  • PG_writeback 表示該物理內(nèi)存頁正在被內(nèi)核的 pdflush 線程回寫到磁盤中。詳情可回看文章《從 Linux 內(nèi)核角度探秘 JDK NIO 文件讀寫本質(zhì)》 。

  • PG_slab 表示該物理內(nèi)存頁屬于 slab 分配器所管理的一部分。

  • PG_swapcache 表示該物理內(nèi)存頁處于 swap cache 中。 struct page 中的 private 指針這時指向 swap_entry_t 。

  • PG_reclaim 表示該物理內(nèi)存頁已經(jīng)被內(nèi)核選中即將要進行回收。

  • PG_buddy 表示該物理內(nèi)存頁是空閑的并且被伙伴系統(tǒng)所管理。

  • PG_compound 表示物理內(nèi)存頁屬于復合頁的其中一部分。

  • PG_private 標志被置位的時候表示該 struct page 結(jié)構(gòu)中的 private 指針指向了具體的對象。不同場景指向的對象不同。

除此之外內(nèi)核還定義了一些標準宏,用來檢查某個物理內(nèi)存頁 page 是否設(shè)置了特定的標志位,以及對這些標志位的操作,這些宏在內(nèi)核中的實現(xiàn)都是原子的,命名格式如下:

  • PageXXX(page):檢查 page 是否設(shè)置了 PG_XXX 標志位

  • SetPageXXX(page):設(shè)置 page 的 PG_XXX 標志位

  • ClearPageXXX(page):清除 page 的 PG_XXX 標志位

  • TestSetPageXXX(page):設(shè)置 page 的 PG_XXX 標志位,并返回原值

另外在很多情況下,內(nèi)核通常需要等待物理頁 page 的某個狀態(tài)改變,才能繼續(xù)恢復工作,內(nèi)核提供了如下兩個輔助函數(shù),來實現(xiàn)在特定狀態(tài)的阻塞等待:

staticinlinevoidwait_on_page_locked(structpage*page)
staticinlinevoidwait_on_page_writeback(structpage*page)

當物理頁面在鎖定的狀態(tài)下,進程調(diào)用了 wait_on_page_locked 函數(shù),那么進程就會阻塞等待知道頁面解鎖。

當物理頁面正在被內(nèi)核回寫到磁盤的過程中,進程調(diào)用了 wait_on_page_writeback 函數(shù)就會進入阻塞狀態(tài)直到臟頁數(shù)據(jù)被回寫到磁盤之后被喚醒。

6.4 復合頁 compound_page 相關(guān)屬性

我們都知道 Linux 管理內(nèi)存的最小單位是 page,每個 page 描述 4K 大小的物理內(nèi)存,但在一些對于內(nèi)存敏感的使用場景中,用戶往往期望使用一些巨型大頁。

巨型大頁就是通過兩個或者多個物理上連續(xù)的內(nèi)存頁 page 組裝成的一個比普通內(nèi)存頁 page 更大的頁,

因為這些巨型頁要比普通的 4K 內(nèi)存頁要大很多,所以遇到缺頁中斷的情況就會相對減少,由于減少了缺頁中斷所以性能會更高。

另外,由于巨型頁比普通頁要大,所以巨型頁需要的頁表項要比普通頁要少,頁表項里保存了虛擬內(nèi)存地址與物理內(nèi)存地址的映射關(guān)系,當 CPU 訪問內(nèi)存的時候需要頻繁通過 MMU 訪問頁表項獲取物理內(nèi)存地址,由于要頻繁訪問,所以頁表項一般會緩存在 TLB 中,因為巨型頁需要的頁表項較少,所以節(jié)約了 TLB 的空間同時降低了 TLB 緩存 MISS 的概率,從而加速了內(nèi)存訪問。

還有一個使用巨型頁受益場景就是,當一個內(nèi)存占用很大的進程(比如 Redis)通過 fork 系統(tǒng)調(diào)用創(chuàng)建子進程的時候,會拷貝父進程的相關(guān)資源,其中就包括父進程的頁表,由于巨型頁使用的頁表項少,所以拷貝的時候性能會提升不少。

以上就是巨型頁存在的原因以及使用的場景,但是在 Linux 內(nèi)存管理架構(gòu)中都是統(tǒng)一通過 struct page 來管理內(nèi)存,而巨型大頁卻是通過兩個或者多個物理上連續(xù)的內(nèi)存頁 page 組裝成的一個比普通內(nèi)存頁 page 更大的頁,那么巨型頁的管理與普通頁的管理如何統(tǒng)一呢?

這就引出了本小節(jié)的主題-----復合頁 compound_page,下面我們就來看下 Linux 如果通過統(tǒng)一的 struct page 結(jié)構(gòu)來描述這些巨型頁(compound_page):

雖然巨型頁(compound_page)是由多個物理上連續(xù)的普通 page 組成的,但是在內(nèi)核的視角里它還是被當做一個特殊內(nèi)存頁來看待。

下圖所示,是由 4 個連續(xù)的普通內(nèi)存頁 page 組成的一個 compound_page:

13b4cd5c-751d-11ed-8abf-dac502259ad0.pngimage.png

組成復合頁的第一個 page 我們稱之為首頁(Head Page),其余的均稱之為尾頁(Tail Page)。

我們來看一下 struct page 中關(guān)于描述 compound_page 的相關(guān)字段:

structpage{
//首頁page中的flags會被設(shè)置為PG_head表示復合頁的第一頁
unsignedlongflags;
//其余尾頁會通過該字段指向首頁
unsignedlongcompound_head;
//用于釋放復合頁的析構(gòu)函數(shù),保存在首頁中
unsignedcharcompound_dtor;
//該復合頁有多少個page組成,order還是分配階的概念,首頁中保存
//本例中的order=2表示由4個普通頁組成
unsignedcharcompound_order;
//該復合頁被多少個進程使用,內(nèi)存頁反向映射的概念,首頁中保存
atomic_tcompound_mapcount;
//復合頁使用計數(shù),首頁中保存
atomic_tcompound_pincount;
}

首頁對應的 struct page 結(jié)構(gòu)里的 flags 會被設(shè)置為 PG_head,表示這是復合頁的第一頁。

另外首頁中還保存關(guān)于復合頁的一些額外信息,比如用于釋放復合頁的析構(gòu)函數(shù)會保存在首頁 struct page 結(jié)構(gòu)里的 compound_dtor 字段中,復合頁的分配階 order 會保存在首頁中的 compound_order 中,以及用于指示復合頁的引用計數(shù) compound_pincount,以及復合頁的反向映射個數(shù)(該復合頁被多少個進程的頁表所映射)compound_mapcount 均在首頁中保存。

復合頁中的所有尾頁都會通過其對應的 struct page 結(jié)構(gòu)中的 compound_head 指向首頁,這樣通過首頁和尾頁就組裝成了一個完整的復合頁 compound_page 。

13c66490-751d-11ed-8abf-dac502259ad0.pngimage.png

6.5 Slab 對象池相關(guān)屬性

本小節(jié)只是對 slab 的一個簡單介紹,大家有個大概的印象就可以了,后面筆者會有一篇專門的文章為大家詳細介紹 slab 的相關(guān)實現(xiàn)細節(jié),到時候還會在重新詳細介紹 struct page 中的相關(guān)屬性。

內(nèi)核中對內(nèi)存頁的分配使用有兩種方式,一種是一頁一頁的分配使用,這種以頁為單位的分配方式內(nèi)核會向相應內(nèi)存區(qū)域 zone 里的伙伴系統(tǒng)申請以及釋放。

另一種方式就是只分配小塊的內(nèi)存,不需要一下分配一頁的內(nèi)存,比如前邊章節(jié)中提到的 struct page ,anon_vma_chain ,anon_vma ,vm_area_struct 結(jié)構(gòu)實例的分配,這些結(jié)構(gòu)通常就是幾十個字節(jié)大小,并不需要按頁來分配。

為了滿足類似這種小內(nèi)存分配的需要,Linux 內(nèi)核使用 slab allocator 分配器來分配,slab 就好比一個對象池,內(nèi)核中的數(shù)據(jù)結(jié)構(gòu)對象都對應于一個 slab 對象池,用于分配這些固定類型對象所需要的內(nèi)存。

它的基本原理是從伙伴系統(tǒng)中申請一整頁內(nèi)存,然后劃分成多個大小相等的小塊內(nèi)存被 slab 所管理。這樣一來 slab 就和物理內(nèi)存頁 page 發(fā)生了關(guān)聯(lián),由于 slab 管理的單元是物理內(nèi)存頁 page 內(nèi)進一步劃分出來的小塊內(nèi)存,所以當 page 被分配給相應 slab 結(jié)構(gòu)之后,struct page 里也會存放 slab 相關(guān)的一些管理數(shù)據(jù)。

structpage{

struct{/*slab,slobandslub*/
union{
structlist_headslab_list;
struct{/*Partialpages*/
structpage*next;
#ifdefCONFIG_64BIT
intpages;/*Nrofpagesleft*/
intpobjects;/*Approximatecount*/
#else
shortintpages;
shortintpobjects;
#endif
};
};
structkmem_cache*slab_cache;/*notslob*/
/*Double-wordboundary*/
void*freelist;/*firstfreeobject*/
union{
void*s_mem;/*slab:firstobject*/
struct{/*SLUB*/
unsignedinuse:16;
unsignedobjects:15;
unsignedfrozen:1;
};
};
};

}
  • struct list_head slab_list :slab 的管理結(jié)構(gòu)中有眾多用于管理 page 的鏈表,比如:完全空閑的 page 鏈表,完全分配的 page 鏈表,部分分配的 page 鏈表,slab_list 用于指定當前 page 位于 slab 中的哪個具體鏈表上。

  • struct page *next : 當 page 位于 slab 結(jié)構(gòu)中的某個管理鏈表上時,next 指針用于指向鏈表中的下一個 page。

  • int pages : 表示 slab 中總共擁有的 page 個數(shù)。

  • int pobjects : 表示 slab 中擁有的特定類型的對象個數(shù)。

  • struct kmem_cache *slab_cache : 用于指向當前 page 所屬的 slab 管理結(jié)構(gòu),通過 slab_cache 將 page 和 slab 關(guān)聯(lián)起來。

  • void *freelist : 指向 page 中的第一個未分配出去的空閑對象,前面介紹過,slab 向伙伴系統(tǒng)申請一個或者多個 page,并將一整頁 page 劃分出多個大小相等的內(nèi)存塊,用于存儲特定類型的對象。

  • void *s_mem : 指向 page 中的第一個對象。

  • unsigned inuse : 表示 slab 中已經(jīng)被分配出去的對象個數(shù),當該值為 0 時,表示 slab 中所管理的對象全都是空閑的,當所有的空閑對象達到一定數(shù)目,該 slab 就會被伙伴系統(tǒng)回收掉。

  • unsigned objects : slab 中所有的對象個數(shù)。

  • unsigned frozen : 當前內(nèi)存頁 page 被 slab 放置在 CPU 本地緩存列表中,frozen = 1,否則 frozen = 0 。

總結(jié)

到這里,關(guān)于 Linux 物理內(nèi)存管理的相關(guān)內(nèi)容筆者就為大家介紹完了,本文的內(nèi)容比較多,尤其是物理內(nèi)存頁反向映射相關(guān)的內(nèi)容比較復雜,涉及到的關(guān)聯(lián)關(guān)系比較多,現(xiàn)在筆者在帶大家總結(jié)一下本文的主要內(nèi)容,方便大家復習回顧:

在本文的開始,筆者首先從 CPU 角度為大家介紹了三種物理內(nèi)存模型:FLATMEM 平坦內(nèi)存模型,DISCONTIGMEM 非連續(xù)內(nèi)存模型,SPARSEMEM 稀疏內(nèi)存模型。

隨后筆者又接著介紹了兩種物理內(nèi)存架構(gòu):一致性內(nèi)存訪問 UMA 架構(gòu),非一致性內(nèi)存訪問 NUMA 架構(gòu)。

在這個基礎(chǔ)之上,又按照內(nèi)核對物理內(nèi)存的組織管理層次,分別介紹了 Node 節(jié)點,物理內(nèi)存區(qū)域 zone 等相關(guān)內(nèi)核結(jié)構(gòu)。它們的層次如下圖所示:

0f61f536-751d-11ed-8abf-dac502259ad0.pngimage.png

在把握了物理內(nèi)存的總體架構(gòu)之后,又引出了眾多細節(jié)性的內(nèi)容,比如:物理內(nèi)存區(qū)域的管理與劃分,物理內(nèi)存區(qū)域中的預留內(nèi)存,物理內(nèi)存區(qū)域中的水位線及其計算方式,物理內(nèi)存區(qū)域中的冷熱頁。

最后,筆者詳細介紹了內(nèi)核如何通過 struct page 結(jié)構(gòu)來描述物理內(nèi)存頁,其中匿名頁反向映射的內(nèi)容比較復雜,需要大家多多梳理回顧一下。

好了,本文的內(nèi)容到這里就全部結(jié)束了,感謝大家的耐心觀看,我們下篇文章見。

審核編輯 :李倩
聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學習之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • Linux
    +關(guān)注

    關(guān)注

    87

    文章

    11123

    瀏覽量

    207910
  • 內(nèi)存管理
    +關(guān)注

    關(guān)注

    0

    文章

    167

    瀏覽量

    14099
  • 變量
    +關(guān)注

    關(guān)注

    0

    文章

    607

    瀏覽量

    28257

原文標題:幾萬字 + 60 張圖 |一步一圖帶你深入理解 Linux 物理內(nèi)存管理

文章出處:【微信號:小林coding,微信公眾號:小林coding】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    深入理解FPD-link III ADAS解串器HUB產(chǎn)品

    電子發(fā)燒友網(wǎng)站提供《深入理解FPD-link III ADAS解串器HUB產(chǎn)品.pdf》資料免費下載
    發(fā)表于 09-06 09:58 ?0次下載
    <b class='flag-5'>深入理解</b>FPD-link III ADAS解串器HUB產(chǎn)品

    技術(shù)干貨驛站 ▏深入理解C語言:基本數(shù)據(jù)類型和變量

    在C語言中,數(shù)據(jù)類型和變量是編程的基礎(chǔ),也是理解更復雜概念的關(guān)鍵。數(shù)據(jù)類型決定了變量的內(nèi)存分配、存儲范圍和操作方式,而變量則是存儲數(shù)據(jù)的容器。本篇文章將從基本數(shù)據(jù)類型和變量兩個方面,帶你深入
    的頭像 發(fā)表于 07-26 17:53 ?1630次閱讀
    技術(shù)干貨驛站 ▏<b class='flag-5'>深入理解</b>C語言:基本數(shù)據(jù)類型和變量

    深入理解Java 8內(nèi)存管理機制及故障排查實戰(zhàn)指南

    Java的自動內(nèi)存管理機制是由 JVM 中的垃圾收集器來實現(xiàn)的,垃圾收集器會定期掃描堆內(nèi)存中的對象,檢測并清除不再使用的對象,以釋放內(nèi)存資源。
    的頭像 發(fā)表于 04-04 08:10 ?843次閱讀
    <b class='flag-5'>深入理解</b>Java 8<b class='flag-5'>內(nèi)存</b><b class='flag-5'>管理</b>機制及故障排查實戰(zhàn)指南

    深入理解 FPGA 的基礎(chǔ)結(jié)構(gòu)

    轉(zhuǎn)載地址:https://zhuanlan.zhihu.com/p/506828648 文章很詳細的介紹了FPGA的基礎(chǔ)結(jié)構(gòu),能更直觀的理解內(nèi)部結(jié)構(gòu)原理。對深入學習很有幫助。 以下是正文: 這
    發(fā)表于 04-03 17:39

    深入理解數(shù)據(jù)備份的關(guān)鍵原則:應用致性與崩潰致性的區(qū)別

    深入理解數(shù)據(jù)備份的關(guān)鍵原則:應用致性與崩潰致性的區(qū)別 在數(shù)字化時代,數(shù)據(jù)備份成為了企業(yè)信息安全的核心環(huán)節(jié)。但在備份過程中,兩個關(guān)鍵概念——應用致性和崩潰
    的頭像 發(fā)表于 03-11 11:29 ?646次閱讀
    <b class='flag-5'>深入理解</b>數(shù)據(jù)備份的關(guān)鍵原則:應用<b class='flag-5'>一</b>致性與崩潰<b class='flag-5'>一</b>致性的區(qū)別

    物理內(nèi)存模型的演變

    內(nèi)存管理概述中,主要是以Linux v2.6.11為例進行分析的,但是計算技術(shù)在不斷發(fā)展,新的存儲架構(gòu)、新的指令集架構(gòu)、新的SoC架構(gòu)等都對物理內(nèi)存
    的頭像 發(fā)表于 02-25 10:35 ?349次閱讀

    Linux內(nèi)核內(nèi)存管理之內(nèi)核非連續(xù)物理內(nèi)存分配

    的主要優(yōu)點是避免了外部碎片,而缺點是需要修改內(nèi)核頁表。顯然,非連續(xù)內(nèi)存區(qū)域的大小必須是4096的倍數(shù)。Linux使用非連續(xù)物理內(nèi)存區(qū)的場景有幾種:(1)為swap區(qū)分配數(shù)據(jù)結(jié)構(gòu);(2)
    的頭像 發(fā)表于 02-23 09:44 ?750次閱讀
    <b class='flag-5'>Linux</b>內(nèi)核<b class='flag-5'>內(nèi)存</b><b class='flag-5'>管理</b>之內(nèi)核非連續(xù)<b class='flag-5'>物理</b><b class='flag-5'>內(nèi)存</b>分配

    深入理解FFmpeg閱讀體驗》+ 書收到了,嶄新的開篇

    今天收到了《深入理解FFmpeg》 嶄新的書,個在2022年較近距離接觸過卻尚未深入研究的領(lǐng)域圖像處理。最近剛好在作這方面的研究,希望自己可以把握這次機會,好好學習下 FFMpeg,相信可以讓自己
    發(fā)表于 01-07 18:57

    Linux內(nèi)核內(nèi)存管理架構(gòu)解析

    內(nèi)存管理子系統(tǒng)可能是linux內(nèi)核中最為復雜的個子系統(tǒng),其支持的功能需求眾多,如頁面映射、頁面分配、頁面回收、頁面交換、冷熱頁面、緊急頁面、頁面碎片
    的頭像 發(fā)表于 01-04 09:24 ?554次閱讀
    <b class='flag-5'>Linux</b>內(nèi)核<b class='flag-5'>內(nèi)存</b><b class='flag-5'>管理</b>架構(gòu)解析

    深入理解Linux RCU:從硬件說起之內(nèi)存屏障

    篇文章我們談到了內(nèi)存Cache,并且描述了典型的Cache致性協(xié)議MESI。Cache的根本目的,是解決內(nèi)存與CPU速度多達兩個數(shù)量級的性能差異。
    的頭像 發(fā)表于 12-25 13:42 ?678次閱讀
    <b class='flag-5'>深入理解</b><b class='flag-5'>Linux</b> RCU:從硬件說起之<b class='flag-5'>內(nèi)存</b>屏障

    一步一步學會使用Channel Analysis

    電子發(fā)燒友網(wǎng)站提供《一步一步學會使用Channel Analysis.rar》資料免費下載
    發(fā)表于 11-21 10:43 ?1次下載
    <b class='flag-5'>一步</b><b class='flag-5'>一步</b>學會使用Channel Analysis

    【書籍評測活動NO.25】深入理解FFmpeg,帶你FFmpeg從入門到精通

    這個領(lǐng)域。由劉歧牽頭編寫的《深入理解FFmpeg》正是這樣本好書,值得向大家推薦。 ——楊成立,開源項目SRS(Simple Realtime Server) 創(chuàng)始人、技術(shù)委員會成員 通過閱讀
    發(fā)表于 11-15 14:26

    如何一步一步設(shè)計開關(guān)電源

    簡介:針對開關(guān)電源很多人覺得很難,其實不然。設(shè)計款開關(guān)電源并不難,難就難在做精,等你真正入門了,積累定的經(jīng)驗,再采用分立的結(jié)構(gòu)進行設(shè)計就簡單多了。萬事開頭難,筆者在這就拋磚引玉,慢慢講解如何一步
    發(fā)表于 11-15 14:24 ?3次下載
    如何<b class='flag-5'>一步</b><b class='flag-5'>一步</b>設(shè)計開關(guān)電源

    Linux 內(nèi)存管理總結(jié)

    、Linux內(nèi)存管理概述 Linux內(nèi)存管理是指對
    的頭像 發(fā)表于 11-10 14:58 ?431次閱讀
    <b class='flag-5'>Linux</b> <b class='flag-5'>內(nèi)存</b><b class='flag-5'>管理</b>總結(jié)

    Linux虛擬地址空間和物理地址空間的關(guān)系

    很多人接觸Linux內(nèi)存管理是從malloc()這個C語言庫函數(shù)開始,也是從那時開始就知道了虛擬內(nèi)存的概念。但很多人可能并不知道虛擬地址是如何轉(zhuǎn)換成
    的頭像 發(fā)表于 10-08 11:40 ?913次閱讀
    <b class='flag-5'>Linux</b>虛擬地址空間和<b class='flag-5'>物理</b>地址空間的關(guān)系