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)形式: 在我們清楚了虛擬內(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ì)造成地址沖突。 而虛擬內(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)存地址空間隔離的功勞。 在我們清楚了虛擬內(nèi)存空間引入的意義之后,筆者緊接著為大家介紹了進(jìn)程用戶態(tài)虛擬內(nèi)存空間分別在 32 位機(jī)器和 64 位機(jī)器上的布局情況: 在了解了用戶態(tài)虛擬內(nèi)存空間的布局之后,緊接著我們又介紹了 Linux 內(nèi)核如何對(duì)用戶態(tài)虛擬內(nèi)存空間進(jìn)行管理以及相應(yīng)的管理數(shù)據(jù)結(jié)構(gòu): 在介紹完用戶態(tài)虛擬內(nèi)存空間的布局以及管理之后,我們隨后又介紹了內(nèi)核態(tài)虛擬內(nèi)存空間的布局情況,并結(jié)合之前介紹的用戶態(tài)虛擬內(nèi)存空間,得到了 Linux 虛擬內(nèi)存空間分別在 32 位和 64 位系統(tǒng)中的整體布局情況: 在虛擬內(nèi)存全部介紹完畢之后,為了能夠承上啟下,于是筆者繼續(xù)在上篇文章的最后一個(gè)小節(jié)從計(jì)算機(jī)組成原理的角度介紹了物理內(nèi)存的物理組織結(jié)構(gòu),方便讓大家理解到底什么是真正的物理內(nèi)存 ?物理內(nèi)存地址到底是什么 ?由此為本文的主題 —— 物理內(nèi)存的管理 ,埋下伏筆~~~ 最后筆者介紹了 CPU 如何通過(guò)物理內(nèi)存地址向物理內(nèi)存讀寫(xiě)數(shù)據(jù)的完整過(guò)程: 在我們回顧完上篇文章介紹的用戶態(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)行管理 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 。 內(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)行偏移操作。
Linux 早期使用的就是這種內(nèi)存模型,因?yàn)樵?Linux 發(fā)展的早期所需要管理的物理內(nèi)存通常不大(比如幾十 MB),那時(shí)的 Linux 使用平坦內(nèi)存模型 FLATMEM 來(lái)管理物理內(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)。 為了組織和管理這些不連續(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)。 內(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è)
我們可以看出 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 的操作。
當(dāng)定位到物理頁(yè) struct page 所在 node 之后,剩下的邏輯就和 FLATMEM 內(nèi)存模型一模一樣了。
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ù)的。 而且每個(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。
由于 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)功能。
在 SPARSEMEM 稀疏內(nèi)存模型下 page_to_pfn 與 pfn_to_page 的計(jì)算邏輯又發(fā)生了變化。
從以上的內(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è)階段:
物理內(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)存上. 。 但是這里會(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ì)改變。 但是在內(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è)。
然后在這些可能會(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ò)總線完成的。
上圖展示的是單核 CPU 訪問(wèn)內(nèi)存的架構(gòu)圖,那么在多核服務(wù)器中多個(gè) CPU 與內(nèi)存之間的架構(gòu)關(guān)系又是什么樣子的呢? 在 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ì)稱多處理器。
但是隨著多核技術(shù)的發(fā)展,服務(wù)器上的 CPU 個(gè)數(shù)會(huì)越來(lái)越多,而 UMA 架構(gòu)下所有 CPU 都是需要通過(guò)總線來(lái)訪問(wèn)內(nèi)存的,這樣總線很快就會(huì)成為性能瓶頸,主要體現(xiàn)在以下兩個(gè)方面:
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)。 如上圖所示,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))。
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)存 ?
我們可以在應(yīng)用程序中通過(guò) libnuma 共享庫(kù)中的 API 調(diào)用 set_mempolicy 接口設(shè)置進(jìn)程的內(nèi)存分配策略。
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)信息
針對(duì)以上具體問(wèn)題,
大家可以關(guā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ò)
通過(guò)
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)綁定,從而可以提升程序的性能。
另外我們還可以通過(guò) 我們可以通過(guò) numactl 命令將 numatest 進(jìn)程分別綁定在相同的 NUMA 節(jié)點(diǎn)上和不同的 NUMA 節(jié)點(diǎn)上,運(yùn)行觀察。
大家肯定一眼就能看出綁定在相同 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)存是最快的。
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)的~~
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)的,單鏈表定義在
每個(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。
在內(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)。 全局?jǐn)?shù)組 node_data[] 定義在文件
node_data[] 數(shù)組大小 MAX_NUMNODES 定義在
4.2 NUMA 節(jié)點(diǎn)描述符 pglist_data 結(jié)構(gòu)
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è)。 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)唯一) 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ù)。 以上內(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。
而直接映射區(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ū)域:
以上這些物理內(nèi)存區(qū)域的劃分定義在
大家可能注意到內(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)存。 如上圖所示,假如現(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è))
如果這些物理頁(yè)處于 ZONE_MOVABLE 區(qū)域,它們就可以被遷移,內(nèi)核可以通過(guò)遷移頁(yè)面來(lái)避免內(nèi)存碎片的問(wèn)題: 內(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ū)域:
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ū)域。 ZONE_NORMAL、ZONE_HIGHMEM 和 ZONE_MOVABLE 是可以出現(xiàn)在所有 NUMA 節(jié)點(diǎn)上的。 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)存。
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)存碎片。
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ì)列。
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)信息。
節(jié)點(diǎn)位圖以及節(jié)點(diǎn)的狀態(tài)掩碼值定義在
節(jié)點(diǎn)的狀態(tài)可通過(guò)以下掩碼表示:
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)存的熱插拔。 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):
內(nèi)核提供了 for_each_node_state 宏用于迭代處于特定狀態(tài)的所有 NUMA 節(jié)點(diǎn)。
比如:for_each_online_node 用于迭代所有 online 的 NUMA 節(jié)點(diǎn):
5. 內(nèi)核如何管理 NUMA 節(jié)點(diǎn)中的物理內(nèi)存區(qū)域在前邊《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ū)域。
我們可以通過(guò)
通過(guò)
內(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 結(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)核還是用了
筆者為了使大家能夠更好地理解內(nèi)核如何使用 struct zone 結(jié)構(gòu)體來(lái)描述內(nèi)存區(qū)域,從而把結(jié)構(gòu)體中的字段按照一定的層次結(jié)構(gòu)重新排列介紹,這并不是原生的字段對(duì)齊方式,這一點(diǎn)需要大家注意?。?!
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)。
這些物理內(nèi)存區(qū)域也會(huì)通過(guò) struct zone 中的 zone_pgdat 指向自己所屬的 NUMA 節(jié)點(diǎn)。 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ò) present_pages 則表示該內(nèi)存區(qū)域內(nèi)所有實(shí)際可用的物理頁(yè)面總數(shù)(不包含內(nèi)存空洞),通過(guò) 在 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)存的分配和釋放。
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ì)信息,前邊介紹的 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ú)外乎有兩種方式:
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)行排列布局: 根據(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ò) 從左到右分別代表了 ZONE_DMA,ZONE_DMA32,ZONE_NORMAL,ZONE_MOVABLE,ZONE_DEVICE 物理內(nèi)存區(qū)域的預(yù)留內(nèi)存比例。
那么每個(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。
各個(gè)內(nèi)存區(qū)域?yàn)榉乐贡桓呶粌?nèi)存區(qū)域過(guò)度擠壓占用,而為自己預(yù)留的內(nèi)存大小,我們可以通過(guò)前邊 此外我們還可以通過(guò) 前面介紹的物理內(nèi)存區(qū)域內(nèi)被伙伴系統(tǒng)所管理的物理頁(yè)數(shù)量 managed_pages 的計(jì)算方式就通過(guò) present_pages 減去這些預(yù)留的物理內(nèi)存頁(yè) reserved_pages 得到的。
5.2 物理內(nèi)存區(qū)域中的水位線內(nèi)存資源是系統(tǒng)中最寶貴的系統(tǒng)資源,是有限的。當(dāng)內(nèi)存資源緊張的時(shí)候,系統(tǒng)的應(yīng)對(duì)方法無(wú)非就是三種:
我們都知道,內(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)核線程。
而另外一種物理頁(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ò) swappiness 用于表示 Swap 機(jī)制的積極程度,數(shù)值越大,Swap 的積極程度越高,內(nèi)核越傾向于回收匿名頁(yè)。數(shù)值越小,Swap 的積極程度越低。內(nèi)核就越傾向于回收文件頁(yè)。
那么到底什么時(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è)高閾值)。 這三條水位線定義在
這三條水位線對(duì)應(yīng)的 watermark 數(shù)值存儲(chǔ)在每個(gè)物理內(nèi)存區(qū)域 struct zone 結(jié)構(gòu)中的 _watermark[NR_WMARK] 數(shù)組中。
我們可以通過(guò) 其中大部分字段的含義筆者已經(jīng)在前面的章節(jié)中為大家介紹過(guò)了,下面我們只介紹和本小節(jié)內(nèi)容相關(guān)的字段含義:
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ù)
通常情況下 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ì)算邏輯
min_free_kbytes 的計(jì)算邏輯定義在內(nèi)核文件
首先我們需要先計(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 方法上面的注釋大家可能看的有點(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。 如果計(jì)算出的 new_min_free_kbytes 大于用戶設(shè)置的內(nèi)核參數(shù)值
隨后內(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ì)算水位線
物理內(nèi)存區(qū)域內(nèi)的三條水位線:WMARK_MIN,WMARK_LOW,WMARK_HIGH 的最終計(jì)算邏輯是在
在 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 * 計(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)?
這段代碼主要是通過(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)整水位線的間距為了避免內(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)核引入了 那么如何使用 watermark_scale_factor 參數(shù)調(diào)整水位線之間的間距呢? 水位線間距計(jì)算公式:(watermark_scale_factor / 10000) * managed_pages 。
在內(nèi)核中水位線間距計(jì)算邏輯是:(WMARK_MIN / 4) 與 (zone_managed_pages * watermark_scale_factor / 10000) 之間較大的那個(gè)值。 用戶可以通過(guò) 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è)緩存。 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)用程序的性能。
本文我們的主題是 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è)呢?
筆者先以內(nèi)核版本 2.6.25 之前的冷熱頁(yè)相關(guān)的管理邏輯為大家講解,因?yàn)檫@個(gè)版本的邏輯比較直觀,大家更容易理解。在這個(gè)基礎(chǔ)之上,筆者會(huì)在介紹內(nèi)核 5.0 版本對(duì)于冷熱頁(yè)管理的邏輯,差別不是很大。
在 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_pages 結(jié)構(gòu)則是最終用于管理 CPU 高速緩存中的熱頁(yè),冷頁(yè)集合的數(shù)據(jù)結(jié)構(gòu):
以上則是內(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 per_cpu_pages 結(jié)構(gòu)的鏈表來(lái)集中管理系統(tǒng)中所有 CPU 高速緩存冷熱頁(yè)。
前面我們提到,內(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è)經(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)介紹。
下面筆者就來(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)景:
我們首先來(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 感到陌生。 在內(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è), 同時(shí)該 pgoff_t index 字段也表示該內(nèi)存頁(yè)中的文件數(shù)據(jù)在文件內(nèi)部的偏移 offset。偏移單位為 page size。
如果當(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ū)域。 這里筆者只列舉出 struct vm_area_struct 結(jié)構(gòu)中與匿名頁(yè)反向映射相關(guān)的字段屬性:
這里大家可能會(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)系(正向映射)。
這里我們主要關(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)。
在 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)。
只要我們實(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 結(jié)構(gòu)通過(guò)其中的 vma 指針和 anon_vma 指針將相關(guān)的匿名頁(yè)與其映射的進(jìn)程虛擬內(nèi)存空間關(guān)聯(lián)了起來(lái)。 從目前來(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。 不同的 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)中尋找一下線索:
我們重點(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 結(jié)構(gòu)中的 rb 字段表示其在對(duì)應(yīng) anon_vma 管理的紅黑樹(shù)中的節(jié)點(diǎn)。 到目前為止,物理內(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)中的
現(xiàn)在整個(gè)匿名頁(yè)到進(jìn)程虛擬內(nèi)存空間的反向映射鏈路關(guān)系,筆者就為大家梳理清楚了,下面我們接著回到 do_anonymous_page 函數(shù)中,來(lái)一一驗(yà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)系。
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)系:
到現(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 的整條反向映射鏈路。
現(xiàn)在讓我們?cè)俅位氐奖拘」?jié) 《6.1 匿名頁(yè)的反向映射》的開(kāi)始,再來(lái)看這段話,是不是感到非常清晰了呢~~
struct page 結(jié)構(gòu)中的
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):
PAGE_MAPPING_ANON 常量定義在內(nèi)核
而對(duì)于文件頁(yè)來(lái)說(shuō),page 結(jié)構(gòu)的 mapping 指針最低位本來(lái)就是 0 ,因?yàn)?address_space 類型的指針實(shí)現(xiàn)總是對(duì)齊至 內(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ù):
邏輯很簡(jiǎn)單,就是表示匿名頁(yè)在對(duì)應(yīng)進(jìn)程虛擬內(nèi)存區(qū)域 VMA 中的偏移。 在本小節(jié)最后,還有一個(gè)與反向映射相關(guān)的重要屬性就是 page 結(jié)構(gòu)中的 _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ò)
為什么會(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 鏈表。工作原理如下:
為什么會(huì)把 active 鏈表和 inactive 鏈表分成兩類,一類是匿名頁(yè),一類是文件頁(yè)? 在本文 《5.2 物理內(nèi)存區(qū)域中的水位線》小節(jié)中,筆者為大家介紹了一個(gè)叫做 swappiness 的內(nèi)核參數(shù), 我們可以通過(guò) swappiness 用于表示 Swap 機(jī)制的積極程度,數(shù)值越大,Swap 的積極程度,越高越傾向于回收匿名頁(yè)。數(shù)值越小,Swap 的積極程度越低,越傾向于回收文件頁(yè)。 因?yàn)榛厥漳涿?yè)和回收文件頁(yè)的代價(jià)是不一樣的,回收匿名頁(yè)代價(jià)會(huì)更高一點(diǎn),所以引入 swappiness 來(lái)控制內(nèi)核回收的傾向。
假設(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 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
在本文 《2.3 SPARSEMEM 稀疏內(nèi)存模型》小節(jié)中,我們提到,內(nèi)核為了能夠更靈活地管理粒度更小的連續(xù)物理內(nèi)存,于是就此引入了 SPARSEMEM 稀疏內(nèi)存模型。 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。
那么在 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ì)物盡其用。
因此這個(gè) unsigned long 類型的 flags 字段中不僅包含上面提到的定位信息還會(huì)包括物理內(nèi)存頁(yè)的一些屬性和標(biāo)志位。flags 字段的高 8 位用來(lái)表示 struct page 的定位信息,剩余低位表示特定的標(biāo)志位。 struct page 與其所屬上層結(jié)構(gòu)轉(zhuǎn)換的相應(yīng)函數(shù)定義在
在我們介紹完了 flags 字段中高位存儲(chǔ)的位置定位信息之后,接下來(lái)就該來(lái)介紹下在低位比特中表示的物理內(nèi)存頁(yè)的那些標(biāo)志位~~ 物理內(nèi)存頁(yè)的這些標(biāo)志位定義在內(nèi)核
除此之外內(nèi)核還定義了一些標(biāo)準(zhǔn)宏,用來(lái)檢查某個(gè)物理內(nèi)存頁(yè) page 是否設(shè)置了特定的標(biāo)志位,以及對(duì)這些標(biāo)志位的操作,這些宏在內(nèi)核中的實(shí)現(xiàn)都是原子的,命名格式如下:
另外在很多情況下,內(nèi)核通常需要等待物理頁(yè) page 的某個(gè)狀態(tài)改變,才能繼續(xù)恢復(fù)工作,內(nèi)核提供了如下兩個(gè)輔助函數(shù),來(lái)實(shí)現(xiàn)在特定狀態(tài)的阻塞等待:
當(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à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: 組成復(fù)合頁(yè)的第一個(gè) page 我們稱之為首頁(yè)(Head Page),其余的均稱之為尾頁(yè)(Tail Page)。 我們來(lái)看一下 struct page 中關(guān)于描述 compound_page 的相關(guān)字段:
首頁(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 。 6.5 Slab 對(duì)象池相關(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ù)。
總結(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)。它們的層次如下圖所示: 在把握了物理內(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)~~~ |
|