小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

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

 水波浪 2024-08-15 發(fā)布于廣東

1. 前文回顧

在上篇文章 《深入理解 Linux 虛擬內(nèi)存管理》 中,筆者分別從進(jìn)程用戶態(tài)和內(nèi)核態(tài)的角度詳細(xì)深入地為大家介紹了 Linux 內(nèi)核如何對(duì)進(jìn)程虛擬內(nèi)存空間進(jìn)行布局以及管理的相關(guān)實(shí)現(xiàn)。在我們深入理解了虛擬內(nèi)存之后,那么何不順帶著也探秘一下物理內(nèi)存的管理呢?

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

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

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

image

image

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

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

image

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

image

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

image

image

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

image

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

image

image

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

image

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

image

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

image

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

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

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

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

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

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

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

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

image

內(nèi)核中使用了一個(gè) mem_map 的全局?jǐn)?shù)組用來(lái)組織所有劃分出來(lái)的物理內(nèi)存頁(yè)。mem_map 全局?jǐn)?shù)組的下標(biāo)就是相應(yīng)物理頁(yè)對(duì)應(yīng)的 PFN 。

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

#if defined(CONFIG_FLATMEM)
#define __pfn_to_page(pfn) (mem_map + ((pfn)-ARCH_PFN_OFFSET))
#define __page_to_pfn(page) ((unsigned long)((page)-mem_map) + ARCH_PFN_OFFSET)
#endif

ARCH_PFN_OFFSET 是 PFN 的起始偏移量。

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

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

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

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

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

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

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

image

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

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

image

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

typedef struct pglist_data {
   #ifdef CONFIG_FLATMEM
	  struct page *node_mem_map;
   #endif
}

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

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

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

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

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

#if defined(CONFIG_DISCONTIGMEM)

#define __pfn_to_page(pfn)			({	unsigned long __pfn = (pfn);			unsigned long __nid = arch_pfn_to_nid(__pfn);  	NODE_DATA(__nid)->node_mem_map + arch_local_page_offset(__pfn, __nid);})

#define __page_to_pfn(pg)						({	const struct page *__pg = (pg);						struct pglist_data *__pgdat = NODE_DATA(page_to_nid(__pg));		(unsigned long)(__pg - __pgdat->node_mem_map) +				 __pgdat->node_start_pfn;					})

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

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

image

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

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

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

struct mem_section {
	unsigned long section_mem_map;
        ...
}

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

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

#ifdef CONFIG_SPARSEMEM_EXTREME
extern struct mem_section *mem_section[NR_SECTION_ROOTS];

image

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

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

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

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

PFN 的高位 bit 存儲(chǔ)的是全局?jǐn)?shù)組 mem_section 中的 section 索引,PFN 的低位 bit 存儲(chǔ)的是 section_mem_map 數(shù)組中具體物理頁(yè) page 的索引。

#if defined(CONFIG_SPARSEMEM)
/*
 * Note: section's mem_map is encoded to reflect its start_pfn.
 * section[i].section_mem_map == mem_map's address - start_pfn;
 */
#define __page_to_pfn(pg)					({	const struct page *__pg = (pg);					int __sec = page_to_section(__pg);				(unsigned long)(__pg - __section_mem_map_addr(__nr_to_section(__sec)));	})

#define __pfn_to_page(pfn)				({	unsigned long __pfn = (pfn);				struct mem_section *__sec = __pfn_to_section(__pfn);		__section_mem_map_addr(__sec) + __pfn;		})
#endif

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

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

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

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

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

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

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

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

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

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

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

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

image

但是這里會(huì)有一個(gè)問(wèn)題,就是并非所有的物理頁(yè)都可以遷移,因?yàn)檫w移意味著物理內(nèi)存地址的變化,而內(nèi)存的熱插拔應(yīng)該對(duì)進(jìn)程來(lái)說(shuō)是透明的,所以這些遷移后的物理頁(yè)映射的虛擬內(nèi)存地址是不能變化的。

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

image

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

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

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

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

然后在這些可能會(huì)被拔出的內(nèi)存中只分配那些可遷移的內(nèi)存頁(yè),這些信息會(huì)在內(nèi)存初始化的時(shí)候被設(shè)置,這樣一來(lái)那些不可遷移的頁(yè)就不會(huì)包含在可能會(huì)拔出的內(nèi)存中,當(dāng)我們需要將這塊內(nèi)存熱拔出時(shí), 因?yàn)槔镞叺膬?nèi)存頁(yè)全部是可遷移的, 從而使內(nèi)存可以被拔除。

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

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

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

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

我們?cè)谏掀恼?《深入理解 Linux 虛擬內(nèi)存管理》的 “ 8.2 CPU 如何讀寫(xiě)主存” 小節(jié)中提到 CPU 與內(nèi)存之間的交互是通過(guò)總線完成的。

image

  • 首先 CPU 將物理內(nèi)存地址作為地址信號(hào)放到系統(tǒng)總線上傳輸。隨后 IO bridge 將系統(tǒng)總線上的地址信號(hào)轉(zhuǎn)換為存儲(chǔ)總線上的電子信號(hào)。

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

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

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

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

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

image

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

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

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

  1. 總線的帶寬壓力會(huì)越來(lái)越大,隨著 CPU 個(gè)數(shù)的增多導(dǎo)致每個(gè) CPU 可用帶寬會(huì)減少

  2. 總線的長(zhǎng)度也會(huì)因此而增加,進(jìn)而增加訪問(wèn)延遲

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

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

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

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

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

image

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

在 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 如何請(qǐng)求內(nèi)存分配的相關(guān)策略,比如:是優(yōu)先請(qǐng)求本地內(nèi)存節(jié)點(diǎn)分配內(nèi)存呢 ?還是優(yōu)先請(qǐng)求指定的 NUMA 節(jié)點(diǎn)分配內(nèi)存 ?是只能在本地內(nèi)存節(jié)點(diǎn)分配呢 ?還是允許當(dāng)本地內(nèi)存不足的情況下可以請(qǐng)求遠(yuǎn)程 NUMA 節(jié)點(diǎn)分配內(nèi)存 ?

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

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

#include <numaif.h>

long set_mempolicy(int mode, const unsigned long *nodemask,
                          unsigned long maxnode);
  • mode : 指定 NUMA 內(nèi)存分配策略。

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

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

libnuma 共享庫(kù) API 文檔:https:///linux/man-pages/man3/numa.3.html#top_of_page

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

3.2.2 NUMA 的使用簡(jiǎn)介

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

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

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

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

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

針對(duì)以上具體問(wèn)題,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 命令可以查看服務(wù)器的 NUMA 配置,上圖中的服務(wù)器配置共包含 4 個(gè) NUMA 節(jié)點(diǎn)(0 - 3),每個(gè) NUMA 節(jié)點(diǎn)中包含 16個(gè) CPU 核心,本地內(nèi)存大小約為 64G。

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

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

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

policy: default
preferred node: current

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

                           node0           node1            node2           node3
numa_hit              1296554257       918018444         1296574252       828018454
numa_miss                8541758        40297198           7544751        41267108
numa_foreign            40288595         8550361          41488585         8450375
interleave_hit             45651           45918            46654           49718
local_node            1231897031       835344122         1141898045       915354158
other_node              64657226        82674322           594657725       82675425 

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

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

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

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

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

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

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

3.2.2.2 綁定 NUMA 節(jié)點(diǎn)

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

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

  • 通過(guò) --cpunodebind 可以指定我們的應(yīng)用程序只能運(yùn)行在哪些 NUMA 節(jié)點(diǎn)上。

numactl --physcpubind=cpus  command

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

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

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

大家肯定一眼就能看出綁定在相同 NUMA 節(jié)點(diǎn)的進(jìn)程運(yùn)行會(huì)更快,因?yàn)橥ㄟ^(guò)前邊對(duì) NUMA 架構(gòu)的介紹,我們知道 CPU 訪問(wèn)本地 NUMA 節(jié)點(diǎn)的內(nèi)存是最快的。

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

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

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

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

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

image

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

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

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

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

extern pg_data_t *pgdat_list;

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

typedef struct pglist_data {
    struct pglist_data *pgdat_next;
}

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

image

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

#ifdef CONFIG_NUMA
extern struct pglist_data *node_data[];
#define NODE_DATA(nid)		(node_data[(nid)])

NODE_DATA(nid) 宏可以通過(guò) NUMA 節(jié)點(diǎn)的 nodeId,找到對(duì)應(yīng)的 struct pglist_data 結(jié)構(gòu)。

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

#ifdef CONFIG_NODES_SHIFT
#define NODES_SHIFT     CONFIG_NODES_SHIFT
#else
#define NODES_SHIFT     0
#endif
#define MAX_NUMNODES    (1 << NODES_SHIFT)

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

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

typedef struct pglist_data {
    // NUMA 節(jié)點(diǎn)id
    int node_id;
    // 指向 NUMA 節(jié)點(diǎn)內(nèi)管理所有物理頁(yè) page 的數(shù)組
    struct page *node_mem_map;
    // NUMA 節(jié)點(diǎn)內(nèi)第一個(gè)物理頁(yè)的 pfn
    unsigned long node_start_pfn;
    // NUMA 節(jié)點(diǎn)內(nèi)所有可用的物理頁(yè)個(gè)數(shù)(不包含內(nèi)存空洞)
    unsigned long node_present_pages;
    // NUMA 節(jié)點(diǎn)內(nèi)所有的物理頁(yè)個(gè)數(shù)(包含內(nèi)存空洞)
    unsigned long node_spanned_pages; 
    // 保證多進(jìn)程可以并發(fā)安全的訪問(wèn) NUMA 節(jié)點(diǎn)
    spinlock_t node_size_lock;
        .............
}

node_id 表示 NUMA 節(jié)點(diǎn)的 id,我們可以通過(guò) numactl -H 命令的輸出結(jié)果查看節(jié)點(diǎn) id。從 0 開(kāi)始依次對(duì) NUMA 節(jié)點(diǎn)進(jìn)行編號(hào)。

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

image

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

image

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

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

image

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

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

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

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

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

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

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

image

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

ZONE_NORMAL 由于也是屬于直接映射區(qū)的一部分,對(duì)應(yīng)的物理內(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 的大小,這樣一來(lái)內(nèi)核剩余可用的虛擬內(nèi)存空間就變?yōu)榱?1G - 896M = 128M。

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

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

所以內(nèi)核會(huì)根據(jù)各個(gè)物理內(nèi)存區(qū)域的功能不同,將 NUMA 節(jié)點(diǎn)內(nèi)的物理內(nèi)存主要?jiǎng)澐譃橐韵滤膫€(gè)物理內(nèi)存區(qū)域:

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

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

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

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

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

enum zone_type {
#ifdef CONFIG_ZONE_DMA
	ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
	ZONE_DMA32,
#endif
	ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
	ZONE_HIGHMEM,
#endif
	ZONE_MOVABLE,
#ifdef CONFIG_ZONE_DEVICE
	ZONE_DEVICE,
#endif
    // 充當(dāng)結(jié)束標(biāo)記, 在內(nèi)核中想要迭代系統(tǒng)中所有內(nèi)存域時(shí), 會(huì)用到該常量
	__MAX_NR_ZONES

};

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

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

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

既然有了這些實(shí)際的物理內(nèi)存區(qū)域,那么內(nèi)核為什么又要?jiǎng)澐殖鲆粋€(gè) ZONE_MOVABLE 這樣的虛擬內(nèi)存區(qū)域呢 ?

因?yàn)殡S著系統(tǒng)的運(yùn)行會(huì)伴隨著不同大小的物理內(nèi)存頁(yè)的分配和釋放,這種內(nèi)存不規(guī)則的分配釋放隨著系統(tǒng)的長(zhǎng)時(shí)間運(yùn)行就會(huì)導(dǎo)致內(nèi)存碎片,內(nèi)存碎片會(huì)使得系統(tǒng)在明明有足夠內(nèi)存的情況下,依然無(wú)法為進(jìn)程分配合適的內(nèi)存。

image

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

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

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

image

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

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

typedef struct pglist_data {
  // NUMA 節(jié)點(diǎn)中的物理內(nèi)存區(qū)域個(gè)數(shù)
	int nr_zones; 
  // NUMA 節(jié)點(diǎn)中的物理內(nèi)存區(qū)域
	struct zone node_zones[MAX_NR_ZONES];
  // NUMA 節(jié)點(diǎn)的備用列表
	struct zonelist node_zonelists[MAX_ZONELISTS];
} pg_data_t;

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

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

image

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

image

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

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

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

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

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

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

typedef struct pglist_data {
        .........
    // 頁(yè)面回收進(jìn)程
    struct task_struct *kswapd;
    wait_queue_head_t kswapd_wait;
    // 內(nèi)存規(guī)整進(jìn)程
    struct task_struct *kcompactd;
    wait_queue_head_t kcompactd_wait;

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

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

kswapd_wait 用于 kswapd 進(jìn)程周期性回收頁(yè)面時(shí)使用到的等待隊(duì)列。

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

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

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

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

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

如果系統(tǒng)中只有一個(gè) NUMA 節(jié)點(diǎn),則沒(méi)有節(jié)點(diǎn)位圖。

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

typedef struct { DECLARE_BITMAP(bits, MAX_NUMNODES); } nodemask_t;
extern nodemask_t node_states[NR_NODE_STATES];

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

enum node_states {
	N_POSSIBLE,		/* The node could become online at some point */
	N_ONLINE,		/* The node is online */
	N_NORMAL_MEMORY,	/* The node has regular memory */
#ifdef CONFIG_HIGHMEM
	N_HIGH_MEMORY,		/* The node has regular or high memory */
#else
	N_HIGH_MEMORY = N_NORMAL_MEMORY,
#endif
#ifdef CONFIG_MOVABLE_NODE
	N_MEMORY,		/* The node has memory(regular, high, movable) */
#else
	N_MEMORY = N_HIGH_MEMORY,
#endif
	N_CPU,		/* The node has one or more cpus */
	NR_NODE_STATES
};

N_POSSIBLE 表示 NUMA 節(jié)點(diǎn)在某個(gè)時(shí)刻可以變?yōu)?online 狀態(tài),N_ONLINE 表示 NUMA 節(jié)點(diǎn)當(dāng)前的狀態(tài)為 online 狀態(tài)。

我們?cè)诒疚摹?.3.1 物理內(nèi)存熱插拔》小節(jié)中提到,在稀疏內(nèi)存模型中,NUMA 節(jié)點(diǎn)的狀態(tài)可以在系統(tǒng)運(yùn)行的過(guò)程中隨時(shí)切換 online ,offline 的狀態(tài),用來(lái)支持內(nèi)存的熱插拔。

image

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

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

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

N_CPU 表示節(jié)點(diǎn)包含一個(gè)或多個(gè) CPU。

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

static inline void node_set_state(int node, enum node_states state)
static inline void node_clear_state(int node, enum node_states state)

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

#define for_each_node_state(__node, __state) 	for_each_node_mask((__node), node_states[__state])

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

#define for_each_online_node(node) for_each_node_state(node, N_ONLINE)

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

image

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

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

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

image

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

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

下圖中我們以 NUMA Node 0 中的 ZONE_NORMAL 區(qū)域?yàn)槔f(shuō)明,大家只需要瀏覽一個(gè)大概,圖中每個(gè)字段的含義筆者會(huì)在本小節(jié)的后面一一為大家介紹~~~

image

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

struct zone {

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

    ZONE_PADDING(_pad1_)

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

    ZONE_PADDING(_pad2_)

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

    ZONE_PADDING(_pad3_)

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

} ____cacheline_internodealigned_in_smp;

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

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

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

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

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

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

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

struct zone {
    // 防止并發(fā)訪問(wèn)該內(nèi)存區(qū)域
    spinlock_t      lock;
    // 內(nèi)存區(qū)域名稱:Normal ,DMA,HighMem
    const char      *name;
    // 指向該內(nèi)存區(qū)域所屬的 NUMA 節(jié)點(diǎn)
    struct pglist_data  *zone_pgdat;
    // 屬于該內(nèi)存區(qū)域中的第一個(gè)物理頁(yè) PFN
    unsigned long       zone_start_pfn;
    // 該內(nèi)存區(qū)域中所有的物理頁(yè)個(gè)數(shù)(包含內(nèi)存空洞)
    unsigned long       spanned_pages;
    // 該內(nèi)存區(qū)域所有可用的物理頁(yè)個(gè)數(shù)(不包含內(nèi)存空洞)
    unsigned long       present_pages;
    // 被伙伴系統(tǒng)所管理的物理頁(yè)數(shù)
    atomic_long_t       managed_pages;
    // 伙伴系統(tǒng)的核心數(shù)據(jù)結(jié)構(gòu)
    struct free_area    free_area[MAX_ORDER];
    // 該內(nèi)存區(qū)域內(nèi)存使用的統(tǒng)計(jì)信息
    atomic_long_t       vm_stat[NR_VM_ZONE_STAT_ITEMS];
} ____cacheline_internodealigned_in_smp;

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

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

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

typedef struct pglist_data {
    // NUMA 節(jié)點(diǎn)中的物理內(nèi)存區(qū)域個(gè)數(shù)
    int nr_zones; 
    // NUMA 節(jié)點(diǎn)中的物理內(nèi)存區(qū)域
    struct zone node_zones[MAX_NR_ZONES];
}

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

image

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

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

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

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

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

image

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

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

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

image

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

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

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

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

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

struct zone {
             ...........

    unsigned long nr_reserved_highatomic;
    long lowmem_reserve[MAX_NR_ZONES];
            
             ...........
}

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

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

那么什么是高位內(nèi)存區(qū)域 ?什么是低位內(nèi)存區(qū)域 ? 高位內(nèi)存區(qū)域?yàn)槭裁磿?huì)對(duì)低位內(nèi)存區(qū)域進(jìn)行侵占擠壓呢 ?

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

image

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

高位內(nèi)存區(qū)域?yàn)槭裁磿?huì)對(duì)低位內(nèi)存區(qū)域進(jìn)行擠壓呢 ?

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

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

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

但是內(nèi)核又不會(huì)允許高位內(nèi)存區(qū)域?qū)Φ臀粌?nèi)存區(qū)域的無(wú)限制擠壓占用,因?yàn)楫吘沟臀粌?nèi)存區(qū)域有它特定的用途,所以每個(gè)內(nèi)存區(qū)域會(huì)給自己預(yù)留一定的內(nèi)存,防止被高位內(nèi)存區(qū)域擠壓占用。而每個(gè)內(nèi)存區(qū)域?yàn)樽约侯A(yù)留的這部分內(nèi)存就存儲(chǔ)在 lowmem_reserve 數(shù)組中。

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

image

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

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

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

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

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

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

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

各個(gè)內(nèi)存區(qū)域?yàn)榉乐贡桓呶粌?nèi)存區(qū)域過(guò)度擠壓占用,而為自己預(yù)留的內(nèi)存大小,我們可以通過(guò)前邊 cat /proc/zoneinfo 命令來(lái)查看,輸出信息的 protection:則表示各個(gè)內(nèi)存區(qū)域預(yù)留內(nèi)存大小。

image

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

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

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

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

內(nèi)存資源是系統(tǒng)中最寶貴的系統(tǒng)資源,是有限的。當(dāng)內(nèi)存資源緊張的時(shí)候,系統(tǒng)的應(yīng)對(duì)方法無(wú)非就是三種:

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

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

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

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

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

對(duì)于文件頁(yè)的回收通常會(huì)比較簡(jiǎn)單,因?yàn)槲募?yè)中的數(shù)據(jù)來(lái)自于磁盤(pán),所以當(dāng)回收文件頁(yè)的時(shí)候直接回收就可以了,當(dāng)進(jìn)程再次讀取文件頁(yè)時(shí),大不了再?gòu)拇疟P(pán)中重新讀取就是了。

但是當(dāng)進(jìn)程已經(jīng)對(duì)文件頁(yè)進(jìn)行修改過(guò)但還沒(méi)來(lái)得及同步回磁盤(pán),此時(shí)文件頁(yè)就是臟頁(yè),不能直接進(jìn)行回收,需要先將臟頁(yè)回寫(xiě)到磁盤(pán)中才能進(jìn)行回收。

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

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

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

當(dāng)內(nèi)存資源緊張需要對(duì)不經(jīng)常使用的那些匿名頁(yè)進(jìn)行回收時(shí),因?yàn)槟涿?yè)的背后沒(méi)有一個(gè)磁盤(pán)中的文件做依托,所以匿名頁(yè)不能像文件頁(yè)那樣直接回收,無(wú)論匿名頁(yè)是不是臟頁(yè),都需要先將匿名頁(yè)中的數(shù)據(jù)先保存在磁盤(pán)空間中,然后在對(duì)匿名頁(yè)進(jìn)行回收。

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

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

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

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

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

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

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

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

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

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

image

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

enum zone_watermarks {
	WMARK_MIN,
	WMARK_LOW,
	WMARK_HIGH,
	NR_WMARK
};

#define min_wmark_pages(z) (z->_watermark[WMARK_MIN] + z->watermark_boost)
#define low_wmark_pages(z) (z->_watermark[WMARK_LOW] + z->watermark_boost)
#define high_wmark_pages(z) (z->_watermark[WMARK_HIGH] + z->watermark_boost)

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

struct zone {
    // 物理內(nèi)存區(qū)域中的水位線
    unsigned long _watermark[NR_WMARK];
    // 優(yōu)化內(nèi)存碎片對(duì)內(nèi)存分配的影響,可以動(dòng)態(tài)改變內(nèi)存區(qū)域的基準(zhǔn)水位線。
    unsigned long watermark_boost;

} ____cacheline_internodealigned_in_smp;

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

image

  • 當(dāng)該物理內(nèi)存區(qū)域的剩余內(nèi)存容量高于 _watermark[WMARK_HIGH] 時(shí),說(shuō)明此時(shí)該物理內(nèi)存區(qū)域中的內(nèi)存容量非常充足,內(nèi)存分配完全沒(méi)有壓力。

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

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

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

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

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

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

image

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

  • free 就是該物理內(nèi)存區(qū)域內(nèi)剩余的內(nèi)存頁(yè)數(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)活躍和非活躍的匿名頁(yè)數(shù)量。

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

5.3 水位線的計(jì)算

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

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

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

image

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

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

5.4 min_free_kbytes 的計(jì)算邏輯

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

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

int __meminit init_per_zone_wmark_min(void)
{
  // 低位內(nèi)存區(qū)域(除高端內(nèi)存之外)的總和
	unsigned long lowmem_kbytes;
  // 待計(jì)算的 min_free_kbytes
	int new_min_free_kbytes;

  // 將低位內(nèi)存區(qū)域內(nèi)存容量總的頁(yè)數(shù)轉(zhuǎn)換為 KB
	lowmem_kbytes = nr_free_buffer_pages() * (PAGE_SIZE >> 10);
  // min_free_kbytes 計(jì)算邏輯:對(duì) lowmem_kbytes * 16 進(jìn)行開(kāi)平方
	new_min_free_kbytes = int_sqrt(lowmem_kbytes * 16);
  // min_free_kbytes 的范圍為 128 到 65536 KB 之間
	if (new_min_free_kbytes > user_min_free_kbytes) {
		min_free_kbytes = new_min_free_kbytes;
		if (min_free_kbytes < 128)
			min_free_kbytes = 128;
		if (min_free_kbytes > 65536)
			min_free_kbytes = 65536;
	} else {
		pr_warn("min_free_kbytes is not updated to %d because user defined value %d is preferred\n",
				new_min_free_kbytes, user_min_free_kbytes);
	}
  // 計(jì)算內(nèi)存區(qū)域內(nèi)的三條水位線
	setup_per_zone_wmarks();
  // 計(jì)算內(nèi)存區(qū)域的預(yù)留內(nèi)存大小,防止被高位內(nèi)存區(qū)域過(guò)度擠壓占用
	setup_per_zone_lowmem_reserve();
        .............省略................
	return 0;
}
core_initcall(init_per_zone_wmark_min)

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

lowmem_kbytes 的計(jì)算邏輯在 nr_free_zone_pages 方法中:

/**
 * nr_free_zone_pages - count number of pages beyond high watermark
 * @offset: The zone index of the highest zone
 *
 * nr_free_zone_pages() counts the number of counts pages which are beyond the
 * high watermark within all zones at or below a given zone index.  For each
 * zone, the number of pages is calculated as:
 *     managed_pages - high_pages
 */
static unsigned long nr_free_zone_pages(int offset)
{
	struct zoneref *z;
	struct zone *zone;

	unsigned long sum = 0;
    // 獲取當(dāng)前 NUMA 節(jié)點(diǎn)中的所有物理內(nèi)存區(qū)域 zone
	struct zonelist *zonelist = node_zonelist(numa_node_id(), GFP_KERNEL);
    // 計(jì)算所有物理內(nèi)存區(qū)域內(nèi) managed_pages - high_pages 的總和
	for_each_zone_zonelist(zone, z, zonelist, offset) {
		unsigned long size = zone->managed_pages;
		unsigned long high = high_wmark_pages(zone);
		if (size > high)
			sum += size - high;
	}
    // lowmem_kbytes 的值
	return sum;
}

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

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

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

接下來(lái)在 init_per_zone_wmark_min 方法中會(huì)對(duì) lowmem_kbytes * 16 進(jìn)行開(kāi)平方得到 new_min_free_kbytes。

image

如果計(jì)算出的 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)核會(huì)根據(jù)這個(gè) min_free_kbytes 在 setup_per_zone_wmarks() 方法中計(jì)算出該物理內(nèi)存區(qū)域的三條水位線。

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

5.5 setup_per_zone_wmarks 計(jì)算水位線

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

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

static void __setup_per_zone_wmarks(void)
{
  // 將 min_free_kbytes 轉(zhuǎn)換為頁(yè)
	unsigned long pages_min = min_free_kbytes >> (PAGE_SHIFT - 10);
  // 所有低位內(nèi)存區(qū)域 managed_pages 之和
	unsigned long lowmem_pages = 0;
	struct zone *zone;
	unsigned long flags;

	/* Calculate total number of !ZONE_HIGHMEM pages */
	for_each_zone(zone) {
		if (!is_highmem(zone))
			lowmem_pages += zone->managed_pages;
	}

  // 循環(huán)計(jì)算各個(gè)內(nèi)存區(qū)域中的水位線
	for_each_zone(zone) {
		u64 tmp;
		tmp = (u64)pages_min * zone->managed_pages;
  // 計(jì)算 WMARK_MIN 水位線的核心方法
		do_div(tmp, lowmem_pages);
		if (is_highmem(zone)) {
            ...........省略高端內(nèi)存區(qū)域............
		} else {
    // WMARK_MIN水位線
			zone->watermark[WMARK_MIN] = tmp;
		}
  // 這里可暫時(shí)忽略
		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é)點(diǎn)中的所有內(nèi)存區(qū)域 zone,計(jì)算每個(gè)內(nèi)存區(qū)域 zone 里的內(nèi)存水位線。其中計(jì)算 WMARK_MIN 水位線的核心邏輯封裝在 do_div 方法中,在 do_div 方法中會(huì)先計(jì)算每個(gè) zone 內(nèi)存容量之間的比例,然后根據(jù)這個(gè)比例去從 min_free_kbytes 中劃分出對(duì)應(yīng) zone 的 WMARK_MIN 水位線來(lái)。

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

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

計(jì)算出了 WMARK_MIN 的值,那么接下來(lái) WMARK_LOW, WMARK_HIGH 的值也就好辦了,它們都是基于 WMARK_MIN 計(jì)算出來(lái)的。

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

此外,大家可能對(duì)下面這段代碼比較有疑問(wèn)?

      /*
         * Set the kswapd watermarks distance according to the
         * scale factor in proportion to available memory, but
         * ensure a minimum size on small systems.
         */
        tmp = max_t(u64, tmp >> 2,
                mult_frac(zone->managed_pages,
                      watermark_scale_factor, 10000));

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

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

image

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

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

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

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

這樣一來(lái)就要求 WMARK_MIN 與 WMARK_LOW 水位線之間的間距不能太小,因?yàn)?WMARK_LOW 水位線之上就不會(huì)喚醒 kswapd 進(jìn)程了。

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

image

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

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

        zone->watermark[WMARK_MIN] = tmp;
        // 水位線間距的計(jì)算邏輯
        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)核中水位線間距計(jì)算邏輯是:(WMARK_MIN / 4) 與 (zone_managed_pages * watermark_scale_factor / 10000) 之間較大的那個(gè)值。

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

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

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

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

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

image

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

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

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

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

image

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

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

struct zone {
    struct per_cpu_pageset	pageset[NR_CPUS];
}

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

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

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

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

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

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

struct per_cpu_pageset {
	struct per_cpu_pages pcp[2];	/* 0: hot.  1: cold */
}

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

struct per_cpu_pages {
	int count;		/* number of pages in the list */
	int high;		/* high watermark, emptying needed */
	int batch;		/* chunk size for buddy add/remove */
	struct list_head list;	/* the list of pages */
};
  • int count :表示集合中包含的物理頁(yè)數(shù)量,如果該結(jié)構(gòu)是熱頁(yè)集合,則表示加載進(jìn) CPU 高速緩存中的物理頁(yè)面?zhèn)€數(shù)。

  • struct list_head list :該 list 是一個(gè)雙向鏈表,保存了當(dāng)前 CPU 的熱頁(yè)或者冷頁(yè)。

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

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

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

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

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

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

struct zone {
	struct per_cpu_pages	__percpu *per_cpu_pageset;

	int pageset_high;
	int pageset_batch;

} ____cacheline_internodealigned_in_smp;

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

struct per_cpu_pages {
	int count;		/* number of pages in the list */
	int high;		/* high watermark, emptying needed */
	int batch;		/* chunk size for buddy add/remove */
        
        .............省略............

	/* Lists of pages, one per migrate type stored on the pcp-lists */
	struct list_head lists[NR_PCP_LISTS];
};

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

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

image

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

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

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

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

那么 Linux 為什么會(huì)默認(rèn)采用 4KB 作為標(biāo)準(zhǔn)物理內(nèi)存頁(yè)的大小呢 ?

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

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

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


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

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

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

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

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

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

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

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


struct page {
    // 存儲(chǔ) page 的定位信息以及相關(guān)標(biāo)志位
    unsigned long flags;        

    union {
        struct {    /* Page cache and anonymous pages */
            // 用來(lái)指向物理頁(yè) page 被放置在了哪個(gè) lru 鏈表上
            struct list_head lru;
            // 如果 page 為文件頁(yè)的話,低位為0,指向 page 所在的 page cache
            // 如果 page 為匿名頁(yè)的話,低位為1,指向其對(duì)應(yīng)虛擬地址空間的匿名映射區(qū) anon_vma
            struct address_space *mapping;
            // 如果 page 為文件頁(yè)的話,index 為 page 在 page cache 中的索引
            // 如果 page 為匿名頁(yè)的話,表示匿名頁(yè)在對(duì)應(yīng)進(jìn)程虛擬內(nèi)存區(qū)域 VMA 中的偏移
            pgoff_t index;
            // 在不同場(chǎng)景下,private 指向的場(chǎng)景信息不同
            unsigned long private;
        };
        
        struct {    /* slab, slob and slub */
            union {
                // 用于指定當(dāng)前 page 位于 slab 中的哪個(gè)具體管理鏈表上。
                struct list_head slab_list;
                struct {
                    // 當(dāng) page 位于 slab 結(jié)構(gòu)中的某個(gè)管理鏈表上時(shí),next 指針用于指向鏈表中的下一個(gè) page
                    struct page *next;
#ifdef CONFIG_64BIT
                    // 表示 slab 中總共擁有的 page 個(gè)數(shù)
                    int pages;  
                    // 表示 slab 中擁有的特定類型的對(duì)象個(gè)數(shù)
                    int pobjects;   
#else
                    short int pages;
                    short int pobjects;
#endif
                };
            };
            // 用于指向當(dāng)前 page 所屬的 slab 管理結(jié)構(gòu)
            struct kmem_cache *slab_cache; 
        
            // 指向 page 中的第一個(gè)未分配出去的空閑對(duì)象
            void *freelist;     
            union {
                // 指向 page 中的第一個(gè)對(duì)象
                void *s_mem;    
                struct {            /* SLUB */
                    // 表示 slab 中已經(jīng)被分配出去的對(duì)象個(gè)數(shù)
                    unsigned inuse:16;
                    // slab 中所有的對(duì)象個(gè)數(shù)
                    unsigned objects:15;
                    // 當(dāng)前內(nèi)存頁(yè) page 被 slab 放置在 CPU 本地緩存列表中,frozen = 1,否則 frozen = 0
                    unsigned frozen:1;
                };
            };
        };
        struct {    /* 復(fù)合頁(yè) compound page 相關(guān)*/
            // 復(fù)合頁(yè)的尾頁(yè)指向首頁(yè)
            unsigned long compound_head;    
            // 用于釋放復(fù)合頁(yè)的析構(gòu)函數(shù),保存在首頁(yè)中
            unsigned char compound_dtor;
            // 該復(fù)合頁(yè)有多少個(gè) page 組成
            unsigned char compound_order;
            // 該復(fù)合頁(yè)被多少個(gè)進(jìn)程使用,內(nèi)存頁(yè)反向映射的概念,首頁(yè)中保存
            atomic_t compound_mapcount;
        };

        // 表示 slab 中需要釋放回收的對(duì)象鏈表
        struct rcu_head rcu_head;
    };

    union {     /* This union is 4 bytes in size. */
        // 表示該 page 映射了多少個(gè)進(jìn)程的虛擬內(nèi)存空間,一個(gè) page 可以被多個(gè)進(jìn)程映射
        atomic_t _mapcount;

    };

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

#if defined(WANT_PAGE_VIRTUAL)
    void *virtual;  // 內(nèi)存頁(yè)對(duì)應(yīng)的虛擬內(nèi)存地址
#endif /* WANT_PAGE_VIRTUAL */

} _struct_page_alignment;

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

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

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

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

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

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

image

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

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

image

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

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

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

6.1 匿名頁(yè)的反向映射

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

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

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

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

image

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

struct vm_area_struct {  

    struct list_head anon_vma_chain;
    struct anon_vma *anon_vma;   
}

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

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

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

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

static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
{
        .............

	if (!vmf->pte) {
		if (vma_is_anonymous(vmf->vma))
            // 處理匿名頁(yè)缺頁(yè)
			return do_anonymous_page(vmf);
		else
            // 處理文件頁(yè)缺頁(yè)
			return do_fault(vmf);
	}

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

	if (vmf->flags & (FAULT_FLAG_WRITE|FAULT_FLAG_UNSHARE)) {
		if (!pte_write(entry))
            // 子進(jìn)程缺頁(yè)處理
			return do_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)匿名頁(yè)反向映射數(shù)據(jù)結(jié)構(gòu)的相互關(guān)聯(lián)。

static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
	struct vm_area_struct *vma = vmf->vma;
	struct page *page;	

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

	if (unlikely(anon_vma_prepare(vma)))
		goto oom;

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

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

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

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

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

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

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

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

struct page {
    struct address_space *mapping; 
    pgoff_t index;  
}

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

struct anon_vma_chain {
    // 匿名頁(yè)關(guān)聯(lián)的進(jìn)程虛擬內(nèi)存空間(vma屬于一個(gè)特定的進(jìn)程,多個(gè)進(jìn)程多個(gè)vma)
    struct vm_area_struct *vma;
    // 匿名頁(yè) page 指向的 anon_vma
    struct anon_vma *anon_vma;
    struct list_head same_vma;   
    struct rb_node rb;         
    unsigned long rb_subtree_last;
#ifdef CONFIG_DEBUG_VM_RB
    unsigned long cached_vma_start, cached_vma_last;
#endif
};

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

image

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

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

image

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

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

struct anon_vma {
    struct anon_vma *root;      /* Root of this anon_vma tree */
    struct rw_semaphore rwsem; 
    atomic_t refcount;
    unsigned degree;
    struct anon_vma *parent;    /* Parent of this anon_vma */
    struct rb_root rb_root; /* Interval tree of private "related" vmas */
};

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

struct anon_vma_chain {
    // 匿名頁(yè)關(guān)聯(lián)的進(jìn)程虛擬內(nèi)存空間(vma屬于一個(gè)特定的進(jìn)程,多個(gè)進(jìn)程多個(gè)vma)
    struct vm_area_struct *vma;
    // 匿名頁(yè) page 指向的 anon_vma
    struct anon_vma *anon_vma;
    // 指向 vm_area_struct 中的 anon_vma_chain 列表
    struct list_head same_vma;   
    // anon_vma 管理的紅黑樹(shù)中該 anon_vma_chain 對(duì)應(yīng)的紅黑樹(shù)節(jié)點(diǎn)
    struct rb_node rb;         
};

struct anon_vma_chain 結(jié)構(gòu)中的 rb 字段表示其在對(duì)應(yīng) anon_vma 管理的紅黑樹(shù)中的節(jié)點(diǎn)。

image

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

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

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

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

struct vm_area_struct {  
    // 存儲(chǔ)該 VMA 中所包含的所有匿名頁(yè) anon_vma
    struct list_head anon_vma_chain;
    // 用于快速判斷 VMA 有沒(méi)有對(duì)應(yīng)的匿名 page
    // 一個(gè) VMA 可以包含多個(gè) page,但是該區(qū)域內(nèi)的所有 page 只需要一個(gè) anon_vma 來(lái)反向映射即可。
    struct anon_vma *anon_vma;   
}

image

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

static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
	struct vm_area_struct *vma = vmf->vma;
	struct page *page;	

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

	if (unlikely(anon_vma_prepare(vma)))
		goto oom;

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

	if (!page)
		goto oom;

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

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

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

int __anon_vma_prepare(struct vm_area_struct *vma)
{
    // 獲取進(jìn)程虛擬內(nèi)存空間
	struct mm_struct *mm = vma->vm_mm;
    // 準(zhǔn)備為匿名頁(yè)分配 anon_vma 以及 anon_vma_chain
	struct anon_vma *anon_vma, *allocated;
	struct anon_vma_chain *avc;
    // 分配 anon_vma_chain 實(shí)例
	avc = anon_vma_chain_alloc(GFP_KERNEL);
	if (!avc)
		goto out_enomem;
    // 在相鄰的虛擬內(nèi)存區(qū)域 VMA 中查找可復(fù)用的 anon_vma
	anon_vma = find_mergeable_anon_vma(vma);
	allocated = NULL;
	if (!anon_vma) {
        // 沒(méi)有可復(fù)用的 anon_vma 則創(chuàng)建一個(gè)新的實(shí)例
		anon_vma = anon_vma_alloc();
		if (unlikely(!anon_vma))
			goto out_enomem_free_avc;
		allocated = anon_vma;
	}

	anon_vma_lock_write(anon_vma);
	/* page_table_lock to protect against threads */
	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);
		/* vma reference or self-parent link for new root */
		anon_vma->degree++;
		allocated = NULL;
		avc = NULL;
	}
        .................
}

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

static void anon_vma_chain_link(struct vm_area_struct *vma,
				struct anon_vma_chain *avc,
				struct anon_vma *anon_vma)
{
    // 通過(guò) anon_vma_chain 關(guān)聯(lián) anon_vma 和對(duì)應(yīng)的 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 管理的紅黑樹(shù) rb_root 中
	anon_vma_interval_tree_insert(avc, &anon_vma->rb_root);
}

image

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

static void __page_set_anon_rmap(struct page *page,
    struct vm_area_struct *vma, unsigned long address, int exclusive)
{
    struct anon_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 = (struct address_space *) anon_vma;
    // page 結(jié)構(gòu)中的 index 表示該匿名頁(yè)在虛擬內(nèi)存區(qū)域 vma 中的偏移
    page->index = linear_page_index(vma, address);
}

現(xiàn)在讓我們?cè)俅位氐奖拘」?jié) 《6.1 匿名頁(yè)的反向映射》的開(kāi)始,再來(lái)看這段話,是不是感到非常清晰了呢~~

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

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

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

    struct anon_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 表示匿名頁(yè)。

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

anon_vma = (struct anon_vma *) (mapping - PAGE_MAPPING_ANON)

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

#define PAGE_MAPPING_ANON	0x1

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

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

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

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

static inline pgoff_t linear_page_index(struct vm_area_struct *vma,
                    unsigned long address)
{
    pgoff_t pgoff;
    if (unlikely(is_vm_hugetlb_page(vma)))
        return linear_hugepage_index(vma, address);
    pgoff = (address - vma->vm_start) >> PAGE_SHIFT;
    pgoff += vma->vm_pgoff;
    return pgoff;
}

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

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

struct page {
    struct address_space *mapping; 
    pgoff_t index;  
    // 表示該 page 映射了多少個(gè)進(jìn)程的虛擬內(nèi)存空間,一個(gè) page 可以被多個(gè)進(jìn)程映射
    atomic_t _mapcount
}

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

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

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

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

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

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

image

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

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

為什么會(huì)有 active 鏈表和 inactive 鏈表?

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

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

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

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

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

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

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

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

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

為什么會(huì)把 active 鏈表和 inactive 鏈表分成兩類,一類是匿名頁(yè),一類是文件頁(yè)?

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

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

因?yàn)榛厥漳涿?yè)和回收文件頁(yè)的代價(jià)是不一樣的,回收匿名頁(yè)代價(jià)會(huì)更高一點(diǎn),所以引入 swappiness 來(lái)控制內(nèi)核回收的傾向。

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

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

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

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

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

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

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

struct page {
   struct list_head lru;
   atomic_t _refcount;
}

struct list_head lru 屬性就是用來(lái)指向物理頁(yè)被放置在了哪個(gè)鏈表上。

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

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

struct page {
    unsigned long flags;
} 

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

image

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

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

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

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

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

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

image

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

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

struct page {
    unsigned long flags;
} 

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

image

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

static inline unsigned long page_to_section(const struct page *page)
{
	return (page->flags >> SECTIONS_PGSHIFT) & SECTIONS_MASK;
}

static inline pg_data_t *page_pgdat(const struct page *page)
{
	return NODE_DATA(page_to_nid(page));
}

static inline struct zone *page_zone(const struct page *page)
{
	return &NODE_DATA(page_to_nid(page))->node_zones[page_zonenum(page)];
}

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

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

enum pageflags {
	PG_locked,		/* Page is locked. Don't touch. */
	PG_referenced,
	PG_uptodate,
	PG_dirty,
	PG_lru,
	PG_active,
	PG_slab,
	PG_reserved,
    PG_compound,
	PG_private,		
	PG_writeback,		
	PG_reclaim,		
#ifdef CONFIG_MMU
	PG_mlocked,		/* Page is vma mlocked */
	PG_swapcache = PG_owner_priv_1,	

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

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

  • PG_referenced 表示該物理頁(yè)面剛剛被訪問(wèn)過(guò)。

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

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

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

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

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

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

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

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

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

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

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

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

  • PG_private 標(biāo)志被置位的時(shí)候表示該 struct page 結(jié)構(gòu)中的 private 指針指向了具體的對(duì)象。不同場(chǎng)景指向的對(duì)象不同。

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

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

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

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

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

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

static inline void wait_on_page_locked(struct page *page)
static inline void wait_on_page_writeback(struct page *page)

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

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

6.4 復(fù)合頁(yè) compound_page 相關(guān)屬性

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

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

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

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

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

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

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

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

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

image

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

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

      struct page {      
            // 首頁(yè) page 中的 flags 會(huì)被設(shè)置為 PG_head 表示復(fù)合頁(yè)的第一頁(yè)
            unsigned long flags;	
            // 其余尾頁(yè)會(huì)通過(guò)該字段指向首頁(yè)
            unsigned long compound_head;   
            // 用于釋放復(fù)合頁(yè)的析構(gòu)函數(shù),保存在首頁(yè)中
            unsigned char compound_dtor;
            // 該復(fù)合頁(yè)有多少個(gè) page 組成,order 還是分配階的概念,首頁(yè)中保存
            // 本例中的 order = 2 表示由 4 個(gè)普通頁(yè)組成
            unsigned char compound_order;
            // 該復(fù)合頁(yè)被多少個(gè)進(jìn)程使用,內(nèi)存頁(yè)反向映射的概念,首頁(yè)中保存
            atomic_t compound_mapcount;
            // 復(fù)合頁(yè)使用計(jì)數(shù),首頁(yè)中保存
            atomic_t compound_pincount;
      }

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

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

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

image

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

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

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

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

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

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

struct page {

        struct {    /* slab, slob and slub */
            union {
                struct list_head slab_list;
                struct {    /* Partial pages */
                    struct page *next;
#ifdef CONFIG_64BIT
                    int pages;  /* Nr of pages left */
                    int pobjects;   /* Approximate count */
#else
                    short int pages;
                    short int pobjects;
#endif
                };
            };
            struct kmem_cache *slab_cache; /* not slob */
            /* Double-word boundary */
            void *freelist;     /* first free object */
            union {
                void *s_mem;    /* slab: first object */
                struct {            /* SLUB */
                    unsigned inuse:16;
                    unsigned objects:15;
                    unsigned frozen:1;
                };
            };
        };

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

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

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

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

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

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

  • void *s_mem : 指向 page 中的第一個(gè)對(duì)象。

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

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

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

總結(jié)

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

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

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

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

image

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

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

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

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買(mǎi)等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多