1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

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

        共 48462字,需瀏覽 97分鐘

         ·

        2022-11-02 01:11

        八股文網(wǎng)站:xiaolincoding.com

        寫在本文開始之前....

        這篇文章很硬核,大家一次性看不完可以,收藏起來(lái)慢慢看。

        內(nèi)存管理子系統(tǒng)可謂是 Linux 內(nèi)核眾多子系統(tǒng)中最為復(fù)雜最為龐大的一個(gè),其中包含了眾多繁雜的概念和原理,通過內(nèi)存管理這條主線我們把可以把操作系統(tǒng)的眾多核心系統(tǒng)給拎出來(lái),比如:進(jìn)程管理子系統(tǒng),網(wǎng)絡(luò)子系統(tǒng),文件子系統(tǒng)等。

        由于內(nèi)存管理子系統(tǒng)過于復(fù)雜龐大,其中涉及到的眾多繁雜的概念又是一環(huán)套一環(huán),層層遞進(jìn)。如何把這些繁雜的概念具有層次感地,并且清晰地,給大家梳理呈現(xiàn)出來(lái)真是一件比較有難度的事情,因此關(guān)于這個(gè)問題,筆者在動(dòng)筆寫這個(gè)內(nèi)存管理源碼解析系列之前也是思考了很久。

        萬(wàn)事開頭難,那么到底什么內(nèi)容適合作為這個(gè)系列的開篇呢 ?筆者還是覺得從大家日常開發(fā)工作中接觸最多最為熟悉的部分開始比較好,比如:在我們?nèi)粘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),并通過這些引用對(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)程通過虛擬內(nèi)存地址訪問這些數(shù)據(jù)結(jié)構(gòu)的時(shí)候,虛擬內(nèi)存地址會(huì)在內(nèi)存管理子系統(tǒng)中被轉(zhuǎn)換成物理內(nèi)存地址,通過物理內(nèi)存地址就可以訪問到真正存儲(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)存地址 ?如何訪問物理內(nèi)存?

        本文筆者就來(lái)為大家詳細(xì)一一解答上述幾個(gè)問題,讓我們馬上開始吧~~~~

        本文概要.png

        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 中填寫自己的收獲地址。

        image.png

        隨后快遞小哥就會(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è)虛擬概念,通過收貨地址這個(gè)虛擬概念將它和現(xiàn)實(shí)世界真實(shí)存在的城市,小區(qū),街道的地理位置一一映射起來(lái),這樣我們就可以通過這個(gè)虛擬概念來(lái)找到現(xiàn)實(shí)世界中的具體地理位置。

        綜上所述,收貨地址是一個(gè)虛擬地址,它是人為定義的,而我們的城市,小區(qū),街道是真實(shí)存在的,他們的地理位置就是物理地址。

        image.png

        比如現(xiàn)在的廣東省深圳市在過去叫寶安縣,河北省的石家莊過去叫常山,安徽省的合肥過去叫瀘州。不管是常山也好,石家莊也好,又或是合肥也好,瀘州也罷,這些都是人為定義的名字而已,但是地方還是那個(gè)地方,它所在的地理位置是不變的。也就說虛擬地址可以人為的變來(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í)地理位置。

        說了這么多,那么到底虛擬內(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è)內(nèi)偏移(12位)。共 48 位組成的虛擬內(nèi)存地址。

        image.png

        虛擬內(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)存地址。

        image.png

        進(jìn)程虛擬內(nèi)存空間中的每一個(gè)字節(jié)都有與其對(duì)應(yīng)的虛擬內(nèi)存地址,一個(gè)虛擬內(nèi)存地址表示進(jìn)程虛擬內(nèi)存空間中的一個(gè)特定的字節(jié)。

        2. 為什么要使用虛擬地址訪問內(nèi)存

        經(jīng)過第一小節(jié)的介紹,我們現(xiàn)在明白了計(jì)算機(jī)世界中的虛擬內(nèi)存地址的含義及其展現(xiàn)形式。那么大家可能會(huì)問了,既然物理內(nèi)存地址可以直接定位到數(shù)據(jù)在內(nèi)存中的存儲(chǔ)位置,那為什么我們不直接使用物理內(nèi)存地址去訪問內(nèi)存而是選擇用虛擬內(nèi)存地址去訪問內(nèi)存呢?

        在回答大家的這個(gè)疑問之前,讓我們先來(lái)看下,如果在程序中直接使用物理內(nèi)存地址會(huì)發(fā)生什么情況?

        假設(shè)現(xiàn)在沒有虛擬內(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è)備上開發(fā)應(yīng)用程序,系統(tǒng)中只有一個(gè)進(jìn)程,這單個(gè)進(jìn)程獨(dú)享所有的物理資源包括內(nèi)存資源。在這種情況下,上述提到的這些直接使用物理內(nèi)存的問題可能還好處理一些,但是仍然具有很高的開發(fā)門檻。

        然而在現(xiàn)代操作系統(tǒng)中往往支持多個(gè)進(jìn)程,需要處理多進(jìn)程之間的協(xié)同問題,在多進(jìn)程系統(tǒng)中直接使用物理內(nèi)存地址操作內(nèi)存所帶來(lái)的上述問題就變得非常復(fù)雜了。

        這里筆者為大家舉一個(gè)簡(jiǎn)單的例子來(lái)說明在多進(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)程用到的代碼是一樣的,都是我們提前寫好的,可以被多次運(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)變量的地址沖突。

        image.png

        所以在直接操作物理內(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è)問題呢?程序的局部性原理再一次救了我們~~

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

        從程序局部性原理的描述中我們可以得出這樣一個(gè)結(jié)論:進(jìn)程在運(yùn)行之后,對(duì)于內(nèi)存的訪問不會(huì)一下子就要訪問全部的內(nèi)存,相反進(jìn)程對(duì)于內(nèi)存的訪問會(huì)表現(xiàn)出明顯的傾向性,更加傾向于訪問最近訪問過的數(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)存的引入正是要解決上述的問題,虛擬內(nèi)存引入之后,進(jìn)程的視角就會(huì)變得非常開闊,每個(gè)進(jìn)程都擁有自己獨(dú)立的虛擬地址空間,進(jìn)程與進(jìn)程之間的虛擬內(nèi)存地址空間是相互隔離,互不干擾的。每個(gè)進(jìn)程都認(rèn)為自己獨(dú)占所有內(nèi)存空間,自己想干什么就干什么。

        image.png

        系統(tǒng)上還運(yùn)行了哪些進(jìn)程和我沒有任何關(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ò)展了可用空間。

        image.png

        這樣進(jìn)程就以為自己獨(dú)占了整個(gè)內(nèi)存空間資源,給進(jìn)程產(chǎn)生了所有內(nèi)存資源都屬于它自己的幻覺,這其實(shí)是 CPU 和操作系統(tǒng)使用的一個(gè)障眼法罷了,任何一個(gè)虛擬內(nèi)存里所存儲(chǔ)的數(shù)據(jù),本質(zhì)上還是保存在真實(shí)的物理內(nèi)存里的。只不過內(nèi)核幫我們做了虛擬內(nèi)存到物理內(nèi)存的這一層映射,將不同進(jìn)程的虛擬地址和不同內(nèi)存的物理地址映射起來(lái)。

        當(dāng) CPU 訪問進(jìn)程的虛擬地址時(shí),經(jīng)過地址翻譯硬件將虛擬地址轉(zhuǎn)換成不同的物理地址,這樣不同的進(jìn)程運(yùn)行的時(shí)候,雖然操作的是同一虛擬地址,但其實(shí)背后寫入的是不同的物理地址,這樣就不會(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é)中,筆者就為大家揭開這層神秘的面紗~~~

        在本小節(jié)內(nè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í)行這些工作的步驟我們通過程序代碼事先編寫好,然后編譯成二進(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)存空間叫做代碼段。

        image.png

        在程序運(yùn)行起來(lái)之后,總要操作變量吧,在程序代碼中我們通常會(huì)定義大量的全局變量和靜態(tài)變量,這些全局變量在程序編譯之后也會(huì)存儲(chǔ)在二進(jìn)制文件中,在程序運(yùn)行之前,這些全局變量也需要被加載進(jìn)內(nèi)存中供程序訪問。所以在虛擬內(nèi)存空間中也需要一段區(qū)域來(lái)存儲(chǔ)這些全局變量。

        • 那些在代碼中被我們指定了初始值的全局變量和靜態(tài)變量在虛擬內(nèi)存空間中的存儲(chǔ)區(qū)域我們叫做數(shù)據(jù)段。

        • 那些沒有指定初始值的全局變量和靜態(tài)變量在虛擬內(nèi)存空間中的存儲(chǔ)區(qū)域我們叫做 BSS 段。這些未初始化的全局變量被加載進(jìn)內(nèi)存之后會(huì)被初始化為 0 值。

        image.png

        上面介紹的這些全局變量和靜態(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 中的堆。

        image.png

        除此之外,我們的程序在運(yùn)行過程中還需要依賴動(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 段,以及通過 mmap 系統(tǒng)調(diào)用映射的共享內(nèi)存區(qū),在虛擬內(nèi)存空間的存儲(chǔ)區(qū)域叫做文件映射與匿名映射區(qū)。

        image.png

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

        image.png

        現(xiàn)在進(jìn)程的虛擬內(nèi)存空間所包含的主要區(qū)域,筆者就為大家介紹完了,我們看到內(nèi)核根據(jù)進(jìn)程運(yùn)行的過程中所需要不同種類的數(shù)據(jù)而為其開辟了對(duì)應(yīng)的地址空間。分別為:

        • 用于存放進(jìn)程程序二進(jìn)制文件中的機(jī)器指令的代碼段

        • 用于存放程序二進(jìn)制文件中定義的全局變量和靜態(tài)變量的數(shù)據(jù)段和 BSS 段。

        • 用于在程序運(yùn)行過程中動(dòng)態(tài)申請(qǐng)內(nèi)存的堆。

        • 用于存放動(dòng)態(tài)鏈接庫(kù)以及內(nèi)存映射區(qū)域的文件映射與匿名映射區(qū)。

        • 用于存放函數(shù)調(diào)用過程中的局部變量和函數(shù)參數(shù)的棧。

        以上就是我們通過一個(gè)程序在運(yùn)行過程中所需要的數(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。

        image.png

        但是用戶態(tài)虛擬內(nèi)存空間中的代碼段并不是從 0x0000 0000 地址開始的,而是從 0x0804 8000 地址開始。

        0x0000 0000 到 0x0804 8000 這段虛擬內(nèi)存地址是一段不可訪問的保留區(qū),因?yàn)樵诖蠖鄶?shù)操作系統(tǒng)中,數(shù)值比較小的地址通常被認(rèn)為不是一個(gè)合法的地址,這塊小地址是不允許訪問的。比如在 C 語(yǔ)言中我們通常會(huì)將一些無(wú)效的指針設(shè)置為 NULL,指向這塊不允許訪問的地址。

        保留區(qū)的上邊就是代碼段和數(shù)據(jù)段,它們是從程序的二進(jìn)制文件中直接加載進(jìn)內(nèi)存中的,BSS 段中的數(shù)據(jù)也存在于二進(jìn)制文件中,因?yàn)閮?nèi)核知道這些數(shù)據(jù)是沒有初值的,所以在二進(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)我們通過 malloc 向內(nèi)核申請(qǐng)很小的一塊內(nèi)存時(shí)(128K 之內(nèi)),就是通過改變 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ū)域就是??臻g了,在這里會(huì)保存函數(shù)運(yùn)行過程所需要的局部變量以及函數(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)核空間地址,但是就是不能訪問。這就好比我們?cè)陲埖昀镫m然可以看到廚房在哪里,但是廚房門上寫著 “廚房重地,閑人免進(jìn)” ,我們就是進(jìn)不去。

        image.png

        4.2 64 位機(jī)器上進(jìn)程虛擬內(nèi)存空間分布

        上小節(jié)中介紹的 32 位虛擬內(nèi)存空間布局和本小節(jié)即將要介紹的 64 位虛擬內(nèi)存空間布局都可以通過 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)存空間,筆者都沒見過這么大的磁盤,在現(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 空洞。

        image.png

        那么這個(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 。

        也就是說內(nèi)核態(tài)的虛擬內(nèi)存地址的高 16 位全部為 1 ,如果一個(gè)試圖訪問內(nèi)核的虛擬地址的高 16 位不全為 1 ,則可以快速判斷這個(gè)訪問是非法的。

        這個(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)核空間,肯定是非法訪問了。

        未來(lái)我們也可以利用這塊 canonical address 空洞,來(lái)擴(kuò)展虛擬內(nèi)存地址的范圍,比如擴(kuò)展到 56 位。

        在我們理解了 canonical address 這個(gè)概念之后,我們?cè)賮?lái)看下 64 位 Linux 系統(tǒng)下的真實(shí)虛擬內(nèi)存空間布局情況:

        image.png

        從上圖中我們可以看出 64 位系統(tǒng)中的虛擬內(nèi)存布局和 32 位系統(tǒng)中的虛擬內(nèi)存布局大體上是差不多的。主要不同的地方有三點(diǎn):

        1. 就是前邊提到的由高 16 位空閑地址造成的  canonical address 空洞。在這段范圍內(nèi)的虛擬內(nèi)存地址是不合法的,因?yàn)樗母?16 位既不全為 0 也不全為 1,不是一個(gè) canonical address,所以稱之為 canonical address 空洞。

        2. 在代碼段跟數(shù)據(jù)段的中間還有一段不可以讀寫的保護(hù)段,它的作用是防止程序在讀寫數(shù)據(jù)段的時(shí)候越界訪問到代碼段,這個(gè)保護(hù)段可以讓越界訪問行為直接崩潰,防止它繼續(xù)往下運(yùn)行。

        3. 用戶態(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ū)域。

        image.png

        唯一不同的是這些核心內(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)存空間管理,那就離不開進(jìn)程在內(nèi)核中的描述符 task_struct 結(jié)構(gòu)。

        struct task_struct {
                // 進(jìn)程id
             pid_t    pid;
                // 用于標(biāo)識(shí)線程所屬的進(jìn)程 pid
             pid_t    tgid;
                // 進(jìn)程打開的文件信息
                struct files_struct  *files;
                // 內(nèi)存描述符表示進(jìn)程虛擬地址空間
                struct mm_struct  *mm;

                .......... 省略 .......
        }

        在進(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)建。

        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)存空間一模一樣的,直接拷貝過來(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)程 ...........

                ....... 開始繼承拷貝父進(jìn)程資源  .......      
            // 繼承父進(jìn)程打開的文件描述符
         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;

                ...... 省略 ......
         // 通過 vfork 或者 clone 系統(tǒng)調(diào)用創(chuàng)建出的子進(jìn)程(線程)和父進(jìn)程共享虛擬內(nèi)存空間
         if (clone_flags & CLONE_VM) {
                // 增加父進(jìn)程虛擬地址空間的引用計(jì)數(shù)
           mmget(oldmm);
                // 直接將父進(jìn)程的虛擬內(nèi)存空間賦值給子進(jì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é)中我們舉的示例是通過  fork() 函數(shù)創(chuàng)建子進(jìn)程的情形,所以這里大家先占時(shí)忽略 if (clone_flags & CLONE_VM) 這個(gè)條件判斷邏輯,我們先跳過往后看~~

        copy_mm  函數(shù)首先會(huì)將父進(jìn)程的虛擬內(nèi)存空間 current->mm 賦值給指針 oldmm。然后通過 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)。

        通過 fork() 函數(shù)創(chuàng)建出的子進(jìn)程,它的虛擬內(nèi)存空間以及相關(guān)頁(yè)表相當(dāng)于父進(jìn)程虛擬內(nèi)存空間的一份拷貝,直接從父進(jìn)程中拷貝到子進(jìn)程中。

        而當(dāng)我們通過 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)存空間就變成共享的了。也就是說父子進(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)說僅僅是一個(gè)共享特定資源的進(jìn)程而已。

        內(nèi)核線程和用戶態(tài)線程的區(qū)別就是內(nè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ì)訪問用戶態(tài)的內(nèi)存,但是它會(huì)訪問內(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ì)訪問用戶空間的內(nèi)存,它僅僅只會(huì)訪問內(nèi)核空間的內(nèi)存,所以直接復(fù)用上一個(gè)用戶態(tài)進(jìn)程的虛擬地址空間就可以避免為內(nèi)核線程分配 mm_struct 和相關(guān)頁(yè)表的開銷,以及避免內(nèi)核線程之間調(diào)度時(shí)地址空間的切換開銷。

        父進(jìn)程與子進(jìn)程的區(qū)別,進(jìn)程與線程的區(qū)別,以及內(nèi)核線程與用戶態(tài)線程的區(qū)別其實(shí)都是圍繞著這個(gè) mm_struct 展開的。

        現(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)核如何通過這么一個(gè) mm_struct 結(jié)構(gòu)體來(lái)管理進(jìn)程的虛擬內(nèi)存空間的。

        5.1 內(nèi)核如何劃分用戶態(tài)和內(nèi)核態(tài)虛擬內(nèi)存空間

        通過 《3. 進(jìn)程虛擬內(nèi)存空間》小節(jié)的介紹我們知道,進(jìn)程的虛擬內(nèi)存空間分為兩個(gè)部分:一部分是用戶態(tài)虛擬內(nèi)存空間,另一部分是內(nèi)核態(tài)虛擬內(nèi)存空間。

        image.png

        那么用戶態(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)地址空間之間的分界線。

        struct mm_struct {
            unsigned long task_size; /* size of task vm space */
        }

        通過前邊小節(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位地址空間.png

        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 的定義。

        /*
         * User space process size: 3GB (default).
         */

        #define TASK_SIZE  __PAGE_OFFSET

        如下圖所示:__PAGE_OFFSET 的值在 32 位系統(tǒng)下為  0xC000 000。

        /arch/arm/Kconfig.png

        而在 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位地址空間.png

        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 的定義。

        #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ū)域,方便大家理解。

        image.png

        前邊我們提到,內(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)的線索。

        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),就是通過改變 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)境變量的位置。它們都位于棧中的最高地址處。

        image.png

        在 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)核是通過一個(gè) mm_struct 結(jié)構(gòu)的內(nèi)存描述符來(lái)表示進(jìn)程的虛擬內(nèi)存空間的,并通過 task_size 域來(lái)劃分用戶態(tài)虛擬內(nèi)存空間和內(nèi)核態(tài)虛擬內(nèi)存空間。

        image.png

        而在劃分出的這些虛擬內(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) 這樣一段左閉右開的虛擬內(nèi)存區(qū)域。

        image.png

        5.4 定義虛擬內(nèi)存區(qū)域的訪問權(quán)限和行為規(guī)范

        vm_page_prot 和 vm_flags 都是用來(lái)標(biāo)記 vm_area_struct 結(jié)構(gòu)表示的這塊虛擬內(nèi)存區(qū)域的訪問權(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í)別的訪問控制權(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)過頁(yè)表的轉(zhuǎn)換才能找到對(duì)應(yīng)的物理頁(yè)面。頁(yè)表中關(guān)于內(nèi)存頁(yè)的訪問權(quán)限就是由 vm_page_prot 決定的。

        vm_flags 則偏向于定于整個(gè)虛擬內(nèi)存區(qū)域的訪問權(quán)限以及行為規(guī)范。描述的是虛擬內(nèi)存區(qū)域中的整體信息,而不是虛擬內(nèi)存區(qū)域中具體的某個(gè)獨(dú)立頁(yè)面。它是一個(gè)抽象的概念??梢酝ㄟ^ vma->vm_page_prot = vm_get_page_prot(vma->vm_flags) 實(shí)現(xiàn)到具體頁(yè)面訪問權(quán)限 vm_page_prot 的轉(zhuǎn)換。

        下面筆者列舉一些常用到的 vm_flags 方便大家有一個(gè)直觀的感受:

        vm_flags訪問權(quán)限
        VM_READ可讀
        VM_WRITE可寫
        VM_EXEC可執(zhí)行
        VM_SHARD可多進(jìn)程之間共享
        VM_IO可映射至設(shè)備 IO 空間
        VM_RESERVED內(nèi)存區(qū)域不可被換出
        VM_SEQ_READ內(nèi)存區(qū)域可能被順序訪問
        VM_RAND_READ內(nèi)存區(qū)域可能被隨機(jī)訪問

        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)限。

        image.png

        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ù),以便加快下次順序訪問速度。

        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ù)讀。

        我們可以通過 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ù)讀部分。

        通過這一系列的介紹,我們可以看到 vm_flags 就是定義整個(gè)虛擬內(nèi)存區(qū)域的訪問權(quán)限以及行為規(guī)范,而內(nèi)存區(qū)域中內(nèi)存的最小單位為頁(yè)(4K),虛擬內(nèi)存區(qū)域中包含了很多這樣的虛擬頁(yè),對(duì)于虛擬內(nèi)存區(qū)域 VMA 設(shè)置的訪問權(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è)屬性。

        image.png

        當(dāng)我們調(diào)用 malloc 申請(qǐng)內(nèi)存時(shí),如果申請(qǐng)的是小塊內(nèi)存(低于 128K)則會(huì)使用 do_brk() 系統(tǒng)調(diào)用通過調(diào)整堆中的 brk 指針大小來(lái)增加或者回收堆內(nèi)存。

        如果申請(qǐng)的是比較大塊的內(nèi)存(超過 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 也就沒有了意義。

        vm_private_data 則用于存儲(chǔ) VMA 中的私有數(shù)據(jù)。具體的存儲(chǔ)內(nèi)容和內(nèi)存映射的類型有關(guān),我們暫不展開論述。

        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)程訪問虛擬內(nèi)存時(shí),訪問的頁(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。

        進(jìn)程中管理文件列表結(jié)構(gòu).png

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

        image.png

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

        系統(tǒng)IO調(diào)用結(jié)構(gòu).png

        對(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)的問題就是在內(nèi)核中這些虛擬內(nèi)存區(qū)域是如何被組織的呢?

        image.png

        我們繼續(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í)是通過一個(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 指針分別指向 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 中又通過 struct vm_area_struct 中的 vm_mm 指針指向了所屬的虛擬內(nèi)存空間 mm_struct。

        image.png

        我們可以通過 cat /proc/pid/maps 或者 pmap pid 查看進(jìn)程的虛擬內(nèi)存空間布局以及其中包含的所有內(nèi)存區(qū)域。這兩個(gè)命令背后的實(shí)現(xiàn)原理就是通過遍歷內(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 比較多的情況下,使用紅黑樹查找特定虛擬內(nèi)存區(qū)域的時(shí)間復(fù)雜度是 O( logN ) ,可以顯著減少查找所需的時(shí)間。

        所以在內(nèi)核中,同樣的內(nèi)存區(qū)域 vm_area_struct 會(huì)有兩種組織形式,一種是雙向鏈表用于高效的遍歷,另一種就是紅黑樹用于高效的查找。

        每個(gè) VMA 區(qū)域都是紅黑樹中的一個(gè)節(jié)點(diǎn),通過 struct vm_area_struct 結(jié)構(gòu)中的 vm_rb 將自己連接到紅黑樹中。

        而紅黑樹中的根節(jié)點(diǎn)存儲(chǔ)在內(nèi)存描述符 struct mm_struct 中的 mm_rb 中:

        struct mm_struct {
             struct rb_root mm_rb;
        }
        image.png

        6. 程序編譯后的二進(jìn)制文件如何映射到虛擬內(nèi)存空間中

        經(jīng)過前邊這么多小節(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)建并初始化的呢?

        image.png

        在 《3. 進(jìn)程虛擬內(nèi)存空間》小節(jié)中,我們介紹進(jìn)程的虛擬內(nèi)存空間時(shí)提到,我們寫的程序代碼編譯之后會(huì)生成一個(gè) ELF 格式的二進(jìn)制文件,這個(gè)二進(jìn)制文件中包含了程序運(yùn)行時(shí)所需要的元信息,比如程序的機(jī)器碼,程序中的全局變量以及靜態(tài)變量等。

        這個(gè) ELF 格式的二進(jìn)制文件中的布局和我們前邊講的虛擬內(nèi)存空間中的布局類似,也是一段一段的,每一段包含了不同的元數(shù)據(jù)。

        磁盤文件中的段我們叫做 Section,內(nèi)存中的段我們叫做 Segment,也就是內(nèi)存區(qū)域。

        磁盤文件中的這些 Section 會(huì)在進(jìn)程運(yùn)行之前加載到內(nèi)存中并映射到內(nèi)存中的 Segment。通常是多個(gè) Section 映射到一個(gè) Segment。

        比如磁盤文件中的 .text,.rodata 等一些只讀的 Section,會(huì)被映射到內(nèi)存的一個(gè)只讀可執(zhí)行的 Segment 里(代碼段)。而 .data,.bss 等一些可讀寫的 Section,則會(huì)被映射到內(nèi)存的一個(gè)具有讀寫權(quán)限的 Segment 里(數(shù)據(jù)段,BSS 段)。

        那么這些 ELF 格式的二進(jìn)制文件中的 Section 是如何加載并映射進(jìn)虛擬內(nèi)存空間的呢?

        內(nèi)核中完成這個(gè)映射過程的函數(shù)是 load_elf_binary ,這個(gè)函數(shù)的作用很大,加載內(nèi)核的是它,啟動(dòng)第一個(gè)用戶態(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)程依賴的動(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)程依賴的動(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)一層一層地拆開這個(gè)黑盒子。

        之前在介紹進(jìn)程虛擬內(nèi)存空間的時(shí)候,筆者提到不同進(jìn)程之間的虛擬內(nèi)存空間是相互隔離的,彼此之間相互獨(dú)立,相互感知不到其他進(jìn)程的存在。使得進(jìn)程以為自己擁有所有的內(nèi)存資源。

        image.png

        而內(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 分別在各自的用戶態(tài)虛擬內(nèi)存空間中訪問虛擬地址 x 。由于進(jìn)程之間的用戶態(tài)虛擬內(nèi)存空間是相互隔離相互獨(dú)立的,雖然在進(jìn)程a,進(jìn)程b,進(jìn)程c 訪問的都是虛擬地址 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)都去訪問虛擬地址 y。這時(shí)它們看到的內(nèi)容就是一樣的了。

        這里筆者和大家澄清一個(gè)經(jīng)常被誤解的概念:由于內(nèi)核會(huì)涉及到物理內(nèi)存的管理,所以很多人會(huì)想當(dāng)然地認(rèn)為只要進(jìn)入了內(nèi)核態(tài)就開始使用物理地址了,這就大錯(cuò)特錯(cuò)了,千萬(wàn)不要這樣理解,進(jìn)程進(jìn)入內(nèi)核態(tài)之后使用的仍然是虛擬內(nèi)存地址,只不過在內(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)核如何劃分用戶態(tài)和內(nèi)核態(tài)虛擬內(nèi)存空間》小節(jié)中我們提到,內(nèi)核在 /arch/x86/include/asm/page_32_types.h 文件中通過 TASK_SIZE 將進(jìn)程虛擬內(nèi)存空間和內(nèi)核虛擬內(nèi)存空間分割開來(lái)。

        /*
         * User space process size: 3GB (default).
         */

        #define TASK_SIZE       __PAGE_OFFSET

        __PAGE_OFFSET 的值在 32 位系統(tǒng)下為 0xC000 000

        image.png

        在 32 位體系結(jié)構(gòu)下進(jìn)程用戶態(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ū)域,我們稱之為直接映射區(qū)或者線性映射區(qū),地址范圍為 3G -- 3G + 896m 。

        之所以這塊 896M 大小的區(qū)域稱為直接映射區(qū)或者線性映射區(qū),是因?yàn)檫@塊連續(xù)的虛擬內(nèi)存地址會(huì)映射到 0 - 896M 這塊連續(xù)的物理內(nèi)存上。

        也就是說 3G -- 3G + 896m 這塊 896M 大小的虛擬內(nèi)存會(huì)直接映射到 0 - 896M 這塊 896M 大小的物理內(nèi)存上,這塊區(qū)域中的虛擬內(nèi)存地址直接減去 0xC000 0000 (3G) 就得到了物理內(nèi)存地址。所以我們稱這塊區(qū)域?yàn)橹苯佑成鋮^(qū)。

        為了方便為大家解釋,我們假設(shè)現(xiàn)在機(jī)器上的物理內(nèi)存為 4G 大小

        image.png

        雖然這塊區(qū)域中的虛擬地址是直接映射到物理地址上,但是內(nèi)核在訪問這段區(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)存)。

        我們可以通過 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ū)域中。

        image.png

        當(dāng)進(jìn)程被創(chuàng)建完畢之后,在內(nèi)核運(yùn)行的過程中,會(huì)涉及內(nèi)核棧的分配,內(nèi)核會(huì)為每個(gè)進(jìn)程分配一個(gè)固定大小的內(nèi)核棧(一般是兩個(gè)頁(yè)大小,依賴具體的體系結(jié)構(gòu)),每個(gè)進(jìn)程的整個(gè)調(diào)用鏈必須放在自己的內(nèi)核棧中,內(nèi)核棧也是分配在直接映射區(qū)。

        與進(jìn)程用戶空間中的棧不同的是,內(nèi)核棧容量小而且是固定的,用戶空間中的棧容量大而且可以動(dòng)態(tài)擴(kuò)展。內(nèi)核棧的溢出危害非常巨大,它會(huì)直接悄無(wú)聲息的覆蓋相鄰內(nèi)存區(qū)域中的數(shù)據(jù),破壞數(shù)據(jù)。

        通過以上內(nèi)容的介紹我們了解到內(nèi)核虛擬內(nèi)存空間最前邊的這段 896M 大小的直接映射區(qū)如何與物理內(nèi)存進(jìn)行映射關(guān)聯(lián),并且清楚了直接映射區(qū)主要用來(lái)存放哪些內(nèi)容。

        寫到這里,筆者覺得還是有必要再次從功能劃分的角度為大家介紹下這塊直接映射區(qū)域。

        我們都知道內(nèi)核對(duì)物理內(nèi)存的管理都是以頁(yè)為最小單位來(lái)管理的,每頁(yè)默認(rèn) 4K 大小,理想狀況下任何種類的數(shù)據(jù)頁(yè)都可以存放在任何頁(yè)框中,沒有什么限制。比如:存放內(nèi)核數(shù)據(jù),用戶數(shù)據(jù),緩沖磁盤數(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 專門讓內(nèi)核用來(lái)為 DMA 分配內(nèi)存,這塊 16M 大小的內(nèi)存區(qū)域我們稱之為 ZONE_DMA。

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

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

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

        image.png

        注意這里的 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ū)域,我們稱之為高端內(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ù)通過直接映射的方式映射到這 128M 大小的虛擬內(nèi)存空間中。

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

        知道了 ZONE_HIGHMEM 區(qū)域的映射原理,我們接著往下看這 128M 大小的內(nèi)核虛擬內(nèi)存空間究竟是如何布局的?

        image.png

        內(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
        image.png

        和用戶態(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ù)的。通過頁(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)行映射,在性能開銷上會(huì)比直接映射大得多。

        關(guān)于 vmalloc 分配內(nèi)存的相關(guān)實(shí)現(xiàn)原理,筆者會(huì)在后面的文章中為大家講解,這里大家只需要明白它在哪塊虛擬內(nèi)存區(qū)域中活動(dòng)即可。

        7.1.4 永久映射區(qū)

        image.png

        而在 PKMAP_BASE 到 FIXADDR_START 之間的這段空間稱為永久映射區(qū)。在內(nèi)核的這段虛擬地址空間中允許建立與物理高端內(nèi)存的長(zhǎng)期映射關(guān)系。比如內(nèi)核通過 alloc_pages() 函數(shù)在物理內(nèi)存的高端內(nèi)存中申請(qǐng)獲取到的物理內(nèi)存頁(yè),這些物理內(nèi)存頁(yè)可以通過調(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ū)

        image.png

        內(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ū)中虛擬地址是固定的,而被映射的物理地址是可以改變的。也就是說,有些虛擬地址在編譯的時(shí)候就固定下來(lái)了,是在內(nèi)核啟動(dòng)過程中被確定的,而這些虛擬地址對(duì)應(yīng)的物理地址不是固定的。采用固定虛擬地址的好處是它相當(dāng)于一個(gè)指針常量(常量的值在編譯時(shí)確定),指向物理地址,如果虛擬地址不固定,則相當(dāng)于一個(gè)指針變量。

        那為什么會(huì)有固定映射這個(gè)概念呢 ?  比如:在內(nèi)核的啟動(dòng)過程中,有些模塊需要使用虛擬內(nè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)干什么的呢?

        image.png

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

        image.png

        但是內(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ū)上,然后將用戶空間緩存區(qū) DirectByteBuffer 中的待寫入數(shù)據(jù)通過這段映射的虛擬地址拷貝到 page cache 中的相應(yīng)緩存頁(yè)中。這時(shí)文件的寫入操作就已經(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;
          // 將用戶緩存區(qū) DirectByteBuffer 中的待寫入數(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)存空間的布局:

        image.png

        7.2 64 位體系內(nèi)核虛擬內(nèi)存空間布局

        內(nèi)核虛擬內(nèi)存空間在 32 位體系下只有 1G 大小,實(shí)在太小了,因此需要精細(xì)化的管理,于是按照功能分類劃分除了很多內(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)核要訪問全部的物理內(nèi)存,直接映射就可以了,不在需要用到《7.1.2 ZONE_HIGHMEM 高端內(nèi)存》小節(jié)中介紹的高端內(nèi)存那種動(dòng)態(tài)映射方式。

        在前邊《5.1 內(nèi)核如何劃分用戶態(tài)和內(nèi)核態(tài)虛擬內(nèi)存空間》小節(jié)中我們提到,內(nèi)核在 /arch/x86/include/asm/page_64_types.h 文件中通過 TASK_SIZE 將進(jìn)程虛擬內(nèi)存空間和內(nèi)核虛擬內(nè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位地址空間.png

        在 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 。

        本小節(jié)我們主要關(guān)注 0xFFFF 8000 0000 0000 - 0xFFFF FFFF FFFF FFFF 這段內(nèi)核虛擬內(nèi)存空間的布局情況。

        image.png

        64 位內(nèi)核虛擬內(nèi)存空間從 0xFFFF 8000 0000 0000 開始到 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ū),這里類似用戶空間中的堆,內(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 開始是 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 開始是大小為 512M 的區(qū)域用于存放內(nèi)核代碼段、全局變量、BSS 等。這里對(duì)應(yīng)到物理內(nèi)存開始的位置,減去 __START_KERNEL_map 就能得到物理內(nèi)存的地址。這里和直接映射區(qū)有點(diǎn)像,但是不矛盾,因?yàn)橹苯佑成鋮^(qū)之前有 8T 的空洞區(qū)域,早就過了內(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)存空間的布局:

        image.png

        8. 到底什么是物理內(nèi)存地址

        聊完了虛擬內(nèi)存,我們接著聊一下物理內(nèi)存,我們平時(shí)所稱的內(nèi)存也叫隨機(jī)訪問存儲(chǔ)器( random-access memory )也叫 RAM 。而 RAM 分為兩類:

        • 一類是靜態(tài) RAM( SRAM ),這類 SRAM 用于 CPU 高速緩存 L1Cache,L2Cache,L3Cache。其特點(diǎn)是訪問速度快,訪問速度為 1 - 30 個(gè)時(shí)鐘周期,但是容量小,造價(jià)高。
        CPU緩存結(jié)構(gòu).png
        • 另一類則是動(dòng)態(tài) RAM ( DRAM ),這類 DRAM 用于我們常說的主存上,其特點(diǎn)的是訪問速度慢(相對(duì)高速緩存),訪問速度為 50 - 200 個(gè)時(shí)鐘周期,但是容量大,造價(jià)便宜些(相對(duì)高速緩存)。

        內(nèi)存由一個(gè)一個(gè)的存儲(chǔ)器模塊(memory module)組成,它們插在主板的擴(kuò)展槽上。常見的存儲(chǔ)器模塊通常以 64 位為單位( 8 個(gè)字節(jié))傳輸數(shù)據(jù)到存儲(chǔ)控制器上或者從存儲(chǔ)控制器傳出數(shù)據(jù)。

        image.png

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

        內(nèi)存結(jié)構(gòu).png

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

        存儲(chǔ)器模塊.png

        而每一個(gè) DRAM 芯片的存儲(chǔ)結(jié)構(gòu)是一個(gè)二維矩陣,二維矩陣中存儲(chǔ)的元素我們稱為超單元(supercell),每個(gè) supercell 大小為一個(gè)字節(jié)(8 bit)。每個(gè) supercell 都由一個(gè)坐標(biāo)地址(i,j)。

        i 表示二維矩陣中的行地址,在計(jì)算機(jī)中行地址稱為 RAS (row access strobe,行訪問選通脈沖)。 j 表示二維矩陣中的列地址,在計(jì)算機(jī)中列地址稱為 CAS (column access strobe,列訪問選通脈沖)。

        下圖中的 supercell 的 RAS = 2,CAS = 2。

        DRAM結(jié)構(gòu).png

        DRAM 芯片中的信息通過引腳流入流出 DRAM 芯片。每個(gè)引腳攜帶 1 bit的信號(hào)。

        圖中 DRAM 芯片包含了兩個(gè)地址引腳( addr ),因?yàn)槲覀円ㄟ^ 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 芯片的訪問

        我們現(xiàn)在就以讀取上圖中坐標(biāo)地址為(2,2)的 supercell 為例,來(lái)說明訪問 DRAM 芯片的過程。

        DRAM芯片訪問.png
        1. 首先存儲(chǔ)控制器將行地址 RAS = 2 通過地址引腳發(fā)送給 DRAM 芯片。

        2. DRAM 芯片根據(jù) RAS = 2 將二維矩陣中的第二行的全部?jī)?nèi)容拷貝到內(nèi)部行緩沖區(qū)中。

        3. 接下來(lái)存儲(chǔ)控制器會(huì)通過地址引腳發(fā)送 CAS = 2 到 DRAM 芯片中。

        4. DRAM芯片從內(nèi)部行緩沖區(qū)中根據(jù) CAS = 2 拷貝出第二列的 supercell 并通過數(shù)據(jù)引腳發(fā)送給存儲(chǔ)控制器。

        DRAM 芯片的 IO 單位為一個(gè) supercell ,也就是一個(gè)字節(jié)(8 bit)。

        8.2 CPU 如何讀寫主存

        前邊我們介紹了內(nèi)存的物理結(jié)構(gòu),以及如何訪問內(nèi)存中的 DRAM 芯片獲取 supercell 中存儲(chǔ)的數(shù)據(jù)(一個(gè)字節(jié))。本小節(jié)我們來(lái)介紹下 CPU 是如何訪問內(nèi)存的:

        CPU與內(nèi)存之間的總線結(jié)構(gòu).png

        CPU 與內(nèi)存之間的數(shù)據(jù)交互是通過總線(bus)完成的,而數(shù)據(jù)在總線上的傳送是通過一系列的步驟完成的,這些步驟稱為總線事務(wù)(bus transaction)。

        其中數(shù)據(jù)從內(nèi)存?zhèn)魉偷?CPU 稱之為讀事務(wù)(read transaction),數(shù)據(jù)從 CPU 傳送到內(nèi)存稱之為寫事務(wù)(write transaction)。

        總線上傳輸?shù)男盘?hào)包括:地址信號(hào),數(shù)據(jù)信號(hào),控制信號(hào)。其中控制總線上傳輸?shù)目刂菩盘?hào)可以同步事務(wù),并能夠標(biāo)識(shí)出當(dāng)前正在被執(zhí)行的事務(wù)信息:

        • 當(dāng)前這個(gè)事務(wù)是到內(nèi)存的?還是到磁盤的?或者是到其他 IO 設(shè)備的?
        • 這個(gè)事務(wù)是讀還是寫?
        • 總線上傳輸?shù)牡刂沸盘?hào)(物理內(nèi)存地址),還是數(shù)據(jù)信號(hào)(數(shù)據(jù))?。

        這里大家需要注意總線上傳輸?shù)牡刂肪鶠槲锢韮?nèi)存地址。比如:在 MESI 緩存一致性協(xié)議中當(dāng) CPU core0 修改字段 a 的值時(shí),其他 CPU 核心會(huì)在總線上嗅探字段 a 的物理內(nèi)存地址,如果嗅探到總線上出現(xiàn)字段 a 的物理內(nèi)存地址,說明有人在修改字段 a,這樣其他 CPU 核心就會(huì)失效字段 a 所在的 cache line 。

        如上圖所示,其中系統(tǒng)總線是連接 CPU 與 IO bridge 的,存儲(chǔ)總線是來(lái)連接 IO bridge 和主存的。

        IO bridge 負(fù)責(zé)將系統(tǒng)總線上的電子信號(hào)轉(zhuǎn)換成存儲(chǔ)總線上的電子信號(hào)。IO bridge 也會(huì)將系統(tǒng)總線和存儲(chǔ)總線連接到IO總線(磁盤等IO設(shè)備)上。這里我們看到 IO bridge 其實(shí)起的作用就是轉(zhuǎn)換不同總線上的電子信號(hào)。

        8.3 CPU 從內(nèi)存讀取數(shù)據(jù)過程

        假設(shè) CPU 現(xiàn)在需要將物理內(nèi)存地址為 A 的內(nèi)容加載到寄存器中進(jìn)行運(yùn)算。

        大家需要注意的是 CPU 只會(huì)訪問虛擬內(nèi)存,在操作總線之前,需要把虛擬內(nèi)存地址轉(zhuǎn)換為物理內(nèi)存地址,總線上傳輸?shù)亩际俏锢韮?nèi)存地址,這里省略了虛擬內(nèi)存地址到物理內(nèi)存地址的轉(zhuǎn)換過程,這部分內(nèi)容筆者會(huì)在后續(xù)文章的相關(guān)章節(jié)詳細(xì)為大家講解,這里我們聚焦如果通過物理內(nèi)存地址讀取內(nèi)存數(shù)據(jù)。

        CPU讀取內(nèi)存.png

        首先 CPU 芯片中的總線接口會(huì)在總線上發(fā)起讀事務(wù)(read transaction)。 該讀事務(wù)分為以下步驟進(jìn)行:

        1. CPU 將物理內(nèi)存地址 A 放到系統(tǒng)總線上。隨后 IO bridge 將信號(hào)傳遞到存儲(chǔ)總線上。

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

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

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

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

        以上就是 CPU 讀取內(nèi)存數(shù)據(jù)到寄存器中的完整過程。

        但是其中還涉及到一個(gè)重要的過程,這里我們還是需要攤開來(lái)介紹一下,那就是存儲(chǔ)控制器如何通過物理內(nèi)存地址 A 從主存中讀取出對(duì)應(yīng)的數(shù)據(jù) X 的?

        接下來(lái)我們結(jié)合前邊介紹的內(nèi)存結(jié)構(gòu)以及從 DRAM 芯片讀取數(shù)據(jù)的過程,來(lái)總體介紹下如何從主存中讀取數(shù)據(jù)。

        8.4 如何根據(jù)物理內(nèi)存地址從主存中讀取數(shù)據(jù)

        前邊介紹到,當(dāng)主存中的存儲(chǔ)控制器感受到了存儲(chǔ)總線上的地址信號(hào)時(shí),會(huì)將內(nèi)存地址從存儲(chǔ)總線上讀取出來(lái)。

        隨后會(huì)通過內(nèi)存地址定位到具體的存儲(chǔ)器模塊。還記得內(nèi)存結(jié)構(gòu)中的存儲(chǔ)器模塊嗎 ?

        內(nèi)存結(jié)構(gòu).png

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

        存儲(chǔ)器模塊.png

        存儲(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 芯片。依次通過 (RAS,CAS) 從 DRAM0 到 DRAM7 讀取到相應(yīng)的 supercell 。

        DRAM芯片訪問.png

        我們知道一個(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ǔ)總線上。

        CPU 總是以 word size 為單位從內(nèi)存中讀取數(shù)據(jù),在 64 位處理器中的 word size 為 8 個(gè)字節(jié)。64 位的內(nèi)存每次只能吞吐 8 個(gè)字節(jié)。

        CPU 每次會(huì)向內(nèi)存讀寫一個(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é),......依次類推 DRAM7 芯片存儲(chǔ)最后一個(gè)高位字節(jié)。

        讀取存儲(chǔ)器模塊數(shù)據(jù).png

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

        內(nèi)存IO單位.png

        而且在程序員眼里連續(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)存寫入數(shù)據(jù)過程

        我們現(xiàn)在假設(shè) CPU 要將寄存器中的數(shù)據(jù) X 寫到物理內(nèi)存地址 A 中。同樣的道理,CPU 芯片中的總線接口會(huì)向總線發(fā)起寫事務(wù)(write transaction)。寫事務(wù)步驟如下:

        1. CPU 將要寫入的物理內(nèi)存地址 A 放入系統(tǒng)總線上。

        2. 通過 IO bridge 的信號(hào)轉(zhuǎn)換,將物理內(nèi)存地址 A 傳遞到存儲(chǔ)總線上。

        3. 存儲(chǔ)控制器感受到存儲(chǔ)總線上的地址信號(hào),將物理內(nèi)存地址 A 從存儲(chǔ)總線上讀取出來(lái),并等待數(shù)據(jù)的到達(dá)。

        4. CPU 將寄存器中的數(shù)據(jù)拷貝到系統(tǒng)總線上,通過 IO bridge 的信號(hào)轉(zhuǎn)換,將數(shù)據(jù)傳遞到存儲(chǔ)總線上。

        5. 存儲(chǔ)控制器感受到存儲(chǔ)總線上的數(shù)據(jù)信號(hào),將數(shù)據(jù)從存儲(chǔ)總線上讀取出來(lái)。

        6. 存儲(chǔ)控制器通過內(nèi)存地址 A 定位到具體的存儲(chǔ)器模塊,最后將數(shù)據(jù)寫入存儲(chǔ)器模塊中的 8 個(gè) DRAM 芯片中。

        總結(jié)

        本文我們從虛擬內(nèi)存地址開始聊起,一直到物理內(nèi)存地址結(jié)束,包含的信息量還是比較大的。首先筆者通過一個(gè)進(jìn)程的運(yùn)行實(shí)例為大家引出了內(nèi)核引入虛擬內(nèi)存空間的目的及其需要解決的問題。

        在我們有了虛擬內(nèi)存空間的概念之后,筆者又近一步為大家介紹了內(nèi)核如何劃分用戶態(tài)虛擬內(nèi)存空間和內(nèi)核態(tài)虛擬內(nèi)存空間,并在次基礎(chǔ)之上分別從 32 位體系結(jié)構(gòu)和 64 位體系結(jié)構(gòu)的角度詳細(xì)闡述了 Linux 虛擬內(nèi)存空間的整體布局分布。

        • 我們可以通過 cat /proc/pid/maps 或者 pmap pid 命令來(lái)查看進(jìn)程用戶態(tài)虛擬內(nèi)存空間的實(shí)際分布。

        • 還可以通過 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ān)的內(nèi)核數(shù)據(jù)結(jié)構(gòu),近一步從內(nèi)核源碼實(shí)現(xiàn)角度加深大家對(duì)虛擬內(nèi)存空間的理解。

        最后筆者介紹了物理內(nèi)存的結(jié)構(gòu),以及 CPU 如何通過物理內(nèi)存地址來(lái)讀寫內(nèi)存中的數(shù)據(jù)。這里筆者需要特地再次強(qiáng)調(diào)的是 CPU 只會(huì)訪問虛擬內(nèi)存地址,只不過在操作總線之前,通過一個(gè)地址轉(zhuǎn)換硬件將虛擬內(nèi)存地址轉(zhuǎn)換為物理內(nèi)存地址,然后將物理內(nèi)存地址作為地址信號(hào)放在總線上傳輸,由于地址轉(zhuǎn)換的內(nèi)容和本文主旨無(wú)關(guān),考慮到文章的篇幅以及復(fù)雜性,筆者就沒有過多的介紹。

        好了,本文的全部?jī)?nèi)容到這里就結(jié)束了,感謝大家的收看,我們下篇文章見~~~

        推薦閱讀:

        真棒! 20 張圖揭開內(nèi)存管理的迷霧,瞬間豁然開朗

        書上沒講的,我來(lái)講!

        字節(jié)一面:TCP 三次握手,問的好細(xì)!

        我的網(wǎng)站茍活半年了!

        瀏覽 25
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            中文字幕少妇在线三级hd | 日韩午夜福利视频 | 搞逼视频网站 | 深爱激情丁香五月天 | 国产午夜精品视频一区二区三区 | 日韩成人无码 | 操操操操逼视频 | 国产专区自拍 | 免费观看日批视频 | 国产日韩二区 |