Linux的內(nèi)存管理方式經(jīng)常會(huì)在面試時(shí)作為操作系統(tǒng)基礎(chǔ)被問道项炼。搞清楚這個(gè)問題的好處很多揭厚,近的話可以應(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)存一般都有限制,比如說4G或者8G劫窒。
但是如果真的這樣直接的使用物理內(nèi)存會(huì)發(fā)生什么狀況本今?
1、進(jìn)程地址空間不能隔離
由于程序直接訪問的是物理內(nèi)存主巍,這個(gè)時(shí)候程序所使用的內(nèi)存空間不是隔離的冠息。惡意程序或者是木馬程序可以輕而易舉的破快其他的程序,系統(tǒng)的安全性也就得不到保障了孕索,這對(duì)用戶來說也是不能容忍的逛艰。
2、內(nèi)存使用的效率低
由于物理內(nèi)存一般都有限制搞旭,當(dāng)物理內(nèi)存不夠用時(shí)散怖,需要把暫時(shí)不需要運(yùn)行的程序放到磁盤上,試想將整個(gè)程序放入磁盤肄渗,我們知道IO操作比較耗時(shí)镇眷,所以這個(gè)過程效率將會(huì)十分低下。
3翎嫡、程序運(yùn)行的地址不能確定
程序每次需要運(yùn)行時(shí)欠动,都需要在內(nèi)存中非配一塊足夠大的空閑區(qū)域,而問題是這個(gè)空閑的位置是不能確定的惑申,這會(huì)帶來一些重定位的問題具伍,重定位的問題確定就是程序中引用的變量和函數(shù)的地址铆遭。
可以通過引入一個(gè)中間層來解決上面的問題。
現(xiàn)在的內(nèi)存管理方法就是在程序和物理內(nèi)存之間引入了虛擬內(nèi)存這個(gè)概念沿猜。虛擬內(nèi)存位于程序和物理內(nèi)存之間,程序只能看見虛擬內(nèi)存碗脊,再也不能直接訪問物理內(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):這種方法是人們最開始使用的一種方法丸逸,基本思路是將程序所需要的內(nèi)存地址空間大小的虛擬空間映射到某個(gè)物理地址空間蹋艺。
每個(gè)程序都有自己的獨(dú)立虛擬的進(jìn)程地址空間。進(jìn)程的只能看到自己的虛擬地址空間黄刚,這就使得進(jìn)程和實(shí)際的物理地址解除耦合捎谨。兩塊大小相同的虛擬地址空間和實(shí)際物理地址空間一一映射,即虛擬地址空間中的每個(gè)字節(jié)對(duì)應(yīng)于實(shí)際地址空間中的每個(gè)字節(jié)憔维,這個(gè)映射過程由軟件來設(shè)置映射的機(jī)制涛救,實(shí)際的轉(zhuǎn)換由硬件來完成。
這種分段的機(jī)制解決了文章一開始提到的3個(gè)問題中的進(jìn)程地址空間隔離(1)和程序地址重定位(3)的問題业扒。(PS:既然隔離了检吆,那么緩沖區(qū)溢出為啥還能那么牛掰?答案最后講凶赁。)
程序A和程序B有自己獨(dú)立的虛擬地址空間咧栗,而且該虛擬地址空間被映射到了互相不重疊的物理地址空間,如果程序A訪問虛擬地址空間的地址不在0x00000000-0x00A00000這個(gè)范圍內(nèi)虱肄,那么內(nèi)核就會(huì)拒絕這個(gè)請(qǐng)求致板,所以它解決了隔離地址空間的問題。我們應(yīng)用程序A只需要關(guān)心其虛擬地址空間0x00000000-0x00A00000咏窿,而其被映射到哪個(gè)物理地址我們無需關(guān)心斟或,所以程序永遠(yuǎn)按照這個(gè)虛擬地址空間來放置變量、代碼集嵌,不需要重新定位萝挤。
分段機(jī)制解決了上面兩個(gè)問題御毅,是一個(gè)很大的進(jìn)步,但是對(duì)于內(nèi)存效率問題仍然無能為力怜珍。因?yàn)檫@種內(nèi)存映射機(jī)制仍然是以程序?yàn)閱挝欢饲?dāng)內(nèi)存不足時(shí)仍然需要將整個(gè)程序交換到磁盤,這樣內(nèi)存使用的效率仍然很低酥泛。事實(shí)上今豆,根據(jù)程序的局部性運(yùn)行原理,一個(gè)程序在運(yùn)行的過程當(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)將磁盤分成若干個(gè)Block一樣院崇,這樣做是分別是為了提高內(nèi)存和磁盤的利用率肆氓。
Linux中一般頁(yè)的大小是4KB,我們把進(jìn)程的地址空間按頁(yè)分割底瓣,把常用的數(shù)據(jù)和代碼頁(yè)裝載到內(nèi)存中谢揪,不常用的代碼和數(shù)據(jù)保存在磁盤中,我們還是以一個(gè)例子來說明,如下圖:
我們可以看到進(jìn)程1和進(jìn)程2的虛擬地址空間都被映射到了不連續(xù)的物理地址空間內(nèi)捐凭。
有一天我們的連續(xù)物理地址空間不夠拨扶,但是不連續(xù)的地址空間很多,如果沒有這種技術(shù)茁肠,我們的程序就沒有辦法運(yùn)行,甚至他們共用了一部分物理地址空間患民,這就是共享內(nèi)存。
進(jìn)程1的虛擬頁(yè)VP2和VP3被交換到了磁盤中垦梆,在程序需要這兩頁(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),他就是專門負(fù)責(zé)從虛擬地址到物理地址轉(zhuǎn)換的京腥,也就是從虛擬頁(yè)找到物理頁(yè)赦肃。
有的時(shí)候,單個(gè)頁(yè)表無法表示所有內(nèi)存頁(yè)信息,我們還需要多級(jí)頁(yè)表的幫助才行他宛。(后面再講船侧。)
下面繼續(xù)聊聊進(jìn)程地址的概念,當(dāng)然都是基于Linux操作系統(tǒng)厅各。
進(jìn)程內(nèi)部通過分段的方式劃分了:數(shù)據(jù)段镜撩、代碼段。數(shù)據(jù)段又可以分為:靜態(tài)數(shù)據(jù)段队塘、棧琐鲁、堆。
由此有幾個(gè)地址需要講一下:
邏輯地址:段基值確定它所在的段居于整個(gè)存儲(chǔ)空間的位置,偏移量確定它在段內(nèi)的位置,這種地址表示方式稱為邏輯地址人灼。機(jī)器語言指令中出現(xiàn)的內(nèi)存地址(&操作符),都是邏輯地址顾翼。
線性地址:又叫虛擬地址投放,是一個(gè)32位無符號(hào)整數(shù),可以用來表示高達(dá)4GB的地址适贸,跟邏輯地址類似灸芳,它也是一個(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中邏輯地址等于線性地址赔硫。為什么這么說呢?因?yàn)長(zhǎng)inux所有的段(用戶代碼段盐肃、用戶數(shù)據(jù)段爪膊、內(nèi)核代碼段、內(nèi)核數(shù)據(jù)段)的線性地址都是從 0x00000000 開始砸王,長(zhǎng)度4G推盛,這樣線性地址=邏輯地址+ 0x00000000,也就是說邏輯地址等于線性地址了处硬。
Linux主要以分頁(yè)的方式實(shí)現(xiàn)內(nèi)存管理小槐。
前面說了Linux中邏輯地址等于線性地址,那么線性地址怎么對(duì)應(yīng)到物理地址呢?這個(gè)大家都知道凿跳,那就是通過分頁(yè)機(jī)制件豌,具體的說,就是通過頁(yè)表查找來對(duì)應(yīng)物理地址控嗜。
準(zhǔn)確的說分頁(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)物理地址的過程析命。
前面說到Linux中邏輯地址等于線性地址主卫,那么我們要轉(zhuǎn)換的線性地址就是0x80495b0。轉(zhuǎn)換的過程是由CPU自動(dòng)完成的鹃愤,Linux所要做的就是準(zhǔn)備好轉(zhuǎn)換所需的頁(yè)目錄表和頁(yè)表(假設(shè)已經(jīng)準(zhǔn)備好簇搅,給頁(yè)目錄表和頁(yè)表分配物理內(nèi)存的過程很復(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)行匹涮,根本無法給每個(gè)進(jìn)程提前建立3G的線性地址頁(yè)表天试。Linux利用CPU的一個(gè)機(jī)制解決了這個(gè)問題。進(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í)候可以通過一系列復(fù)雜的算法給分配一個(gè)物理頁(yè)带兜,并把物理頁(yè)的地址填入表項(xiàng)中,進(jìn)程再恢復(fù)執(zhí)行吨灭。當(dāng)然進(jìn)程在這個(gè)過程中是被蒙蔽的刚照,它自己的感覺還是正常訪問到了物理內(nèi)存。