萬(wàn)字帶你深入理解 Linux 虛擬內(nèi)存管理(上)
前言
內(nèi)存管理子系統(tǒng)可謂是 Linux 內(nèi)核眾多子系統(tǒng)中最為復(fù)雜最為龐大的一個(gè),其中包含了眾多繁雜的概念和原理,通過(guò)內(nèi)存管理這條主線我們把可以把操作系統(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)筆寫這個(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)建的類,調(diào)用的函數(shù),在函數(shù)中定義的局部變量以及 new 出來(lái)的數(shù)據(jù)容器(Map,List,Set .....等)都需要存儲(chǔ)在物理內(nèi)存中的某個(gè)角落。
而我們?cè)诔绦蛑芯帉憳I(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ú)論是在用戶態(tài)還是在內(nèi)核態(tài)能夠看到的都是虛擬內(nèi)存空間,物理內(nèi)存空間被操作系統(tǒng)所屏蔽進(jìn)程是看不到的。
進(jìn)程通過(guò)虛擬內(nèi)存地址訪問(wèn)這些數(shù)據(jù)結(jié)構(gòu)的時(shí)候,虛擬內(nèi)存地址會(huì)在內(nèi)存管理子系統(tǒng)中被轉(zhuǎn)換成物理內(nèi)存地址,通過(guò)物理內(nèi)存地址就可以訪問(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)存地址 ?如何訪問(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ì)填寫收件人地址以及寄件人地址。以及在日常網(wǎng)上購(gòu)物時(shí),都會(huì)在相應(yīng)電商 APP 中填寫自己的收獲地址。

隨后快遞小哥就會(huì)根據(jù)我們填寫的收貨地址找到我們的真實(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)?,但是物理地址永遠(yuǎn)是不變的。
現(xiàn)在讓我們把視角在切換到計(jì)算機(jī)的世界,在計(jì)算機(jī)的世界里內(nèi)存地址用來(lái)定義數(shù)據(jù)在內(nèi)存中的存儲(chǔ)位置的,內(nèi)存地址也分為虛擬地址和物理地址。而虛擬地址也是人為設(shè)計(jì)的一個(gè)概念,類比我們現(xiàn)實(shí)世界中的收貨地址,而物理地址則是數(shù)據(jù)在物理內(nèi)存中的真實(shí)存儲(chǔ)位置,類比現(xiàn)實(shí)世界中的城市,街道,小區(qū)的真實(shí)地理位置。
說(shuō)了這么多,那么到底虛擬內(nèi)存地址長(zhǎng)什么樣子呢?
我們還是以日常生活中的收貨地址為例做出類比,我們都很熟悉收貨地址的格式: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è)表項(xiàng)(9位)+?頁(yè)內(nèi)偏移(12位)。共 48 位組成的虛擬內(nèi)存地址。

虛擬內(nèi)存地址中的全局頁(yè)目錄項(xiàng)就類比我們?nèi)粘I钪惺斋@地址里的省,上層頁(yè)目錄項(xiàng)就類比市,中間層頁(yè)目錄項(xiàng)類比區(qū)縣,頁(yè)表項(xiàng)類比街道小區(qū),頁(yè)內(nè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. 為什么要使用虛擬地址訪問(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)存地址去訪問(wèn)內(nèi)存而是選擇用虛擬內(nèi)存地址去訪問(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ā)門檻。
然而在現(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 程序。
在程序代碼相同的情況下,我們用這份代碼同時(shí)啟動(dòng)三個(gè) JVM 進(jìn)程,我們暫時(shí)將進(jìn)程依次命名為 a , b , c 。
這三個(gè)進(jìn)程用到的代碼是一樣的,都是我們提前寫好的,可以被多次運(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ù)被訪問(wèn),則不久之后該數(shù)據(jù)可能再次被訪問(wèn)??臻g局部性是指一旦程序訪問(wèn)了某個(gè)存儲(chǔ)單元,則不久之后,其附近的存儲(chǔ)單元也將被訪問(wèn)。
從程序局部性原理的描述中我們可以得出這樣一個(gè)結(jié)論:進(jìn)程在運(yùn)行之后,對(duì)于內(nèi)存的訪問(wèn)不會(huì)一下子就要訪問(wèn)全部的內(nèi)存,相反進(jìn)程對(duì)于內(nèi)存的訪問(wèn)會(huì)表現(xiàn)出明顯的傾向性,更加傾向于訪問(wèn)最近訪問(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 訪問(wèn)進(jìn)程的虛擬地址時(shí),經(jīng)過(guò)地址翻譯硬件將虛擬地址轉(zhuǎn)換成不同的物理地址,這樣不同的進(jìn)程運(yùn)行的時(shí)候,雖然操作的是同一虛擬地址,但其實(shí)背后寫入的是不同的物理地址,這樣就不會(huì)沖突了。
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【749907784】整理了一些個(gè)人覺(jué)得比較好的學(xué)習(xí)書(shū)籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。。ê曨l教程、電子書(shū)、實(shí)戰(zhàn)項(xiàng)目及代碼)? ? ?


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)程用戶態(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ò)程序代碼事先編寫好,然后編譯成二進(jì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)存中供程序訪問(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ò)程中還需要依賴動(dòng)態(tài)鏈接庫(kù),這些動(dòng)態(tài)鏈接庫(kù)以 .so 文件的形式存放在磁盤中,比如 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ò)程中所需要不同種類的數(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。
其中用戶態(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。

但是用戶態(tài)虛擬內(nèi)存空間中的代碼段并不是從 0x0000 0000 地址開(kāi)始的,而是從 0x0804 8000 地址開(kāi)始。
0x0000 0000 到 0x0804 8000 這段虛擬內(nèi)存地址是一段不可訪問(wèn)的保留區(qū),因?yàn)樵诖蠖鄶?shù)操作系統(tǒng)中,數(shù)值比較小的地址通常被認(rèn)為不是一個(gè)合法的地址,這塊小地址是不允許訪問(wèn)的。比如在 C 語(yǔ)言中我們通常會(huì)將一些無(wú)效的指針設(shè)置為 NULL,指向這塊不允許訪問(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í)所依賴的動(dòng)態(tài)鏈接庫(kù)中的代碼段,數(shù)據(jù)段,BSS 段就加載在這里。還有我們調(diào)用 mmap 映射出來(lái)的一段虛擬內(nèi)存空間也保存在這個(gè)區(qū)域。注意:在文件映射與匿名映射區(qū)的地址增長(zhǎng)方向是從高地址向低地址增長(zhǎng)。
接下來(lái)用戶態(tài)虛擬內(nèi)存空間的最后一塊區(qū)域就是棧空間了,在這里會(huì)保存函數(shù)運(yùn)行過(guò)程所需要的局部變量以及函數(shù)參數(shù)等函數(shù)調(diào)用信息。??臻g中的地址增長(zhǎng)方向是從高地址向低地址增長(zhǎng)。每次進(jìn)程申請(qǐng)新的棧地址時(shí),其地址值是在減少的。
在內(nèi)核中使用 start_stack 標(biāo)識(shí)棧的起始位置,RSP 寄存器中保存棧頂指針 stack pointer,RBP 寄存器中保存的是?;刂?。
在棧空間的下邊也有一段待分配區(qū)域用于擴(kuò)展??臻g,在??臻g的上邊就是內(nèi)核空間了,進(jìn)程雖然可以看到這段內(nèi)核空間地址,但是就是不能訪問(wèn)。這就好比我們?cè)陲埖昀镫m然可以看到廚房在哪里,但是廚房門上寫著 “廚房重地,閑人免進(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ò)這么大的磁盤,在現(xiàn)實(shí)情況中根本不會(huì)用到這么大范圍的內(nèi)存空間,
事實(shí)上在目前的 64 位系統(tǒng)下只使用了 48 位來(lái)描述虛擬內(nèi)存空間,尋址范圍為 ?2^48 ,所能表達(dá)的虛擬內(nèi)存空間為 256TB。
其中低 128 T 表示用戶態(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)就在用戶態(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 的用戶態(tài)地址空間:0x0000 0000 0000 0000 - 0x0000 7FFF FFFF F000 范圍中,所以虛擬內(nèi)存地址的高 16 位全部為 0 。
如果一個(gè)虛擬內(nèi)存地址的高 16 位全部為 0 ,那么我們就可以直接判斷出這是一個(gè)用戶空間的虛擬內(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è)試圖訪問(wèn)內(nèi)核的虛擬地址的高 16 位不全為 1 ,則可以快速判斷這個(gè)訪問(wèn)是非法的。
這個(gè)高 16 位的空閑地址被稱為 canonical 。如果虛擬內(nèi)存地址中的高 16 位全部為 0 (表示用戶空間虛擬內(nèi)存地址)或者全部為 1 (表示內(nèi)核空間虛擬內(nèi)存地址),這種地址的形式我們叫做 canonical form,對(duì)應(yīng)的地址我們稱作 canonical address 。
那么處于 canonical address 空洞 :0x0000 7FFF FFFF F000 - 0xFFFF 8000 0000 0000 范圍內(nèi)的地址的高 16 位 不全為 0 也不全為 1 。如果某個(gè)虛擬地址落在這段 canonical address 空洞區(qū)域中,那就是既不在用戶空間,也不在內(nèi)核空間,肯定是非法訪問(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,所以稱之為 canonical address 空洞。
在代碼段跟數(shù)據(jù)段的中間還有一段不可以讀寫的保護(hù)段,它的作用是防止程序在讀寫數(shù)據(jù)段的時(shí)候越界訪問(wèn)到代碼段,這個(gè)保護(hù)段可以讓越界訪問(wèn)行為直接崩潰,防止它繼續(xù)往下運(yùn)行。
用戶態(tài)虛擬內(nèi)存空間與內(nèi)核態(tài)虛擬內(nèi)存空間分別占用 128T,其中低128T 分配給用戶態(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)。
在進(jìn)程描述符 task_struct 結(jié)構(gòu)中,有一個(gè)專門描述進(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)建。
隨后會(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)。
這里我們重點(diǎn)關(guān)注 copy_mm 函數(shù),正是在這里完成了子進(jìn)程虛擬內(nèi)存空間 mm_struct 結(jié)構(gòu)的的創(chuàng)建以及初始化。
由于本小節(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)程就變成了我們熟悉的線程,是否共享地址空間幾乎是進(jìn)程和線程之間的本質(zhì)區(qū)別。Linux 內(nèi)核并不區(qū)別對(duì)待它們,線程對(duì)于內(nèi)核來(lái)說(shuō)僅僅是一個(gè)共享特定資源的進(jìn)程而已。
內(nèi)核線程和用戶態(tài)線程的區(qū)別就是內(nèi)核線程沒(méi)有相關(guān)的內(nèi)存描述符 mm_struct ,內(nèi)核線程對(duì)應(yīng)的 task_struct 結(jié)構(gòu)中的 mm 域指向 Null,所以內(nèi)核線程之間調(diào)度是不涉及地址空間切換的。
當(dāng)一個(gè)內(nèi)核線程被調(diào)度時(shí),它會(huì)發(fā)現(xiàn)自己的虛擬地址空間為 Null,雖然它不會(huì)訪問(wèn)用戶態(tài)的內(nèi)存,但是它會(huì)訪問(wèn)內(nèi)核內(nèi)存,聰明的內(nèi)核會(huì)將調(diào)度之前的上一個(gè)用戶態(tài)進(jìn)程的虛擬內(nèi)存空間 mm_struct 直接賦值給內(nèi)核線程,因?yàn)閮?nèi)核線程不會(huì)訪問(wèn)用戶空間的內(nèi)存,它僅僅只會(huì)訪問(wèn)內(nèi)核空間的內(nèi)存,所以直接復(fù)用上一個(gè)用戶態(tài)進(jìn)程的虛擬地址空間就可以避免為內(nèi)核線程分配 mm_struct 和相關(guān)頁(yè)表的開(kāi)銷,以及避免內(nèi)核線程之間調(diào)度時(shí)地址空間的切換開(kāi)銷。
父進(jìn)程與子進(jìn)程的區(qū)別,進(jìn)程與線程的區(qū)別,以及內(nèi)核線程與用戶態(tài)線程的區(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)核如何劃分用戶態(tài)和內(nèi)核態(tài)虛擬內(nèi)存空間
通過(guò) 《3. 進(jìn)程虛擬內(nèi)存空間》小節(jié)的介紹我們知道,進(jìn)程的虛擬內(nèi)存空間分為兩個(gè)部分:一部分是用戶態(tài)虛擬內(nèi)存空間,另一部分是內(nèi)核態(tài)虛擬內(nèi)存空間。

那么用戶態(tài)的地址空間和內(nèi)核態(tài)的地址空間在內(nèi)核中是如何被劃分的呢?
這就用到了進(jìn)程的內(nèi)存描述符 mm_struct 結(jié)構(gòu)體中的 task_size 變量,task_size 定義了用戶態(tài)地址空間與內(nèi)核態(tài)地址空間之間的分界線。
通過(guò)前邊小節(jié)的內(nèi)容介紹,我們知道在 ?32 位系統(tǒng)中用戶態(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)中用戶地址空間和內(nèi)核地址空間的分界線在 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 的定義。
如下圖所示:__PAGE_OFFSET 的值在 32 位系統(tǒng)下為 ?0xC000 000。

而在 64 位系統(tǒng)中,只使用了其中的低 48 位來(lái)表示虛擬內(nèi)存地址。其中用戶態(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)中用戶地址空間和內(nèi)核地址空間的分界線在 ?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 的定義。
我們來(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
文件中:
而內(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)的線索。
內(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í)所依賴的動(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è)可以換出到硬盤上,而有些頁(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)劃分用戶態(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)。
每個(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ū)域的訪問(wèn)權(quán)限和行為規(guī)范
vm_page_prot 和 vm_flags 都是用來(lái)標(biāo)記 vm_area_struct 結(jié)構(gòu)表示的這塊虛擬內(nèi)存區(qū)域的訪問(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í)別的訪問(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è)的訪問(wèn)權(quán)限就是由 vm_page_prot 決定的。
vm_flags 則偏向于定于整個(gè)虛擬內(nèi)存區(qū)域的訪問(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è)面訪問(wèn)權(quán)限 vm_page_prot 的轉(zhuǎn)換。
下面筆者列舉一些常用到的 vm_flags 方便大家有一個(gè)直觀的感受:

VM_READ,VM_WRITE,VM_EXEC 定義了虛擬內(nèi)存區(qū)域是否可以被讀取,寫入,執(zhí)行等權(quán)限。
比如代碼段這塊內(nèi)存區(qū)域的權(quán)限是可讀,可執(zhí)行,但是不可寫。數(shù)據(jù)段具有可讀可寫的權(quán)限但是不可執(zhí)行。堆則具有可讀可寫,可執(zhí)行的權(quán)限(Java 中的字節(jié)碼存儲(chǔ)在堆中,所以需要可執(zhí)行權(quán)限),棧一般是可讀可寫的權(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è)置的話則為私有映射。這個(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ū)域非常重要,不能被換出到磁盤中。
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ù),以便加快下次順序訪問(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 文件讀寫本質(zhì)》中的第 9 小節(jié)文件頁(yè)預(yù)讀部分。
通過(guò)這一系列的介紹,我們可以看到 vm_flags 就是定義整個(gè)虛擬內(nèi)存區(qū)域的訪問(wèn)權(quán)限以及行為規(guī)范,而內(nèi)存區(qū)域中內(nèi)存的最小單位為頁(yè)(4K),虛擬內(nèi)存區(qū)域中包含了很多這樣的虛擬頁(yè),對(duì)于虛擬內(nèi)存區(qū)域 VMA 設(shè)置的訪問(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)存上我們稱之為匿名映射,映射到文件中我們稱之為文件映射。
那么這個(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)存映射的類型有關(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ù)指針。
當(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)程訪問(wèn)虛擬內(nèi)存時(shí),訪問(wèn)的頁(yè)面不在物理內(nèi)存中,可能是未分配物理內(nèi)存也可能是被置換到磁盤中,這時(shí)就會(huì)產(chǎn)生缺頁(yè)異常,fault 函數(shù)就會(huì)被調(diào)用。
當(dāng)一個(gè)只讀的頁(yè)面將要變?yōu)榭蓪憰r(shí),page_mkwrite 函數(shù)會(huì)被調(diào)用。
struct vm_operations_struct 結(jié)構(gòu)中定義的都是對(duì)虛擬內(nèi)存區(qū)域 VMA 的相關(guān)操作函數(shù)指針。
內(nèi)核中這種類似的用法其實(shí)有很多,在內(nèi)核中每個(gè)特定領(lǐng)域的描述符都會(huì)定義相關(guān)的操作。比如在前邊的文章?《從 Linux 內(nèi)核角度探秘 JDK NIO 文件讀寫本質(zhì)》?中我們介紹到內(nèi)核中的文件描述符 struct file 中定義的 struct file_operations ?*f_op。里面定義了內(nèi)核針對(duì)文件操作的函數(shù)指針,具體的實(shí)現(xiàn)根據(jù)不同的文件類型有所不同。
針對(duì) Socket 文件類型,這里的 file_operations 指向的是 socket_file_ops。

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

socket 相關(guān)的操作接口定義在 inet_stream_ops 函數(shù)集合中,負(fù)責(zé)對(duì)上給用戶提供接口。而 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)的一些屬性:
在內(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ū)域。
在每個(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 中:

文章篇幅有限,下文繼續(xù)講解
原文作者:bin的技術(shù)小屋
