Linux中的內(nèi)存管理

Linux的內(nèi)存管理方式經(jīng)常會(huì)在面試時(shí)作為操作系統(tǒng)基礎(chǔ)被問(wèn)道。搞清楚這個(gè)問(wèn)題的好處很多,近的話可以應(yīng)付面試,遠(yuǎn)的可以提高對(duì)于操作系統(tǒng)底層的認(rèn)識(shí),為程序的性能優(yōu)化打下基礎(chǔ)。

我們對(duì)于計(jì)算機(jī)內(nèi)存,最直觀和簡(jiǎn)陋的概念就是機(jī)器的物理內(nèi)存,程序都被放在物理內(nèi)存上執(zhí)行。物理內(nèi)存一般都有限制,比如說(shuō)4G或者8G。

但是如果真的這樣直接的使用物理內(nèi)存會(huì)發(fā)生什么狀況?

1、進(jìn)程地址空間不能隔離

由于程序直接訪問(wèn)的是物理內(nèi)存,這個(gè)時(shí)候程序所使用的內(nèi)存空間不是隔離的。惡意程序或者是木馬程序可以輕而易舉的破快其他的程序,系統(tǒng)的安全性也就得不到保障了,這對(duì)用戶來(lái)說(shuō)也是不能容忍的。

2、內(nèi)存使用的效率低

由于物理內(nèi)存一般都有限制,當(dāng)物理內(nèi)存不夠用時(shí),需要把暫時(shí)不需要運(yùn)行的程序放到磁盤(pán)上,試想將整個(gè)程序放入磁盤(pán),我們知道IO操作比較耗時(shí),所以這個(gè)過(guò)程效率將會(huì)十分低下。

3、程序運(yùn)行的地址不能確定

程序每次需要運(yùn)行時(shí),都需要在內(nèi)存中非配一塊足夠大的空閑區(qū)域,而問(wèn)題是這個(gè)空閑的位置是不能確定的,這會(huì)帶來(lái)一些重定位的問(wèn)題,重定位的問(wèn)題確定就是程序中引用的變量和函數(shù)的地址。

可以通過(guò)引入一個(gè)中間層來(lái)解決上面的問(wèn)題。

現(xiàn)在的內(nèi)存管理方法就是在程序和物理內(nèi)存之間引入了虛擬內(nèi)存這個(gè)概念。虛擬內(nèi)存位于程序和物理內(nèi)存之間,程序只能看見(jiàn)虛擬內(nèi)存,再也不能直接訪問(wèn)物理內(nèi)存。每個(gè)程序都有自己獨(dú)立的進(jìn)程地址空間,這樣就做到了進(jìn)程隔離。這里的進(jìn)程地址空間是指虛擬地址。顧名思義既然是虛擬地址,也就是虛的,不是現(xiàn)實(shí)存在的地址空間。

既然我們?cè)诔绦蚝臀锢淼刂房臻g之間增加了虛擬地址,那么就要解決怎么從虛擬地址映射到物理地址,因?yàn)槌绦蜃罱K肯定是運(yùn)行在物理內(nèi)存中的,主要有分段和分頁(yè)兩種技術(shù)。

分段(Segmentation):這種方法是人們最開(kāi)始使用的一種方法,基本思路是將程序所需要的內(nèi)存地址空間大小的虛擬空間映射到某個(gè)物理地址空間。

段映射機(jī)制

每個(gè)程序都有自己的獨(dú)立虛擬的進(jìn)程地址空間。進(jìn)程的只能看到自己的虛擬地址空間,這就使得進(jìn)程和實(shí)際的物理地址解除耦合。兩塊大小相同的虛擬地址空間和實(shí)際物理地址空間一一映射,即虛擬地址空間中的每個(gè)字節(jié)對(duì)應(yīng)于實(shí)際地址空間中的每個(gè)字節(jié),這個(gè)映射過(guò)程由軟件來(lái)設(shè)置映射的機(jī)制,實(shí)際的轉(zhuǎn)換由硬件來(lái)完成。

這種分段的機(jī)制解決了文章一開(kāi)始提到的3個(gè)問(wèn)題中的進(jìn)程地址空間隔離(1)和程序地址重定位(3)的問(wèn)題。(PS:既然隔離了,那么緩沖區(qū)溢出為啥還能那么牛掰?答案最后講。)

程序A和程序B有自己獨(dú)立的虛擬地址空間,而且該虛擬地址空間被映射到了互相不重疊的物理地址空間,如果程序A訪問(wèn)虛擬地址空間的地址不在0x00000000-0x00A00000這個(gè)范圍內(nèi),那么內(nèi)核就會(huì)拒絕這個(gè)請(qǐng)求,所以它解決了隔離地址空間的問(wèn)題。我們應(yīng)用程序A只需要關(guān)心其虛擬地址空間0x00000000-0x00A00000,而其被映射到哪個(gè)物理地址我們無(wú)需關(guān)心,所以程序永遠(yuǎn)按照這個(gè)虛擬地址空間來(lái)放置變量、代碼,不需要重新定位。

分段機(jī)制解決了上面兩個(gè)問(wèn)題,是一個(gè)很大的進(jìn)步,但是對(duì)于內(nèi)存效率問(wèn)題仍然無(wú)能為力。因?yàn)檫@種內(nèi)存映射機(jī)制仍然是以程序?yàn)閱挝唬?dāng)內(nèi)存不足時(shí)仍然需要將整個(gè)程序交換到磁盤(pán),這樣內(nèi)存使用的效率仍然很低。事實(shí)上,根據(jù)程序的局部性運(yùn)行原理,一個(gè)程序在運(yùn)行的過(guò)程當(dāng)中,在某個(gè)時(shí)間段內(nèi),只有一小部分?jǐn)?shù)據(jù)會(huì)被經(jīng)常用到。所以我們需要更加小粒度的內(nèi)存分割和映射方法,此時(shí)是否會(huì)想到Linux中的Buddy算法和slab內(nèi)存分配機(jī)制呢,哈哈。另一種將虛擬地址轉(zhuǎn)換為物理地址的方法分頁(yè)機(jī)制應(yīng)運(yùn)而生了。

分頁(yè)機(jī)制就是把內(nèi)存地址空間分為若干個(gè)很小的固定大小的頁(yè),每一頁(yè)的大小由內(nèi)存決定,就像Linux中ext文件系統(tǒng)將磁盤(pán)分成若干個(gè)Block一樣,這樣做是分別是為了提高內(nèi)存和磁盤(pán)的利用率。

Linux中一般頁(yè)的大小是4KB,我們把進(jìn)程的地址空間按頁(yè)分割,把常用的數(shù)據(jù)和代碼頁(yè)裝載到內(nèi)存中,不常用的代碼和數(shù)據(jù)保存在磁盤(pán)中,我們還是以一個(gè)例子來(lái)說(shuō)明,如下圖:

分頁(yè)機(jī)制

我們可以看到進(jìn)程1和進(jìn)程2的虛擬地址空間都被映射到了不連續(xù)的物理地址空間內(nèi)。

有一天我們的連續(xù)物理地址空間不夠,但是不連續(xù)的地址空間很多,如果沒(méi)有這種技術(shù),我們的程序就沒(méi)有辦法運(yùn)行,甚至他們共用了一部分物理地址空間,這就是共享內(nèi)存。

進(jìn)程1的虛擬頁(yè)VP2和VP3被交換到了磁盤(pán)中,在程序需要這兩頁(yè)的時(shí)候,Linux內(nèi)核會(huì)產(chǎn)生一個(gè)缺頁(yè)異常,然后異常管理程序會(huì)將其讀到內(nèi)存中。

分頁(yè)機(jī)制的實(shí)現(xiàn)需要硬件的實(shí)現(xiàn),這個(gè)硬件名字叫做MMU(Memory Management Unit),他就是專門(mén)負(fù)責(zé)從虛擬地址到物理地址轉(zhuǎn)換的,也就是從虛擬頁(yè)找到物理頁(yè)。

有的時(shí)候,單個(gè)頁(yè)表無(wú)法表示所有內(nèi)存頁(yè)信息,我們還需要多級(jí)頁(yè)表的幫助才行。(后面再講。)

下面繼續(xù)聊聊進(jìn)程地址的概念,當(dāng)然都是基于Linux操作系統(tǒng)。

進(jìn)程內(nèi)部通過(guò)分段的方式劃分了:數(shù)據(jù)段、代碼段。數(shù)據(jù)段又可以分為:靜態(tài)數(shù)據(jù)段、棧、堆。

由此有幾個(gè)地址需要講一下:

  • 邏輯地址:段基值確定它所在的段居于整個(gè)存儲(chǔ)空間的位置,偏移量確定它在段內(nèi)的位置,這種地址表示方式稱為邏輯地址。機(jī)器語(yǔ)言指令中出現(xiàn)的內(nèi)存地址(&操作符),都是邏輯地址。

  • 線性地址:又叫虛擬地址,是一個(gè)32位無(wú)符號(hào)整數(shù),可以用來(lái)表示高達(dá)4GB的地址,跟邏輯地址類(lèi)似,它也是一個(gè)不真實(shí)的地址,如果邏輯地址是對(duì)應(yīng)的硬件平臺(tái)段式管理轉(zhuǎn)換前地址的話,那么線性地址則對(duì)應(yīng)了硬件頁(yè)式內(nèi)存的轉(zhuǎn)換前地址。

  • 物理地址:用于內(nèi)存芯片級(jí)的單元尋址,與處理器和CPU連接的地址總線相對(duì)應(yīng)。

CPU將一個(gè)虛擬內(nèi)存空間中的地址轉(zhuǎn)換為物理地址,需要進(jìn)行兩步:首先將給定一個(gè)邏輯地址,CPU要利用其段式內(nèi)存管理單元,先將為個(gè)邏輯地址轉(zhuǎn)換成一個(gè)線性地址,再利用其頁(yè)式內(nèi)存管理單元,轉(zhuǎn)換為最終物理地址。

邏輯地址----段式內(nèi)存管理單元----線性地址----頁(yè)式內(nèi)存管理單元----物理地址

Linux中邏輯地址等于線性地址。為什么這么說(shuō)呢?因?yàn)長(zhǎng)inux所有的段(用戶代碼段、用戶數(shù)據(jù)段、內(nèi)核代碼段、內(nèi)核數(shù)據(jù)段)的線性地址都是從 0x00000000 開(kāi)始,長(zhǎng)度4G,這樣線性地址=邏輯地址+ 0x00000000,也就是說(shuō)邏輯地址等于線性地址了。

Linux主要以分頁(yè)的方式實(shí)現(xiàn)內(nèi)存管理。

前面說(shuō)了Linux中邏輯地址等于線性地址,那么線性地址怎么對(duì)應(yīng)到物理地址呢?這個(gè)大家都知道,那就是通過(guò)分頁(yè)機(jī)制,具體的說(shuō),就是通過(guò)頁(yè)表查找來(lái)對(duì)應(yīng)物理地址。

準(zhǔn)確的說(shuō)分頁(yè)是CPU提供的一種機(jī)制,Linux只是根據(jù)這種機(jī)制的規(guī)則,利用它實(shí)現(xiàn)了內(nèi)存管理。

分頁(yè)的基本原理是把內(nèi)存劃分成大小固定的若干單元,每個(gè)單元稱為一頁(yè)(page),每頁(yè)包含4k字節(jié)的地址空間(為簡(jiǎn)化分析,我們不考慮擴(kuò)展分頁(yè)的情況)。這樣每一頁(yè)的起始地址都是4k字節(jié)對(duì)齊的。為了能轉(zhuǎn)換成物理地址,我們需要給CPU提供當(dāng)前任務(wù)的線性地址轉(zhuǎn)物理地址的查找表,即頁(yè)表(page table)。注意,為了實(shí)現(xiàn)每個(gè)任務(wù)的平坦的虛擬內(nèi)存,每個(gè)任務(wù)都有自己的頁(yè)目錄表和頁(yè)表

32位的線性地址被分成3個(gè)部分:最高10位 Directory 頁(yè)目錄表偏移量,中間10位 Table是頁(yè)表偏移量,最低12位Offset是物理頁(yè)內(nèi)的字節(jié)偏移量。

頁(yè)目錄表的大小為4k(剛好是一個(gè)頁(yè)的大小),包含1024項(xiàng),每個(gè)項(xiàng)4字節(jié)(32位),項(xiàng)目里存儲(chǔ)的內(nèi)容就是頁(yè)表的物理地址。如果頁(yè)目錄表中的頁(yè)表尚未分配,則物理地址填0。

頁(yè)表的大小也是4k,同樣包含1024項(xiàng),每個(gè)項(xiàng)4字節(jié),內(nèi)容為最終物理頁(yè)的物理內(nèi)存起始地址。

每個(gè)活動(dòng)的任務(wù),必須要先分配給它一個(gè)頁(yè)目錄表,并把頁(yè)目錄表的物理地址存入cr3寄存器。頁(yè)表可以提前分配好,也可以在用到的時(shí)候再分配

以 mov 0x80495b0, %eax 中的地址為例分析一下線性地址轉(zhuǎn)物理地址的過(guò)程。

前面說(shuō)到Linux中邏輯地址等于線性地址,那么我們要轉(zhuǎn)換的線性地址就是0x80495b0。轉(zhuǎn)換的過(guò)程是由CPU自動(dòng)完成的,Linux所要做的就是準(zhǔn)備好轉(zhuǎn)換所需的頁(yè)目錄表和頁(yè)表(假設(shè)已經(jīng)準(zhǔn)備好,給頁(yè)目錄表和頁(yè)表分配物理內(nèi)存的過(guò)程很復(fù)雜,后面再分析)。

內(nèi)核先將當(dāng)前任務(wù)的頁(yè)目錄表的物理地址填入cr3寄存器。

線性地址 0x80495b0 轉(zhuǎn)換成二進(jìn)制后是 0000 1000 0000 0100 1001 0101 1011 0000,最高10位0000 1000 00的十進(jìn)制是32,CPU查看頁(yè)目錄表第32項(xiàng),里面存放的是頁(yè)表的物理地址。線性地址中間10位00 0100 1001 的十進(jìn)制是73,頁(yè)表的第73項(xiàng)存儲(chǔ)的是最終物理頁(yè)的物理起始地址。物理頁(yè)基地址加上線性地址中最低12位的偏移量,CPU就找到了線性地址最終對(duì)應(yīng)的物理內(nèi)存單元。

我們知道Linux中用戶進(jìn)程線性地址能尋址的范圍是0 - 3G,那么是不是需要提前先把這3G虛擬內(nèi)存的頁(yè)表都建立好呢?一般情況下,物理內(nèi)存是遠(yuǎn)遠(yuǎn)小于3G的,加上同時(shí)有很多進(jìn)程都在運(yùn)行,根本無(wú)法給每個(gè)進(jìn)程提前建立3G的線性地址頁(yè)表。Linux利用CPU的一個(gè)機(jī)制解決了這個(gè)問(wèn)題。進(jìn)程創(chuàng)建后我們可以給頁(yè)目錄表的表項(xiàng)值都填0,CPU在查找頁(yè)表時(shí),如果表項(xiàng)的內(nèi)容為0,則會(huì)引發(fā)一個(gè)缺頁(yè)異常,進(jìn)程暫停執(zhí)行,Linux內(nèi)核這時(shí)候可以通過(guò)一系列復(fù)雜的算法給分配一個(gè)物理頁(yè),并把物理頁(yè)的地址填入表項(xiàng)中,進(jìn)程再恢復(fù)執(zhí)行。當(dāng)然進(jìn)程在這個(gè)過(guò)程中是被蒙蔽的,它自己的感覺(jué)還是正常訪問(wèn)到了物理內(nèi)存。

線性地址轉(zhuǎn)物理地址
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容