3.5 萬(wàn)字 + 60 張圖 |一步一圖帶你深入理解 Linux 虛擬內(nèi)存管理

寫(xiě)在本文開(kāi)始之前....
這篇文章很硬核,大家一次性看不完可以,收藏起來(lái)慢慢看。

內(nèi)存管理子系統(tǒng)可謂是 Linux 內(nèi)核眾多子系統(tǒng)中最為復(fù)雜最為龐大的一個(gè),其中包含了眾多繁雜的概念和原理,通過(guò)內(nèi)存管理這條主線(xiàn)我們把可以把操作系統(tǒng)的眾多核心系統(tǒng)給拎出來(lái),比如:進(jìn)程管理子系統(tǒng),網(wǎng)絡(luò)子系統(tǒng),文件子系統(tǒng)等。
由于內(nèi)存管理子系統(tǒng)過(guò)于復(fù)雜龐大,其中涉及到的眾多繁雜的概念又是一環(huán)套一環(huán),層層遞進(jìn)。如何把這些繁雜的概念具有層次感地,并且清晰地,給大家梳理呈現(xiàn)出來(lái)真是一件比較有難度的事情,因此關(guān)于這個(gè)問(wèn)題,筆者在動(dòng)筆寫(xiě)這個(gè)內(nèi)存管理源碼解析系列之前也是思考了很久。
萬(wàn)事開(kāi)頭難,那么到底什么內(nèi)容適合作為這個(gè)系列的開(kāi)篇呢 ?筆者還是覺(jué)得從大家日常開(kāi)發(fā)工作中接觸最多最為熟悉的部分開(kāi)始比較好,比如:在我們?nèi)粘i_(kāi)發(fā)中創(chuàng)建的類(lèi),調(diào)用的函數(shù),在函數(shù)中定義的局部變量以及 new 出來(lái)的數(shù)據(jù)容器(Map,List,Set .....等)都需要存儲(chǔ)在物理內(nèi)存中的某個(gè)角落。
而我們?cè)诔绦蛑芯帉?xiě)業(yè)務(wù)邏輯代碼的時(shí)候,往往需要引用這些創(chuàng)建出來(lái)的數(shù)據(jù)結(jié)構(gòu),并通過(guò)這些引用對(duì)相關(guān)數(shù)據(jù)結(jié)構(gòu)進(jìn)行業(yè)務(wù)處理。
當(dāng)程序運(yùn)行起來(lái)之后就變成了進(jìn)程,而這些業(yè)務(wù)數(shù)據(jù)結(jié)構(gòu)的引用在進(jìn)程的視角里全都都是虛擬內(nèi)存地址,因?yàn)檫M(jìn)程無(wú)論是在用戶(hù)態(tài)還是在內(nèi)核態(tài)能夠看到的都是虛擬內(nèi)存空間,物理內(nèi)存空間被操作系統(tǒng)所屏蔽進(jìn)程是看不到的。
進(jìn)程通過(guò)虛擬內(nèi)存地址訪(fǎng)問(wèn)這些數(shù)據(jù)結(jié)構(gòu)的時(shí)候,虛擬內(nèi)存地址會(huì)在內(nèi)存管理子系統(tǒng)中被轉(zhuǎn)換成物理內(nèi)存地址,通過(guò)物理內(nèi)存地址就可以訪(fǎng)問(wèn)到真正存儲(chǔ)這些數(shù)據(jù)結(jié)構(gòu)的物理內(nèi)存了。隨后就可以對(duì)這塊物理內(nèi)存進(jìn)行各種業(yè)務(wù)操作,從而完成業(yè)務(wù)邏輯。
那么到底什么是虛擬內(nèi)存地址 ?
Linux 內(nèi)核為啥要引入虛擬內(nèi)存而不直接使用物理內(nèi)存 ?
虛擬內(nèi)存空間到底長(zhǎng)啥樣?
內(nèi)核如何管理虛擬內(nèi)存?
什么又是物理內(nèi)存地址 ?如何訪(fǎng)問(wèn)物理內(nèi)存?
本文筆者就來(lái)為大家詳細(xì)一一解答上述幾個(gè)問(wèn)題,讓我們馬上開(kāi)始吧~~~~

1. 到底什么是虛擬內(nèi)存地址
首先人們提出地址這個(gè)概念的目的就是用來(lái)方便定位現(xiàn)實(shí)世界中某一個(gè)具體事物的真實(shí)地理位置,它是一種用于定位的概念模型。
舉一個(gè)生活中的例子,比如大家在日常生活中給親朋好友郵寄一些本地特產(chǎn)時(shí),都會(huì)填寫(xiě)收件人地址以及寄件人地址。以及在日常網(wǎng)上購(gòu)物時(shí),都會(huì)在相應(yīng)電商 APP 中填寫(xiě)自己的收獲地址。

隨后快遞小哥就會(huì)根據(jù)我們填寫(xiě)的收貨地址找到我們的真實(shí)住所,將我們網(wǎng)購(gòu)的商品送達(dá)到我們的手里。
收貨地址是用來(lái)定位我們?cè)诂F(xiàn)實(shí)世界中真實(shí)住所地理位置的,而現(xiàn)實(shí)世界中我們所在的城市,街道,小區(qū),房屋都是一磚一瓦,一草一木真實(shí)存在的。但收貨地址這個(gè)概念模型在現(xiàn)實(shí)世界中并不真實(shí)存在,它只是人們提出的一個(gè)虛擬概念,通過(guò)收貨地址這個(gè)虛擬概念將它和現(xiàn)實(shí)世界真實(shí)存在的城市,小區(qū),街道的地理位置一一映射起來(lái),這樣我們就可以通過(guò)這個(gè)虛擬概念來(lái)找到現(xiàn)實(shí)世界中的具體地理位置。
綜上所述,收貨地址是一個(gè)虛擬地址,它是人為定義的,而我們的城市,小區(qū),街道是真實(shí)存在的,他們的地理位置就是物理地址。

比如現(xiàn)在的廣東省深圳市在過(guò)去叫寶安縣,河北省的石家莊過(guò)去叫常山,安徽省的合肥過(guò)去叫瀘州。不管是常山也好,石家莊也好,又或是合肥也好,瀘州也罷,這些都是人為定義的名字而已,但是地方還是那個(gè)地方,它所在的地理位置是不變的。也就說(shuō)虛擬地址可以人為的變來(lái)變?nèi)ィ俏锢淼刂酚肋h(yuǎn)是不變的。
現(xiàn)在讓我們把視角在切換到計(jì)算機(jī)的世界,在計(jì)算機(jī)的世界里內(nèi)存地址用來(lái)定義數(shù)據(jù)在內(nèi)存中的存儲(chǔ)位置的,內(nèi)存地址也分為虛擬地址和物理地址。而虛擬地址也是人為設(shè)計(jì)的一個(gè)概念,類(lèi)比我們現(xiàn)實(shí)世界中的收貨地址,而物理地址則是數(shù)據(jù)在物理內(nèi)存中的真實(shí)存儲(chǔ)位置,類(lèi)比現(xiàn)實(shí)世界中的城市,街道,小區(qū)的真實(shí)地理位置。
說(shuō)了這么多,那么到底虛擬內(nèi)存地址長(zhǎng)什么樣子呢?
我們還是以日常生活中的收貨地址為例做出類(lèi)比,我們都很熟悉收貨地址的格式:xx省xx市xx區(qū)xx街道xx小區(qū)xx室,它是按照地區(qū)層次遞進(jìn)的。同樣,在計(jì)算機(jī)世界中的虛擬內(nèi)存地址也有這樣的遞進(jìn)關(guān)系。
這里我們以 Intel Core i7 處理器為例,64 位虛擬地址的格式為:全局頁(yè)目錄項(xiàng)(9位)+ 上層頁(yè)目錄項(xiàng)(9位)+ 中間頁(yè)目錄項(xiàng)(9位)+ 頁(yè)內(nèi)偏移(12位)。共 48 位組成的虛擬內(nèi)存地址。

虛擬內(nèi)存地址中的全局頁(yè)目錄項(xiàng)就類(lèi)比我們?nèi)粘I钪惺斋@地址里的省,上層頁(yè)目錄項(xiàng)就類(lèi)比市,中間層頁(yè)目錄項(xiàng)類(lèi)比區(qū)縣,頁(yè)表項(xiàng)類(lèi)比街道小區(qū),頁(yè)內(nèi)偏移類(lèi)比我們所在的樓棟和幾層幾號(hào)。
這里大家只需要大體明白虛擬內(nèi)存地址到底長(zhǎng)什么樣子,它的格式是什么,能夠和日常生活中的收貨地址對(duì)比理解起來(lái)就可以了,至于頁(yè)目錄項(xiàng),頁(yè)表項(xiàng)以及頁(yè)內(nèi)偏移這些計(jì)算機(jī)世界中的概念,大家暫時(shí)先不用管,后續(xù)文章中筆者會(huì)慢慢給大家解釋清楚。
32 位虛擬地址的格式為:頁(yè)目錄項(xiàng)(10位)+ 頁(yè)表項(xiàng)(10位) + 頁(yè)內(nèi)偏移(12位)。共 32 位組成的虛擬內(nèi)存地址。

進(jìn)程虛擬內(nèi)存空間中的每一個(gè)字節(jié)都有與其對(duì)應(yīng)的虛擬內(nèi)存地址,一個(gè)虛擬內(nèi)存地址表示進(jìn)程虛擬內(nèi)存空間中的一個(gè)特定的字節(jié)。
2. 為什么要使用虛擬地址訪(fǎng)問(wèn)內(nèi)存
經(jīng)過(guò)第一小節(jié)的介紹,我們現(xiàn)在明白了計(jì)算機(jī)世界中的虛擬內(nèi)存地址的含義及其展現(xiàn)形式。那么大家可能會(huì)問(wèn)了,既然物理內(nèi)存地址可以直接定位到數(shù)據(jù)在內(nèi)存中的存儲(chǔ)位置,那為什么我們不直接使用物理內(nèi)存地址去訪(fǎng)問(wèn)內(nèi)存而是選擇用虛擬內(nèi)存地址去訪(fǎng)問(wèn)內(nèi)存呢?
在回答大家的這個(gè)疑問(wèn)之前,讓我們先來(lái)看下,如果在程序中直接使用物理內(nèi)存地址會(huì)發(fā)生什么情況?
假設(shè)現(xiàn)在沒(méi)有虛擬內(nèi)存地址,我們?cè)诔绦蛑袑?duì)內(nèi)存的操作全都都是使用物理內(nèi)存地址,在這種情況下,程序員就需要精確的知道每一個(gè)變量在內(nèi)存中的具體位置,我們需要手動(dòng)對(duì)物理內(nèi)存進(jìn)行布局,明確哪些數(shù)據(jù)存儲(chǔ)在內(nèi)存的哪些位置,除此之外我們還需要考慮為每個(gè)進(jìn)程究竟要分配多少內(nèi)存??jī)?nèi)存緊張的時(shí)候該怎么辦?如何避免進(jìn)程與進(jìn)程之間的地址沖突?等等一系列復(fù)雜且瑣碎的細(xì)節(jié)。
如果我們?cè)趩芜M(jìn)程系統(tǒng)中比如嵌入式設(shè)備上開(kāi)發(fā)應(yīng)用程序,系統(tǒng)中只有一個(gè)進(jìn)程,這單個(gè)進(jìn)程獨(dú)享所有的物理資源包括內(nèi)存資源。在這種情況下,上述提到的這些直接使用物理內(nèi)存的問(wèn)題可能還好處理一些,但是仍然具有很高的開(kāi)發(fā)門(mén)檻。
然而在現(xiàn)代操作系統(tǒng)中往往支持多個(gè)進(jìn)程,需要處理多進(jìn)程之間的協(xié)同問(wèn)題,在多進(jìn)程系統(tǒng)中直接使用物理內(nèi)存地址操作內(nèi)存所帶來(lái)的上述問(wèn)題就變得非常復(fù)雜了。
這里筆者為大家舉一個(gè)簡(jiǎn)單的例子來(lái)說(shuō)明在多進(jìn)程系統(tǒng)中直接使用物理內(nèi)存地址的復(fù)雜性。
比如我們現(xiàn)在有這樣一個(gè)簡(jiǎn)單的 Java 程序。
public static void main(String[] args) throws Exception {
string i = args[0];
..........
}
在程序代碼相同的情況下,我們用這份代碼同時(shí)啟動(dòng)三個(gè) JVM 進(jìn)程,我們暫時(shí)將進(jìn)程依次命名為 a , b , c 。
這三個(gè)進(jìn)程用到的代碼是一樣的,都是我們提前寫(xiě)好的,可以被多次運(yùn)行。由于我們是直接操作物理內(nèi)存地址,假設(shè)變量 i 保存在 0x354 這個(gè)物理地址上。這三個(gè)進(jìn)程運(yùn)行起來(lái)之后,同時(shí)操作這個(gè) 0x354 物理地址,這樣這個(gè)變量 i 的值不就混亂了嗎? 三個(gè)進(jìn)程就會(huì)出現(xiàn)變量的地址沖突。

所以在直接操作物理內(nèi)存的情況下,我們需要知道每一個(gè)變量的位置都被安排在了哪里,而且還要注意和多個(gè)進(jìn)程同時(shí)運(yùn)行的時(shí)候,不能共用同一個(gè)地址,否則就會(huì)造成地址沖突。
現(xiàn)實(shí)中一個(gè)程序會(huì)有很多的變量和函數(shù),這樣一來(lái)我們給它們都需要計(jì)算一個(gè)合理的位置,還不能與其他進(jìn)程沖突,這就很復(fù)雜了。
那么我們?cè)撊绾谓鉀Q這個(gè)問(wèn)題呢?程序的局部性原理再一次救了我們~~
程序局部性原理表現(xiàn)為:時(shí)間局部性和空間局部性。時(shí)間局部性是指如果程序中的某條指令一旦執(zhí)行,則不久之后該指令可能再次被執(zhí)行;如果某塊數(shù)據(jù)被訪(fǎng)問(wèn),則不久之后該數(shù)據(jù)可能再次被訪(fǎng)問(wèn)。空間局部性是指一旦程序訪(fǎng)問(wèn)了某個(gè)存儲(chǔ)單元,則不久之后,其附近的存儲(chǔ)單元也將被訪(fǎng)問(wèn)。
從程序局部性原理的描述中我們可以得出這樣一個(gè)結(jié)論:進(jìn)程在運(yùn)行之后,對(duì)于內(nèi)存的訪(fǎng)問(wèn)不會(huì)一下子就要訪(fǎng)問(wèn)全部的內(nèi)存,相反進(jìn)程對(duì)于內(nèi)存的訪(fǎng)問(wèn)會(huì)表現(xiàn)出明顯的傾向性,更加傾向于訪(fǎng)問(wèn)最近訪(fǎng)問(wèn)過(guò)的數(shù)據(jù)以及熱點(diǎn)數(shù)據(jù)附近的數(shù)據(jù)。
根據(jù)這個(gè)結(jié)論我們就清楚了,無(wú)論一個(gè)進(jìn)程實(shí)際可以占用的內(nèi)存資源有多大,根據(jù)程序局部性原理,在某一段時(shí)間內(nèi),進(jìn)程真正需要的物理內(nèi)存其實(shí)是很少的一部分,我們只需要為每個(gè)進(jìn)程分配很少的物理內(nèi)存就可以保證進(jìn)程的正常執(zhí)行運(yùn)轉(zhuǎn)。
而虛擬內(nèi)存的引入正是要解決上述的問(wèn)題,虛擬內(nèi)存引入之后,進(jìn)程的視角就會(huì)變得非常開(kāi)闊,每個(gè)進(jìn)程都擁有自己獨(dú)立的虛擬地址空間,進(jìn)程與進(jìn)程之間的虛擬內(nèi)存地址空間是相互隔離,互不干擾的。每個(gè)進(jìn)程都認(rèn)為自己獨(dú)占所有內(nèi)存空間,自己想干什么就干什么。

系統(tǒng)上還運(yùn)行了哪些進(jìn)程和我沒(méi)有任何關(guān)系。這樣一來(lái)我們就可以將多進(jìn)程之間協(xié)同的相關(guān)復(fù)雜細(xì)節(jié)統(tǒng)統(tǒng)交給內(nèi)核中的內(nèi)存管理模塊來(lái)處理,極大地解放了程序員的心智負(fù)擔(dān)。這一切都是因?yàn)樘摂M內(nèi)存能夠提供內(nèi)存地址空間的隔離,極大地?cái)U(kuò)展了可用空間。

這樣進(jìn)程就以為自己獨(dú)占了整個(gè)內(nèi)存空間資源,給進(jìn)程產(chǎn)生了所有內(nèi)存資源都屬于它自己的幻覺(jué),這其實(shí)是 CPU 和操作系統(tǒng)使用的一個(gè)障眼法罷了,任何一個(gè)虛擬內(nèi)存里所存儲(chǔ)的數(shù)據(jù),本質(zhì)上還是保存在真實(shí)的物理內(nèi)存里的。只不過(guò)內(nèi)核幫我們做了虛擬內(nèi)存到物理內(nèi)存的這一層映射,將不同進(jìn)程的虛擬地址和不同內(nèi)存的物理地址映射起來(lái)。
當(dāng) CPU 訪(fǎng)問(wèn)進(jìn)程的虛擬地址時(shí),經(jīng)過(guò)地址翻譯硬件將虛擬地址轉(zhuǎn)換成不同的物理地址,這樣不同的進(jìn)程運(yùn)行的時(shí)候,雖然操作的是同一虛擬地址,但其實(shí)背后寫(xiě)入的是不同的物理地址,這樣就不會(huì)沖突了。
3. 進(jìn)程虛擬內(nèi)存空間
上小節(jié)中,我們介紹了為了防止多進(jìn)程運(yùn)行時(shí)造成的內(nèi)存地址沖突,內(nèi)核引入了虛擬內(nèi)存地址,為每個(gè)進(jìn)程提供了一個(gè)獨(dú)立的虛擬內(nèi)存空間,使得進(jìn)程以為自己獨(dú)占全部?jī)?nèi)存資源。
那么這個(gè)進(jìn)程獨(dú)占的虛擬內(nèi)存空間到底是什么樣子呢?在本小節(jié)中,筆者就為大家揭開(kāi)這層神秘的面紗~~~
在本小節(jié)內(nèi)容開(kāi)始之前,我們先想象一下,如果我們是內(nèi)核的設(shè)計(jì)人員,我們?cè)搹哪男┓矫鎭?lái)規(guī)劃進(jìn)程的虛擬內(nèi)存空間呢?
本小節(jié)我們只討論進(jìn)程用戶(hù)態(tài)虛擬內(nèi)存空間的布局,我們先把內(nèi)核態(tài)的虛擬內(nèi)存空間當(dāng)做一個(gè)黑盒來(lái)看待,在后面的小節(jié)中筆者再來(lái)詳細(xì)介紹內(nèi)核態(tài)相關(guān)內(nèi)容。
首先我們會(huì)想到的是一個(gè)進(jìn)程運(yùn)行起來(lái)是為了執(zhí)行我們交代給進(jìn)程的工作,執(zhí)行這些工作的步驟我們通過(guò)程序代碼事先編寫(xiě)好,然后編譯成二進(jìn)制文件存放在磁盤(pán)中,CPU 會(huì)執(zhí)行二進(jìn)制文件中的機(jī)器碼來(lái)驅(qū)動(dòng)進(jìn)程的運(yùn)行。所以在進(jìn)程運(yùn)行之前,這些存放在二進(jìn)制文件中的機(jī)器碼需要被加載進(jìn)內(nèi)存中,而用于存放這些機(jī)器碼的虛擬內(nèi)存空間叫做代碼段。

在程序運(yùn)行起來(lái)之后,總要操作變量吧,在程序代碼中我們通常會(huì)定義大量的全局變量和靜態(tài)變量,這些全局變量在程序編譯之后也會(huì)存儲(chǔ)在二進(jìn)制文件中,在程序運(yùn)行之前,這些全局變量也需要被加載進(jìn)內(nèi)存中供程序訪(fǎng)問(wèn)。所以在虛擬內(nèi)存空間中也需要一段區(qū)域來(lái)存儲(chǔ)這些全局變量。
那些在代碼中被我們指定了初始值的全局變量和靜態(tài)變量在虛擬內(nèi)存空間中的存儲(chǔ)區(qū)域我們叫做數(shù)據(jù)段。
那些沒(méi)有指定初始值的全局變量和靜態(tài)變量在虛擬內(nèi)存空間中的存儲(chǔ)區(qū)域我們叫做 BSS 段。這些未初始化的全局變量被加載進(jìn)內(nèi)存之后會(huì)被初始化為 0 值。

上面介紹的這些全局變量和靜態(tài)變量都是在編譯期間就確定的,但是我們程序在運(yùn)行期間往往需要?jiǎng)討B(tài)的申請(qǐng)內(nèi)存,所以在虛擬內(nèi)存空間中也需要一塊區(qū)域來(lái)存放這些動(dòng)態(tài)申請(qǐng)的內(nèi)存,這塊區(qū)域就叫做堆。注意這里的堆指的是 OS 堆并不是 JVM 中的堆。

除此之外,我們的程序在運(yùn)行過(guò)程中還需要依賴(lài)動(dòng)態(tài)鏈接庫(kù),這些動(dòng)態(tài)鏈接庫(kù)以 .so 文件的形式存放在磁盤(pán)中,比如 C 程序中的 glibc,里邊對(duì)系統(tǒng)調(diào)用進(jìn)行了封裝。glibc 庫(kù)里提供的用于動(dòng)態(tài)申請(qǐng)堆內(nèi)存的 malloc 函數(shù)就是對(duì)系統(tǒng)調(diào)用 sbrk 和 mmap 的封裝。這些動(dòng)態(tài)鏈接庫(kù)也有自己的對(duì)應(yīng)的代碼段,數(shù)據(jù)段,BSS 段,也需要一起被加載進(jìn)內(nèi)存中。
還有用于內(nèi)存文件映射的系統(tǒng)調(diào)用 mmap,會(huì)將文件與內(nèi)存進(jìn)行映射,那么映射的這塊內(nèi)存(虛擬內(nèi)存)也需要在虛擬地址空間中有一塊區(qū)域存儲(chǔ)。
這些動(dòng)態(tài)鏈接庫(kù)中的代碼段,數(shù)據(jù)段,BSS 段,以及通過(guò) mmap 系統(tǒng)調(diào)用映射的共享內(nèi)存區(qū),在虛擬內(nèi)存空間的存儲(chǔ)區(qū)域叫做文件映射與匿名映射區(qū)。

最后我們?cè)诔绦蜻\(yùn)行的時(shí)候總該要調(diào)用各種函數(shù)吧,那么調(diào)用函數(shù)過(guò)程中使用到的局部變量和函數(shù)參數(shù)也需要一塊內(nèi)存區(qū)域來(lái)保存。這一塊區(qū)域在虛擬內(nèi)存空間中叫做棧。

現(xiàn)在進(jìn)程的虛擬內(nèi)存空間所包含的主要區(qū)域,筆者就為大家介紹完了,我們看到內(nèi)核根據(jù)進(jìn)程運(yùn)行的過(guò)程中所需要不同種類(lèi)的數(shù)據(jù)而為其開(kāi)辟了對(duì)應(yīng)的地址空間。分別為:
用于存放進(jìn)程程序二進(jìn)制文件中的機(jī)器指令的代碼段
用于存放程序二進(jìn)制文件中定義的全局變量和靜態(tài)變量的數(shù)據(jù)段和 BSS 段。
用于在程序運(yùn)行過(guò)程中動(dòng)態(tài)申請(qǐng)內(nèi)存的堆。
用于存放動(dòng)態(tài)鏈接庫(kù)以及內(nèi)存映射區(qū)域的文件映射與匿名映射區(qū)。
用于存放函數(shù)調(diào)用過(guò)程中的局部變量和函數(shù)參數(shù)的棧。
以上就是我們通過(guò)一個(gè)程序在運(yùn)行過(guò)程中所需要的數(shù)據(jù)所規(guī)劃出的虛擬內(nèi)存空間的分布,這些只是一個(gè)大概的規(guī)劃,那么在真實(shí)的 Linux 系統(tǒng)中,進(jìn)程的虛擬內(nèi)存空間的具體規(guī)劃又是如何的呢?我們接著往下看~~
4. Linux 進(jìn)程虛擬內(nèi)存空間
在上小節(jié)中我們介紹了進(jìn)程虛擬內(nèi)存空間中各個(gè)內(nèi)存區(qū)域的一個(gè)大概分布,在此基礎(chǔ)之上,本小節(jié)筆者就帶大家分別從 32 位 和 64 位機(jī)器上看下在 Linux 系統(tǒng)中進(jìn)程虛擬內(nèi)存空間的真實(shí)分布情況。
4.1 32 位機(jī)器上進(jìn)程虛擬內(nèi)存空間分布
在 32 位機(jī)器上,指針的尋址范圍為 2^32,所能表達(dá)的虛擬內(nèi)存空間為 4 GB。所以在 32 位機(jī)器上進(jìn)程的虛擬內(nèi)存地址范圍為:0x0000 0000 - 0xFFFF FFFF。
其中用戶(hù)態(tài)虛擬內(nèi)存空間為 3 GB,虛擬內(nèi)存地址范圍為:0x0000 0000 - 0xC000 000 。
內(nèi)核態(tài)虛擬內(nèi)存空間為 1 GB,虛擬內(nèi)存地址范圍為:0xC000 000 - 0xFFFF FFFF。

但是用戶(hù)態(tài)虛擬內(nèi)存空間中的代碼段并不是從 0x0000 0000 地址開(kāi)始的,而是從 0x0804 8000 地址開(kāi)始。
0x0000 0000 到 0x0804 8000 這段虛擬內(nèi)存地址是一段不可訪(fǎng)問(wèn)的保留區(qū),因?yàn)樵诖蠖鄶?shù)操作系統(tǒng)中,數(shù)值比較小的地址通常被認(rèn)為不是一個(gè)合法的地址,這塊小地址是不允許訪(fǎng)問(wèn)的。比如在 C 語(yǔ)言中我們通常會(huì)將一些無(wú)效的指針設(shè)置為 NULL,指向這塊不允許訪(fǎng)問(wèn)的地址。
保留區(qū)的上邊就是代碼段和數(shù)據(jù)段,它們是從程序的二進(jìn)制文件中直接加載進(jìn)內(nèi)存中的,BSS 段中的數(shù)據(jù)也存在于二進(jìn)制文件中,因?yàn)閮?nèi)核知道這些數(shù)據(jù)是沒(méi)有初值的,所以在二進(jìn)制文件中只會(huì)記錄 BSS 段的大小,在加載進(jìn)內(nèi)存時(shí)會(huì)生成一段 0 填充的內(nèi)存空間。
緊挨著 BSS 段的上邊就是我們經(jīng)常使用到的堆空間,從圖中的紅色箭頭我們可以知道在堆空間中地址的增長(zhǎng)方向是從低地址到高地址增長(zhǎng)。
內(nèi)核中使用 start_brk 標(biāo)識(shí)堆的起始位置,brk 標(biāo)識(shí)堆當(dāng)前的結(jié)束位置。當(dāng)堆申請(qǐng)新的內(nèi)存空間時(shí),只需要將 brk 指針增加對(duì)應(yīng)的大小,回收地址時(shí)減少對(duì)應(yīng)的大小即可。比如當(dāng)我們通過(guò) malloc 向內(nèi)核申請(qǐng)很小的一塊內(nèi)存時(shí)(128K 之內(nèi)),就是通過(guò)改變 brk 位置實(shí)現(xiàn)的。
堆空間的上邊是一段待分配區(qū)域,用于擴(kuò)展堆空間的使用。接下來(lái)就來(lái)到了文件映射與匿名映射區(qū)域。進(jìn)程運(yùn)行時(shí)所依賴(lài)的動(dòng)態(tài)鏈接庫(kù)中的代碼段,數(shù)據(jù)段,BSS 段就加載在這里。還有我們調(diào)用 mmap 映射出來(lái)的一段虛擬內(nèi)存空間也保存在這個(gè)區(qū)域。注意:在文件映射與匿名映射區(qū)的地址增長(zhǎng)方向是從高地址向低地址增長(zhǎng)。
接下來(lái)用戶(hù)態(tài)虛擬內(nèi)存空間的最后一塊區(qū)域就是棧空間了,在這里會(huì)保存函數(shù)運(yùn)行過(guò)程所需要的局部變量以及函數(shù)參數(shù)等函數(shù)調(diào)用信息。棧空間中的地址增長(zhǎng)方向是從高地址向低地址增長(zhǎng)。每次進(jìn)程申請(qǐng)新的棧地址時(shí),其地址值是在減少的。
在內(nèi)核中使用 start_stack 標(biāo)識(shí)棧的起始位置,RSP 寄存器中保存棧頂指針 stack pointer,RBP 寄存器中保存的是棧基地址。
在棧空間的下邊也有一段待分配區(qū)域用于擴(kuò)展棧空間,在棧空間的上邊就是內(nèi)核空間了,進(jìn)程雖然可以看到這段內(nèi)核空間地址,但是就是不能訪(fǎng)問(wèn)。這就好比我們?cè)陲埖昀镫m然可以看到廚房在哪里,但是廚房門(mén)上寫(xiě)著 “廚房重地,閑人免進(jìn)” ,我們就是進(jìn)不去。

4.2 64 位機(jī)器上進(jìn)程虛擬內(nèi)存空間分布
上小節(jié)中介紹的 32 位虛擬內(nèi)存空間布局和本小節(jié)即將要介紹的 64 位虛擬內(nèi)存空間布局都可以通過(guò) cat /proc/pid/maps 或者 pmap pid 來(lái)查看某個(gè)進(jìn)程的實(shí)際虛擬內(nèi)存布局。
我們知道在 32 位機(jī)器上,指針的尋址范圍為 2^32,所能表達(dá)的虛擬內(nèi)存空間為 4 GB。
那么我們理所應(yīng)當(dāng)?shù)臅?huì)認(rèn)為在 64 位機(jī)器上,指針的尋址范圍為 2^64,所能表達(dá)的虛擬內(nèi)存空間為 16 EB 。虛擬內(nèi)存地址范圍為:0x0000 0000 0000 0000 0000 - 0xFFFF FFFF FFFF FFFF 。
好家伙 !!! 16 EB 的內(nèi)存空間,筆者都沒(méi)見(jiàn)過(guò)這么大的磁盤(pán),在現(xiàn)實(shí)情況中根本不會(huì)用到這么大范圍的內(nèi)存空間,
事實(shí)上在目前的 64 位系統(tǒng)下只使用了 48 位來(lái)描述虛擬內(nèi)存空間,尋址范圍為 2^48 ,所能表達(dá)的虛擬內(nèi)存空間為 256TB。
其中低 128 T 表示用戶(hù)態(tài)虛擬內(nèi)存空間,虛擬內(nèi)存地址范圍為:0x0000 0000 0000 0000 - 0x0000 7FFF FFFF F000 。
高 128 T 表示內(nèi)核態(tài)虛擬內(nèi)存空間,虛擬內(nèi)存地址范圍為:0xFFFF 8000 0000 0000 - 0xFFFF FFFF FFFF FFFF 。
這樣一來(lái)就在用戶(hù)態(tài)虛擬內(nèi)存空間與內(nèi)核態(tài)虛擬內(nèi)存空間之間形成了一段 0x0000 7FFF FFFF F000 - 0xFFFF 8000 0000 0000 的地址空洞,我們把這個(gè)空洞叫做 canonical address 空洞。

那么這個(gè) canonical address 空洞是如何形成的呢?
我們都知道在 64 位機(jī)器上的指針尋址范圍為 2^64,但是在實(shí)際使用中我們只使用了其中的低 48 位來(lái)表示虛擬內(nèi)存地址,那么這多出的高 16 位就形成了這個(gè)地址空洞。
大家注意到在低 128T 的用戶(hù)態(tài)地址空間:0x0000 0000 0000 0000 - 0x0000 7FFF FFFF F000 范圍中,所以虛擬內(nèi)存地址的高 16 位全部為 0 。
如果一個(gè)虛擬內(nèi)存地址的高 16 位全部為 0 ,那么我們就可以直接判斷出這是一個(gè)用戶(hù)空間的虛擬內(nèi)存地址。
同樣的道理,在高 128T 的內(nèi)核態(tài)虛擬內(nèi)存空間:0xFFFF 8000 0000 0000 - 0xFFFF FFFF FFFF FFFF 范圍中,所以虛擬內(nèi)存地址的高 16 位全部為 1 。
也就是說(shuō)內(nèi)核態(tài)的虛擬內(nèi)存地址的高 16 位全部為 1 ,如果一個(gè)試圖訪(fǎng)問(wèn)內(nèi)核的虛擬地址的高 16 位不全為 1 ,則可以快速判斷這個(gè)訪(fǎng)問(wèn)是非法的。
這個(gè)高 16 位的空閑地址被稱(chēng)為 canonical 。如果虛擬內(nèi)存地址中的高 16 位全部為 0 (表示用戶(hù)空間虛擬內(nèi)存地址)或者全部為 1 (表示內(nèi)核空間虛擬內(nèi)存地址),這種地址的形式我們叫做 canonical form,對(duì)應(yīng)的地址我們稱(chēng)作 canonical address 。
那么處于 canonical address 空洞 :0x0000 7FFF FFFF F000 - 0xFFFF 8000 0000 0000 范圍內(nèi)的地址的高 16 位 不全為 0 也不全為 1 。如果某個(gè)虛擬地址落在這段 canonical address 空洞區(qū)域中,那就是既不在用戶(hù)空間,也不在內(nèi)核空間,肯定是非法訪(fǎng)問(wèn)了。
未來(lái)我們也可以利用這塊 canonical address 空洞,來(lái)擴(kuò)展虛擬內(nèi)存地址的范圍,比如擴(kuò)展到 56 位。
在我們理解了 canonical address 這個(gè)概念之后,我們?cè)賮?lái)看下 64 位 Linux 系統(tǒng)下的真實(shí)虛擬內(nèi)存空間布局情況:

從上圖中我們可以看出 64 位系統(tǒng)中的虛擬內(nèi)存布局和 32 位系統(tǒng)中的虛擬內(nèi)存布局大體上是差不多的。主要不同的地方有三點(diǎn):
就是前邊提到的由高 16 位空閑地址造成的 canonical address 空洞。在這段范圍內(nèi)的虛擬內(nèi)存地址是不合法的,因?yàn)樗母?16 位既不全為 0 也不全為 1,不是一個(gè) canonical address,所以稱(chēng)之為 canonical address 空洞。
在代碼段跟數(shù)據(jù)段的中間還有一段不可以讀寫(xiě)的保護(hù)段,它的作用是防止程序在讀寫(xiě)數(shù)據(jù)段的時(shí)候越界訪(fǎng)問(wèn)到代碼段,這個(gè)保護(hù)段可以讓越界訪(fǎng)問(wèn)行為直接崩潰,防止它繼續(xù)往下運(yùn)行。
用戶(hù)態(tài)虛擬內(nèi)存空間與內(nèi)核態(tài)虛擬內(nèi)存空間分別占用 128T,其中低128T 分配給用戶(hù)態(tài)虛擬內(nèi)存空間,高 128T 分配給內(nèi)核態(tài)虛擬內(nèi)存空間。
5. 進(jìn)程虛擬內(nèi)存空間的管理
在上一小節(jié)中,筆者為大家介紹了 Linux 操作系統(tǒng)在 32 位機(jī)器上和 64 位機(jī)器上進(jìn)程虛擬內(nèi)存空間的布局分布,我們發(fā)現(xiàn)無(wú)論是在 32 位機(jī)器上還是在 64 位機(jī)器上,進(jìn)程虛擬內(nèi)存空間的核心區(qū)域分布的相對(duì)位置是不變的,它們都包含下圖所示的這幾個(gè)核心內(nèi)存區(qū)域。

唯一不同的是這些核心內(nèi)存區(qū)域在 32 位機(jī)器和 64 位機(jī)器上的絕對(duì)位置分布會(huì)有所不同。
那么在此基礎(chǔ)之上,內(nèi)核如何為進(jìn)程管理這些虛擬內(nèi)存區(qū)域呢?這將是本小節(jié)重點(diǎn)為大家介紹的內(nèi)容~~
既然我們要介紹進(jìn)程的虛擬內(nèi)存空間管理,那就離不開(kāi)進(jìn)程在內(nèi)核中的描述符 task_struct 結(jié)構(gòu)。
struct task_struct {
// 進(jìn)程id
pid_t pid;
// 用于標(biāo)識(shí)線(xiàn)程所屬的進(jìn)程 pid
pid_t tgid;
// 進(jìn)程打開(kāi)的文件信息
struct files_struct *files;
// 內(nèi)存描述符表示進(jìn)程虛擬地址空間
struct mm_struct *mm;
.......... 省略 .......
}
在進(jìn)程描述符 task_struct 結(jié)構(gòu)中,有一個(gè)專(zhuān)門(mén)描述進(jìn)程虛擬地址空間的內(nèi)存描述符 mm_struct 結(jié)構(gòu),這個(gè)結(jié)構(gòu)體中包含了前邊幾個(gè)小節(jié)中介紹的進(jìn)程虛擬內(nèi)存空間的全部信息。
每個(gè)進(jìn)程都有唯一的 mm_struct 結(jié)構(gòu)體,也就是前邊提到的每個(gè)進(jìn)程的虛擬地址空間都是獨(dú)立,互不干擾的。
當(dāng)我們調(diào)用 fork() 函數(shù)創(chuàng)建進(jìn)程的時(shí)候,表示進(jìn)程地址空間的 mm_struct 結(jié)構(gòu)會(huì)隨著進(jìn)程描述符 task_struct 的創(chuàng)建而創(chuàng)建。
long _do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr,
unsigned long tls)
{
......... 省略 ..........
struct pid *pid;
struct task_struct *p;
......... 省略 ..........
// 為進(jìn)程創(chuàng)建 task_struct 結(jié)構(gòu),用父進(jìn)程的資源填充 task_struct 信息
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace, tls, NUMA_NO_NODE);
......... 省略 ..........
}
隨后會(huì)在 copy_process 函數(shù)中創(chuàng)建 task_struct 結(jié)構(gòu),并拷貝父進(jìn)程的相關(guān)資源到新進(jìn)程的 task_struct 結(jié)構(gòu)里,其中就包括拷貝父進(jìn)程的虛擬內(nèi)存空間 mm_struct 結(jié)構(gòu)。這里可以看出子進(jìn)程在新創(chuàng)建出來(lái)之后它的虛擬內(nèi)存空間是和父進(jìn)程的虛擬內(nèi)存空間一模一樣的,直接拷貝過(guò)來(lái)。
static __latent_entropy struct task_struct *copy_process(
unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *child_tidptr,
struct pid *pid,
int trace,
unsigned long tls,
int node)
{
struct task_struct *p;
// 創(chuàng)建 task_struct 結(jié)構(gòu)
p = dup_task_struct(current, node);
....... 初始化子進(jìn)程 ...........
....... 開(kāi)始繼承拷貝父進(jìn)程資源 .......
// 繼承父進(jìn)程打開(kāi)的文件描述符
retval = copy_files(clone_flags, p);
// 繼承父進(jìn)程所屬的文件系統(tǒng)
retval = copy_fs(clone_flags, p);
// 繼承父進(jìn)程注冊(cè)的信號(hào)以及信號(hào)處理函數(shù)
retval = copy_sighand(clone_flags, p);
retval = copy_signal(clone_flags, p);
// 繼承父進(jìn)程的虛擬內(nèi)存空間
retval = copy_mm(clone_flags, p);
// 繼承父進(jìn)程的 namespaces
retval = copy_namespaces(clone_flags, p);
// 繼承父進(jìn)程的 IO 信息
retval = copy_io(clone_flags, p);
...........省略.........
// 分配 CPU
retval = sched_fork(clone_flags, p);
// 分配 pid
pid = alloc_pid(p->nsproxy->pid_ns_for_children);
. ..........省略.........
}
這里我們重點(diǎn)關(guān)注 copy_mm 函數(shù),正是在這里完成了子進(jìn)程虛擬內(nèi)存空間 mm_struct 結(jié)構(gòu)的的創(chuàng)建以及初始化。
static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
{
// 子進(jìn)程虛擬內(nèi)存空間,父進(jìn)程虛擬內(nèi)存空間
struct mm_struct *mm, *oldmm;
int retval;
...... 省略 ......
tsk->mm = NULL;
tsk->active_mm = NULL;
// 獲取父進(jìn)程虛擬內(nèi)存空間
oldmm = current->mm;
if (!oldmm)
return 0;
...... 省略 ......
// 通過(guò) vfork 或者 clone 系統(tǒng)調(diào)用創(chuàng)建出的子進(jìn)程(線(xiàn)程)和父進(jìn)程共享虛擬內(nèi)存空間
if (clone_flags & CLONE_VM) {
// 增加父進(jìn)程虛擬地址空間的引用計(jì)數(shù)
mmget(oldmm);
// 直接將父進(jìn)程的虛擬內(nèi)存空間賦值給子進(jìn)程(線(xiàn)程)
// 線(xiàn)程共享其所屬進(jìn)程的虛擬內(nèi)存空間
mm = oldmm;
goto good_mm;
}
retval = -ENOMEM;
// 如果是 fork 系統(tǒng)調(diào)用創(chuàng)建出的子進(jìn)程,則將父進(jìn)程的虛擬內(nèi)存空間以及相關(guān)頁(yè)表拷貝到子進(jìn)程中的 mm_struct 結(jié)構(gòu)中。
mm = dup_mm(tsk);
if (!mm)
goto fail_nomem;
good_mm:
// 將拷貝出來(lái)的父進(jìn)程虛擬內(nèi)存空間 mm_struct 賦值給子進(jìn)程
tsk->mm = mm;
tsk->active_mm = mm;
return 0;
...... 省略 ......
由于本小節(jié)中我們舉的示例是通過(guò) fork() 函數(shù)創(chuàng)建子進(jìn)程的情形,所以這里大家先占時(shí)忽略 if (clone_flags & CLONE_VM) 這個(gè)條件判斷邏輯,我們先跳過(guò)往后看~~
copy_mm 函數(shù)首先會(huì)將父進(jìn)程的虛擬內(nèi)存空間 current->mm 賦值給指針 oldmm。然后通過(guò) dup_mm 函數(shù)將父進(jìn)程的虛擬內(nèi)存空間以及相關(guān)頁(yè)表拷貝到子進(jìn)程的 mm_struct 結(jié)構(gòu)中。最后將拷貝出來(lái)的 mm_struct 賦值給子進(jìn)程的 task_struct 結(jié)構(gòu)。
通過(guò) fork() 函數(shù)創(chuàng)建出的子進(jìn)程,它的虛擬內(nèi)存空間以及相關(guān)頁(yè)表相當(dāng)于父進(jìn)程虛擬內(nèi)存空間的一份拷貝,直接從父進(jìn)程中拷貝到子進(jìn)程中。
而當(dāng)我們通過(guò) vfork 或者 clone 系統(tǒng)調(diào)用創(chuàng)建出的子進(jìn)程,首先會(huì)設(shè)置 CLONE_VM 標(biāo)識(shí),這樣來(lái)到 copy_mm 函數(shù)中就會(huì)進(jìn)入 if (clone_flags & CLONE_VM) 條件中,在這個(gè)分支中會(huì)將父進(jìn)程的虛擬內(nèi)存空間以及相關(guān)頁(yè)表直接賦值給子進(jìn)程。這樣一來(lái)父進(jìn)程和子進(jìn)程的虛擬內(nèi)存空間就變成共享的了。也就是說(shuō)父子進(jìn)程之間使用的虛擬內(nèi)存空間是一樣的,并不是一份拷貝。
子進(jìn)程共享了父進(jìn)程的虛擬內(nèi)存空間,這樣子進(jìn)程就變成了我們熟悉的線(xiàn)程,是否共享地址空間幾乎是進(jìn)程和線(xiàn)程之間的本質(zhì)區(qū)別。Linux 內(nèi)核并不區(qū)別對(duì)待它們,線(xiàn)程對(duì)于內(nèi)核來(lái)說(shuō)僅僅是一個(gè)共享特定資源的進(jìn)程而已。
內(nèi)核線(xiàn)程和用戶(hù)態(tài)線(xiàn)程的區(qū)別就是內(nèi)核線(xiàn)程沒(méi)有相關(guān)的內(nèi)存描述符 mm_struct ,內(nèi)核線(xiàn)程對(duì)應(yīng)的 task_struct 結(jié)構(gòu)中的 mm 域指向 Null,所以?xún)?nèi)核線(xiàn)程之間調(diào)度是不涉及地址空間切換的。
當(dāng)一個(gè)內(nèi)核線(xiàn)程被調(diào)度時(shí),它會(huì)發(fā)現(xiàn)自己的虛擬地址空間為 Null,雖然它不會(huì)訪(fǎng)問(wèn)用戶(hù)態(tài)的內(nèi)存,但是它會(huì)訪(fǎng)問(wèn)內(nèi)核內(nèi)存,聰明的內(nèi)核會(huì)將調(diào)度之前的上一個(gè)用戶(hù)態(tài)進(jìn)程的虛擬內(nèi)存空間 mm_struct 直接賦值給內(nèi)核線(xiàn)程,因?yàn)閮?nèi)核線(xiàn)程不會(huì)訪(fǎng)問(wèn)用戶(hù)空間的內(nèi)存,它僅僅只會(huì)訪(fǎng)問(wèn)內(nèi)核空間的內(nèi)存,所以直接復(fù)用上一個(gè)用戶(hù)態(tài)進(jìn)程的虛擬地址空間就可以避免為內(nèi)核線(xiàn)程分配 mm_struct 和相關(guān)頁(yè)表的開(kāi)銷(xiāo),以及避免內(nèi)核線(xiàn)程之間調(diào)度時(shí)地址空間的切換開(kāi)銷(xiāo)。
父進(jìn)程與子進(jìn)程的區(qū)別,進(jìn)程與線(xiàn)程的區(qū)別,以及內(nèi)核線(xiàn)程與用戶(hù)態(tài)線(xiàn)程的區(qū)別其實(shí)都是圍繞著這個(gè) mm_struct 展開(kāi)的。
現(xiàn)在我們知道了表示進(jìn)程虛擬內(nèi)存空間的 mm_struct 結(jié)構(gòu)是如何被創(chuàng)建出來(lái)的相關(guān)背景,那么接下來(lái)筆者就帶大家深入 mm_struct 結(jié)構(gòu)內(nèi)部,來(lái)看一下內(nèi)核如何通過(guò)這么一個(gè) mm_struct 結(jié)構(gòu)體來(lái)管理進(jìn)程的虛擬內(nèi)存空間的。
5.1 內(nèi)核如何劃分用戶(hù)態(tài)和內(nèi)核態(tài)虛擬內(nèi)存空間
通過(guò) 《3. 進(jìn)程虛擬內(nèi)存空間》小節(jié)的介紹我們知道,進(jìn)程的虛擬內(nèi)存空間分為兩個(gè)部分:一部分是用戶(hù)態(tài)虛擬內(nèi)存空間,另一部分是內(nèi)核態(tài)虛擬內(nèi)存空間。

那么用戶(hù)態(tài)的地址空間和內(nèi)核態(tài)的地址空間在內(nèi)核中是如何被劃分的呢?
這就用到了進(jìn)程的內(nèi)存描述符 mm_struct 結(jié)構(gòu)體中的 task_size 變量,task_size 定義了用戶(hù)態(tài)地址空間與內(nèi)核態(tài)地址空間之間的分界線(xiàn)。
struct mm_struct {
unsigned long task_size; /* size of task vm space */
}
通過(guò)前邊小節(jié)的內(nèi)容介紹,我們知道在 32 位系統(tǒng)中用戶(hù)態(tài)虛擬內(nèi)存空間為 3 GB,虛擬內(nèi)存地址范圍為:0x0000 0000 - 0xC000 000 。
內(nèi)核態(tài)虛擬內(nèi)存空間為 1 GB,虛擬內(nèi)存地址范圍為:0xC000 000 - 0xFFFF FFFF。

32 位系統(tǒng)中用戶(hù)地址空間和內(nèi)核地址空間的分界線(xiàn)在 0xC000 000 地址處,那么自然進(jìn)程的 mm_struct 結(jié)構(gòu)中的 task_size 為 0xC000 000。
我們來(lái)看下內(nèi)核在 /arch/x86/include/asm/page_32_types.h 文件中關(guān)于 TASK_SIZE 的定義。
/*
* User space process size: 3GB (default).
*/
#define TASK_SIZE __PAGE_OFFSET
如下圖所示:__PAGE_OFFSET 的值在 32 位系統(tǒng)下為 0xC000 000。

而在 64 位系統(tǒng)中,只使用了其中的低 48 位來(lái)表示虛擬內(nèi)存地址。其中用戶(hù)態(tài)虛擬內(nèi)存空間為低 128 T,虛擬內(nèi)存地址范圍為:0x0000 0000 0000 0000 - 0x0000 7FFF FFFF F000 。
內(nèi)核態(tài)虛擬內(nèi)存空間為高 128 T,虛擬內(nèi)存地址范圍為:0xFFFF 8000 0000 0000 - 0xFFFF FFFF FFFF FFFF 。

64 位系統(tǒng)中用戶(hù)地址空間和內(nèi)核地址空間的分界線(xiàn)在 0x0000 7FFF FFFF F000 地址處,那么自然進(jìn)程的 mm_struct 結(jié)構(gòu)中的 task_size 為 0x0000 7FFF FFFF F000 。
我們來(lái)看下內(nèi)核在 /arch/x86/include/asm/page_64_types.h 文件中關(guān)于 TASK_SIZE 的定義。
#define TASK_SIZE (test_thread_flag(TIF_ADDR32) ? \
IA32_PAGE_OFFSET : TASK_SIZE_MAX)
#define TASK_SIZE_MAX task_size_max()
#define task_size_max() ((_AC(1,UL) << __VIRTUAL_MASK_SHIFT) - PAGE_SIZE)
#define __VIRTUAL_MASK_SHIFT 47
我們來(lái)看下在 64 位系統(tǒng)中內(nèi)核如何來(lái)計(jì)算 TASK_SIZE,在 task_size_max() 的計(jì)算邏輯中 1 左移 47 位得到的地址是 0x0000800000000000,然后減去一個(gè) PAGE_SIZE (默認(rèn)為 4K),就是 0x00007FFFFFFFF000,共 128T。所以在 64 位系統(tǒng)中的 TASK_SIZE 為 0x00007FFFFFFFF000 。
這里我們可以看出,64 位虛擬內(nèi)存空間的布局是和物理內(nèi)存頁(yè) page 的大小有關(guān)的,物理內(nèi)存頁(yè) page 默認(rèn)大小 PAGE_SIZE 為 4K。
PAGE_SIZE 定義在 /arch/x86/include/asm/page_types.h文件中:
/* PAGE_SHIFT determines the page size */
#define PAGE_SHIFT 12
#define PAGE_SIZE (_AC(1,UL) << PAGE_SHIFT)
而內(nèi)核空間的起始地址是 0xFFFF 8000 0000 0000 。在 0x00007FFFFFFFF000 - 0xFFFF 8000 0000 0000 之間的內(nèi)存區(qū)域就是我們?cè)?《4.2 64 位機(jī)器上進(jìn)程虛擬內(nèi)存空間分布》小節(jié)中介紹的 canonical address 空洞。
5.2 內(nèi)核如何布局進(jìn)程虛擬內(nèi)存空間
在我們理解了內(nèi)核是如何劃分進(jìn)程虛擬內(nèi)存空間和內(nèi)核虛擬內(nèi)存空間之后,那么在 《3. 進(jìn)程虛擬內(nèi)存空間》小節(jié)中介紹的那些虛擬內(nèi)存區(qū)域在內(nèi)核中又是如何劃分的呢?
接下來(lái)筆者就為大家介紹下內(nèi)核是如何劃分進(jìn)程虛擬內(nèi)存空間中的這些內(nèi)存區(qū)域的,本小節(jié)的示例圖中,筆者只保留了進(jìn)程虛擬內(nèi)存空間中的核心區(qū)域,方便大家理解。

前邊我們提到,內(nèi)核中采用了一個(gè)叫做內(nèi)存描述符的 mm_struct 結(jié)構(gòu)體來(lái)表示進(jìn)程虛擬內(nèi)存空間的全部信息。在本小節(jié)中筆者就帶大家到 mm_struct 結(jié)構(gòu)體內(nèi)部去尋找下相關(guān)的線(xiàn)索。
struct mm_struct {
unsigned long task_size; /* size of task vm space */
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
unsigned long mmap_base; /* base of mmap area */
unsigned long total_vm; /* Total pages mapped */
unsigned long locked_vm; /* Pages that have PG_mlocked set */
unsigned long pinned_vm; /* Refcount permanently increased */
unsigned long data_vm; /* VM_WRITE & ~VM_SHARED & ~VM_STACK */
unsigned long exec_vm; /* VM_EXEC & ~VM_WRITE & ~VM_STACK */
unsigned long stack_vm; /* VM_STACK */
...... 省略 ........
}
內(nèi)核中用 mm_struct 結(jié)構(gòu)體中的上述屬性來(lái)定義上圖中虛擬內(nèi)存空間里的不同內(nèi)存區(qū)域。
start_code 和 end_code 定義代碼段的起始和結(jié)束位置,程序編譯后的二進(jìn)制文件中的機(jī)器碼被加載進(jìn)內(nèi)存之后就存放在這里。
start_data 和 end_data 定義數(shù)據(jù)段的起始和結(jié)束位置,二進(jìn)制文件中存放的全局變量和靜態(tài)變量被加載進(jìn)內(nèi)存中就存放在這里。
后面緊挨著的是 BSS 段,用于存放未被初始化的全局變量和靜態(tài)變量,這些變量在加載進(jìn)內(nèi)存時(shí)會(huì)生成一段 0 填充的內(nèi)存區(qū)域 (BSS 段), BSS 段的大小是固定的,
下面就是 OS 堆了,在堆中內(nèi)存地址的增長(zhǎng)方向是由低地址向高地址增長(zhǎng), start_brk 定義堆的起始位置,brk 定義堆當(dāng)前的結(jié)束位置。
我們使用 malloc 申請(qǐng)小塊內(nèi)存時(shí)(低于 128K),就是通過(guò)改變 brk 位置調(diào)整堆大小實(shí)現(xiàn)的。
接下來(lái)就是內(nèi)存映射區(qū),在內(nèi)存映射區(qū)內(nèi)存地址的增長(zhǎng)方向是由高地址向低地址增長(zhǎng),mmap_base 定義內(nèi)存映射區(qū)的起始地址。進(jìn)程運(yùn)行時(shí)所依賴(lài)的動(dòng)態(tài)鏈接庫(kù)中的代碼段,數(shù)據(jù)段,BSS 段以及我們調(diào)用 mmap 映射出來(lái)的一段虛擬內(nèi)存空間就保存在這個(gè)區(qū)域。
start_stack 是棧的起始位置在 RBP 寄存器中存儲(chǔ),棧的結(jié)束位置也就是棧頂指針 stack pointer 在 RSP 寄存器中存儲(chǔ)。在棧中內(nèi)存地址的增長(zhǎng)方向也是由高地址向低地址增長(zhǎng)。
arg_start 和 arg_end 是參數(shù)列表的位置, env_start 和 env_end 是環(huán)境變量的位置。它們都位于棧中的最高地址處。

在 mm_struct 結(jié)構(gòu)體中除了上述用于劃分虛擬內(nèi)存區(qū)域的變量之外,還定義了一些虛擬內(nèi)存與物理內(nèi)存映射內(nèi)容相關(guān)的統(tǒng)計(jì)變量,操作系統(tǒng)會(huì)把物理內(nèi)存劃分成一頁(yè)一頁(yè)的區(qū)域來(lái)進(jìn)行管理,所以物理內(nèi)存到虛擬內(nèi)存之間的映射也是按照頁(yè)為單位進(jìn)行的。這部分內(nèi)容筆者會(huì)在后續(xù)的文章中詳細(xì)介紹,大家這里只需要有個(gè)概念就行。
mm_struct 結(jié)構(gòu)體中的 total_vm 表示在進(jìn)程虛擬內(nèi)存空間中總共與物理內(nèi)存映射的頁(yè)的總數(shù)。
注意映射這個(gè)概念,它表示只是將虛擬內(nèi)存與物理內(nèi)存建立關(guān)聯(lián)關(guān)系,并不代表真正的分配物理內(nèi)存。
當(dāng)內(nèi)存吃緊的時(shí)候,有些頁(yè)可以換出到硬盤(pán)上,而有些頁(yè)因?yàn)楸容^重要,不能換出。locked_vm 就是被鎖定不能換出的內(nèi)存頁(yè)總數(shù),pinned_vm 表示既不能換出,也不能移動(dòng)的內(nèi)存頁(yè)總數(shù)。
data_vm 表示數(shù)據(jù)段中映射的內(nèi)存頁(yè)數(shù)目,exec_vm 是代碼段中存放可執(zhí)行文件的內(nèi)存頁(yè)數(shù)目,stack_vm 是棧中所映射的內(nèi)存頁(yè)數(shù)目,這些變量均是表示進(jìn)程虛擬內(nèi)存空間中的虛擬內(nèi)存使用情況。
現(xiàn)在關(guān)于內(nèi)核如何對(duì)進(jìn)程虛擬內(nèi)存空間進(jìn)行布局的內(nèi)容我們已經(jīng)清楚了,那么布局之后劃分出的這些虛擬內(nèi)存區(qū)域在內(nèi)核中又是如何被管理的呢?我們接著往下看~~~
5.3 內(nèi)核如何管理虛擬內(nèi)存區(qū)域
在上小節(jié)的介紹中,我們知道內(nèi)核是通過(guò)一個(gè) mm_struct 結(jié)構(gòu)的內(nèi)存描述符來(lái)表示進(jìn)程的虛擬內(nèi)存空間的,并通過(guò) task_size 域來(lái)劃分用戶(hù)態(tài)虛擬內(nèi)存空間和內(nèi)核態(tài)虛擬內(nèi)存空間。

而在劃分出的這些虛擬內(nèi)存空間中如上圖所示,里邊又包含了許多特定的虛擬內(nèi)存區(qū)域,比如:代碼段,數(shù)據(jù)段,堆,內(nèi)存映射區(qū),棧。那么這些虛擬內(nèi)存區(qū)域在內(nèi)核中又是如何表示的呢?
本小節(jié)中,筆者將為大家介紹一個(gè)新的結(jié)構(gòu)體 vm_area_struct,正是這個(gè)結(jié)構(gòu)體描述了這些虛擬內(nèi)存區(qū)域 VMA(virtual memory area)。
struct vm_area_struct {
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address
within vm_mm. */
/*
* Access permissions of this VMA.
*/
pgprot_t vm_page_prot;
unsigned long vm_flags;
struct anon_vma *anon_vma; /* Serialized by page_table_lock */
struct file * vm_file; /* File we map to (can be NULL). */
unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE
units */
void * vm_private_data; /* was vm_pte (shared mem) */
/* Function pointers to deal with this struct. */
const struct vm_operations_struct *vm_ops;
}
每個(gè) vm_area_struct 結(jié)構(gòu)對(duì)應(yīng)于虛擬內(nèi)存空間中的唯一虛擬內(nèi)存區(qū)域 VMA,vm_start 指向了這塊虛擬內(nèi)存區(qū)域的起始地址(最低地址),vm_start 本身包含在這塊虛擬內(nèi)存區(qū)域內(nèi)。vm_end 指向了這塊虛擬內(nèi)存區(qū)域的結(jié)束地址(最高地址),而 vm_end 本身包含在這塊虛擬內(nèi)存區(qū)域之外,所以 vm_area_struct 結(jié)構(gòu)描述的是 [vm_start,vm_end) 這樣一段左閉右開(kāi)的虛擬內(nèi)存區(qū)域。

5.4 定義虛擬內(nèi)存區(qū)域的訪(fǎng)問(wèn)權(quán)限和行為規(guī)范
vm_page_prot 和 vm_flags 都是用來(lái)標(biāo)記 vm_area_struct 結(jié)構(gòu)表示的這塊虛擬內(nèi)存區(qū)域的訪(fǎng)問(wèn)權(quán)限和行為規(guī)范。
上邊小節(jié)中我們也提到,內(nèi)核會(huì)將整塊物理內(nèi)存劃分為一頁(yè)一頁(yè)大小的區(qū)域,以頁(yè)為單位來(lái)管理這些物理內(nèi)存,每頁(yè)大小默認(rèn) 4K 。而虛擬內(nèi)存最終也是要和物理內(nèi)存一一映射起來(lái)的,所以在虛擬內(nèi)存空間中也有虛擬頁(yè)的概念與之對(duì)應(yīng),虛擬內(nèi)存中的虛擬頁(yè)映射到物理內(nèi)存中的物理頁(yè)。無(wú)論是在虛擬內(nèi)存空間中還是在物理內(nèi)存中,內(nèi)核管理內(nèi)存的最小單位都是頁(yè)。
vm_page_prot 偏向于定義底層內(nèi)存管理架構(gòu)中頁(yè)這一級(jí)別的訪(fǎng)問(wèn)控制權(quán)限,它可以直接應(yīng)用在底層頁(yè)表中,它是一個(gè)具體的概念。
頁(yè)表用于管理虛擬內(nèi)存到物理內(nèi)存之間的映射關(guān)系,這部分內(nèi)容筆者后續(xù)會(huì)詳細(xì)講解,這里大家有個(gè)初步的概念就行。
虛擬內(nèi)存區(qū)域 VMA 由許多的虛擬頁(yè) (page) 組成,每個(gè)虛擬頁(yè)需要經(jīng)過(guò)頁(yè)表的轉(zhuǎn)換才能找到對(duì)應(yīng)的物理頁(yè)面。頁(yè)表中關(guān)于內(nèi)存頁(yè)的訪(fǎng)問(wèn)權(quán)限就是由 vm_page_prot 決定的。
vm_flags 則偏向于定于整個(gè)虛擬內(nèi)存區(qū)域的訪(fǎng)問(wèn)權(quán)限以及行為規(guī)范。描述的是虛擬內(nèi)存區(qū)域中的整體信息,而不是虛擬內(nèi)存區(qū)域中具體的某個(gè)獨(dú)立頁(yè)面。它是一個(gè)抽象的概念。可以通過(guò) vma->vm_page_prot = vm_get_page_prot(vma->vm_flags) 實(shí)現(xiàn)到具體頁(yè)面訪(fǎng)問(wèn)權(quán)限 vm_page_prot 的轉(zhuǎn)換。
下面筆者列舉一些常用到的 vm_flags 方便大家有一個(gè)直觀(guān)的感受:
| vm_flags | 訪(fǎng)問(wèn)權(quán)限 |
|---|---|
| VM_READ | 可讀 |
| VM_WRITE | 可寫(xiě) |
| VM_EXEC | 可執(zhí)行 |
| VM_SHARD | 可多進(jìn)程之間共享 |
| VM_IO | 可映射至設(shè)備 IO 空間 |
| VM_RESERVED | 內(nèi)存區(qū)域不可被換出 |
| VM_SEQ_READ | 內(nèi)存區(qū)域可能被順序訪(fǎng)問(wèn) |
| VM_RAND_READ | 內(nèi)存區(qū)域可能被隨機(jī)訪(fǎng)問(wèn) |
VM_READ,VM_WRITE,VM_EXEC 定義了虛擬內(nèi)存區(qū)域是否可以被讀取,寫(xiě)入,執(zhí)行等權(quán)限。
比如代碼段這塊內(nèi)存區(qū)域的權(quán)限是可讀,可執(zhí)行,但是不可寫(xiě)。數(shù)據(jù)段具有可讀可寫(xiě)的權(quán)限但是不可執(zhí)行。堆則具有可讀可寫(xiě),可執(zhí)行的權(quán)限(Java 中的字節(jié)碼存儲(chǔ)在堆中,所以需要可執(zhí)行權(quán)限),棧一般是可讀可寫(xiě)的權(quán)限,一般很少有可執(zhí)行權(quán)限。而文件映射與匿名映射區(qū)存放了共享鏈接庫(kù),所以也需要可執(zhí)行的權(quán)限。

VM_SHARD 用于指定這塊虛擬內(nèi)存區(qū)域映射的物理內(nèi)存是否可以在多進(jìn)程之間共享,以便完成進(jìn)程間通訊。
設(shè)置這個(gè)值即為 mmap 的共享映射,不設(shè)置的話(huà)則為私有映射。這個(gè)等后面我們講到 mmap 的相關(guān)實(shí)現(xiàn)時(shí)還會(huì)再次提起。
VM_IO 的設(shè)置表示這塊虛擬內(nèi)存區(qū)域可以映射至設(shè)備 IO 空間中。通常在設(shè)備驅(qū)動(dòng)程序執(zhí)行 mmap 進(jìn)行 IO 空間映射時(shí)才會(huì)被設(shè)置。
VM_RESERVED 的設(shè)置表示在內(nèi)存緊張的時(shí)候,這塊虛擬內(nèi)存區(qū)域非常重要,不能被換出到磁盤(pán)中。
VM_SEQ_READ 的設(shè)置用來(lái)暗示內(nèi)核,應(yīng)用程序?qū)@塊虛擬內(nèi)存區(qū)域的讀取是會(huì)采用順序讀的方式進(jìn)行,內(nèi)核會(huì)根據(jù)實(shí)際情況決定預(yù)讀后續(xù)的內(nèi)存頁(yè)數(shù),以便加快下次順序訪(fǎng)問(wèn)速度。
VM_RAND_READ 的設(shè)置會(huì)暗示內(nèi)核,應(yīng)用程序會(huì)對(duì)這塊虛擬內(nèi)存區(qū)域進(jìn)行隨機(jī)讀取,內(nèi)核則會(huì)根據(jù)實(shí)際情況減少預(yù)讀的內(nèi)存頁(yè)數(shù)甚至停止預(yù)讀。
我們可以通過(guò) posix_fadvise,madvise 系統(tǒng)調(diào)用來(lái)暗示內(nèi)核是否對(duì)相關(guān)內(nèi)存區(qū)域進(jìn)行順序讀取或者隨機(jī)讀取。相關(guān)的詳細(xì)內(nèi)容,大家可以看下筆者上篇文章 《從 Linux 內(nèi)核角度探秘 JDK NIO 文件讀寫(xiě)本質(zhì)》中的第 9 小節(jié)文件頁(yè)預(yù)讀部分。
通過(guò)這一系列的介紹,我們可以看到 vm_flags 就是定義整個(gè)虛擬內(nèi)存區(qū)域的訪(fǎng)問(wèn)權(quán)限以及行為規(guī)范,而內(nèi)存區(qū)域中內(nèi)存的最小單位為頁(yè)(4K),虛擬內(nèi)存區(qū)域中包含了很多這樣的虛擬頁(yè),對(duì)于虛擬內(nèi)存區(qū)域 VMA 設(shè)置的訪(fǎng)問(wèn)權(quán)限也會(huì)全部復(fù)制到區(qū)域中包含的內(nèi)存頁(yè)中。
5.5 關(guān)聯(lián)內(nèi)存映射中的映射關(guān)系
接下來(lái)的三個(gè)屬性 anon_vma,vm_file,vm_pgoff 分別和虛擬內(nèi)存映射相關(guān),虛擬內(nèi)存區(qū)域可以映射到物理內(nèi)存上,也可以映射到文件中,映射到物理內(nèi)存上我們稱(chēng)之為匿名映射,映射到文件中我們稱(chēng)之為文件映射。
那么這個(gè)映射關(guān)系在內(nèi)核中該如何表示呢?這就用到了 vm_area_struct 結(jié)構(gòu)體中的上述三個(gè)屬性。

當(dāng)我們調(diào)用 malloc 申請(qǐng)內(nèi)存時(shí),如果申請(qǐng)的是小塊內(nèi)存(低于 128K)則會(huì)使用 do_brk() 系統(tǒng)調(diào)用通過(guò)調(diào)整堆中的 brk 指針大小來(lái)增加或者回收堆內(nèi)存。
如果申請(qǐng)的是比較大塊的內(nèi)存(超過(guò) 128K)時(shí),則會(huì)調(diào)用 mmap 在上圖虛擬內(nèi)存空間中的文件映射與匿名映射區(qū)創(chuàng)建出一塊 VMA 內(nèi)存區(qū)域(這里是匿名映射)。這塊匿名映射區(qū)域就用 struct anon_vma 結(jié)構(gòu)表示。
當(dāng)調(diào)用 mmap 進(jìn)行文件映射時(shí),vm_file 屬性就用來(lái)關(guān)聯(lián)被映射的文件。這樣一來(lái)虛擬內(nèi)存區(qū)域就與映射文件關(guān)聯(lián)了起來(lái)。vm_pgoff 則表示映射進(jìn)虛擬內(nèi)存中的文件內(nèi)容,在文件中的偏移。
當(dāng)然在匿名映射中,vm_area_struct 結(jié)構(gòu)中的 vm_file 就為 null,vm_pgoff 也就沒(méi)有了意義。
vm_private_data 則用于存儲(chǔ) VMA 中的私有數(shù)據(jù)。具體的存儲(chǔ)內(nèi)容和內(nèi)存映射的類(lèi)型有關(guān),我們暫不展開(kāi)論述。
5.6 針對(duì)虛擬內(nèi)存區(qū)域的相關(guān)操作
struct vm_area_struct 結(jié)構(gòu)中還有一個(gè) vm_ops 用來(lái)指向針對(duì)虛擬內(nèi)存區(qū)域 VMA 的相關(guān)操作的函數(shù)指針。
struct vm_operations_struct {
void (*open)(struct vm_area_struct * area);
void (*close)(struct vm_area_struct * area);
vm_fault_t (*fault)(struct vm_fault *vmf);
vm_fault_t (*page_mkwrite)(struct vm_fault *vmf);
..... 省略 .......
}
當(dāng)指定的虛擬內(nèi)存區(qū)域被加入到進(jìn)程虛擬內(nèi)存空間中時(shí),open 函數(shù)會(huì)被調(diào)用
當(dāng)虛擬內(nèi)存區(qū)域 VMA 從進(jìn)程虛擬內(nèi)存空間中被刪除時(shí),close 函數(shù)會(huì)被調(diào)用
當(dāng)進(jìn)程訪(fǎng)問(wèn)虛擬內(nèi)存時(shí),訪(fǎng)問(wèn)的頁(yè)面不在物理內(nèi)存中,可能是未分配物理內(nèi)存也可能是被置換到磁盤(pán)中,這時(shí)就會(huì)產(chǎn)生缺頁(yè)異常,fault 函數(shù)就會(huì)被調(diào)用。
當(dāng)一個(gè)只讀的頁(yè)面將要變?yōu)榭蓪?xiě)時(shí),page_mkwrite 函數(shù)會(huì)被調(diào)用。
struct vm_operations_struct 結(jié)構(gòu)中定義的都是對(duì)虛擬內(nèi)存區(qū)域 VMA 的相關(guān)操作函數(shù)指針。
內(nèi)核中這種類(lèi)似的用法其實(shí)有很多,在內(nèi)核中每個(gè)特定領(lǐng)域的描述符都會(huì)定義相關(guān)的操作。比如在前邊的文章 《從 Linux 內(nèi)核角度探秘 JDK NIO 文件讀寫(xiě)本質(zhì)》 中我們介紹到內(nèi)核中的文件描述符 struct file 中定義的 struct file_operations *f_op。里面定義了內(nèi)核針對(duì)文件操作的函數(shù)指針,具體的實(shí)現(xiàn)根據(jù)不同的文件類(lèi)型有所不同。
針對(duì) Socket 文件類(lèi)型,這里的 file_operations 指向的是 socket_file_ops。

在 ext4 文件系統(tǒng)中管理的文件對(duì)應(yīng)的 file_operations 指向 ext4_file_operations,專(zhuān)門(mén)用于操作 ext4 文件系統(tǒng)中的文件。還有針對(duì) page cache 頁(yè)高速緩存相關(guān)操作定義的 address_space_operations 。

還有我們?cè)?《從 Linux 內(nèi)核角度看 IO 模型的演變》一文中介紹到,socket 相關(guān)的操作接口定義在 inet_stream_ops 函數(shù)集合中,負(fù)責(zé)對(duì)上給用戶(hù)提供接口。而 socket 與內(nèi)核協(xié)議棧之間的操作接口定義在 struct sock 中的 sk_prot 指針上,這里指向 tcp_prot 協(xié)議操作函數(shù)集合。

對(duì) socket 發(fā)起的系統(tǒng) IO 調(diào)用時(shí),在內(nèi)核中首先會(huì)調(diào)用 socket 的文件結(jié)構(gòu) struct file 中的 file_operations 文件操作集合,然后調(diào)用 struct socket 中的 ops 指向的 inet_stream_opssocket 操作函數(shù),最終調(diào)用到 struct sock 中 sk_prot 指針指向的 tcp_prot 內(nèi)核協(xié)議棧操作函數(shù)接口集合。
5.7 虛擬內(nèi)存區(qū)域在內(nèi)核中是如何被組織的
在上一小節(jié)中,我們介紹了內(nèi)核中用來(lái)表示虛擬內(nèi)存區(qū)域 VMA 的結(jié)構(gòu)體 struct vm_area_struct ,并詳細(xì)為大家剖析了 struct vm_area_struct 中的一些重要的關(guān)鍵屬性。
現(xiàn)在我們已經(jīng)熟悉了這些虛擬內(nèi)存區(qū)域,那么接下來(lái)的問(wèn)題就是在內(nèi)核中這些虛擬內(nèi)存區(qū)域是如何被組織的呢?

我們繼續(xù)來(lái)到 struct vm_area_struct 結(jié)構(gòu)中,來(lái)看一下與組織結(jié)構(gòu)相關(guān)的一些屬性:
struct vm_area_struct {
struct vm_area_struct *vm_next, *vm_prev;
struct rb_node vm_rb;
struct list_head anon_vma_chain;
struct mm_struct *vm_mm; /* The address space we belong to. */
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address
within vm_mm. */
/*
* Access permissions of this VMA.
*/
pgprot_t vm_page_prot;
unsigned long vm_flags;
struct anon_vma *anon_vma; /* Serialized by page_table_lock */
struct file * vm_file; /* File we map to (can be NULL). */
unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE
units */
void * vm_private_data; /* was vm_pte (shared mem) */
/* Function pointers to deal with this struct. */
const struct vm_operations_struct *vm_ops;
}
在內(nèi)核中其實(shí)是通過(guò)一個(gè) struct vm_area_struct 結(jié)構(gòu)的雙向鏈表將虛擬內(nèi)存空間中的這些虛擬內(nèi)存區(qū)域 VMA 串聯(lián)起來(lái)的。
vm_area_struct 結(jié)構(gòu)中的 vm_next ,vm_prev 指針?lè)謩e指向 VMA 節(jié)點(diǎn)所在雙向鏈表中的后繼節(jié)點(diǎn)和前驅(qū)節(jié)點(diǎn),內(nèi)核中的這個(gè) VMA 雙向鏈表是有順序的,所有 VMA 節(jié)點(diǎn)按照低地址到高地址的增長(zhǎng)方向排序。
雙向鏈表中的最后一個(gè) VMA 節(jié)點(diǎn)的 vm_next 指針指向 NULL,雙向鏈表的頭指針存儲(chǔ)在內(nèi)存描述符 struct mm_struct 結(jié)構(gòu)中的 mmap 中,正是這個(gè) mmap 串聯(lián)起了整個(gè)虛擬內(nèi)存空間中的虛擬內(nèi)存區(qū)域。
struct mm_struct {
struct vm_area_struct *mmap; /* list of VMAs */
}
在每個(gè)虛擬內(nèi)存區(qū)域 VMA 中又通過(guò) struct vm_area_struct 中的 vm_mm 指針指向了所屬的虛擬內(nèi)存空間 mm_struct。

我們可以通過(guò) cat /proc/pid/maps 或者 pmap pid 查看進(jìn)程的虛擬內(nèi)存空間布局以及其中包含的所有內(nèi)存區(qū)域。這兩個(gè)命令背后的實(shí)現(xiàn)原理就是通過(guò)遍歷內(nèi)核中的這個(gè) vm_area_struct 雙向鏈表獲取的。
內(nèi)核中關(guān)于這些虛擬內(nèi)存區(qū)域的操作除了遍歷之外還有許多需要根據(jù)特定虛擬內(nèi)存地址在虛擬內(nèi)存空間中查找特定的虛擬內(nèi)存區(qū)域。
尤其在進(jìn)程虛擬內(nèi)存空間中包含的內(nèi)存區(qū)域 VMA 比較多的情況下,使用紅黑樹(shù)查找特定虛擬內(nèi)存區(qū)域的時(shí)間復(fù)雜度是 O( logN ) ,可以顯著減少查找所需的時(shí)間。
所以在內(nèi)核中,同樣的內(nèi)存區(qū)域 vm_area_struct 會(huì)有兩種組織形式,一種是雙向鏈表用于高效的遍歷,另一種就是紅黑樹(shù)用于高效的查找。
每個(gè) VMA 區(qū)域都是紅黑樹(shù)中的一個(gè)節(jié)點(diǎn),通過(guò) struct vm_area_struct 結(jié)構(gòu)中的 vm_rb 將自己連接到紅黑樹(shù)中。
而紅黑樹(shù)中的根節(jié)點(diǎn)存儲(chǔ)在內(nèi)存描述符 struct mm_struct 中的 mm_rb 中:
struct mm_struct {
struct rb_root mm_rb;
}

6. 程序編譯后的二進(jìn)制文件如何映射到虛擬內(nèi)存空間中
經(jīng)過(guò)前邊這么多小節(jié)的內(nèi)容介紹,現(xiàn)在我們已經(jīng)熟悉了進(jìn)程虛擬內(nèi)存空間的布局,以及內(nèi)核如何管理這些虛擬內(nèi)存區(qū)域,并對(duì)進(jìn)程的虛擬內(nèi)存空間有了一個(gè)完整全面的認(rèn)識(shí)。
現(xiàn)在我們?cè)賮?lái)回到最初的起點(diǎn),進(jìn)程的虛擬內(nèi)存空間 mm_struct 以及這些虛擬內(nèi)存區(qū)域 vm_area_struct 是如何被創(chuàng)建并初始化的呢?

在 《3. 進(jìn)程虛擬內(nèi)存空間》小節(jié)中,我們介紹進(jìn)程的虛擬內(nèi)存空間時(shí)提到,我們寫(xiě)的程序代碼編譯之后會(huì)生成一個(gè) ELF 格式的二進(jìn)制文件,這個(gè)二進(jìn)制文件中包含了程序運(yùn)行時(shí)所需要的元信息,比如程序的機(jī)器碼,程序中的全局變量以及靜態(tài)變量等。
這個(gè) ELF 格式的二進(jìn)制文件中的布局和我們前邊講的虛擬內(nèi)存空間中的布局類(lèi)似,也是一段一段的,每一段包含了不同的元數(shù)據(jù)。
磁盤(pán)文件中的段我們叫做 Section,內(nèi)存中的段我們叫做 Segment,也就是內(nèi)存區(qū)域。
磁盤(pán)文件中的這些 Section 會(huì)在進(jìn)程運(yùn)行之前加載到內(nèi)存中并映射到內(nèi)存中的 Segment。通常是多個(gè) Section 映射到一個(gè) Segment。
比如磁盤(pán)文件中的 .text,.rodata 等一些只讀的 Section,會(huì)被映射到內(nèi)存的一個(gè)只讀可執(zhí)行的 Segment 里(代碼段)。而 .data,.bss 等一些可讀寫(xiě)的 Section,則會(huì)被映射到內(nèi)存的一個(gè)具有讀寫(xiě)權(quán)限的 Segment 里(數(shù)據(jù)段,BSS 段)。
那么這些 ELF 格式的二進(jìn)制文件中的 Section 是如何加載并映射進(jìn)虛擬內(nèi)存空間的呢?
內(nèi)核中完成這個(gè)映射過(guò)程的函數(shù)是 load_elf_binary ,這個(gè)函數(shù)的作用很大,加載內(nèi)核的是它,啟動(dòng)第一個(gè)用戶(hù)態(tài)進(jìn)程 init 的是它,fork 完了以后,調(diào)用 exec 運(yùn)行一個(gè)二進(jìn)制程序的也是它。當(dāng) exec 運(yùn)行一個(gè)二進(jìn)制程序的時(shí)候,除了解析 ELF 的格式之外,另外一個(gè)重要的事情就是建立上述提到的內(nèi)存映射。
static int load_elf_binary(struct linux_binprm *bprm)
{
...... 省略 ........
// 設(shè)置虛擬內(nèi)存空間中的內(nèi)存映射區(qū)域起始地址 mmap_base
setup_new_exec(bprm);
...... 省略 ........
// 創(chuàng)建并初始化棧對(duì)應(yīng)的 vm_area_struct 結(jié)構(gòu)。
// 設(shè)置 mm->start_stack 就是棧的起始地址也就是棧底,并將 mm->arg_start 是指向棧底的。
retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
executable_stack);
...... 省略 ........
// 將二進(jìn)制文件中的代碼部分映射到虛擬內(nèi)存空間中
error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
elf_prot, elf_flags, total_size);
...... 省略 ........
// 創(chuàng)建并初始化堆對(duì)應(yīng)的的 vm_area_struct 結(jié)構(gòu)
// 設(shè)置 current->mm->start_brk = current->mm->brk,設(shè)置堆的起始地址 start_brk,結(jié)束地址 brk。 起初兩者相等表示堆是空的
retval = set_brk(elf_bss, elf_brk, bss_prot);
...... 省略 ........
// 將進(jìn)程依賴(lài)的動(dòng)態(tài)鏈接庫(kù) .so 文件映射到虛擬內(nèi)存空間中的內(nèi)存映射區(qū)域
elf_entry = load_elf_interp(&loc->interp_elf_ex,
interpreter,
&interp_map_addr,
load_bias, interp_elf_phdata);
...... 省略 ........
// 初始化內(nèi)存描述符 mm_struct
current->mm->end_code = end_code;
current->mm->start_code = start_code;
current->mm->start_data = start_data;
current->mm->end_data = end_data;
current->mm->start_stack = bprm->p;
...... 省略 ........
}
setup_new_exec 設(shè)置虛擬內(nèi)存空間中的內(nèi)存映射區(qū)域起始地址 mmap_base
setup_arg_pages 創(chuàng)建并初始化棧對(duì)應(yīng)的 vm_area_struct 結(jié)構(gòu)。置 mm->start_stack 就是棧的起始地址也就是棧底,并將 mm->arg_start 是指向棧底的。
elf_map 將 ELF 格式的二進(jìn)制文件中.text ,.data,.bss 部分映射到虛擬內(nèi)存空間中的代碼段,數(shù)據(jù)段,BSS 段中。
set_brk 創(chuàng)建并初始化堆對(duì)應(yīng)的的 vm_area_struct 結(jié)構(gòu),設(shè)置
current->mm->start_brk = current->mm->brk,設(shè)置堆的起始地址 start_brk,結(jié)束地址 brk。 起初兩者相等表示堆是空的。load_elf_interp 將進(jìn)程依賴(lài)的動(dòng)態(tài)鏈接庫(kù) .so 文件映射到虛擬內(nèi)存空間中的內(nèi)存映射區(qū)域
初始化內(nèi)存描述符 mm_struct
7. 內(nèi)核虛擬內(nèi)存空間
現(xiàn)在我們已經(jīng)知道了進(jìn)程虛擬內(nèi)存空間在內(nèi)核中的布局以及管理,那么內(nèi)核態(tài)的虛擬內(nèi)存空間又是什么樣子的呢?本小節(jié)筆者就帶大家來(lái)一層一層地拆開(kāi)這個(gè)黑盒子。
之前在介紹進(jìn)程虛擬內(nèi)存空間的時(shí)候,筆者提到不同進(jìn)程之間的虛擬內(nèi)存空間是相互隔離的,彼此之間相互獨(dú)立,相互感知不到其他進(jìn)程的存在。使得進(jìn)程以為自己擁有所有的內(nèi)存資源。

而內(nèi)核態(tài)虛擬內(nèi)存空間是所有進(jìn)程共享的,不同進(jìn)程進(jìn)入內(nèi)核態(tài)之后看到的虛擬內(nèi)存空間全部是一樣的。
什么意思呢?比如上圖中的進(jìn)程 a,進(jìn)程 b,進(jìn)程 c 分別在各自的用戶(hù)態(tài)虛擬內(nèi)存空間中訪(fǎng)問(wèn)虛擬地址 x 。由于進(jìn)程之間的用戶(hù)態(tài)虛擬內(nèi)存空間是相互隔離相互獨(dú)立的,雖然在進(jìn)程a,進(jìn)程b,進(jìn)程c 訪(fǎng)問(wèn)的都是虛擬地址 x 但是看到的內(nèi)容卻是不一樣的(背后可能映射到不同的物理內(nèi)存中)。
但是當(dāng)進(jìn)程 a,進(jìn)程 b,進(jìn)程 c 進(jìn)入到內(nèi)核態(tài)之后情況就不一樣了,由于內(nèi)核虛擬內(nèi)存空間是各個(gè)進(jìn)程共享的,所以它們?cè)趦?nèi)核空間中看到的內(nèi)容全部是一樣的,比如進(jìn)程 a,進(jìn)程 b,進(jìn)程 c 在內(nèi)核態(tài)都去訪(fǎng)問(wèn)虛擬地址 y。這時(shí)它們看到的內(nèi)容就是一樣的了。
這里筆者和大家澄清一個(gè)經(jīng)常被誤解的概念:由于內(nèi)核會(huì)涉及到物理內(nèi)存的管理,所以很多人會(huì)想當(dāng)然地認(rèn)為只要進(jìn)入了內(nèi)核態(tài)就開(kāi)始使用物理地址了,這就大錯(cuò)特錯(cuò)了,千萬(wàn)不要這樣理解,進(jìn)程進(jìn)入內(nèi)核態(tài)之后使用的仍然是虛擬內(nèi)存地址,只不過(guò)在內(nèi)核中使用的虛擬內(nèi)存地址被限制在了內(nèi)核態(tài)虛擬內(nèi)存空間范圍中,這也是本小節(jié)筆者要為大家介紹的主題。
在清楚了這個(gè)基本概念之后,下面筆者分別從 32 位體系 和 64 位體系下為大家介紹內(nèi)核態(tài)虛擬內(nèi)存空間的布局。
7.1 32 位體系內(nèi)核虛擬內(nèi)存空間布局
在前邊《5.1 內(nèi)核如何劃分用戶(hù)態(tài)和內(nèi)核態(tài)虛擬內(nèi)存空間》小節(jié)中我們提到,內(nèi)核在 /arch/x86/include/asm/page_32_types.h 文件中通過(guò) TASK_SIZE 將進(jìn)程虛擬內(nèi)存空間和內(nèi)核虛擬內(nèi)存空間分割開(kāi)來(lái)。
/*
* User space process size: 3GB (default).
*/
#define TASK_SIZE __PAGE_OFFSET
__PAGE_OFFSET 的值在 32 位系統(tǒng)下為 0xC000 000

在 32 位體系結(jié)構(gòu)下進(jìn)程用戶(hù)態(tài)虛擬內(nèi)存空間為 3 GB,虛擬內(nèi)存地址范圍為:0x0000 0000 - 0xC000 000 。內(nèi)核態(tài)虛擬內(nèi)存空間為 1 GB,虛擬內(nèi)存地址范圍為:0xC000 000 - 0xFFFF FFFF。
本小節(jié)我們主要關(guān)注 0xC000 000 - 0xFFFF FFFF 這段虛擬內(nèi)存地址區(qū)域也就是內(nèi)核虛擬內(nèi)存空間的布局情況。
7.1.1 直接映射區(qū)
在總共大小 1G 的內(nèi)核虛擬內(nèi)存空間中,位于最前邊有一塊 896M 大小的區(qū)域,我們稱(chēng)之為直接映射區(qū)或者線(xiàn)性映射區(qū),地址范圍為 3G -- 3G + 896m 。
之所以這塊 896M 大小的區(qū)域稱(chēng)為直接映射區(qū)或者線(xiàn)性映射區(qū),是因?yàn)檫@塊連續(xù)的虛擬內(nèi)存地址會(huì)映射到 0 - 896M 這塊連續(xù)的物理內(nèi)存上。
也就是說(shuō) 3G -- 3G + 896m 這塊 896M 大小的虛擬內(nèi)存會(huì)直接映射到 0 - 896M 這塊 896M 大小的物理內(nèi)存上,這塊區(qū)域中的虛擬內(nèi)存地址直接減去 0xC000 0000 (3G) 就得到了物理內(nèi)存地址。所以我們稱(chēng)這塊區(qū)域?yàn)橹苯佑成鋮^(qū)。
為了方便為大家解釋?zhuān)覀兗僭O(shè)現(xiàn)在機(jī)器上的物理內(nèi)存為 4G 大小

雖然這塊區(qū)域中的虛擬地址是直接映射到物理地址上,但是內(nèi)核在訪(fǎng)問(wèn)這段區(qū)域的時(shí)候還是走的虛擬內(nèi)存地址,內(nèi)核也會(huì)為這塊空間建立映射頁(yè)表。關(guān)于頁(yè)表的概念筆者后續(xù)會(huì)為大家詳細(xì)講解,這里大家只需要簡(jiǎn)單理解為頁(yè)表保存了虛擬地址到物理地址的映射關(guān)系即可。
大家這里只需要記得內(nèi)核態(tài)虛擬內(nèi)存空間的前 896M 區(qū)域是直接映射到物理內(nèi)存中的前 896M 區(qū)域中的,直接映射區(qū)中的映射關(guān)系是一比一映射。映射關(guān)系是固定的不會(huì)改變。
明白了這個(gè)關(guān)系之后,我們接下來(lái)就看一下這塊直接映射區(qū)域在物理內(nèi)存中究竟存的是什么內(nèi)容~~~
在這段 896M 大小的物理內(nèi)存中,前 1M 已經(jīng)在系統(tǒng)啟動(dòng)的時(shí)候被系統(tǒng)占用,1M 之后的物理內(nèi)存存放的是內(nèi)核代碼段,數(shù)據(jù)段,BSS 段(這些信息起初存放在 ELF格式的二進(jìn)制文件中,在系統(tǒng)啟動(dòng)的時(shí)候被加載進(jìn)內(nèi)存)。
我們可以通過(guò)
cat /proc/iomem命令查看具體物理內(nèi)存布局情況。
當(dāng)我們使用 fork 系統(tǒng)調(diào)用創(chuàng)建進(jìn)程的時(shí)候,內(nèi)核會(huì)創(chuàng)建一系列進(jìn)程相關(guān)的描述符,比如之前提到的進(jìn)程的核心數(shù)據(jù)結(jié)構(gòu) task_struct,進(jìn)程的內(nèi)存空間描述符 mm_struct,以及虛擬內(nèi)存區(qū)域描述符 vm_area_struct 等。
這些進(jìn)程相關(guān)的數(shù)據(jù)結(jié)構(gòu)也會(huì)存放在物理內(nèi)存前 896M 的這段區(qū)域中,當(dāng)然也會(huì)被直接映射至內(nèi)核態(tài)虛擬內(nèi)存空間中的 3G -- 3G + 896m 這段直接映射區(qū)域中。

當(dāng)進(jìn)程被創(chuàng)建完畢之后,在內(nèi)核運(yùn)行的過(guò)程中,會(huì)涉及內(nèi)核棧的分配,內(nèi)核會(huì)為每個(gè)進(jìn)程分配一個(gè)固定大小的內(nèi)核棧(一般是兩個(gè)頁(yè)大小,依賴(lài)具體的體系結(jié)構(gòu)),每個(gè)進(jìn)程的整個(gè)調(diào)用鏈必須放在自己的內(nèi)核棧中,內(nèi)核棧也是分配在直接映射區(qū)。
與進(jìn)程用戶(hù)空間中的棧不同的是,內(nèi)核棧容量小而且是固定的,用戶(hù)空間中的棧容量大而且可以動(dòng)態(tài)擴(kuò)展。內(nèi)核棧的溢出危害非常巨大,它會(huì)直接悄無(wú)聲息的覆蓋相鄰內(nèi)存區(qū)域中的數(shù)據(jù),破壞數(shù)據(jù)。
通過(guò)以上內(nèi)容的介紹我們了解到內(nèi)核虛擬內(nèi)存空間最前邊的這段 896M 大小的直接映射區(qū)如何與物理內(nèi)存進(jìn)行映射關(guān)聯(lián),并且清楚了直接映射區(qū)主要用來(lái)存放哪些內(nèi)容。
寫(xiě)到這里,筆者覺(jué)得還是有必要再次從功能劃分的角度為大家介紹下這塊直接映射區(qū)域。
我們都知道內(nèi)核對(duì)物理內(nèi)存的管理都是以頁(yè)為最小單位來(lái)管理的,每頁(yè)默認(rèn) 4K 大小,理想狀況下任何種類(lèi)的數(shù)據(jù)頁(yè)都可以存放在任何頁(yè)框中,沒(méi)有什么限制。比如:存放內(nèi)核數(shù)據(jù),用戶(hù)數(shù)據(jù),緩沖磁盤(pán)數(shù)據(jù)等。
但是實(shí)際的計(jì)算機(jī)體系結(jié)構(gòu)受到硬件方面的限制制約,間接導(dǎo)致限制了頁(yè)框的使用方式。
比如在 X86 體系結(jié)構(gòu)下,ISA 總線(xiàn)的 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 專(zhuān)門(mén)讓內(nèi)核用來(lái)為 DMA 分配內(nèi)存,這塊 16M 大小的內(nèi)存區(qū)域我們稱(chēng)之為 ZONE_DMA。
用于 DMA 的內(nèi)存必須從 ZONE_DMA 區(qū)域中分配。
而直接映射區(qū)中剩下的部分也就是從 16M 到 896M(不包含 896M)這段區(qū)域,我們稱(chēng)之為 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)存上。

注意這里的 ZONE_DMA 和 ZONE_NORMAL 是內(nèi)核針對(duì)物理內(nèi)存區(qū)域的劃分。
現(xiàn)在物理內(nèi)存中的前 896M 的區(qū)域也就是前邊介紹的 ZONE_DMA 和 ZONE_NORMAL 區(qū)域到內(nèi)核虛擬內(nèi)存空間的映射筆者就為大家介紹完了,它們都是采用直接映射的方式,一比一就行映射。
7.1.2 ZONE_HIGHMEM 高端內(nèi)存
而物理內(nèi)存 896M 以上的區(qū)域被內(nèi)核劃分為 ZONE_HIGHMEM 區(qū)域,我們稱(chēng)之為高端內(nèi)存。
本例中我們的物理內(nèi)存假設(shè)為 4G,高端內(nèi)存區(qū)域?yàn)?4G - 896M = 3200M,那么這塊 3200M 大小的 ZONE_HIGHMEM 區(qū)域該如何映射到內(nèi)核虛擬內(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)的一部分一部分的分批映射,先映射正在使用的這部分,使用完畢解除映射,接著映射其他部分。
知道了 ZONE_HIGHMEM 區(qū)域的映射原理,我們接著往下看這 128M 大小的內(nèi)核虛擬內(nèi)存空間究竟是如何布局的?

內(nèi)核虛擬內(nèi)存空間中的 3G + 896M 這塊地址在內(nèi)核中定義為 high_memory,high_memory 往上有一段 8M 大小的內(nèi)存空洞。空洞范圍為:high_memory 到 VMALLOC_START 。
VMALLOC_START 定義在內(nèi)核源碼 /arch/x86/include/asm/pgtable_32_areas.h 文件中:
#define VMALLOC_OFFSET (8 * 1024 * 1024)
#define VMALLOC_START ((unsigned long)high_memory + VMALLOC_OFFSET)
7.1.3 vmalloc 動(dòng)態(tài)映射區(qū)
接下來(lái) VMALLOC_START 到 VMALLOC_END 之間的這塊區(qū)域成為動(dòng)態(tài)映射區(qū)。采用動(dòng)態(tài)映射的方式映射物理內(nèi)存中的高端內(nèi)存。
#ifdef CONFIG_HIGHMEM
# define VMALLOC_END (PKMAP_BASE - 2 * PAGE_SIZE)
#else
# define VMALLOC_END (LDT_BASE_ADDR - 2 * PAGE_SIZE)
#endif

和用戶(hù)態(tài)進(jìn)程使用 malloc 申請(qǐng)內(nèi)存一樣,在這塊動(dòng)態(tài)映射區(qū)內(nèi)核是使用 vmalloc 進(jìn)行內(nèi)存分配。由于之前介紹的動(dòng)態(tài)映射的原因,vmalloc 分配的內(nèi)存在虛擬內(nèi)存上是連續(xù)的,但是物理內(nèi)存是不連續(xù)的。通過(guò)頁(yè)表來(lái)建立物理內(nèi)存與虛擬內(nèi)存之間的映射關(guān)系,從而可以將不連續(xù)的物理內(nèi)存映射到連續(xù)的虛擬內(nèi)存上。
由于 vmalloc 獲得的物理內(nèi)存頁(yè)是不連續(xù)的,因此它只能將這些物理內(nèi)存頁(yè)一個(gè)一個(gè)地進(jìn)行映射,在性能開(kāi)銷(xiāo)上會(huì)比直接映射大得多。
關(guān)于 vmalloc 分配內(nèi)存的相關(guān)實(shí)現(xiàn)原理,筆者會(huì)在后面的文章中為大家講解,這里大家只需要明白它在哪塊虛擬內(nèi)存區(qū)域中活動(dòng)即可。
7.1.4 永久映射區(qū)

而在 PKMAP_BASE 到 FIXADDR_START 之間的這段空間稱(chēng)為永久映射區(qū)。在內(nèi)核的這段虛擬地址空間中允許建立與物理高端內(nèi)存的長(zhǎng)期映射關(guān)系。比如內(nèi)核通過(guò) alloc_pages() 函數(shù)在物理內(nèi)存的高端內(nèi)存中申請(qǐng)獲取到的物理內(nèi)存頁(yè),這些物理內(nèi)存頁(yè)可以通過(guò)調(diào)用 kmap 映射到永久映射區(qū)中。
LAST_PKMAP 表示永久映射區(qū)可以映射的頁(yè)數(shù)限制。
#define PKMAP_BASE \
((LDT_BASE_ADDR - PAGE_SIZE) & PMD_MASK)
#define LAST_PKMAP 1024
8.1.5 固定映射區(qū)

內(nèi)核虛擬內(nèi)存空間中的下一個(gè)區(qū)域?yàn)楣潭ㄓ成鋮^(qū),區(qū)域范圍為:FIXADDR_START 到 FIXADDR_TOP。
FIXADDR_START 和 FIXADDR_TOP 定義在內(nèi)核源碼 /arch/x86/include/asm/fixmap.h 文件中:
#define FIXADDR_START (FIXADDR_TOP - FIXADDR_SIZE)
extern unsigned long __FIXADDR_TOP; // 0xFFFF F000
#define FIXADDR_TOP ((unsigned long)__FIXADDR_TOP)
在內(nèi)核虛擬內(nèi)存空間的直接映射區(qū)中,直接映射區(qū)中的虛擬內(nèi)存地址與物理內(nèi)存前 896M 的空間的映射關(guān)系都是預(yù)設(shè)好的,一比一映射。
在固定映射區(qū)中的虛擬內(nèi)存地址可以自由映射到物理內(nèi)存的高端地址上,但是與動(dòng)態(tài)映射區(qū)以及永久映射區(qū)不同的是,在固定映射區(qū)中虛擬地址是固定的,而被映射的物理地址是可以改變的。也就是說(shuō),有些虛擬地址在編譯的時(shí)候就固定下來(lái)了,是在內(nèi)核啟動(dòng)過(guò)程中被確定的,而這些虛擬地址對(duì)應(yīng)的物理地址不是固定的。采用固定虛擬地址的好處是它相當(dāng)于一個(gè)指針常量(常量的值在編譯時(shí)確定),指向物理地址,如果虛擬地址不固定,則相當(dāng)于一個(gè)指針變量。
那為什么會(huì)有固定映射這個(gè)概念呢 ? 比如:在內(nèi)核的啟動(dòng)過(guò)程中,有些模塊需要使用虛擬內(nèi)存并映射到指定的物理地址上,而且這些模塊也沒(méi)有辦法等待完整的內(nèi)存管理模塊初始化之后再進(jìn)行地址映射。因此,內(nèi)核固定分配了一些虛擬地址,這些地址有固定的用途,使用該地址的模塊在初始化的時(shí)候,將這些固定分配的虛擬地址映射到指定的物理地址上去。
7.1.6 臨時(shí)映射區(qū)
在內(nèi)核虛擬內(nèi)存空間中的最后一塊區(qū)域?yàn)榕R時(shí)映射區(qū),那么這塊臨時(shí)映射區(qū)是用來(lái)干什么的呢?

筆者在之前文章 《從 Linux 內(nèi)核角度探秘 JDK NIO 文件讀寫(xiě)本質(zhì)》 的 “ 12.3 iov_iter_copy_from_user_atomic ” 小節(jié)中介紹在 Buffered IO 模式下進(jìn)行文件寫(xiě)入的時(shí)候,在下圖中的第四步,內(nèi)核會(huì)調(diào)用 iov_iter_copy_from_user_atomic 函數(shù)將用戶(hù)空間緩沖區(qū) DirectByteBuffer 中的待寫(xiě)入數(shù)據(jù)拷貝到 page cache 中。

但是內(nèi)核又不能直接進(jìn)行拷貝,因?yàn)榇藭r(shí)從 page cache 中取出的緩存頁(yè) page 是物理地址,而在內(nèi)核中是不能夠直接操作物理地址的,只能操作虛擬地址。
那怎么辦呢?所以就需要使用 kmap_atomic 將緩存頁(yè)臨時(shí)映射到內(nèi)核空間的一段虛擬地址上,這段虛擬地址就位于內(nèi)核虛擬內(nèi)存空間中的臨時(shí)映射區(qū)上,然后將用戶(hù)空間緩存區(qū) DirectByteBuffer 中的待寫(xiě)入數(shù)據(jù)通過(guò)這段映射的虛擬地址拷貝到 page cache 中的相應(yīng)緩存頁(yè)中。這時(shí)文件的寫(xiě)入操作就已經(jīng)完成了。
由于是臨時(shí)映射,所以在拷貝完成之后,調(diào)用 kunmap_atomic 將這段映射再解除掉。
size_t iov_iter_copy_from_user_atomic(struct page *page,
struct iov_iter *i, unsigned long offset, size_t bytes)
{
// 將緩存頁(yè)臨時(shí)映射到內(nèi)核虛擬地址空間的臨時(shí)映射區(qū)中
char *kaddr = kmap_atomic(page),
*p = kaddr + offset;
// 將用戶(hù)緩存區(qū) DirectByteBuffer 中的待寫(xiě)入數(shù)據(jù)拷貝到文件緩存頁(yè)中
iterate_all_kinds(i, bytes, v,
copyin((p += v.iov_len) - v.iov_len, v.iov_base, v.iov_len),
memcpy_from_page((p += v.bv_len) - v.bv_len, v.bv_page,
v.bv_offset, v.bv_len),
memcpy((p += v.iov_len) - v.iov_len, v.iov_base, v.iov_len)
)
// 解除內(nèi)核虛擬地址空間與緩存頁(yè)之間的臨時(shí)映射,這里映射只是為了臨時(shí)拷貝數(shù)據(jù)用
kunmap_atomic(kaddr);
return bytes;
}
7.1.7 32位體系結(jié)構(gòu)下 Linux 虛擬內(nèi)存空間整體布局
到現(xiàn)在為止,整個(gè)內(nèi)核虛擬內(nèi)存空間在 32 位體系下的布局,筆者就為大家詳細(xì)介紹完畢了,我們?cè)俅谓Y(jié)合前邊《4.1 32 位機(jī)器上進(jìn)程虛擬內(nèi)存空間分布》小節(jié)中介紹的進(jìn)程虛擬內(nèi)存空間和本小節(jié)介紹的內(nèi)核虛擬內(nèi)存空間來(lái)整體回顧下 32 位體系結(jié)構(gòu) Linux 的整個(gè)虛擬內(nèi)存空間的布局:

7.2 64 位體系內(nèi)核虛擬內(nèi)存空間布局
內(nèi)核虛擬內(nèi)存空間在 32 位體系下只有 1G 大小,實(shí)在太小了,因此需要精細(xì)化的管理,于是按照功能分類(lèi)劃分除了很多內(nèi)核虛擬內(nèi)存區(qū)域,這樣就顯得非常復(fù)雜。
到了 64 位體系下,內(nèi)核虛擬內(nèi)存空間的布局和管理就變得容易多了,因?yàn)檫M(jìn)程虛擬內(nèi)存空間和內(nèi)核虛擬內(nèi)存空間各自占用 128T 的虛擬內(nèi)存,實(shí)在是太大了,我們可以在這里邊隨意翱翔,隨意揮霍。
因此在 64 位體系下的內(nèi)核虛擬內(nèi)存空間與物理內(nèi)存的映射就變得非常簡(jiǎn)單,由于虛擬內(nèi)存空間足夠的大,即便是內(nèi)核要訪(fǎng)問(wèn)全部的物理內(nèi)存,直接映射就可以了,不在需要用到《7.1.2 ZONE_HIGHMEM 高端內(nèi)存》小節(jié)中介紹的高端內(nèi)存那種動(dòng)態(tài)映射方式。
在前邊《5.1 內(nèi)核如何劃分用戶(hù)態(tài)和內(nèi)核態(tài)虛擬內(nèi)存空間》小節(jié)中我們提到,內(nèi)核在 /arch/x86/include/asm/page_64_types.h 文件中通過(guò) TASK_SIZE 將進(jìn)程虛擬內(nèi)存空間和內(nèi)核虛擬內(nèi)存空間分割開(kāi)來(lái)。
#define TASK_SIZE (test_thread_flag(TIF_ADDR32) ? \
IA32_PAGE_OFFSET : TASK_SIZE_MAX)
#define TASK_SIZE_MAX task_size_max()
#define task_size_max() ((_AC(1,UL) << __VIRTUAL_MASK_SHIFT) - PAGE_SIZE)
#define __VIRTUAL_MASK_SHIFT 47
64 位系統(tǒng)中的 TASK_SIZE 為 0x00007FFFFFFFF000

在 64 位系統(tǒng)中,只使用了其中的低 48 位來(lái)表示虛擬內(nèi)存地址。其中用戶(hù)態(tài)虛擬內(nèi)存空間為低 128 T,虛擬內(nèi)存地址范圍為:0x0000 0000 0000 0000 - 0x0000 7FFF FFFF F000 。
內(nèi)核態(tài)虛擬內(nèi)存空間為高 128 T,虛擬內(nèi)存地址范圍為:0xFFFF 8000 0000 0000 - 0xFFFF FFFF FFFF FFFF 。
本小節(jié)我們主要關(guān)注 0xFFFF 8000 0000 0000 - 0xFFFF FFFF FFFF FFFF 這段內(nèi)核虛擬內(nèi)存空間的布局情況。

64 位內(nèi)核虛擬內(nèi)存空間從 0xFFFF 8000 0000 0000 開(kāi)始到 0xFFFF 8800 0000 0000 這段地址空間是一個(gè) 8T 大小的內(nèi)存空洞區(qū)域。
緊著著 8T 大小的內(nèi)存空洞下一個(gè)區(qū)域就是 64T 大小的直接映射區(qū)。這個(gè)區(qū)域中的虛擬內(nèi)存地址減去 PAGE_OFFSET 就直接得到了物理內(nèi)存地址。
PAGE_OFFSET 變量定義在 /arch/x86/include/asm/page_64_types.h 文件中:
#define __PAGE_OFFSET_BASE _AC(0xffff880000000000, UL)
#define __PAGE_OFFSET __PAGE_OFFSET_BASE
從圖中 VMALLOC_START 到 VMALLOC_END 的這段區(qū)域是 32T 大小的 vmalloc 映射區(qū),這里類(lèi)似用戶(hù)空間中的堆,內(nèi)核在這里使用 vmalloc 系統(tǒng)調(diào)用申請(qǐng)內(nèi)存。
VMALLOC_START 和 VMALLOC_END 變量定義在 /arch/x86/include/asm/pgtable_64_types.h 文件中:
#define __VMALLOC_BASE_L4 0xffffc90000000000UL
#define VMEMMAP_START __VMEMMAP_BASE_L4
#define VMALLOC_END (VMALLOC_START + (VMALLOC_SIZE_TB << 40) - 1)
從 VMEMMAP_START 開(kāi)始是 1T 大小的虛擬內(nèi)存映射區(qū),用于存放物理頁(yè)面的描述符 struct page 結(jié)構(gòu)用來(lái)表示物理內(nèi)存頁(yè)。
VMEMMAP_START 變量定義在 /arch/x86/include/asm/pgtable_64_types.h 文件中:
#define __VMEMMAP_BASE_L4 0xffffea0000000000UL
# define VMEMMAP_START __VMEMMAP_BASE_L4
從 __START_KERNEL_map 開(kāi)始是大小為 512M 的區(qū)域用于存放內(nèi)核代碼段、全局變量、BSS 等。這里對(duì)應(yīng)到物理內(nèi)存開(kāi)始的位置,減去 __START_KERNEL_map 就能得到物理內(nèi)存的地址。這里和直接映射區(qū)有點(diǎn)像,但是不矛盾,因?yàn)橹苯佑成鋮^(qū)之前有 8T 的空洞區(qū)域,早就過(guò)了內(nèi)核代碼在物理內(nèi)存中加載的位置。
__START_KERNEL_map 變量定義在 /arch/x86/include/asm/page_64_types.h 文件中:
#define __START_KERNEL_map _AC(0xffffffff80000000, UL)
7.2.1 64位體系結(jié)構(gòu)下 Linux 虛擬內(nèi)存空間整體布局
到現(xiàn)在為止,整個(gè)內(nèi)核虛擬內(nèi)存空間在 64 位體系下的布局筆者就為大家詳細(xì)介紹完畢了,我們?cè)俅谓Y(jié)合前邊《4.2 64 位機(jī)器上進(jìn)程虛擬內(nèi)存空間分布》小節(jié)介紹的進(jìn)程虛擬內(nèi)存空間和本小節(jié)介紹的內(nèi)核虛擬內(nèi)存空間來(lái)整體回顧下 64 位體系結(jié)構(gòu) Linux 的整個(gè)虛擬內(nèi)存空間的布局:

8. 到底什么是物理內(nèi)存地址
聊完了虛擬內(nèi)存,我們接著聊一下物理內(nèi)存,我們平時(shí)所稱(chēng)的內(nèi)存也叫隨機(jī)訪(fǎng)問(wèn)存儲(chǔ)器( random-access memory )也叫 RAM 。而 RAM 分為兩類(lèi):
一類(lèi)是靜態(tài) RAM( SRAM),這類(lèi) SRAM 用于 CPU 高速緩存 L1Cache,L2Cache,L3Cache。其特點(diǎn)是訪(fǎng)問(wèn)速度快,訪(fǎng)問(wèn)速度為 1 - 30 個(gè)時(shí)鐘周期,但是容量小,造價(jià)高。

另一類(lèi)則是動(dòng)態(tài) RAM ( DRAM),這類(lèi) DRAM 用于我們常說(shuō)的主存上,其特點(diǎn)的是訪(fǎng)問(wèn)速度慢(相對(duì)高速緩存),訪(fǎng)問(wèn)速度為 50 - 200 個(gè)時(shí)鐘周期,但是容量大,造價(jià)便宜些(相對(duì)高速緩存)。
內(nèi)存由一個(gè)一個(gè)的存儲(chǔ)器模塊(memory module)組成,它們插在主板的擴(kuò)展槽上。常見(jiàn)的存儲(chǔ)器模塊通常以 64 位為單位( 8 個(gè)字節(jié))傳輸數(shù)據(jù)到存儲(chǔ)控制器上或者從存儲(chǔ)控制器傳出數(shù)據(jù)。

如圖所示內(nèi)存條上黑色的元器件就是存儲(chǔ)器模塊(memory module)。多個(gè)存儲(chǔ)器模塊連接到存儲(chǔ)控制器上,就聚合成了主存。

而 DRAM 芯片就包裝在存儲(chǔ)器模塊中,每個(gè)存儲(chǔ)器模塊中包含 8 個(gè) DRAM 芯片,依次編號(hào)為 0 - 7 。

而每一個(gè) DRAM 芯片的存儲(chǔ)結(jié)構(gòu)是一個(gè)二維矩陣,二維矩陣中存儲(chǔ)的元素我們稱(chēng)為超單元(supercell),每個(gè) supercell 大小為一個(gè)字節(jié)(8 bit)。每個(gè) supercell 都由一個(gè)坐標(biāo)地址(i,j)。
i 表示二維矩陣中的行地址,在計(jì)算機(jī)中行地址稱(chēng)為 RAS (row access strobe,行訪(fǎng)問(wèn)選通脈沖)。 j 表示二維矩陣中的列地址,在計(jì)算機(jī)中列地址稱(chēng)為 CAS (column access strobe,列訪(fǎng)問(wèn)選通脈沖)。
下圖中的 supercell 的 RAS = 2,CAS = 2。

DRAM 芯片中的信息通過(guò)引腳流入流出 DRAM 芯片。每個(gè)引腳攜帶 1 bit的信號(hào)。
圖中 DRAM 芯片包含了兩個(gè)地址引腳( addr ),因?yàn)槲覀円ㄟ^(guò) RAS,CAS 來(lái)定位要獲取的 supercell 。還有 8 個(gè)數(shù)據(jù)引腳(data),因?yàn)?DRAM 芯片的 IO 單位為一個(gè)字節(jié)(8 bit),所以需要 8 個(gè) data 引腳從 DRAM 芯片傳入傳出數(shù)據(jù)。
注意這里只是為了解釋地址引腳和數(shù)據(jù)引腳的概念,實(shí)際硬件中的引腳數(shù)量是不一定的。
8.1 DRAM 芯片的訪(fǎng)問(wèn)
我們現(xiàn)在就以讀取上圖中坐標(biāo)地址為(2,2)的 supercell 為例,來(lái)說(shuō)明訪(fǎng)問(wèn) DRAM 芯片的過(guò)程。

首先存儲(chǔ)控制器將行地址 RAS = 2 通過(guò)地址引腳發(fā)送給 DRAM 芯片。
DRAM 芯片根據(jù) RAS = 2 將二維矩陣中的第二行的全部?jī)?nèi)容拷貝到內(nèi)部行緩沖區(qū)中。
接下來(lái)存儲(chǔ)控制器會(huì)通過(guò)地址引腳發(fā)送 CAS = 2 到 DRAM 芯片中。
DRAM芯片從內(nèi)部行緩沖區(qū)中根據(jù) CAS = 2 拷貝出第二列的 supercell 并通過(guò)數(shù)據(jù)引腳發(fā)送給存儲(chǔ)控制器。
DRAM 芯片的 IO 單位為一個(gè) supercell ,也就是一個(gè)字節(jié)(8 bit)。
8.2 CPU 如何讀寫(xiě)主存
前邊我們介紹了內(nèi)存的物理結(jié)構(gòu),以及如何訪(fǎng)問(wèn)內(nèi)存中的 DRAM 芯片獲取 supercell 中存儲(chǔ)的數(shù)據(jù)(一個(gè)字節(jié))。本小節(jié)我們來(lái)介紹下 CPU 是如何訪(fǎng)問(wèn)內(nèi)存的:

CPU 與內(nèi)存之間的數(shù)據(jù)交互是通過(guò)總線(xiàn)(bus)完成的,而數(shù)據(jù)在總線(xiàn)上的傳送是通過(guò)一系列的步驟完成的,這些步驟稱(chēng)為總線(xiàn)事務(wù)(bus transaction)。
其中數(shù)據(jù)從內(nèi)存?zhèn)魉偷?CPU 稱(chēng)之為讀事務(wù)(read transaction),數(shù)據(jù)從 CPU 傳送到內(nèi)存稱(chēng)之為寫(xiě)事務(wù)(write transaction)。
總線(xiàn)上傳輸?shù)男盘?hào)包括:地址信號(hào),數(shù)據(jù)信號(hào),控制信號(hào)。其中控制總線(xiàn)上傳輸?shù)目刂菩盘?hào)可以同步事務(wù),并能夠標(biāo)識(shí)出當(dāng)前正在被執(zhí)行的事務(wù)信息:
當(dāng)前這個(gè)事務(wù)是到內(nèi)存的?還是到磁盤(pán)的?或者是到其他 IO 設(shè)備的? 這個(gè)事務(wù)是讀還是寫(xiě)? 總線(xiàn)上傳輸?shù)牡刂沸盘?hào)(物理內(nèi)存地址),還是數(shù)據(jù)信號(hào)(數(shù)據(jù))?。
這里大家需要注意總線(xiàn)上傳輸?shù)牡刂肪鶠槲锢韮?nèi)存地址。比如:在 MESI 緩存一致性協(xié)議中當(dāng) CPU core0 修改字段 a 的值時(shí),其他 CPU 核心會(huì)在總線(xiàn)上嗅探字段 a 的物理內(nèi)存地址,如果嗅探到總線(xiàn)上出現(xiàn)字段 a 的物理內(nèi)存地址,說(shuō)明有人在修改字段 a,這樣其他 CPU 核心就會(huì)失效字段 a 所在的 cache line 。
如上圖所示,其中系統(tǒng)總線(xiàn)是連接 CPU 與 IO bridge 的,存儲(chǔ)總線(xiàn)是來(lái)連接 IO bridge 和主存的。
IO bridge 負(fù)責(zé)將系統(tǒng)總線(xiàn)上的電子信號(hào)轉(zhuǎn)換成存儲(chǔ)總線(xiàn)上的電子信號(hào)。IO bridge 也會(huì)將系統(tǒng)總線(xiàn)和存儲(chǔ)總線(xiàn)連接到IO總線(xiàn)(磁盤(pán)等IO設(shè)備)上。這里我們看到 IO bridge 其實(shí)起的作用就是轉(zhuǎn)換不同總線(xiàn)上的電子信號(hào)。
8.3 CPU 從內(nèi)存讀取數(shù)據(jù)過(guò)程
假設(shè) CPU 現(xiàn)在需要將物理內(nèi)存地址為 A 的內(nèi)容加載到寄存器中進(jìn)行運(yùn)算。
大家需要注意的是 CPU 只會(huì)訪(fǎng)問(wèn)虛擬內(nèi)存,在操作總線(xiàn)之前,需要把虛擬內(nèi)存地址轉(zhuǎn)換為物理內(nèi)存地址,總線(xiàn)上傳輸?shù)亩际俏锢韮?nèi)存地址,這里省略了虛擬內(nèi)存地址到物理內(nèi)存地址的轉(zhuǎn)換過(guò)程,這部分內(nèi)容筆者會(huì)在后續(xù)文章的相關(guān)章節(jié)詳細(xì)為大家講解,這里我們聚焦如果通過(guò)物理內(nèi)存地址讀取內(nèi)存數(shù)據(jù)。

首先 CPU 芯片中的總線(xiàn)接口會(huì)在總線(xiàn)上發(fā)起讀事務(wù)(read transaction)。 該讀事務(wù)分為以下步驟進(jìn)行:
CPU 將物理內(nèi)存地址 A 放到系統(tǒng)總線(xiàn)上。隨后 IO bridge 將信號(hào)傳遞到存儲(chǔ)總線(xiàn)上。
主存感受到存儲(chǔ)總線(xiàn)上的地址信號(hào)并通過(guò)存儲(chǔ)控制器將存儲(chǔ)總線(xiàn)上的物理內(nèi)存地址 A 讀取出來(lái)。
存儲(chǔ)控制器通過(guò)物理內(nèi)存地址 A 定位到具體的存儲(chǔ)器模塊,從 DRAM 芯片中取出物理內(nèi)存地址 A 對(duì)應(yīng)的數(shù)據(jù) X。
存儲(chǔ)控制器將讀取到的數(shù)據(jù) X 放到存儲(chǔ)總線(xiàn)上,隨后 IO bridge 將存儲(chǔ)總線(xiàn)上的數(shù)據(jù)信號(hào)轉(zhuǎn)換為系統(tǒng)總線(xiàn)上的數(shù)據(jù)信號(hào),然后繼續(xù)沿著系統(tǒng)總線(xiàn)傳遞。
CPU 芯片感受到系統(tǒng)總線(xiàn)上的數(shù)據(jù)信號(hào),將數(shù)據(jù)從系統(tǒng)總線(xiàn)上讀取出來(lái)并拷貝到寄存器中。
以上就是 CPU 讀取內(nèi)存數(shù)據(jù)到寄存器中的完整過(guò)程。
但是其中還涉及到一個(gè)重要的過(guò)程,這里我們還是需要攤開(kāi)來(lái)介紹一下,那就是存儲(chǔ)控制器如何通過(guò)物理內(nèi)存地址 A 從主存中讀取出對(duì)應(yīng)的數(shù)據(jù) X 的?
接下來(lái)我們結(jié)合前邊介紹的內(nèi)存結(jié)構(gòu)以及從 DRAM 芯片讀取數(shù)據(jù)的過(guò)程,來(lái)總體介紹下如何從主存中讀取數(shù)據(jù)。
8.4 如何根據(jù)物理內(nèi)存地址從主存中讀取數(shù)據(jù)
前邊介紹到,當(dāng)主存中的存儲(chǔ)控制器感受到了存儲(chǔ)總線(xiàn)上的地址信號(hào)時(shí),會(huì)將內(nèi)存地址從存儲(chǔ)總線(xiàn)上讀取出來(lái)。
隨后會(huì)通過(guò)內(nèi)存地址定位到具體的存儲(chǔ)器模塊。還記得內(nèi)存結(jié)構(gòu)中的存儲(chǔ)器模塊嗎 ?

而每個(gè)存儲(chǔ)器模塊中包含了 8 個(gè) DRAM 芯片,編號(hào)從 0 - 7 。

存儲(chǔ)控制器會(huì)將物理內(nèi)存地址轉(zhuǎn)換為 DRAM 芯片中 supercell 在二維矩陣中的坐標(biāo)地址(RAS,CAS)。并將這個(gè)坐標(biāo)地址發(fā)送給對(duì)應(yīng)的存儲(chǔ)器模塊。隨后存儲(chǔ)器模塊會(huì)將 RAS 和 CAS 廣播到存儲(chǔ)器模塊中的所有 DRAM 芯片。依次通過(guò) (RAS,CAS) 從 DRAM0 到 DRAM7 讀取到相應(yīng)的 supercell 。

我們知道一個(gè) supercell 存儲(chǔ)了一個(gè)字節(jié)( 8 bit ) 數(shù)據(jù),這里我們從 DRAM0 到 DRAM7 依次讀取到了 8 個(gè) supercell 也就是 8 個(gè)字節(jié),然后將這 8 個(gè)字節(jié)返回給存儲(chǔ)控制器,由存儲(chǔ)控制器將數(shù)據(jù)放到存儲(chǔ)總線(xiàn)上。
CPU 總是以 word size 為單位從內(nèi)存中讀取數(shù)據(jù),在 64 位處理器中的 word size 為 8 個(gè)字節(jié)。64 位的內(nèi)存每次只能吞吐 8 個(gè)字節(jié)。
CPU 每次會(huì)向內(nèi)存讀寫(xiě)一個(gè) cache line 大小的數(shù)據(jù)( 64 個(gè)字節(jié)),但是內(nèi)存一次只能吞吐 8 個(gè)字節(jié)。
所以在物理內(nèi)存地址對(duì)應(yīng)的存儲(chǔ)器模塊中,DRAM0 芯片存儲(chǔ)第一個(gè)低位字節(jié)( supercell ),DRAM1 芯片存儲(chǔ)第二個(gè)字節(jié),......依次類(lèi)推 DRAM7 芯片存儲(chǔ)最后一個(gè)高位字節(jié)。

由于存儲(chǔ)器模塊中這種由 8 個(gè) DRAM 芯片組成的物理存儲(chǔ)結(jié)構(gòu)的限制,內(nèi)存讀取數(shù)據(jù)只能是按照物理內(nèi)存地址,8 個(gè)字節(jié) 8 個(gè)字節(jié)地順序讀取數(shù)據(jù)。所以說(shuō)內(nèi)存一次讀取和寫(xiě)入的單位是 8 個(gè)字節(jié)。

而且在程序員眼里連續(xù)的物理內(nèi)存地址實(shí)際上在物理上是不連續(xù)的。因?yàn)檫@連續(xù)的 8 個(gè)字節(jié)其實(shí)是存儲(chǔ)于不同的 DRAM 芯片上的。每個(gè) DRAM 芯片存儲(chǔ)一個(gè)字節(jié)(supercell)
8.5 CPU 向內(nèi)存寫(xiě)入數(shù)據(jù)過(guò)程
我們現(xiàn)在假設(shè) CPU 要將寄存器中的數(shù)據(jù) X 寫(xiě)到物理內(nèi)存地址 A 中。同樣的道理,CPU 芯片中的總線(xiàn)接口會(huì)向總線(xiàn)發(fā)起寫(xiě)事務(wù)(write transaction)。寫(xiě)事務(wù)步驟如下:
CPU 將要寫(xiě)入的物理內(nèi)存地址 A 放入系統(tǒng)總線(xiàn)上。
通過(guò) IO bridge 的信號(hào)轉(zhuǎn)換,將物理內(nèi)存地址 A 傳遞到存儲(chǔ)總線(xiàn)上。
存儲(chǔ)控制器感受到存儲(chǔ)總線(xiàn)上的地址信號(hào),將物理內(nèi)存地址 A 從存儲(chǔ)總線(xiàn)上讀取出來(lái),并等待數(shù)據(jù)的到達(dá)。
CPU 將寄存器中的數(shù)據(jù)拷貝到系統(tǒng)總線(xiàn)上,通過(guò) IO bridge 的信號(hào)轉(zhuǎn)換,將數(shù)據(jù)傳遞到存儲(chǔ)總線(xiàn)上。
存儲(chǔ)控制器感受到存儲(chǔ)總線(xiàn)上的數(shù)據(jù)信號(hào),將數(shù)據(jù)從存儲(chǔ)總線(xiàn)上讀取出來(lái)。
存儲(chǔ)控制器通過(guò)內(nèi)存地址 A 定位到具體的存儲(chǔ)器模塊,最后將數(shù)據(jù)寫(xiě)入存儲(chǔ)器模塊中的 8 個(gè) DRAM 芯片中。
總結(jié)
本文我們從虛擬內(nèi)存地址開(kāi)始聊起,一直到物理內(nèi)存地址結(jié)束,包含的信息量還是比較大的。首先筆者通過(guò)一個(gè)進(jìn)程的運(yùn)行實(shí)例為大家引出了內(nèi)核引入虛擬內(nèi)存空間的目的及其需要解決的問(wèn)題。
在我們有了虛擬內(nèi)存空間的概念之后,筆者又近一步為大家介紹了內(nèi)核如何劃分用戶(hù)態(tài)虛擬內(nèi)存空間和內(nèi)核態(tài)虛擬內(nèi)存空間,并在次基礎(chǔ)之上分別從 32 位體系結(jié)構(gòu)和 64 位體系結(jié)構(gòu)的角度詳細(xì)闡述了 Linux 虛擬內(nèi)存空間的整體布局分布。
我們可以通過(guò)
cat /proc/pid/maps或者pmap pid命令來(lái)查看進(jìn)程用戶(hù)態(tài)虛擬內(nèi)存空間的實(shí)際分布。還可以通過(guò)
cat /proc/iomem命令來(lái)查看進(jìn)程內(nèi)核態(tài)虛擬內(nèi)存空間的的實(shí)際分布。
在我們清楚了 Linux 虛擬內(nèi)存空間的整體布局分布之后,筆者又介紹了 Linux 內(nèi)核如何對(duì)分布在虛擬內(nèi)存空間中的各個(gè)虛擬內(nèi)存區(qū)域進(jìn)行管理,以及每個(gè)虛擬內(nèi)存區(qū)域的作用。在這個(gè)過(guò)程中還介紹了相關(guān)的內(nèi)核數(shù)據(jù)結(jié)構(gòu),近一步從內(nèi)核源碼實(shí)現(xiàn)角度加深大家對(duì)虛擬內(nèi)存空間的理解。
最后筆者介紹了物理內(nèi)存的結(jié)構(gòu),以及 CPU 如何通過(guò)物理內(nèi)存地址來(lái)讀寫(xiě)內(nèi)存中的數(shù)據(jù)。這里筆者需要特地再次強(qiáng)調(diào)的是 CPU 只會(huì)訪(fǎng)問(wèn)虛擬內(nèi)存地址,只不過(guò)在操作總線(xiàn)之前,通過(guò)一個(gè)地址轉(zhuǎn)換硬件將虛擬內(nèi)存地址轉(zhuǎn)換為物理內(nèi)存地址,然后將物理內(nèi)存地址作為地址信號(hào)放在總線(xiàn)上傳輸,由于地址轉(zhuǎn)換的內(nèi)容和本文主旨無(wú)關(guān),考慮到文章的篇幅以及復(fù)雜性,筆者就沒(méi)有過(guò)多的介紹。
好了,本文的全部?jī)?nèi)容到這里就結(jié)束了,感謝大家的收看,我們下篇文章見(jiàn)~~~
推薦閱讀:
真棒! 20 張圖揭開(kāi)內(nèi)存管理的迷霧,瞬間豁然開(kāi)朗
字節(jié)一面:TCP 三次握手,問(wèn)的好細(xì)!
