欧美性猛交xxxx免费看_牛牛在线视频国产免费_天堂草原电视剧在线观看免费_国产粉嫩高清在线观看_国产欧美日本亚洲精品一5区

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

Linux內(nèi)存管理體系介紹

Linux閱碼場(chǎng) ? 來源:Linux閱碼場(chǎng) ? 作者:Linux閱碼場(chǎng) ? 2022-08-08 09:28 ? 次閱讀

1.1 內(nèi)存管理的意義

1.2 原始內(nèi)存管理

1.3 分段內(nèi)存管理

1.4 分頁(yè)內(nèi)存管理

1.5 內(nèi)存管理的目標(biāo)

1.6 Linux內(nèi)存管理體系

2.1 物理內(nèi)存節(jié)點(diǎn)

2.2 物理內(nèi)存區(qū)域

2.3 物理內(nèi)存頁(yè)面

2.4 物理內(nèi)存模型

2.5 三級(jí)區(qū)劃關(guān)系

3.1 Buddy System

3.1.1 伙伴系統(tǒng)的內(nèi)存來源

3.1.2 伙伴系統(tǒng)的管理數(shù)據(jù)結(jié)構(gòu)

3.1.3 伙伴系統(tǒng)的算法邏輯

3.1.4 伙伴系統(tǒng)的接口

3.1.5 伙伴系統(tǒng)的實(shí)現(xiàn)

3.2 Slab Allocator

3.2.1 Slab接口

3.2.2 Slab實(shí)現(xiàn)

3.2.3 Slob實(shí)現(xiàn)

3.2.4 Slub實(shí)現(xiàn)

3.3 Kmalloc

3.4 Vmalloc

3.5 CMA

4.1 內(nèi)存規(guī)整

4.2 頁(yè)幀回收

4.3 交換區(qū)

4.4 OOM Killer

5.1 ZRAM

5.2 ZSwap

5.3 ZCache

6.1 頁(yè)表

6.2 MMU

6.3 缺頁(yè)異常

7.1 內(nèi)核空間

7.2 用戶空間

8.1 總體統(tǒng)計(jì)

8.2 進(jìn)程統(tǒng)計(jì)

一、內(nèi)存管理概覽

內(nèi)存是計(jì)算機(jī)最重要的資源之一,內(nèi)存管理是操作系統(tǒng)最重要的任務(wù)之一。內(nèi)存管理并不是簡(jiǎn)單地管理一下內(nèi)存而已,它還直接影響著操作系統(tǒng)的風(fēng)格以及用戶空間編程的模式。可以說內(nèi)存管理的方式是一個(gè)系統(tǒng)刻入DNA的秉性。既然內(nèi)存管理那么重要,那么今天我們就來全面系統(tǒng)地講一講Linux內(nèi)存管理。

1.1 內(nèi)存管理的意義

外存是程序存儲(chǔ)的地方,內(nèi)存是進(jìn)程運(yùn)行的地方。外存相當(dāng)于是軍營(yíng),內(nèi)存相當(dāng)于是戰(zhàn)場(chǎng)。選擇一個(gè)良好的戰(zhàn)場(chǎng)才有利于軍隊(duì)打勝仗,實(shí)現(xiàn)一個(gè)完善的內(nèi)存管理機(jī)制才能讓進(jìn)程多快好省地運(yùn)行。如何更好地實(shí)現(xiàn)內(nèi)存管理一直是操作系統(tǒng)發(fā)展的一大主題。在此過程中內(nèi)存管理的基本模式也經(jīng)歷了好幾代的發(fā)展,下面我們就來看一下。

1.2 原始內(nèi)存管理

最初的時(shí)候,內(nèi)存管理是十分的簡(jiǎn)陋,大家都運(yùn)行在物理內(nèi)存上,內(nèi)核和進(jìn)程運(yùn)行在一個(gè)空間中,內(nèi)存分配算法有首次適應(yīng)算法(FirstFit)、最佳適應(yīng)算法(BestFit)、最差適應(yīng)算法(WorstFit)等。顯然,這樣的內(nèi)存管理方式問題是很明顯的。內(nèi)核與進(jìn)程之間沒有做隔離,進(jìn)程可以隨意訪問(干擾、竊取)內(nèi)核的數(shù)據(jù)。而且進(jìn)程和內(nèi)核沒有權(quán)限的區(qū)分,進(jìn)程可以隨意做一些敏感操作。還有一個(gè)問題就是當(dāng)時(shí)的物理內(nèi)存非常少,能同時(shí)運(yùn)行的進(jìn)程比較少,運(yùn)行進(jìn)程的吞吐量比較少。

1.3 分段內(nèi)存管理

于是第二代內(nèi)存管理方式,分段內(nèi)存管理誕生了。分段內(nèi)存管理需要硬件的支持和軟件的配合。在分段內(nèi)存中,軟件可以把物理內(nèi)存分成一個(gè)一個(gè)的段,每個(gè)段都有段基址和段限長(zhǎng),還有段類型和段權(quán)限。段基址和段限長(zhǎng)確定一個(gè)段的范圍,可以防止內(nèi)存訪問越界。段與段之間也可以互相訪問,但是不能隨便訪問,有一定的規(guī)則限制。段類型分為代碼段和數(shù)據(jù)段,正好對(duì)應(yīng)程序的代碼和數(shù)據(jù),代碼段是只讀和可執(zhí)行的,數(shù)據(jù)段有只讀數(shù)據(jù)段和讀寫數(shù)據(jù)段。代碼段是不可寫的,只讀數(shù)據(jù)段也是不可寫,數(shù)據(jù)段是不可執(zhí)行的,這樣又增加了一層安全性。段權(quán)限分為有特權(quán)(內(nèi)核權(quán)限)和無特權(quán)(用戶權(quán)限),內(nèi)核的代碼段和數(shù)據(jù)段都設(shè)置為特權(quán)段,進(jìn)程的代碼段和數(shù)據(jù)段都設(shè)置為用戶段,這樣進(jìn)程就不能隨意訪問內(nèi)核了。當(dāng)CPU執(zhí)行特權(quán)段代碼的時(shí)候會(huì)把自己設(shè)置為特權(quán)模式,此時(shí)CPU可以執(zhí)行所以的指令。當(dāng)CPU執(zhí)行用戶段代碼的時(shí)候會(huì)把自己設(shè)置為用戶模式,此時(shí)CPU只能執(zhí)行普通指令,不能執(zhí)行敏感指令。

至此,分段內(nèi)存管理完美解決了原始內(nèi)存管理存在的大部分問題:進(jìn)程與內(nèi)核之間的隔離實(shí)現(xiàn)了,進(jìn)程不能隨意訪問內(nèi)核了;CPU特權(quán)級(jí)實(shí)現(xiàn)了,進(jìn)程無法再執(zhí)行敏感指令了;內(nèi)存訪問的安全性提高了,越界訪問和野指針問題得到了一定程度的遏制。但是分段內(nèi)存管理還有一個(gè)嚴(yán)重的問題沒有解決,那就是當(dāng)時(shí)的物理內(nèi)存非常少的問題。為此當(dāng)時(shí)想的辦法是用軟件方法來解決,而且是進(jìn)程自己解決。程序員在編寫程序的時(shí)候就要想好,把程序分成幾個(gè)模塊,關(guān)聯(lián)不大的模塊,它們占用相同的物理地址。然后再編寫一個(gè)overlay manager,在程序運(yùn)行的時(shí)候,動(dòng)態(tài)地加載即將會(huì)運(yùn)行的模塊,覆蓋掉暫時(shí)不用的模塊。這樣一個(gè)程序占用較少的物理內(nèi)存,也能順利地運(yùn)行下去。顯然這樣的方法很麻煩,每個(gè)程序都要寫overlay manager也不太優(yōu)雅。

1.4 分頁(yè)內(nèi)存管理

于是第三代內(nèi)存管理方式,虛擬內(nèi)存管理(分頁(yè)內(nèi)存管理)誕生了。虛擬內(nèi)存管理也是需要硬件的支持和軟件的配合。在虛擬內(nèi)存中,CPU訪問任何內(nèi)存都是通過虛擬內(nèi)存地址來訪問的,但是實(shí)際上最終訪問內(nèi)存還是得用物理內(nèi)存地址。所以在CPU中存在一個(gè)MMU,負(fù)責(zé)把虛擬地址轉(zhuǎn)化為物理地址,然后再去訪問內(nèi)存。而MMU把虛擬地址轉(zhuǎn)化為物理的過程需要頁(yè)表的支持,頁(yè)表是由內(nèi)核負(fù)責(zé)創(chuàng)建和維護(hù)的。一套頁(yè)表可以用來表達(dá)一個(gè)虛擬內(nèi)存空間,不同的進(jìn)程可以用不同的頁(yè)表集,頁(yè)表集是可以不停地切換的,哪個(gè)進(jìn)程正在運(yùn)行就切換到哪個(gè)進(jìn)程的頁(yè)表集。于是一個(gè)進(jìn)程就只能訪問自己的虛擬內(nèi)存空間,而訪問不了別人的虛擬內(nèi)存空間,這樣就實(shí)現(xiàn)了進(jìn)程之間的隔離。一個(gè)虛擬內(nèi)存空間又分為兩部分,內(nèi)核空間和用戶空間,內(nèi)核空間只有一個(gè),用戶空間有N個(gè),所有的虛擬內(nèi)存空間都共享同一個(gè)內(nèi)核空間。內(nèi)核運(yùn)行在內(nèi)核空間,進(jìn)程運(yùn)行在用戶空間,內(nèi)核空間有特權(quán),用戶空間無特權(quán),用戶空間不能隨意訪問內(nèi)核空間。這樣進(jìn)程和內(nèi)核之間的隔離就形成了。內(nèi)核空間的代碼運(yùn)行的時(shí)候,CPU會(huì)把自己設(shè)置為特權(quán)模式,可以執(zhí)行所有的指令。用戶空間運(yùn)行的時(shí)候,CPU會(huì)把自己設(shè)置為用戶模式,只能執(zhí)行普通指令,不能執(zhí)行敏感指令。

至此,分段內(nèi)存實(shí)現(xiàn)的功能,虛擬內(nèi)存都做到了,下面就是虛擬內(nèi)存如何解決物理內(nèi)存不足的問題了。系統(tǒng)剛啟動(dòng)的時(shí)候還是運(yùn)行在物理內(nèi)存上的,內(nèi)核也被全部加載到了物理內(nèi)存。然后內(nèi)核建立頁(yè)表體系并開啟分頁(yè)機(jī)制,內(nèi)核的物理內(nèi)存和虛擬內(nèi)存就建立映射了,整個(gè)系統(tǒng)就運(yùn)行在虛擬內(nèi)存上了。后面運(yùn)行進(jìn)程的時(shí)候就不是這樣了,內(nèi)核會(huì)記錄進(jìn)程的虛擬內(nèi)存分配情況,但是并不會(huì)馬上分配物理內(nèi)存建立頁(yè)表映射,而是讓進(jìn)程先運(yùn)行著。進(jìn)程運(yùn)行的時(shí)候,CPU都是通過MMU訪問虛擬內(nèi)存地址的,MMU會(huì)用頁(yè)表去解析虛擬內(nèi)存,如果找到了其對(duì)應(yīng)的物理地址就直接訪問,如果頁(yè)表項(xiàng)是空的,就會(huì)觸發(fā)缺頁(yè)異常,在缺頁(yè)異常中會(huì)去分配物理內(nèi)存并建立頁(yè)表映射。然后再重新執(zhí)行剛才的那條指令,然后CPU還是通過MMU訪問內(nèi)存,由于頁(yè)表建立好了,這下就可以訪問到物理內(nèi)存了。當(dāng)物理內(nèi)存不足的時(shí)候,內(nèi)核還會(huì)把一部分物理內(nèi)存解除映射,把其內(nèi)容存放到外存中,等其再次需要的時(shí)候再加載回來。這樣,一個(gè)進(jìn)程運(yùn)行的時(shí)候并不需要立馬加載其全部?jī)?nèi)容到物理內(nèi)存,進(jìn)程只需要少量的物理內(nèi)存就能順利地運(yùn)行,于是系統(tǒng)運(yùn)行進(jìn)程的吞吐量就大大提高了。

分頁(yè)內(nèi)存管理不僅實(shí)現(xiàn)了分段內(nèi)存管理的功能,還有額外的優(yōu)點(diǎn),于是分段內(nèi)存管理就沒有存在的意義了。但是這里面還有一個(gè)歷史包袱問題。對(duì)于那些比較新的CPU,比如ARM、RISC-V,它們沒有歷史包袱,直接實(shí)現(xiàn)的就是分頁(yè)內(nèi)存管理,根本不存在分段機(jī)制。但是對(duì)于x86就不一樣了,x86是從直接物理內(nèi)存、分段內(nèi)存、分頁(yè)內(nèi)存一步一步走過來的,有著沉重的歷史包袱。在x86 32上,分段機(jī)制和分頁(yè)機(jī)制是并存的,系統(tǒng)可以選擇只使用分段機(jī)制或者兩種機(jī)制都使用。Linux的選擇是使用分頁(yè)機(jī)制,并在邏輯上屏蔽分段機(jī)制,因?yàn)榉侄螜C(jī)制是不能禁用的。邏輯上屏蔽分段機(jī)制的方法是,所有段的段基址都是0,段限長(zhǎng)都是最大值,這樣就相當(dāng)于是不分段了。分段機(jī)制無法禁用的原因是因?yàn)镃PU特權(quán)級(jí)是在分段機(jī)制中實(shí)現(xiàn)的,分頁(yè)機(jī)制沒有單獨(dú)的CPU特權(quán)級(jí)機(jī)制。所以Linux創(chuàng)建了4個(gè)段,__KERNEL_CS、__KERNEL_DS用于內(nèi)核空間,__USER_CS、__USER_DS用于用戶空間,它們?cè)跁?huì)空間切換時(shí)自動(dòng)切換,這樣CPU特權(quán)級(jí)就跟著切換了。對(duì)于x86 64,從硬件上基本屏蔽了分段,因?yàn)橛布?guī)定CS、DS、ES、SS這些段的段基址必須是0,段限長(zhǎng)必須是最大值,軟件設(shè)置其它值也沒用。

因此我們?cè)谶@里要強(qiáng)調(diào)一句,分段機(jī)制早就是歷史了,x86 64已經(jīng)從硬件上屏蔽了分段機(jī)制,Linux早就從軟件上屏蔽了分段機(jī)制。X86 CPU的寄存器CS、DS、ES、FS和內(nèi)核的__KERNEL_CS、__KERNEL_DS、__USER_CS、__USER_DS,已經(jīng)不具有分段的意義了,它們的作用是為了實(shí)現(xiàn)CPU特權(quán)級(jí)的切換。

1.5 內(nèi)存管理的目標(biāo)

內(nèi)存管理的目標(biāo)除了前面所說的進(jìn)程之間的隔離、進(jìn)程與內(nèi)核之間的隔離、減少物理內(nèi)存并發(fā)使用的數(shù)量之外,還有以下幾個(gè)目標(biāo)。

1.減少內(nèi)存碎片,包括外部碎片和內(nèi)部碎片。外部碎片是指還在內(nèi)存分配器中的內(nèi)存,但是由于比較分散,無法滿足用戶大塊連續(xù)內(nèi)存分配的申請(qǐng)。內(nèi)部碎片是指你申請(qǐng)了5個(gè)字節(jié)的內(nèi)存,分配器給你分配了8個(gè)字節(jié)的內(nèi)存,其中3個(gè)字節(jié)的內(nèi)存是內(nèi)部碎片。內(nèi)存管理要盡量同時(shí)減少外部碎片和內(nèi)部碎片。

2.內(nèi)存分配接口要靈活多樣,同時(shí)滿足多種不同的內(nèi)存分配需求。既要滿足大塊連續(xù)內(nèi)存分配的需求,又能滿足小塊零碎內(nèi)存分配的需求。

3.內(nèi)存分配效率要高。內(nèi)存分配要盡量快地完成,比如說你設(shè)計(jì)了一種算法,能完全解決內(nèi)存碎片問題,但是內(nèi)存算法實(shí)現(xiàn)得特別復(fù)雜,每次分配都需要1毫秒的時(shí)間,這就不可取了。

4.提高物理內(nèi)存的利用率。比如及時(shí)回收物理內(nèi)存、對(duì)內(nèi)存進(jìn)行壓縮。

1.6 Linux內(nèi)存管理體系

Linux內(nèi)存管理的整體模式是虛擬內(nèi)存管理(分頁(yè)內(nèi)存管理),并在此基礎(chǔ)上建立了一個(gè)龐大的內(nèi)存管理體系。我們先來看一下總體結(jié)構(gòu)圖。21746c96-15e5-11ed-ba43-dac502259ad0.png整個(gè)體系分為3部分,左邊是物理內(nèi)存,右邊是虛擬內(nèi)存,中間是虛擬內(nèi)存映射(分頁(yè)機(jī)制)。我們先從物理內(nèi)存說起,內(nèi)存管理的基礎(chǔ)還是物理內(nèi)存的管理。

物理內(nèi)存那么大,應(yīng)該怎么管理呢?首先要對(duì)物理內(nèi)存進(jìn)行層級(jí)區(qū)劃,其原理可以類比于我國(guó)的行政區(qū)劃管理。我國(guó)幅員遼闊,國(guó)家直接管理個(gè)人肯定是不行的,我國(guó)采取的是省縣鄉(xiāng)三級(jí)管理體系。把整個(gè)國(guó)家按照一定的規(guī)則和歷史原因分成若干個(gè)省,每個(gè)省由省長(zhǎng)管理。每個(gè)省再分成若干個(gè)縣,每個(gè)縣由縣長(zhǎng)管理。每個(gè)縣再分成若干個(gè)鄉(xiāng),每個(gè)鄉(xiāng)由鄉(xiāng)長(zhǎng)管理,鄉(xiāng)長(zhǎng)直接管理個(gè)人。(注意,類比是理解工具,不是論證工具)。對(duì)應(yīng)的,物理內(nèi)存也是采用類似的三級(jí)區(qū)域劃分的方式來管理的,三個(gè)層級(jí)分別叫做節(jié)點(diǎn)(node)、區(qū)域(zone)、頁(yè)面(page),對(duì)應(yīng)到省、縣、鄉(xiāng)。系統(tǒng)首先把整個(gè)物理內(nèi)存劃分為N個(gè)節(jié)點(diǎn),內(nèi)存節(jié)點(diǎn)只是叫節(jié)點(diǎn),大家不能把它看成一個(gè)點(diǎn),要把它看成是相當(dāng)于一個(gè)省的大區(qū)域。每個(gè)節(jié)點(diǎn)都有一個(gè)節(jié)點(diǎn)描述符,相當(dāng)于是省長(zhǎng)。節(jié)點(diǎn)下面再劃分區(qū)域,每個(gè)區(qū)域都有區(qū)域描述符,相當(dāng)于是縣長(zhǎng)。區(qū)域下面再劃分頁(yè)面,每個(gè)頁(yè)面都有頁(yè)面描述符,相當(dāng)于是鄉(xiāng)長(zhǎng)。頁(yè)面再下面就是字節(jié)了,相當(dāng)于是個(gè)人。

對(duì)物理內(nèi)存建立三級(jí)區(qū)域劃分之后,就可以在其基礎(chǔ)之上建立分配體系了。物理內(nèi)存的分配體系可以類比于一個(gè)公司的銷售體系,有工廠直接進(jìn)行大額銷售,有批發(fā)公司進(jìn)行大量批發(fā),有小賣部進(jìn)行日常零售。物理內(nèi)存的三級(jí)分配體系分別是buddy system、slab allocator和kmalloc。buddy system相當(dāng)于是工廠銷售,slab allocator相當(dāng)于是批發(fā)公司,kmalloc相當(dāng)于是小賣部,分別滿足人們不同規(guī)模的需求。

物理內(nèi)存有分配也有釋放,但是當(dāng)分配速度大于釋放速度的時(shí)候,物理內(nèi)存就會(huì)逐漸變得不夠用了。此時(shí)我們就要進(jìn)行內(nèi)存回收了。內(nèi)存回收首先考慮的是內(nèi)存規(guī)整,也就是內(nèi)存碎片整理,因?yàn)橛锌赡芪覀儾皇强捎脙?nèi)存不足了,而是內(nèi)存太分散了,沒法分配連續(xù)的內(nèi)存。內(nèi)存規(guī)整之后如果還是分配不到內(nèi)存的話,就會(huì)進(jìn)行頁(yè)幀回收。內(nèi)核的物理內(nèi)存是不換頁(yè)的,所以內(nèi)核只會(huì)進(jìn)行緩存回收。用戶空間的物理內(nèi)存是可以換頁(yè)的,所以會(huì)對(duì)用戶空間的物理內(nèi)存進(jìn)行換頁(yè)以便回收其物理內(nèi)存。用戶空間的物理內(nèi)存分為文件頁(yè)和匿名頁(yè)。對(duì)于文件頁(yè),如果其是clean的,可以直接丟棄內(nèi)容,回收其物理內(nèi)存,如果其是dirty的,則會(huì)先把其內(nèi)容寫回到文件,然后再回收內(nèi)存。對(duì)于匿名頁(yè),如果系統(tǒng)配置的有swap區(qū)的話,則會(huì)把其內(nèi)容先寫入swap區(qū),然后再回收,如果系統(tǒng)沒有swap區(qū)的話則不會(huì)進(jìn)行回收。把進(jìn)程占用的但是當(dāng)前并不在使用的物理內(nèi)存進(jìn)行回收,并分配給新的進(jìn)程來使用的過程就叫做換頁(yè)。進(jìn)程被換頁(yè)的物理內(nèi)存后面如果再被使用到的話,還會(huì)通過缺頁(yè)異常再換入內(nèi)存。如果頁(yè)幀回收之后還沒有得到足夠的物理內(nèi)存,內(nèi)核將會(huì)使用最后一招,OOM Killer。OOM Killer會(huì)按照一定的規(guī)則選擇一個(gè)進(jìn)程將其殺死,然后其物理內(nèi)存就被釋放了。

內(nèi)核還有三個(gè)內(nèi)存壓縮技術(shù)zram、zswap、zcache,圖里并沒有畫出來。它們產(chǎn)生的原因并不相同,zram和zswap產(chǎn)生的原因是因?yàn)榘涯涿?yè)寫入swap區(qū)是IO操作,是非常耗時(shí)的,使用zram和zswap可以達(dá)到用空間換時(shí)間的效果。zcache產(chǎn)生的原因是因?yàn)閮?nèi)核一般都有大量的pagecache,pagecache是對(duì)文件的緩存,有些文件緩存暫時(shí)用不到,可以對(duì)它們進(jìn)行壓縮,以節(jié)省內(nèi)存空間,到用的時(shí)候再解壓縮,以達(dá)到用時(shí)間換空間的效果。

物理內(nèi)存的這些操作都是在內(nèi)核里進(jìn)行的,但是CPU訪問內(nèi)存用的并不是物理內(nèi)存地址,而是虛擬內(nèi)存地址。內(nèi)核需要建立頁(yè)表把虛擬內(nèi)存映射到物理內(nèi)存上,然后CPU就可以通過MMU用虛擬地址來訪問物理內(nèi)存了。虛擬內(nèi)存地址空間分為兩部分,內(nèi)核空間和用戶空間。內(nèi)核空間只有一個(gè),其頁(yè)表映射是在內(nèi)核啟動(dòng)的早期就建立的。用戶空間有N個(gè),用戶空間是隨著進(jìn)程的創(chuàng)建而建立的,但是其頁(yè)表映射并不是馬上建立,而是在程序的運(yùn)行過程中通過缺頁(yè)異常逐步建立的。內(nèi)核頁(yè)表建立好了之后就不會(huì)再取消了,所以內(nèi)核是不換頁(yè)的,用戶頁(yè)表建立之后可能會(huì)因?yàn)閮?nèi)存回收而取消,所以用戶空間是換頁(yè)的。內(nèi)核頁(yè)表是在內(nèi)核啟動(dòng)時(shí)建立的,所以內(nèi)核空間的映射是線性映射,用戶空間的頁(yè)表是在運(yùn)行時(shí)動(dòng)態(tài)創(chuàng)建的,不可能做到線性映射,所以是隨機(jī)映射。

有些書上會(huì)說用戶空間是分頁(yè)的,內(nèi)核是不分頁(yè)的,這是對(duì)英語(yǔ)paging的錯(cuò)誤翻譯,paging在這里不是分頁(yè)的意思,而是換頁(yè)的意思。分頁(yè)是指整個(gè)分頁(yè)機(jī)制,換頁(yè)是內(nèi)存回收中的操作,兩者的含義是完全不同的。

現(xiàn)在我們對(duì)Linux內(nèi)存管理體系已經(jīng)有了宏觀上的了解,下面我們就來對(duì)每個(gè)模塊進(jìn)行具體地分析。

二、物理內(nèi)存區(qū)劃

內(nèi)核對(duì)物理內(nèi)存進(jìn)行了三級(jí)區(qū)劃。為什么要進(jìn)行三級(jí)區(qū)劃,具體怎么劃分的呢?這個(gè)不是軟件隨意決定的,而是和硬件因素有關(guān)。下面我們來看一下每一層級(jí)劃分的原因,以及軟件上是如果描述的。

2.1 物理內(nèi)存節(jié)點(diǎn)

我國(guó)的省為什么要按照現(xiàn)在的這個(gè)形狀來劃分呢,主要是依據(jù)山川地形還有民俗風(fēng)情等歷史原因。那么物理內(nèi)存劃分為節(jié)點(diǎn)的原因是什么呢?這就要從UMA、NUMA說起了。我們用三個(gè)圖來看一下。

218c455a-15e5-11ed-ba43-dac502259ad0.png219ee12e-15e5-11ed-ba43-dac502259ad0.png21aee40c-15e5-11ed-ba43-dac502259ad0.png圖中的CPU都是物理CPU。當(dāng)一個(gè)系統(tǒng)中的CPU越來越多、內(nèi)存越來越多的時(shí)候,內(nèi)存總線就會(huì)成為一個(gè)系統(tǒng)的瓶頸。如果大家都還擠在同一個(gè)總線上,速度必然很慢。于是我們可以采取一種方法,把一部分CPU和一部分內(nèi)存直連在一起,構(gòu)成一個(gè)節(jié)點(diǎn),不同節(jié)點(diǎn)之間CPU訪問內(nèi)存采用間接方式。節(jié)點(diǎn)內(nèi)的內(nèi)存訪問速度就會(huì)很快,節(jié)點(diǎn)之間的內(nèi)存訪問速度雖然很慢,但是我們可以盡量減少節(jié)點(diǎn)之間的內(nèi)存訪問,這樣系統(tǒng)總的內(nèi)存訪問速度就會(huì)很快。

Linux中的代碼對(duì)UMA和NUMA是統(tǒng)一處理的,因?yàn)閁MA可以看成是只有一個(gè)節(jié)點(diǎn)的NUMA。如果編譯內(nèi)核時(shí)配置了CONFIG_NUMA,內(nèi)核支持NUMA架構(gòu)的計(jì)算機(jī),內(nèi)核中會(huì)定義節(jié)點(diǎn)指針數(shù)組來表示各個(gè)node。如果編譯內(nèi)核時(shí)沒有配置CONFIG_NUMA,則內(nèi)核只支持UMA架構(gòu)的計(jì)算機(jī),內(nèi)核中會(huì)定義一個(gè)內(nèi)存節(jié)點(diǎn)。這樣所有其它的代碼都可以統(tǒng)一處理了。

下面我們先來看一下節(jié)點(diǎn)描述符的定義。linux-src/include/linux/mmzone.h

typedefstructpglist_data{
/*
*node_zonescontainsjustthezonesforTHISnode.Notallofthe
*zonesmaybepopulated,butitisthefulllist.Itisreferencedby
*thisnode'snode_zonelistsaswellasothernode'snode_zonelists.
*/
structzonenode_zones[MAX_NR_ZONES];

/*
*node_zonelistscontainsreferencestoallzonesinallnodes.
*Generallythefirstzoneswillbereferencestothisnode's
*node_zones.
*/
structzonelistnode_zonelists[MAX_ZONELISTS];

intnr_zones;/*numberofpopulatedzonesinthisnode*/
#ifdefCONFIG_FLATMEM/*means!SPARSEMEM*/
structpage*node_mem_map;
#ifdefCONFIG_PAGE_EXTENSION
structpage_ext*node_page_ext;
#endif
#endif
#ifdefined(CONFIG_MEMORY_HOTPLUG)||defined(CONFIG_DEFERRED_STRUCT_PAGE_INIT)
/*
*Mustbeheldanytimeyouexpectnode_start_pfn,
*node_present_pages,node_spanned_pagesornr_zonestostayconstant.
*Alsosynchronizespgdat->first_deferred_pfnduringdeferredpage
*init.
*
*pgdat_resize_lock()andpgdat_resize_unlock()areprovidedto
*manipulatenode_size_lockwithoutcheckingforCONFIG_MEMORY_HOTPLUG
*orCONFIG_DEFERRED_STRUCT_PAGE_INIT.
*
*Nestsabovezone->lockandzone->span_seqlock
*/
spinlock_tnode_size_lock;
#endif
unsignedlongnode_start_pfn;
unsignedlongnode_present_pages;/*totalnumberofphysicalpages*/
unsignedlongnode_spanned_pages;/*totalsizeofphysicalpage
range,includingholes*/
intnode_id;
wait_queue_head_tkswapd_wait;
wait_queue_head_tpfmemalloc_wait;
structtask_struct*kswapd;/*Protectedby
mem_hotplug_begin/end()*/
intkswapd_order;
enumzone_typekswapd_highest_zoneidx;

intkswapd_failures;/*Numberof'reclaimed==0'runs*/

#ifdefCONFIG_COMPACTION
intkcompactd_max_order;
enumzone_typekcompactd_highest_zoneidx;
wait_queue_head_tkcompactd_wait;
structtask_struct*kcompactd;
boolproactive_compact_trigger;
#endif
/*
*Thisisaper-nodereserveofpagesthatarenotavailable
*touserspaceallocations.
*/
unsignedlongtotalreserve_pages;

#ifdefCONFIG_NUMA
/*
*nodereclaimbecomesactiveifmoreunmappedpagesexist.
*/
unsignedlongmin_unmapped_pages;
unsignedlongmin_slab_pages;
#endif/*CONFIG_NUMA*/

/*Write-intensivefieldsusedbypagereclaim*/
ZONE_PADDING(_pad1_)

#ifdefCONFIG_DEFERRED_STRUCT_PAGE_INIT
/*
*Ifmemoryinitialisationonlargemachinesisdeferredthenthis
*isthefirstPFNthatneedstobeinitialised.
*/
unsignedlongfirst_deferred_pfn;
#endif/*CONFIG_DEFERRED_STRUCT_PAGE_INIT*/

#ifdefCONFIG_TRANSPARENT_HUGEPAGE
structdeferred_splitdeferred_split_queue;
#endif

/*Fieldscommonlyaccessedbythepagereclaimscanner*/

/*
*NOTE:THISISUNUSEDIFMEMCGISENABLED.
*
*Usemem_cgroup_lruvec()tolookuplruvecs.
*/
structlruvec__lruvec;

unsignedlongflags;

ZONE_PADDING(_pad2_)

/*Per-nodevmstats*/
structper_cpu_nodestat__percpu*per_cpu_nodestats;
atomic_long_tvm_stat[NR_VM_NODE_STAT_ITEMS];
}pg_data_t;

對(duì)于UMA,內(nèi)核會(huì)定義唯一的一個(gè)節(jié)點(diǎn)。linux-src/mm/memblock.c

#ifndefCONFIG_NUMA
structpglist_data__refdatacontig_page_data;
EXPORT_SYMBOL(contig_page_data);
#endif

查找內(nèi)存節(jié)點(diǎn)的代碼如下:linux-src/include/linux/mmzone.h

externstructpglist_datacontig_page_data;
staticinlinestructpglist_data*NODE_DATA(intnid)
{
return&contig_page_data;
}

對(duì)于NUMA,內(nèi)核會(huì)定義內(nèi)存節(jié)點(diǎn)指針數(shù)組,不同架構(gòu)定義的不一定相同,我們以x86為例。linux-src/arch/x86/mm/numa.c

structpglist_data*node_data[MAX_NUMNODES]__read_mostly;
EXPORT_SYMBOL(node_data);

查找內(nèi)存節(jié)點(diǎn)的代碼如下:linux-src/arch/x86/include/asm/mmzone_64.h

externstructpglist_data*node_data[];
#defineNODE_DATA(nid)(node_data[nid])

可以看出對(duì)于UMA,Linux是統(tǒng)一定義一個(gè)內(nèi)存節(jié)點(diǎn)的,對(duì)于NUMA,Linux是在各架構(gòu)代碼下定義內(nèi)存節(jié)點(diǎn)的。由于我們常見的電腦手機(jī)都是UMA的,后面的我們都以UMA為例進(jìn)行講解。pglist_data各自字段的含義我們?cè)谟玫綍r(shí)再進(jìn)行分析。

2.2 物理內(nèi)存區(qū)域

內(nèi)存節(jié)點(diǎn)下面再劃分為不同的區(qū)域。劃分區(qū)域的原因是什么呢?主要是因?yàn)楦鞣N軟硬件的限制導(dǎo)致的。目前Linux中最多可以有6個(gè)區(qū)域,這些區(qū)域并不是每個(gè)都必然存在,有的是由config控制的。有些區(qū)域就算代碼中配置了,但是在系統(tǒng)運(yùn)行的時(shí)候也可能為空。下面我們依次介紹一下這6個(gè)區(qū)域。

ZONE_DMA由配置項(xiàng)CONFIG_ZONE_DMA決定是否存在。在x86上DMA內(nèi)存區(qū)域是物理內(nèi)存的前16M,這是因?yàn)樵缙诘腎SA總線上的DMA控制器只有24根地址總線,只能訪問16M物理內(nèi)存。為了兼容這些老的設(shè)備,所以需要專門開辟前16M物理內(nèi)存作為一個(gè)區(qū)域供這些設(shè)備進(jìn)行DMA操作時(shí)去分配物理內(nèi)存。

ZONE_DMA32:由配置項(xiàng)CONFIG_ZONE_DMA32決定是否存在。后來的DMA控制器有32根地址總線,可以訪問4G物理內(nèi)存了。但是在32位的系統(tǒng)上最多只支持4G物理內(nèi)存,所以沒必要專門劃分一個(gè)區(qū)域。但是到了64位系統(tǒng)時(shí)候,很多CPU能支持48位到52位的物理內(nèi)存,于是此時(shí)就有必要專門開個(gè)區(qū)域給32位的DMA控制器使用了。

ZONE_NORMAL:常規(guī)內(nèi)存,無配置項(xiàng)控制,必然存在,除了其它幾個(gè)內(nèi)存區(qū)域之外的內(nèi)存都是常規(guī)內(nèi)存ZONE_NORMAL。

ZONE_HIGHMEM:高端內(nèi)存,由配置項(xiàng)CONFIG_HIGHMEM決定是否存在。只在32位系統(tǒng)上有,這是因?yàn)?2位系統(tǒng)的內(nèi)核空間只有1G,這1G虛擬空間中還有128M用于其它用途,所以只有896M虛擬內(nèi)存空間用于直接映射物理內(nèi)存,而32位系統(tǒng)支持的物理內(nèi)存有4G,大于896M的物理內(nèi)存是無法直接映射到內(nèi)核空間的,所以把它們劃為高端內(nèi)存進(jìn)行特殊處理。對(duì)于64位系統(tǒng),從理論上來說,內(nèi)核空間最大263-1,物理內(nèi)存最大264,好像內(nèi)核空間還是不夠用。但是從現(xiàn)實(shí)來說,內(nèi)核空間的一般配置為247,高達(dá)128T,物理內(nèi)存暫時(shí)還遠(yuǎn)遠(yuǎn)沒有這么多。所以從現(xiàn)實(shí)的角度來說,64位系統(tǒng)是不需要高端內(nèi)存區(qū)域的。

ZONE_MOVABLE:可移動(dòng)內(nèi)存,無配置項(xiàng)控制,必然存在,用于可熱插拔的內(nèi)存。內(nèi)核啟動(dòng)參數(shù)movablecore用于指定此區(qū)域的大小。內(nèi)核參數(shù)kernelcore也可用于指定非可移動(dòng)內(nèi)存的大小,剩余的內(nèi)存都是可移動(dòng)內(nèi)存。如果兩者同時(shí)指定的話,則會(huì)優(yōu)先保證非可移動(dòng)內(nèi)存的大小至少有kernelcore這么大。如果兩者都沒指定,則可移動(dòng)內(nèi)存大小為0。

ZONE_DEVICE:設(shè)備內(nèi)存,由配置項(xiàng)CONFIG_ZONE_DEVICE決定是否存在,用于放置持久內(nèi)存(也就是掉電后內(nèi)容不會(huì)消失的內(nèi)存)。一般的計(jì)算機(jī)中沒有這種內(nèi)存,默認(rèn)的內(nèi)存分配也不會(huì)從這里分配內(nèi)存。持久內(nèi)存可用于內(nèi)核崩潰時(shí)保存相關(guān)的調(diào)試信息。

下面我們先來看一下這幾個(gè)內(nèi)存區(qū)域的類型定義。linux-src/include/linux/mmzone.h

enumzone_type{
#ifdefCONFIG_ZONE_DMA
ZONE_DMA,
#endif
#ifdefCONFIG_ZONE_DMA32
ZONE_DMA32,
#endif
ZONE_NORMAL,
#ifdefCONFIG_HIGHMEM
ZONE_HIGHMEM,
#endif
ZONE_MOVABLE,
#ifdefCONFIG_ZONE_DEVICE
ZONE_DEVICE,
#endif
__MAX_NR_ZONES
};

我們?cè)賮砜匆幌聟^(qū)域描述符的定義。linux-src/include/linux/mmzone.h

structzone{
/*Read-mostlyfields*/

/*zonewatermarks,accesswith*_wmark_pages(zone)macros*/
unsignedlong_watermark[NR_WMARK];
unsignedlongwatermark_boost;

unsignedlongnr_reserved_highatomic;

/*
*Wedon'tknowifthememorythatwe'regoingtoallocatewillbe
*freeableor/anditwillbereleasedeventually,sotoavoidtotally
*wastingseveralGBoframwemustreservesomeofthelowerzone
*memory(otherwisewerisktorunOOMonthelowerzonesdespite
*therebeingtonsoffreeableramonthehigherzones).Thisarrayis
*recalculatedatruntimeifthesysctl_lowmem_reserve_ratiosysctl
*changes.
*/
longlowmem_reserve[MAX_NR_ZONES];

#ifdefCONFIG_NUMA
intnode;
#endif
structpglist_data*zone_pgdat;
structper_cpu_pages__percpu*per_cpu_pageset;
structper_cpu_zonestat__percpu*per_cpu_zonestats;
/*
*thehighandbatchvaluesarecopiedtoindividualpagesetsfor
*fasteraccess
*/
intpageset_high;
intpageset_batch;

#ifndefCONFIG_SPARSEMEM
/*
*Flagsforapageblock_nr_pagesblock.Seepageblock-flags.h.
*InSPARSEMEM,thismapisstoredinstructmem_section
*/
unsignedlong*pageblock_flags;
#endif/*CONFIG_SPARSEMEM*/

/*zone_start_pfn==zone_start_paddr>>PAGE_SHIFT*/
unsignedlongzone_start_pfn;

atomic_long_tmanaged_pages;
unsignedlongspanned_pages;
unsignedlongpresent_pages;
#ifdefined(CONFIG_MEMORY_HOTPLUG)
unsignedlongpresent_early_pages;
#endif
#ifdefCONFIG_CMA
unsignedlongcma_pages;
#endif

constchar*name;

#ifdefCONFIG_MEMORY_ISOLATION
/*
*Numberofisolatedpageblock.Itisusedtosolveincorrect
*freepagecountingproblemduetoracyretrievingmigratetype
*ofpageblock.Protectedbyzone->lock.
*/
unsignedlongnr_isolate_pageblock;
#endif

#ifdefCONFIG_MEMORY_HOTPLUG
/*seespanned/present_pagesformoredescription*/
seqlock_tspan_seqlock;
#endif

intinitialized;

/*Write-intensivefieldsusedfromthepageallocator*/
ZONE_PADDING(_pad1_)

/*freeareasofdifferentsizes*/
structfree_areafree_area[MAX_ORDER];

/*zoneflags,seebelow*/
unsignedlongflags;

/*Primarilyprotectsfree_area*/
spinlock_tlock;

/*Write-intensivefieldsusedbycompactionandvmstats.*/
ZONE_PADDING(_pad2_)

/*
*Whenfreepagesarebelowthispoint,additionalstepsaretaken
*whenreadingthenumberoffreepagestoavoidper-cpucounter
*driftallowingwatermarkstobebreached
*/
unsignedlongpercpu_drift_mark;

#ifdefinedCONFIG_COMPACTION||definedCONFIG_CMA
/*pfnwherecompactionfreescannershouldstart*/
unsignedlongcompact_cached_free_pfn;
/*pfnwherecompactionmigrationscannershouldstart*/
unsignedlongcompact_cached_migrate_pfn[ASYNC_AND_SYNC];
unsignedlongcompact_init_migrate_pfn;
unsignedlongcompact_init_free_pfn;
#endif

#ifdefCONFIG_COMPACTION
/*
*Oncompactionfailure,1<

Zone結(jié)構(gòu)體中各個(gè)字段的含義我們?cè)谟玫降臅r(shí)候再進(jìn)行解釋。

2.3 物理內(nèi)存頁(yè)面

每個(gè)內(nèi)存區(qū)域下面再劃分為若干個(gè)面積比較小但是又不太小的頁(yè)面。頁(yè)面的大小一般都是4K,這是由硬件規(guī)定的。內(nèi)存節(jié)點(diǎn)和內(nèi)存區(qū)域從邏輯上來說并不是非得有,只不過是由于各種硬件限制或者特殊需求才有的。內(nèi)存頁(yè)面倒不是因?yàn)橛布拗撇庞械?,主要是出于邏輯原因才有的。?yè)面是分頁(yè)內(nèi)存機(jī)制和底層內(nèi)存分配的最小單元。如果沒有頁(yè)面的話,直接以字節(jié)為單位進(jìn)行管理顯然太麻煩了,所以需要有一個(gè)較小的基本單位,這個(gè)單位就叫做頁(yè)面。頁(yè)面的大小選多少合適呢?太大了不好,太小了也不好,這個(gè)數(shù)值還得是2的整數(shù)次冪,所以4K就非常合適。為啥是2的整數(shù)次冪呢?因?yàn)橛?jì)算機(jī)是用二進(jìn)制實(shí)現(xiàn)的,2的整數(shù)次冪做各種運(yùn)算和特殊處理比較方便,后面用到的時(shí)候就能體會(huì)到。為啥是4K呢?因?yàn)樽钤?a target="_blank">Intel選擇的就是4K,后面大部分CPU也都跟著選4K作為頁(yè)面的大小了。

物理內(nèi)存頁(yè)面也叫做頁(yè)幀。物理內(nèi)存從開始起每4K、4K的,構(gòu)成一個(gè)個(gè)頁(yè)幀,這些頁(yè)幀的編號(hào)依次是0、1、2、3......。頁(yè)幀的編號(hào)也叫做pfn(page frame number)。很顯然,一個(gè)頁(yè)幀的物理地址和它的pfn有一個(gè)簡(jiǎn)單的數(shù)學(xué)關(guān)系,那就是其物理地址除以4K就是其pfn,其pfn乘以4K就是其物理地址。由于4K是2的整數(shù)次冪,所以這個(gè)乘除運(yùn)算可以轉(zhuǎn)化為移位運(yùn)算。下面我們看一下相關(guān)的宏操作。

linux-src/include/linux/pfn.h

#definePFN_ALIGN(x)(((unsignedlong)(x)+(PAGE_SIZE-1))&PAGE_MASK)
#definePFN_UP(x)(((x)+PAGE_SIZE-1)>>PAGE_SHIFT)
#definePFN_DOWN(x)((x)>>PAGE_SHIFT)
#definePFN_PHYS(x)((phys_addr_t)(x)<>PAGE_SHIFT))

PAGE_SHIFT的值在大部分平臺(tái)上都是等于12,2的12次方冪正好就是4K。

下面我們來看一下頁(yè)面描述符的定義。linux-src/include/linux/mm_types.h

structpage{
unsignedlongflags;/*Atomicflags,somepossibly
*updatedasynchronously*/
/*
*Fivewords(20/40bytes)areavailableinthisunion.
*WARNING:bit0ofthefirstwordisusedforPageTail().That
*meanstheotherusersofthisunionMUSTNOTusethebitto
*avoidcollisionandfalse-positivePageTail().
*/
union{
struct{/*Pagecacheandanonymouspages*/
/**
*@lru:Pageoutlist,eg.active_listprotectedby
*lruvec->lru_lock.Sometimesusedasagenericlist
*bythepageowner.
*/
structlist_headlru;
/*Seepage-flags.hforPAGE_MAPPING_FLAGS*/
structaddress_space*mapping;
pgoff_tindex;/*Ouroffsetwithinmapping.*/
/**
*@private:Mapping-privateopaquedata.
*Usuallyusedforbuffer_headsifPagePrivate.
*Usedforswp_entry_tifPageSwapCache.
*IndicatesorderinthebuddysystemifPageBuddy.
*/
unsignedlongprivate;
};
struct{/*page_poolusedbynetstack*/
/**
*@pp_magic:magicvaluetoavoidrecyclingnon
*page_poolallocatedpages.
*/
unsignedlongpp_magic;
structpage_pool*pp;
unsignedlong_pp_mapping_pad;
unsignedlongdma_addr;
union{
/**
*dma_addr_upper:mightrequirea64-bit
*valueon32-bitarchitectures.
*/
unsignedlongdma_addr_upper;
/**
*Forfragpagesupport,notsupportedin
*32-bitarchitectureswith64-bitDMA.
*/
atomic_long_tpp_frag_count;
};
};
struct{/*slab,slobandslub*/
union{
structlist_headslab_list;
struct{/*Partialpages*/
structpage*next;
#ifdefCONFIG_64BIT
intpages;/*Nrofpagesleft*/
intpobjects;/*Approximatecount*/
#else
shortintpages;
shortintpobjects;
#endif
};
};
structkmem_cache*slab_cache;/*notslob*/
/*Double-wordboundary*/
void*freelist;/*firstfreeobject*/
union{
void*s_mem;/*slab:firstobject*/
unsignedlongcounters;/*SLUB*/
struct{/*SLUB*/
unsignedinuse:16;
unsignedobjects:15;
unsignedfrozen:1;
};
};
};
struct{/*Tailpagesofcompoundpage*/
unsignedlongcompound_head;/*Bitzeroisset*/

/*Firsttailpageonly*/
unsignedcharcompound_dtor;
unsignedcharcompound_order;
atomic_tcompound_mapcount;
unsignedintcompound_nr;/*1<ptl*/
unsignedlong_pt_pad_2;/*mapping*/
union{
structmm_struct*pt_mm;/*x86pgdsonly*/
atomic_tpt_frag_refcount;/*powerpc*/
};
#ifALLOC_SPLIT_PTLOCKS
spinlock_t*ptl;
#else
spinlock_tptl;
#endif
};
struct{/*ZONE_DEVICEpages*/
/**@pgmap:Pointstothehostingdevicepagemap.*/
structdev_pagemap*pgmap;
void*zone_device_data;
/*
*ZONE_DEVICEprivatepagesarecountedasbeing
*mappedsothenext3wordsholdthemapping,index,
*andprivatefieldsfromthesourceanonymousor
*pagecachepagewhilethepageismigratedtodevice
*privatememory.
*ZONE_DEVICEMEMORY_DEVICE_FS_DAXpagesalso
*usethemapping,index,andprivatefieldswhen
*pmembackedDAXfilesaremapped.
*/
};

/**@rcu_head:YoucanusethistofreeapagebyRCU.*/
structrcu_headrcu_head;
};

union{/*Thisunionis4bytesinsize.*/
/*
*Ifthepagecanbemappedtouserspace,encodesthenumber
*oftimesthispageisreferencedbyapagetable.
*/
atomic_t_mapcount;

/*
*IfthepageisneitherPageSlabnormappabletouserspace,
*thevaluestoredheremayhelpdeterminewhatthispage
*isusedfor.Seepage-flags.hforalistofpagetypes
*whicharecurrentlystoredhere.
*/
unsignedintpage_type;

unsignedintactive;/*SLAB*/
intunits;/*SLOB*/
};

/*Usagecount.*DONOTUSEDIRECTLY*.Seepage_ref.h*/
atomic_t_refcount;

#ifdefCONFIG_MEMCG
unsignedlongmemcg_data;
#endif

/*
*OnmachineswhereallRAMismappedintokerneladdressspace,
*wecansimplycalculatethevirtualaddress.Onmachineswith
*highmemsomememoryismappedintokernelvirtualmemory
*dynamically,soweneedaplacetostorethataddress.
*Notethatthisfieldcouldbe16bitsonx86...;)
*
*Architectureswithslowmultiplicationcandefine
*WANT_PAGE_VIRTUALinasm/page.h
*/
#ifdefined(WANT_PAGE_VIRTUAL)
void*virtual;/*Kernelvirtualaddress(NULLif
notkmapped,ie.highmem)*/
#endif/*WANT_PAGE_VIRTUAL*/

#ifdefLAST_CPUPID_NOT_IN_PAGE_FLAGS
int_last_cpupid;
#endif
}_struct_page_alignment;

可以看到頁(yè)面描述符的定義非常復(fù)雜,各種共用體套共用體。為什么這么復(fù)雜呢?這是因?yàn)槲锢韮?nèi)存的每個(gè)頁(yè)幀都需要有一個(gè)頁(yè)面描述符。對(duì)于4G的物理內(nèi)存來說,需要有4G/4K=1M也就是100多萬個(gè)頁(yè)面描述符。所以竭盡全力地減少頁(yè)面描述符的大小是非常必要的。又由于頁(yè)面描述符記錄的很多數(shù)據(jù)不都是同時(shí)在使用的,所以可以使用共用體來減少頁(yè)面描述符的大小。頁(yè)面描述符中各個(gè)字段的含義,我們?cè)谟玫降臅r(shí)候再進(jìn)行解釋。

2.4 物理內(nèi)存模型

計(jì)算機(jī)中有很多名稱叫做內(nèi)存模型的概念,它們的含義并不相同,大家要注意區(qū)分。此處講的內(nèi)存模型是Linux對(duì)物理內(nèi)存地址空間連續(xù)性的抽象,用來表示物理內(nèi)存的地址空間是否有空洞以及該如何處理空洞,因此這個(gè)概念也被叫做內(nèi)存連續(xù)性模型。由于內(nèi)存熱插拔也會(huì)導(dǎo)致物理內(nèi)存地址空間產(chǎn)生空洞,因此Linux內(nèi)存模型也是內(nèi)存熱插拔的基礎(chǔ)。

最開始的時(shí)候是沒有內(nèi)存模型的,后來有了其它的內(nèi)存模型,這個(gè)最開始的情況就被叫做平坦內(nèi)存模型(Flat Memory)。平坦內(nèi)存模型看到的物理內(nèi)存就是連續(xù)的沒有空洞的內(nèi)存。后來為了處理物理內(nèi)存有空洞的情況以及內(nèi)存熱插拔問題,又開發(fā)出了離散內(nèi)存模型(Discontiguous Memory)。但是離散內(nèi)存模型的實(shí)現(xiàn)復(fù)用了NUMA的代碼,導(dǎo)致NUMA和內(nèi)存模型的耦合,實(shí)際上二者在邏輯上是正交的。內(nèi)核后來又開發(fā)了稀疏內(nèi)存模型(Sparse Memory),其實(shí)現(xiàn)和NUMA不再耦合在一起了,而且稀疏內(nèi)存模型能同時(shí)處理平坦內(nèi)存、稀疏內(nèi)存、極度稀疏內(nèi)存,還能很好地支持內(nèi)存熱插拔。于是離散內(nèi)存模型就先被棄用了,后又被移出了內(nèi)核?,F(xiàn)在內(nèi)核中就只有平坦內(nèi)存模型和稀疏內(nèi)存模型了。而且在很多架構(gòu)中,如x86、ARM64,稀疏內(nèi)存模型已經(jīng)變成了唯一的可選項(xiàng)了,也就是必選內(nèi)存模型。

系統(tǒng)有一個(gè)頁(yè)面描述符的數(shù)組,用來描述系統(tǒng)中的所有頁(yè)幀。這個(gè)數(shù)組是在系統(tǒng)啟動(dòng)時(shí)創(chuàng)建的,然后有一個(gè)全局的指針變量會(huì)指向這個(gè)數(shù)組。這個(gè)變量的名字在平坦內(nèi)存中叫做mem_map,是全分配的,在稀疏內(nèi)存中叫做vmemmap,內(nèi)存空洞對(duì)應(yīng)的頁(yè)表描述符是不被映射的。學(xué)過C語(yǔ)言的人都知道指針與數(shù)組之間的關(guān)系,指針之間的減法以及指針與整數(shù)之間的加法與數(shù)組下標(biāo)的關(guān)系。因此我們可以把頁(yè)面描述符指針和頁(yè)幀號(hào)相互轉(zhuǎn)換。

我們來看一下頁(yè)面描述符數(shù)組指針的定義和指針與頁(yè)幀號(hào)之間的轉(zhuǎn)換操作。linux-src/mm/memory.c

#ifndefCONFIG_NUMA
structpage*mem_map;
EXPORT_SYMBOL(mem_map);
#endif

linux-src/arch/x86/include/asm/pgtable_64.h

#definevmemmap((structpage*)VMEMMAP_START)

linux-src/arch/x86/include/asm/pgtable_64_types.h

#ifdefCONFIG_DYNAMIC_MEMORY_LAYOUT
#defineVMEMMAP_STARTvmemmap_base
#else
#defineVMEMMAP_START__VMEMMAP_BASE_L4
#endif/*CONFIG_DYNAMIC_MEMORY_LAYOUT*/

linux-src/include/asm-generic/memory_model.h

#ifdefined(CONFIG_FLATMEM)

#ifndefARCH_PFN_OFFSET
#defineARCH_PFN_OFFSET(0UL)
#endif

#define__pfn_to_page(pfn)(mem_map+((pfn)-ARCH_PFN_OFFSET))
#define__page_to_pfn(page)((unsignedlong)((page)-mem_map)+
ARCH_PFN_OFFSET)

#elifdefined(CONFIG_SPARSEMEM_VMEMMAP)

/*memmapisvirtuallycontiguous.*/
#define__pfn_to_page(pfn)(vmemmap+(pfn))
#define__page_to_pfn(page)(unsignedlong)((page)-vmemmap)

#elifdefined(CONFIG_SPARSEMEM)
/*
*Note:section'smem_mapisencodedtoreflectitsstart_pfn.
*section[i].section_mem_map==mem_map'saddress-start_pfn;
*/
#define__page_to_pfn(pg)
({conststructpage*__pg=(pg);
int__sec=page_to_section(__pg);
(unsignedlong)(__pg-__section_mem_map_addr(__nr_to_section(__sec)));
})

#define__pfn_to_page(pfn)
({unsignedlong__pfn=(pfn);
structmem_section*__sec=__pfn_to_section(__pfn);
__section_mem_map_addr(__sec)+__pfn;
})
#endif/*CONFIG_FLATMEM/SPARSEMEM*/

/*
*ConvertaphysicaladdresstoaPageFrameNumberandback
*/
#define__phys_to_pfn(paddr)PHYS_PFN(paddr)
#define__pfn_to_phys(pfn)PFN_PHYS(pfn)

#definepage_to_pfn__page_to_pfn
#definepfn_to_page__pfn_to_page

2.5 三級(jí)區(qū)劃關(guān)系

我們對(duì)物理內(nèi)存的三級(jí)區(qū)劃有了簡(jiǎn)單的了解,下面我們?cè)賹?duì)它們之間的關(guān)系進(jìn)行更進(jìn)一步地分析。雖然在節(jié)點(diǎn)描述符中包含了所有的區(qū)域類型,但是除了第一個(gè)節(jié)點(diǎn)能包含所有的區(qū)域類型之外,其它的節(jié)點(diǎn)并不能包含所有的區(qū)域類型,因?yàn)橛行﹨^(qū)域類型(DMA、DMA32)必須從物理內(nèi)存的起點(diǎn)開始。Normal、HighMem和Movable是可以出現(xiàn)在所有的節(jié)點(diǎn)上的。頁(yè)面編號(hào)(pfn)是從物理內(nèi)存的起點(diǎn)開始編號(hào),不是每個(gè)節(jié)點(diǎn)或者區(qū)域重新編號(hào)的。所有區(qū)域的范圍都必須是整數(shù)倍個(gè)頁(yè)面,不能出現(xiàn)半個(gè)頁(yè)面。節(jié)點(diǎn)描述符不僅記錄自己所包含的區(qū)域,還會(huì)記錄自己的起始頁(yè)幀號(hào)和跨越頁(yè)幀數(shù)量,區(qū)域描述符也會(huì)記錄自己的起始頁(yè)幀號(hào)和跨越頁(yè)幀數(shù)量。

下面我們來畫個(gè)圖看一下節(jié)點(diǎn)與頁(yè)面之間的關(guān)系以及x86上具體的區(qū)分劃分情況。21c8c1ec-15e5-11ed-ba43-dac502259ad0.png

三、物理內(nèi)存分配

當(dāng)我們把物理內(nèi)存區(qū)劃弄明白之后,再來學(xué)習(xí)物理內(nèi)存分配就比較容易了。物理內(nèi)存分配最底層的是頁(yè)幀分配。頁(yè)幀分配的分配單元是區(qū)域,分配粒度是頁(yè)面。如何進(jìn)行頁(yè)幀分配呢?Linux采取的算法叫做伙伴系統(tǒng)(buddy system)。只有伙伴系統(tǒng)還不行,因?yàn)榛锇橄到y(tǒng)進(jìn)行的是大粒度的分配,我們還需要批發(fā)與零售,于是便有了slab allocator和kmalloc。這幾種內(nèi)存分配方法分配的都是線性映射的內(nèi)存,當(dāng)系統(tǒng)連續(xù)內(nèi)存不足的時(shí)候,Linux還提供了vmalloc用來分配非線性映射的內(nèi)存。下面我們畫圖來看一下它們之間的關(guān)系。21d8feb8-15e5-11ed-ba43-dac502259ad0.pngBuddy System既是直接的內(nèi)存分配接口,也是所有其它內(nèi)存分配器的底層分配器。Slab建立在Buddy的基礎(chǔ)之上,Kmalloc又建立在Slab的基礎(chǔ)之上。Vmalloc和CMA也是建立在Buddy的基礎(chǔ)之上。Linux采取的這種內(nèi)存分配體系提供了豐富靈活的內(nèi)存接口,還能同時(shí)減少外部碎片和內(nèi)部碎片。

3.1 Buddy System

伙伴系統(tǒng)的基本管理單位是區(qū)域,最小分配粒度是頁(yè)面。因?yàn)榛锇橄到y(tǒng)是建立在物理內(nèi)存的三級(jí)區(qū)劃上的,所以最小分配粒度是頁(yè)面,不能比頁(yè)面再小了?;竟芾韱挝皇菂^(qū)域,是因?yàn)槊總€(gè)區(qū)域的內(nèi)存都有特殊的用途或者用法,不能隨便混用,所以不能用節(jié)點(diǎn)作為基本管理單位?;锇橄到y(tǒng)并不是直接管理一個(gè)個(gè)頁(yè)幀的,而是把頁(yè)幀組成頁(yè)塊(pageblock)來管理,頁(yè)塊是由連續(xù)的2^n^個(gè)頁(yè)幀組成,n叫做這個(gè)頁(yè)塊的階,n的范圍是0到10。而且2^n^個(gè)頁(yè)幀還有對(duì)齊的要求,首頁(yè)幀的頁(yè)幀號(hào)(pfn)必須能除盡2^n^,比如3階頁(yè)塊的首頁(yè)幀(pfn)必須除以8(2^3^)能除盡,10階頁(yè)塊的首頁(yè)幀必須除以1024(2^10^)能除盡。0階頁(yè)塊只包含一個(gè)頁(yè)幀,任意一個(gè)頁(yè)幀都可以構(gòu)成一個(gè)0階頁(yè)塊,而且符合對(duì)齊要求,因?yàn)槿魏握麛?shù)除以1(2^0^)都能除盡。

3.1.1 伙伴系統(tǒng)的內(nèi)存來源

伙伴系統(tǒng)管理的內(nèi)存并不是全部的物理內(nèi)存,而是內(nèi)核在完成初步的初始化之后的未使用內(nèi)存。內(nèi)核在剛啟動(dòng)的時(shí)候有一個(gè)簡(jiǎn)單的早期內(nèi)存管理器,它會(huì)記錄系統(tǒng)的所有物理內(nèi)存以及在它之前就被占用的內(nèi)存,并為內(nèi)核提供早期的內(nèi)存分配服務(wù)。當(dāng)內(nèi)核的基礎(chǔ)初始化完成之后,它就會(huì)把所有剩余可用的物理內(nèi)存交給伙伴系統(tǒng)來管理,然后自己就退出歷史舞臺(tái)了。早期內(nèi)存管理器會(huì)首先嘗試把頁(yè)幀以10階頁(yè)塊的方式加入伙伴系統(tǒng),不夠10階的以9階頁(yè)塊的方式加入伙伴系統(tǒng),以此類推,直到以0階頁(yè)塊的方式把所有可用頁(yè)幀都加入到伙伴系統(tǒng)。顯而易見,內(nèi)核剛啟動(dòng)的時(shí)候高階頁(yè)塊比較多,低階頁(yè)塊比較少。早期內(nèi)存管理器以前是bootmem,后來是bootmem和memblock共存,可以通過config選擇使用哪一個(gè),現(xiàn)在是只有memblock了,bootmem已經(jīng)被移出了內(nèi)核。

3.1.2 伙伴系統(tǒng)的管理數(shù)據(jù)結(jié)構(gòu)

伙伴系統(tǒng)的管理數(shù)據(jù)定義在區(qū)域描述符中,是結(jié)構(gòu)體free_area的數(shù)組,數(shù)組大小是11,因?yàn)閺?到10有11個(gè)數(shù)。free_area的定義如下所示:linux-src/include/linux/mmzone.h

structfree_area{
structlist_headfree_list[MIGRATE_TYPES];
unsignedlongnr_free;
};

enummigratetype{
MIGRATE_UNMOVABLE,
MIGRATE_MOVABLE,
MIGRATE_RECLAIMABLE,
MIGRATE_PCPTYPES,/*thenumberoftypesonthepcplists*/
MIGRATE_HIGHATOMIC=MIGRATE_PCPTYPES,
#ifdefCONFIG_CMA
/*
*MIGRATE_CMAmigrationtypeisdesignedtomimictheway
*ZONE_MOVABLEworks.Onlymovablepagescanbeallocated
*fromMIGRATE_CMApageblocksandpageallocatornever
*implicitlychangemigrationtypeofMIGRATE_CMApageblock.
*
*Thewaytouseitistochangemigratetypeofarangeof
*pageblockstoMIGRATE_CMAwhichcanbedoneby
*__free_pageblock_cma()function.Whatisimportantthough
*isthatarangeofpageblocksmustbealignedto
*MAX_ORDER_NR_PAGESshouldbiggestpagebebiggerthan
*asinglepageblock.
*/
MIGRATE_CMA,
#endif
#ifdefCONFIG_MEMORY_ISOLATION
MIGRATE_ISOLATE,/*can'tallocatefromhere*/
#endif
MIGRATE_TYPES
};

可以看到free_area的定義非常簡(jiǎn)單,就是由MIGRATE_TYPES個(gè)鏈表組成,鏈表連接的是同一個(gè)階的遷移類型相同的頁(yè)幀。遷移類型是內(nèi)核為了減少內(nèi)存碎片而提出的技術(shù),不同區(qū)域的頁(yè)塊有不同的默認(rèn)遷移類型,比如DMA、NORMAL默認(rèn)都是不可遷移(MIGRATE_UNMOVABLE)的頁(yè)塊,HIGHMEM、MOVABLE區(qū)域默認(rèn)都是可遷移(MIGRATE_UNMOVABLE)的頁(yè)塊。我們申請(qǐng)的內(nèi)存有時(shí)候是不可移動(dòng)的內(nèi)存,比如內(nèi)核線性映射的內(nèi)存,有時(shí)候是可以移動(dòng)的內(nèi)存,比如用戶空間缺頁(yè)異常分配的內(nèi)存。我們把不同遷移類型的內(nèi)存分開進(jìn)行分配,在進(jìn)行內(nèi)存碎片整理的時(shí)候就比較方便,不會(huì)出現(xiàn)一片可移動(dòng)內(nèi)存中夾著一個(gè)不可移動(dòng)的內(nèi)存(這種情況就很礙事)。如果要分配的遷移類型的內(nèi)存不足時(shí)就需要從其它的遷移類型中進(jìn)行盜頁(yè)了。內(nèi)核定義了每種遷移類型的后備類型,如下所示:linux-src/mm/page_alloc.c

/*
*Thisarraydescribestheorderlistsarefallenbacktowhen
*thefreelistsforthedesirablemigratetypearedepleted
*/
staticintfallbacks[MIGRATE_TYPES][3]={
[MIGRATE_UNMOVABLE]={MIGRATE_RECLAIMABLE,MIGRATE_MOVABLE,MIGRATE_TYPES},
[MIGRATE_MOVABLE]={MIGRATE_RECLAIMABLE,MIGRATE_UNMOVABLE,MIGRATE_TYPES},
[MIGRATE_RECLAIMABLE]={MIGRATE_UNMOVABLE,MIGRATE_MOVABLE,MIGRATE_TYPES},
#ifdefCONFIG_CMA
[MIGRATE_CMA]={MIGRATE_TYPES},/*Neverused*/
#endif
#ifdefCONFIG_MEMORY_ISOLATION
[MIGRATE_ISOLATE]={MIGRATE_TYPES},/*Neverused*/
#endif
};

一種遷移類型的頁(yè)塊被盜頁(yè)之后,它的遷移類型就改變了,所以一個(gè)頁(yè)塊的遷移類型是會(huì)改變的,有可能變來變?nèi)ァ.?dāng)物理內(nèi)存比較少時(shí),這種變來變?nèi)ゾ蜁?huì)特別頻繁,這樣遷移類型帶來的好處就得不償失了。因此內(nèi)核定義了一個(gè)變量page_group_by_mobility_disabled,當(dāng)物理內(nèi)存比較少時(shí)就禁用遷移類型。

伙伴系統(tǒng)管理頁(yè)塊的方式可以用下圖來表示:21ea8584-15e5-11ed-ba43-dac502259ad0.png

3.1.3 伙伴系統(tǒng)的算法邏輯

伙伴系統(tǒng)對(duì)外提供的接口只能分配某一階的頁(yè)塊,并不能隨意分配若干個(gè)頁(yè)幀。當(dāng)分配n階頁(yè)塊時(shí),伙伴系統(tǒng)會(huì)優(yōu)先查找n階頁(yè)塊的鏈表,如果不為空的話就拿出來一個(gè)分配。如果為空的就去找n+1階頁(yè)塊的鏈表,如果不為空的話,就拿出來一個(gè),并分成兩個(gè)n階頁(yè)塊,其中一個(gè)加入n階頁(yè)塊的鏈表中,另一個(gè)分配出去。如果n+1階頁(yè)塊鏈表也是空的話,就去找n+2階頁(yè)塊的鏈表,如果不為空的話,就拿出來一個(gè),然后分成兩個(gè)n+1階的頁(yè)塊,其中一個(gè)加入到n+1階的鏈表中去,剩下的一個(gè)再分成兩個(gè)n階頁(yè)塊,其中一個(gè)放入n階頁(yè)塊的鏈表中去,另一個(gè)分配出去。如果n+2階頁(yè)塊的鏈表也是空的,那就去找n+3階頁(yè)塊的鏈表,重復(fù)此邏輯,直到找到10階頁(yè)塊的鏈表。如果10階頁(yè)塊的鏈表也是空的話,那就去找后備遷移類型的頁(yè)塊去分配,此時(shí)從最高階的頁(yè)塊鏈表往低階頁(yè)塊的鏈表開始查找,直到查到為止。如果后備頁(yè)塊也分配不到內(nèi)存,那么就會(huì)進(jìn)行內(nèi)存回收,這是下一章的內(nèi)容。

用戶用完內(nèi)存還給伙伴系統(tǒng)的時(shí)候,并不是直接還給其對(duì)應(yīng)的n階頁(yè)塊的鏈表就行了,而是會(huì)先進(jìn)行合并。比如你申請(qǐng)了一個(gè)0階頁(yè)塊,用完了之后要?dú)w還,我們假設(shè)其頁(yè)幀號(hào)是5,來推演一下其歸還過程。如果此時(shí)發(fā)現(xiàn)4號(hào)頁(yè)幀也是free的,則4和5會(huì)合并成一個(gè)1階頁(yè)塊,首頁(yè)幀號(hào)是4。如果4號(hào)頁(yè)幀不是free的,則5號(hào)頁(yè)幀直接還給0階頁(yè)塊鏈表中去。如果6號(hào)頁(yè)幀free呢,會(huì)不會(huì)和5號(hào)頁(yè)幀合并?不會(huì),因?yàn)椴粷M足頁(yè)幀號(hào)對(duì)齊要求。如果5和6合并,將會(huì)成為一個(gè)1階頁(yè)塊,1階頁(yè)塊要求其首頁(yè)幀的頁(yè)號(hào)必須除以2(2^1^)能除盡,而5除以2除不盡,所以5和6不能合并。而4和5合并之后,4除以2(2^1^)是能除盡的。4和5合并成一個(gè)1階頁(yè)塊之后還要查看是否能繼續(xù)合并,如果此時(shí)有一個(gè)1階頁(yè)塊是free的,由6和7組成的,此時(shí)它們就會(huì)合并成一個(gè)2階頁(yè)塊,包含4、5、6、7共4個(gè)頁(yè)幀,而且符合對(duì)齊要求,4除以4(2^2^)是能除盡的。如果此時(shí)有一個(gè)1階頁(yè)塊是free的,由2和3組成的,那么就不能合并,因?yàn)楹喜⒑蟮氖醉?yè)幀是2,2除以4(2^2^)是除不盡的。繼續(xù)此流程,如果合并后的n階頁(yè)塊的前面或者后面還有free的同階頁(yè)塊,而且也符合對(duì)齊要求,就會(huì)繼續(xù)合并,直到無法合并或者已經(jīng)到達(dá)了10階頁(yè)塊,才會(huì)停止合并,然后把其插入到對(duì)應(yīng)的頁(yè)塊鏈表中去。

3.1.4 伙伴系統(tǒng)的接口

下面我們來看一下伙伴系統(tǒng)的接口。伙伴系統(tǒng)提供了兩類接口,一類是返回頁(yè)表描述符的,一類是返回虛擬內(nèi)存地址的。linux-src/include/linux/gfp.h

structpage*alloc_pages(gfp_tgfp,unsignedintorder);
#definealloc_page(gfp_mask)alloc_pages(gfp_mask,0)
structpage*alloc_pages_node(intnid,gfp_tgfp_mask,unsignedintorder);
void__free_pages(structpage*page,unsignedintorder);
#define__free_page(page)__free_pages((page),0)

釋放的接口很簡(jiǎn)單,只需要一個(gè)頁(yè)表描述符指針加一個(gè)階數(shù)。分配的接口中,有的會(huì)指定nodeid,就從那個(gè)節(jié)點(diǎn)中分配內(nèi)存。不指定nodeid的接口,如果是在UMA中,那就從唯一的節(jié)點(diǎn)中分配內(nèi)存,如果是NUMA,會(huì)按照一定的策略選擇在哪個(gè)節(jié)點(diǎn)中分配內(nèi)存。最復(fù)雜的參數(shù)是gfp,gfp是標(biāo)記參數(shù),可以分為兩類標(biāo)記,一類是指定分配區(qū)域的,一類是指定分配行為的,下面我們來看一下。linux-src/include/linux/gfp.h

#define___GFP_DMA0x01u
#define___GFP_HIGHMEM0x02u
#define___GFP_DMA320x04u
#define___GFP_MOVABLE0x08u
#define___GFP_RECLAIMABLE0x10u
#define___GFP_HIGH0x20u
#define___GFP_IO0x40u
#define___GFP_FS0x80u
#define___GFP_ZERO0x100u
#define___GFP_ATOMIC0x200u
#define___GFP_DIRECT_RECLAIM0x400u
#define___GFP_KSWAPD_RECLAIM0x800u
#define___GFP_WRITE0x1000u
#define___GFP_NOWARN0x2000u
#define___GFP_RETRY_MAYFAIL0x4000u
#define___GFP_NOFAIL0x8000u
#define___GFP_NORETRY0x10000u
#define___GFP_MEMALLOC0x20000u
#define___GFP_COMP0x40000u
#define___GFP_NOMEMALLOC0x80000u
#define___GFP_HARDWALL0x100000u
#define___GFP_THISNODE0x200000u
#define___GFP_ACCOUNT0x400000u
#define___GFP_ZEROTAGS0x800000u
#define___GFP_SKIP_KASAN_POISON0x1000000u
#ifdefCONFIG_LOCKDEP
#define___GFP_NOLOCKDEP0x2000000u
#else
#define___GFP_NOLOCKDEP0
#endif

其中前4個(gè)是指定分配區(qū)域的,內(nèi)核里一共定義了6類區(qū)域,為啥只有4個(gè)指示符呢?因?yàn)閆ONE_DEVICE有特殊用途,不在一般的內(nèi)存分配管理中,當(dāng)不指定區(qū)域類型時(shí)默認(rèn)就是ZONE_NORMAL,所以4個(gè)就夠了。是不是指定了哪個(gè)區(qū)域就只能在哪個(gè)區(qū)域分配內(nèi)存呢,不是的。每個(gè)區(qū)域都有后備區(qū)域,當(dāng)其內(nèi)存不足時(shí),會(huì)從其后備區(qū)域中分配內(nèi)存。后備區(qū)域是在節(jié)點(diǎn)描述符中定義,我們來看一下:linux-src/include/linux/mmzone.h

typedefstructpglist_data{
structzonelistnode_zonelists[MAX_ZONELISTS];
}pg_data_t;

enum{
ZONELIST_FALLBACK,/*zonelistwithfallback*/
#ifdefCONFIG_NUMA
/*
*TheNUMAzonelistsaredoubledbecauseweneedzoneliststhat
*restricttheallocationstoasinglenodefor__GFP_THISNODE.
*/
ZONELIST_NOFALLBACK,/*zonelistwithoutfallback(__GFP_THISNODE)*/
#endif
MAX_ZONELISTS
};

structzonelist{
structzoneref_zonerefs[MAX_ZONES_PER_ZONELIST+1];
};

structzoneref{
structzone*zone;/*Pointertoactualzone*/
intzone_idx;/*zone_idx(zoneref->zone)*/
};

在UMA上,后備區(qū)域只有一個(gè)鏈表,就是本節(jié)點(diǎn)內(nèi)的后備區(qū)域,在NUMA中后備區(qū)域有兩個(gè)鏈表,包括本節(jié)點(diǎn)內(nèi)的后備區(qū)域和其它節(jié)點(diǎn)的后備區(qū)域。這些后備區(qū)域是在內(nèi)核啟動(dòng)時(shí)初始化的。對(duì)于本節(jié)點(diǎn)的后備區(qū)域,是按照區(qū)域類型的id排列的,高id的排在前面,低id的排在后面,后面的是前面的后備,前面的區(qū)域內(nèi)存不足時(shí)可以從后面的區(qū)域里分配內(nèi)存,反過來則不行。比如MOVABLE區(qū)域的內(nèi)存不足時(shí)可以從NORMAL區(qū)域來分配,NORMAL區(qū)域的內(nèi)存不足時(shí)可以從DMA區(qū)域來分配,反過來則不行。對(duì)于其它節(jié)點(diǎn)的后備區(qū)域,除了會(huì)符合前面的規(guī)則之外,還會(huì)考慮后備區(qū)域是按照節(jié)點(diǎn)優(yōu)先的順序來排列還是按照區(qū)域類型優(yōu)先的順序來排列。

下面我們?cè)賮砜匆幌路峙湫袨榈膄lag都是什么含義。

__GFP_HIGH:調(diào)用者的優(yōu)先級(jí)很高,要盡量滿足分配請(qǐng)求。

__GFP_ATOMIC:調(diào)用者處在原子場(chǎng)景中,分配過程不能回收頁(yè)或者睡眠,一般是中斷處理程序會(huì)用。

__GFP_IO:可以進(jìn)行磁盤IO操作。

__GFP_FS:可以進(jìn)行文件系統(tǒng)的操作。

__GFP_KSWAPD_RECLAIM:當(dāng)內(nèi)存不足時(shí)允許異步回收。

__GFP_RECLAIM:當(dāng)內(nèi)存不足時(shí)允許同步回收和異步回收。

__GFP_REPEAT:允許重試,重試多次以后還是沒有內(nèi)存就返回失敗。

__GFP_NOFAIL:不能失敗,必須無限次重試。

__GFP_NORETRY:不要重試,當(dāng)直接回收和內(nèi)存規(guī)整之后還是分配不到內(nèi)存的話就返回失敗。

__GFP_ZERO:把要分配的頁(yè)清零。

還有一些其它的flag就不再一一進(jìn)行介紹了。

如果我們每次分配內(nèi)存都把這些flag一一進(jìn)行組合,那就太麻煩了,所以系統(tǒng)為我們定義了一些常用的組合,如下所示:linux-src/include/linux/gfp.h

#defineGFP_ATOMIC(__GFP_HIGH|__GFP_ATOMIC|__GFP_KSWAPD_RECLAIM)
#defineGFP_KERNEL(__GFP_RECLAIM|__GFP_IO|__GFP_FS)
#defineGFP_NOIO(__GFP_RECLAIM)
#defineGFP_NOFS(__GFP_RECLAIM|__GFP_IO)
#defineGFP_USER(__GFP_RECLAIM|__GFP_IO|__GFP_FS|__GFP_HARDWALL)
#defineGFP_DMA__GFP_DMA
#defineGFP_DMA32__GFP_DMA32
#defineGFP_HIGHUSER(GFP_USER|__GFP_HIGHMEM)
#defineGFP_HIGHUSER_MOVABLE(GFP_HIGHUSER|__GFP_MOVABLE|__GFP_SKIP_KASAN_POISON)

中斷中分配內(nèi)存一般用GFP_ATOMIC,內(nèi)核自己使用的內(nèi)存一般用GFP_KERNEL,為用戶空間分配內(nèi)存一般用GFP_HIGHUSER_MOVABLE。

我們?cè)賮砜匆幌轮苯臃祷靥摂M內(nèi)存的接口函數(shù)。linux-src/include/linux/gfp.h

unsignedlong__get_free_pages(gfp_tgfp_mask,unsignedintorder);
#define__get_free_page(gfp_mask)__get_free_pages((gfp_mask),0)
#define__get_dma_pages(gfp_mask,order)__get_free_pages((gfp_mask)|GFP_DMA,(order))
unsignedlongget_zeroed_page(gfp_tgfp_mask);
voidfree_pages(unsignedlongaddr,unsignedintorder);
#definefree_page(addr)free_pages((addr),0)

此接口不能分配HIGHMEM中的內(nèi)存,因?yàn)镠IGHMEM中的內(nèi)存不是直接映射到內(nèi)核空間中去的。除此之外這個(gè)接口和前面的沒有區(qū)別,其參數(shù)函數(shù)也跟前面的一樣,就不再贅述了。

3.1.5 伙伴系統(tǒng)的實(shí)現(xiàn)

下面我們?cè)賮砜匆幌禄锇橄到y(tǒng)的分配算法。linux-src/mm/page_alloc.c

/*
*Thisisthe'heart'ofthezonedbuddyallocator.
*/
structpage*__alloc_pages(gfp_tgfp,unsignedintorder,intpreferred_nid,
nodemask_t*nodemask)
{
structpage*page;

/*Firstallocationattempt*/
page=get_page_from_freelist(alloc_gfp,order,alloc_flags,&ac);
if(likely(page))
gotoout;

page=__alloc_pages_slowpath(alloc_gfp,order,&ac);

out:
returnpage;
}

伙伴系統(tǒng)的所有分配接口最終都會(huì)使用__alloc_pages這個(gè)函數(shù)來進(jìn)行分配。對(duì)這個(gè)函數(shù)進(jìn)行刪減之后,其邏輯也比較簡(jiǎn)單清晰,先使用函數(shù)get_page_from_freelist直接從free_area中進(jìn)行分配,如果分配不到就使用函數(shù) __alloc_pages_slowpath進(jìn)行內(nèi)存回收。內(nèi)存回收的內(nèi)容在下一章里面講。

3.2 Slab Allocator

伙伴系統(tǒng)的最小分配粒度是頁(yè)面,但是內(nèi)核中有很多大量的同一類型結(jié)構(gòu)體的分配請(qǐng)求,比如說進(jìn)程的結(jié)構(gòu)體task_struct,如果使用伙伴系統(tǒng)來分配顯然不合適,如果自己分配一個(gè)頁(yè)面,然后可以分割成多個(gè)task_struct,顯然也很麻煩,于是內(nèi)核中給我們提供了slab分配機(jī)制來滿足這種需求。Slab的基本思想很簡(jiǎn)單,就是自己先從伙伴系統(tǒng)中分配一些頁(yè)面,然后把這些頁(yè)面切割成一個(gè)個(gè)同樣大小的基本塊,用戶就可以從slab中申請(qǐng)分配一個(gè)同樣大小的內(nèi)存塊了。如果slab中的內(nèi)存不夠用了,它會(huì)再向伙伴系統(tǒng)進(jìn)行申請(qǐng)。不同的slab其基本塊的大小并不相同,內(nèi)核的每個(gè)模塊都要為自己的特定需求分配特定的slab,然后再?gòu)倪@個(gè)slab中分配內(nèi)存。

剛開始的時(shí)候內(nèi)核中就只有一個(gè)slab,其接口和實(shí)現(xiàn)都叫slab。但是后來內(nèi)核中又出現(xiàn)了兩個(gè)slab實(shí)現(xiàn),slob和slub。slob是針對(duì)嵌入式系統(tǒng)進(jìn)行優(yōu)化的,slub是針對(duì)內(nèi)存比較多的系統(tǒng)進(jìn)行優(yōu)化的,它們的接口還是slab。由于現(xiàn)在的計(jì)算機(jī)內(nèi)存普遍都比較大,連手機(jī)的的內(nèi)存都6G、8G起步了,所以現(xiàn)在除了嵌入式系統(tǒng)之外,內(nèi)核默認(rèn)使用的都是slub。下面我們畫個(gè)圖看一下它們的關(guān)系。21fc935a-15e5-11ed-ba43-dac502259ad0.png可以看到Slab在不同的語(yǔ)境下有不同的含義,有時(shí)候指的是整個(gè)Slab機(jī)制,有時(shí)候指的是Slab接口,有時(shí)候指的是Slab實(shí)現(xiàn)。如果我們?cè)谟懻搯栴}的時(shí)候遇到了歧義,可以加上漢語(yǔ)后綴以明確語(yǔ)義。

3.2.1 Slab接口

下面我們來看一下slab的接口:linux-src/include/linux/slab.h

structkmem_cache*kmem_cache_create(constchar*name,unsignedintsize,
unsignedintalign,slab_flags_tflags,
void(*ctor)(void*));
voidkmem_cache_destroy(structkmem_cache*);
void*kmem_cache_alloc(structkmem_cache*,gfp_tflags);
voidkmem_cache_free(structkmem_cache*,void*);

我們?cè)谑褂胹lab時(shí)首先要?jiǎng)?chuàng)建slab,創(chuàng)建slab用的是接口kmem_cache_create,其中最重要的參數(shù)是size,它是基本塊的大小,一般我們都會(huì)傳遞sizeof某個(gè)結(jié)構(gòu)體。創(chuàng)建完slab之后,我們用kmem_cache_alloc從slab中分配內(nèi)存,第一個(gè)參數(shù)指定哪個(gè)是從哪個(gè)slab中分配,第二個(gè)參數(shù)gfp指定如果slab的內(nèi)存不足了如何從伙伴系統(tǒng)中去分配內(nèi)存,gfp的函數(shù)和前面伙伴系統(tǒng)中講的相同,此處就不再贅述了,函數(shù)返回的是一個(gè)指針,其指向的內(nèi)存大小就是slab在創(chuàng)建時(shí)指定的基本塊的大小。當(dāng)我們用完一塊內(nèi)存時(shí),就要用kmem_cache_free把它還給slab,第一個(gè)參數(shù)指定是哪個(gè)slab,第二個(gè)參數(shù)是我們要返回的內(nèi)存。如果我們想要釋放整個(gè)slab的話,就使用接口kmem_cache_destroy。

3.2.2 Slab實(shí)現(xiàn)

暫略

3.2.3 Slob實(shí)現(xiàn)

暫略

3.2.4 Slub實(shí)現(xiàn)

暫略

3.3 Kmalloc

內(nèi)存中還有一些偶發(fā)的零碎的內(nèi)存分配需求,一個(gè)模塊如果僅僅為了分配一次5字節(jié)的內(nèi)存,就去創(chuàng)建一個(gè)slab,那顯然不劃算。為此內(nèi)核創(chuàng)建了一個(gè)統(tǒng)一的零碎內(nèi)存分配器kmalloc,用戶可以直接請(qǐng)求kmalloc分配若干個(gè)字節(jié)的內(nèi)存。Kmalloc底層用的還是slab機(jī)制,kmalloc在啟動(dòng)的時(shí)候會(huì)預(yù)先創(chuàng)建一些不同大小的slab,用戶請(qǐng)求分配任意大小的內(nèi)存,kmalloc都會(huì)去大小剛剛滿足的slab中去分配內(nèi)存。

下面我們來看一下kmalloc的接口:linux-src/include/linux/slab.h

void*kmalloc(size_tsize,gfp_tflags);
voidkfree(constvoid*);

可以看到kmalloc的接口很簡(jiǎn)單,使用接口kmalloc就可以分配內(nèi)存,第一個(gè)參數(shù)是你要分配的內(nèi)存大小,第二個(gè)參數(shù)和伙伴系統(tǒng)的參數(shù)是一樣的,這里就不再贅述了,返回值是一個(gè)內(nèi)存指針,用這個(gè)指針就可以訪問分配到的內(nèi)存了。內(nèi)存使用完了之后用kfree進(jìn)行釋放,參數(shù)是剛才分配到的內(nèi)存指針。

我們以slub實(shí)現(xiàn)為例講一下kmalloc的邏輯。Kmalloc中會(huì)定義一個(gè)全局的slab指針的二維數(shù)組,第一維下標(biāo)代表的是kmalloc的類型,默認(rèn)有四種類型,分別有DMA和NORMAL,這兩個(gè)代表的是gfp中的區(qū)域,還有兩個(gè)是CGROUP和RECLAIM,CGROUP代表的是在memcg中分配內(nèi)存,RECLAIM代表的是可回收內(nèi)存。第二維下標(biāo)代表的是基本塊大小的2的對(duì)數(shù),不過下標(biāo)0、1、2是例外,有特殊含義。在系統(tǒng)初始化的時(shí)候,會(huì)初始化這個(gè)數(shù)組,創(chuàng)建每一個(gè)slab,下標(biāo)0除外,下標(biāo)1對(duì)應(yīng)的slab的基本塊大小是96,下標(biāo)2對(duì)應(yīng)的slab的基本塊的大小是192。在用kmalloc分配內(nèi)存的時(shí)候,會(huì)先處理特殊情況,當(dāng)size是0的時(shí)候直接返回空指針,當(dāng)size大于8k的時(shí)候會(huì)則直接使用伙伴系統(tǒng)進(jìn)行分配。然后先根據(jù)gfp參數(shù)選擇kmalloc的類型,再根據(jù)size的大小選擇index。如果2^n-1^+1 < size <= 2^n^,則index等于n,但是有特殊情況,當(dāng) 64 < size <= 96時(shí),index等于1,當(dāng) 128 < size <= 192時(shí),index等于2。Type和index都確定好之后,就找到了具體的slab了,就可以從這個(gè)slab中分配內(nèi)存了。

3.4 Vmalloc

暫略

3.5 CMA

暫略

四、物理內(nèi)存回收

內(nèi)存作為系統(tǒng)最寶貴的資源,總是不夠用的。當(dāng)內(nèi)存不足的時(shí)候就要對(duì)內(nèi)存進(jìn)行回收了。內(nèi)存回收按照回收時(shí)機(jī)可以分為同步回收和異步回收,同步回收是指在分配內(nèi)存的時(shí)候發(fā)現(xiàn)無法分配到內(nèi)存就進(jìn)行回收,異步回收是指有專門的線程定期進(jìn)行檢測(cè),如果發(fā)現(xiàn)內(nèi)存不足就進(jìn)行回收。內(nèi)存回收的類型有兩種,一是內(nèi)存規(guī)整,也就是內(nèi)存碎片整理,它不會(huì)增加可用內(nèi)存的總量,但是會(huì)增加連續(xù)可用內(nèi)存的量,二是頁(yè)幀回收,它會(huì)把物理頁(yè)幀的內(nèi)容寫入到外存中去,然后解除其與虛擬內(nèi)存的映射,這樣可用物理內(nèi)存的量就增加了。內(nèi)存回收的時(shí)機(jī)和類型是正交關(guān)系,同步回收中會(huì)使用內(nèi)存規(guī)整和頁(yè)幀回收,異步回收中也會(huì)使用內(nèi)存規(guī)整和頁(yè)幀回收。在異步回收中,內(nèi)存規(guī)整有單獨(dú)的線程kcompactd,此類線程一個(gè)node一個(gè),線程名是[kcompactd/nodeid],頁(yè)幀回收也有單獨(dú)的線程kswapd,此類線程也是一個(gè)node一個(gè),線程名是[kswapd/nodeid]。在同步回收中,還有一個(gè)大殺器,那就是OOM Killer,OOM是內(nèi)存耗盡的意思,當(dāng)內(nèi)存耗盡,其它所有的內(nèi)存回收方法也回收不到內(nèi)存的時(shí)候,就會(huì)使用這個(gè)大殺器。下面我們畫個(gè)圖來看一下:220db536-15e5-11ed-ba43-dac502259ad0.png

4.1 內(nèi)存規(guī)整

系統(tǒng)運(yùn)行的時(shí)間長(zhǎng)了,內(nèi)存一會(huì)兒分配一會(huì)兒釋放,慢慢地可用內(nèi)存就會(huì)變得很碎片化不連續(xù)。雖然總的可用內(nèi)存還不少,但是卻無法分配大塊連續(xù)內(nèi)存,此時(shí)就需要進(jìn)行內(nèi)存規(guī)整了。內(nèi)存規(guī)整是以區(qū)域?yàn)榛締挝?,找到可用移?dòng)的頁(yè)幀,把它們都移到同一端,然后連續(xù)可用內(nèi)存的量就增大了。其邏輯如下圖所示:222ea64c-15e5-11ed-ba43-dac502259ad0.png

4.2 頁(yè)幀回收

內(nèi)存規(guī)整只是增加了連續(xù)內(nèi)存的量,但是可用內(nèi)存的量并沒有增加,當(dāng)可用內(nèi)存量不足的時(shí)候就要進(jìn)行頁(yè)幀回收。對(duì)于內(nèi)核來說,其虛擬內(nèi)存和物理內(nèi)存的映射關(guān)系是不能解除的,所以必須同時(shí)回收物理內(nèi)存和虛擬內(nèi)存。對(duì)此采取的辦法是讓內(nèi)核的每個(gè)模塊都注冊(cè)shrinker,當(dāng)內(nèi)存緊張時(shí)通過shrinker的回調(diào)函數(shù)通知每個(gè)模塊盡量釋放自己暫時(shí)用不到的內(nèi)存。對(duì)于用戶空間,其虛擬內(nèi)存和物理內(nèi)存的映射關(guān)系是可以解除的,我們可以先把其物理內(nèi)存上的內(nèi)容保存到外存上去,然后再解除映射關(guān)系,這樣其物理內(nèi)存就被回收了,就可以拿做它用了。如果程序后來又用到了這段內(nèi)存,程序訪問其虛擬內(nèi)存的時(shí)候就會(huì)發(fā)生缺頁(yè)異常,在缺頁(yè)異常里再給它分配物理內(nèi)存,并把其內(nèi)容從外存中加載建立,這樣程序還是能正常運(yùn)行的。進(jìn)程的內(nèi)存頁(yè)可以分為兩種類型:一種是文件頁(yè),其內(nèi)容來源于文件,如程序的代碼區(qū)、數(shù)據(jù)區(qū);一種是匿名頁(yè),沒有內(nèi)容來源,由內(nèi)核直接為其分配內(nèi)存,如進(jìn)程的堆和棧。對(duì)于文件頁(yè),有兩種情況:一種情況是文件頁(yè)是clean的,也就是和外存中的內(nèi)容是一樣的,此時(shí)我們可以直接丟棄文件頁(yè),后面用到時(shí)再?gòu)耐獯嬷屑虞d進(jìn)來;另一種情況是文件頁(yè)是dirty的,也就是其經(jīng)歷過修改,和外存中的內(nèi)容不同,此時(shí)要先把文件頁(yè)的內(nèi)容寫入到外存中,然后才能回收其內(nèi)存。對(duì)于匿名頁(yè),由于其沒有文件做后備,沒辦法對(duì)其進(jìn)行回收。此時(shí)就需要swap作為匿名頁(yè)的后備存儲(chǔ)了,有了swap之后,匿名頁(yè)也可以進(jìn)行回收了。Swap是外存中的一片空間,可以是一個(gè)分區(qū),也可以是文件,具體原理請(qǐng)看下一節(jié)。

頁(yè)幀回收時(shí)如何選擇回收哪些文件頁(yè)、匿名頁(yè),不回收哪些文件頁(yè)、匿名頁(yè)呢,以及文件頁(yè)和匿名頁(yè)各回收多少比例呢??jī)?nèi)核把所有的文件頁(yè)放到兩個(gè)鏈表上,活躍文件頁(yè)和不活躍文件頁(yè),回收的時(shí)候只會(huì)回收不活躍文件頁(yè)。內(nèi)核把所有的匿名頁(yè)也放到兩個(gè)鏈表上,活躍匿名頁(yè)和不活躍匿名頁(yè),回收的時(shí)候只會(huì)回收不活躍匿名頁(yè)。有一個(gè)參數(shù)/proc/sys/vm/swappiness控制著匿名頁(yè)和文件頁(yè)之間的回收比例。

在回收文件頁(yè)和匿名頁(yè)的時(shí)候是需要把它們的虛擬內(nèi)存映射給解除掉的。由于一個(gè)物理頁(yè)幀可能會(huì)同時(shí)映射到多個(gè)虛擬內(nèi)存上,包括映射到多個(gè)進(jìn)程或者同一個(gè)進(jìn)程的不同地址上,所以我們需要找到一個(gè)物理頁(yè)幀所映射的所有虛擬內(nèi)存。如何找到物理內(nèi)存所映射的虛擬內(nèi)存呢,這個(gè)過程就叫做反向映射(rmap)。之所以叫反向映射是因?yàn)檎5挠成涠际菑奶摂M內(nèi)存映射到物理內(nèi)存。

4.3 交換區(qū)

暫略

4.4 OOM Killer

如果用盡了上述所說的各種辦法還是無法回收到足夠的物理內(nèi)存,那就只能使出殺手锏了,OOM Killer,通過殺死進(jìn)程來回收內(nèi)存。其觸發(fā)點(diǎn)在linux-src/mm/page_alloc.c:__alloc_pages_may_oom,當(dāng)使用各種方法都回收不到內(nèi)存時(shí)會(huì)調(diào)用out_of_memory函數(shù)。

下面我們來看一下out_of_memory函數(shù)的實(shí)現(xiàn)(經(jīng)過高度刪減):linux-src/mm/oom_kill.c:out_of_memory

boolout_of_memory(structoom_control*oc)
{
select_bad_process(oc);
oom_kill_process(oc,"Outofmemory");
}

out_of_memory函數(shù)的代碼邏輯還是非常簡(jiǎn)單清晰的,總共有兩步,1.先選擇一個(gè)要?dú)⑺赖倪M(jìn)程,2.殺死它。oom_kill_process函數(shù)的目的很簡(jiǎn)單,但是實(shí)現(xiàn)過程也有點(diǎn)復(fù)雜,這里就不展開分析了,大家可以自行去看一下代碼。我們重點(diǎn)分析一下select_bad_process函數(shù)的邏輯,select_bad_process主要是依靠oom_score來進(jìn)行進(jìn)程選擇的。我們先來看一下和oom_score有關(guān)的三個(gè)文件。

/proc//oom_score系統(tǒng)計(jì)算出來的oom_score值,只讀文件,取值范圍0 –- 1000,0代表never kill,1000代表aways kill,值越大,進(jìn)程被選中的概率越大。

/proc//oom_score_adj讓用戶空間調(diào)節(jié)oom_score的接口,root可讀寫,取值范圍 -1000 --- 1000,默認(rèn)為0,若為 -1000,則oom_score加上此值一定小于等于0,從而變成never kill進(jìn)程。OS可以把一些關(guān)鍵的系統(tǒng)進(jìn)程的oom_score_adj設(shè)為-1000,從而避免被oom kill。

/proc//oom_adj舊的接口文件,為兼容而保留,root可讀寫,取值范圍 -16 — 15,會(huì)被線性映射到oom_score_adj,特殊值 -17代表 OOM_DISABLE。大家盡量不要再用此接口。

下面我們來分析一下select_bad_process函數(shù)的實(shí)現(xiàn):

staticvoidselect_bad_process(structoom_control*oc)
{
oc->chosen_points=LONG_MIN;
structtask_struct*p;

rcu_read_lock();
for_each_process(p)
if(oom_evaluate_task(p,oc))
break;
rcu_read_unlock();
}

函數(shù)首先把chosen_points初始化為最小的Long值,這個(gè)值是用來比較所有的oom_score值,最后誰的值最大就選中哪個(gè)進(jìn)程。然后函數(shù)已經(jīng)遍歷所有進(jìn)程,計(jì)算其oom_score,并更新chosen_points和被選中的task,有點(diǎn)類似于選擇排序。我們繼續(xù)看oom_evaluate_task函數(shù)是如何評(píng)估每個(gè)進(jìn)程的函數(shù)。

staticintoom_evaluate_task(structtask_struct*task,void*arg)
{
structoom_control*oc=arg;
longpoints;
if(oom_unkillable_task(task))
gotonext;
/*pmaynothavefreeablememoryinnodemask*/
if(!is_memcg_oom(oc)&&!oom_cpuset_eligible(task,oc))
gotonext;
if(oom_task_origin(task)){
points=LONG_MAX;
gotoselect;
}
points=oom_badness(task,oc->totalpages);
if(points==LONG_MIN||pointschosen_points)
gotonext;
select:
if(oc->chosen)
put_task_struct(oc->chosen);
get_task_struct(task);
oc->chosen=task;
oc->chosen_points=points;
next:
return0;
abort:
if(oc->chosen)
put_task_struct(oc->chosen);
oc->chosen=(void*)-1UL;
return1;
}

此函數(shù)首先會(huì)跳過所有不適合kill的進(jìn)程,如init進(jìn)程、內(nèi)核線程、OOM_DISABLE進(jìn)程等。然后通過select_bad_process算出此進(jìn)程的得分points 也就是oom_score,并和上一次的勝出進(jìn)程進(jìn)行比較,如果小的會(huì)話就會(huì)goto next 返回,如果大的話就會(huì)更新oc->chosen 的task 和 chosen_points 也就是目前最高的oom_score。那么 oom_badness是如何計(jì)算的呢?

longoom_badness(structtask_struct*p,unsignedlongtotalpages)
{
longpoints;
longadj;
if(oom_unkillable_task(p))
returnLONG_MIN;
p=find_lock_task_mm(p);
if(!p)
returnLONG_MIN;
adj=(long)p->signal->oom_score_adj;
if(adj==OOM_SCORE_ADJ_MIN||
test_bit(MMF_OOM_SKIP,&p->mm->flags)||
in_vfork(p)){
task_unlock(p);
returnLONG_MIN;
}
points=get_mm_rss(p->mm)+get_mm_counter(p->mm,MM_SWAPENTS)+
mm_pgtables_bytes(p->mm)/PAGE_SIZE;
task_unlock(p);
adj*=totalpages/1000;
points+=adj;
returnpoints;
}

oom_badness首先把unkiller的進(jìn)程也就是init進(jìn)程內(nèi)核線程直接返回 LONG_MIN,這樣它們就不會(huì)被選中而殺死了,這里看好像和前面的檢測(cè)冗余了,但是實(shí)際上這個(gè)函數(shù)還被/proc//oom_score的show函數(shù)調(diào)用用來顯示數(shù)值,所以還是有必要的,這里也說明了一點(diǎn),oom_score的值是不保留的,每次都是即時(shí)計(jì)算。然后又把oom_score_adj為-1000的進(jìn)程直接也返回LONG_MIN,這樣用戶空間專門設(shè)置的進(jìn)程就不會(huì)被kill了。最后就是計(jì)算oom_score了,計(jì)算方法比較簡(jiǎn)單,就是此進(jìn)程使用的RSS駐留內(nèi)存、頁(yè)表、swap之和越大,也就是此進(jìn)程所用的總內(nèi)存越大,oom_score的值就越大,邏輯簡(jiǎn)單直接,誰用的物理內(nèi)存最多就殺誰,這樣就能夠回收更多的物理內(nèi)存,而且使用內(nèi)存最多的進(jìn)程很可能是內(nèi)存泄漏了,所以此算法雖然很簡(jiǎn)單,但是也很合理。

可能很多人會(huì)覺得這里講的不對(duì),和自己在網(wǎng)上的看到的邏輯不一樣,那是因?yàn)榫W(wǎng)上有很多講oom_score算法的文章都是基于2.6版本的內(nèi)核講的,那個(gè)算法比較復(fù)雜,會(huì)考慮進(jìn)程的nice值,nice值小的,oom_score會(huì)相應(yīng)地降低,也會(huì)考慮進(jìn)程的運(yùn)行時(shí)間,運(yùn)行時(shí)間越長(zhǎng),oom_score值也會(huì)相應(yīng)地降低,因?yàn)楫?dāng)時(shí)認(rèn)為進(jìn)程運(yùn)行的時(shí)間長(zhǎng)消耗內(nèi)存多是合理的。但是這個(gè)算法會(huì)讓那些緩慢內(nèi)存泄漏的進(jìn)程逃脫制裁。因此后來這個(gè)算法就改成現(xiàn)在這樣的了,只考慮誰用的內(nèi)存多就殺誰,簡(jiǎn)潔高效。

五、物理內(nèi)存壓縮

暫略

5.1 ZRAM

5.2 ZSwap

5.3 ZCache

六、虛擬內(nèi)存映射

開啟分頁(yè)內(nèi)存機(jī)制之后,CPU訪問一切內(nèi)存都要通過虛擬內(nèi)存地址訪問,CPU把虛擬內(nèi)存地址發(fā)送給MMU,MMU把虛擬內(nèi)存地址轉(zhuǎn)換為物理內(nèi)存地址,然后再用物理內(nèi)存地址通過MC(內(nèi)存控制器)訪問內(nèi)存。MMU里面有兩個(gè)部件,TLB和PTW。TLB可以意譯地址轉(zhuǎn)換緩存器,它是緩存虛擬地址解析結(jié)果的地方。PTW可以意譯為虛擬地址解析器,它負(fù)責(zé)解析頁(yè)表,把虛擬地址轉(zhuǎn)換為物理地址,然后再送去MC進(jìn)行訪問。同時(shí)其轉(zhuǎn)換結(jié)果也會(huì)被送去TLB進(jìn)行緩存,下次再訪問相同虛擬地址的時(shí)候就不用再去解析了,可以直接用緩存的結(jié)果。22447ddc-15e5-11ed-ba43-dac502259ad0.png

6.1 頁(yè)表

虛擬地址映射的基本單位是頁(yè)面不是字節(jié),一個(gè)虛擬內(nèi)存的頁(yè)面會(huì)被映射到一個(gè)物理頁(yè)幀上。MMU把虛擬地址轉(zhuǎn)換為物理地址的方法是通過查找頁(yè)表。一個(gè)頁(yè)表的大小也是一個(gè)頁(yè)面,4K大小,頁(yè)表的內(nèi)容可以看做是頁(yè)表項(xiàng)的數(shù)組,一個(gè)頁(yè)表項(xiàng)是一個(gè)物理地址,指向一個(gè)物理頁(yè)幀,在32位系統(tǒng)上,物理地址是32位也就是4個(gè)字節(jié),所以一個(gè)頁(yè)表有4K/4=1024項(xiàng),每一項(xiàng)指向一個(gè)物理頁(yè)幀,大小是4K,所以一個(gè)頁(yè)表可以表達(dá)4M的虛擬內(nèi)存,要想表達(dá)4G的虛擬內(nèi)存空間,需要有1024個(gè)頁(yè)表才行,每個(gè)頁(yè)表4K,一共需要4M的物理內(nèi)存。4M的物理內(nèi)存看起來好像不大,但是每個(gè)進(jìn)程都需要有4M的物理內(nèi)存做頁(yè)表,如果有100個(gè)進(jìn)程,那就需要有400M物理內(nèi)存,這就太浪費(fèi)物理內(nèi)存了,而且大部分時(shí)候,一個(gè)進(jìn)程的大部分虛擬內(nèi)存空間并沒有使用。為此我們可以采取兩級(jí)頁(yè)表的方法來進(jìn)行虛擬內(nèi)存映射。在多級(jí)頁(yè)表體系中,最后一級(jí)頁(yè)表還叫頁(yè)表,其它的頁(yè)表叫做頁(yè)目錄,但是我們有時(shí)候也會(huì)都叫做頁(yè)表。對(duì)于兩級(jí)頁(yè)表體系,一級(jí)頁(yè)表還是一個(gè)頁(yè)面,4K大小,每個(gè)頁(yè)表項(xiàng)還是4個(gè)字節(jié),一共有1024項(xiàng),一級(jí)頁(yè)表的頁(yè)表項(xiàng)是二級(jí)頁(yè)表的物理地址,指向二級(jí)頁(yè)表,二級(jí)頁(yè)表的內(nèi)容和前面一樣。一級(jí)頁(yè)表只有一個(gè),4K,有1024項(xiàng),指向1024個(gè)二級(jí)頁(yè)表,一個(gè)一級(jí)頁(yè)表項(xiàng)也就是一個(gè)二級(jí)頁(yè)表可以表達(dá)4M虛擬內(nèi)存,一級(jí)頁(yè)表總共能表達(dá)4G虛擬內(nèi)存,此時(shí)所有頁(yè)表占用的物理內(nèi)存是4M加4K。看起來使用二級(jí)頁(yè)表好像還多用了4K內(nèi)存,但是在大多數(shù)情況下,很多二級(jí)頁(yè)表都用不上,所以不用分配內(nèi)存。如果一個(gè)進(jìn)程只用了8M物理內(nèi)存,那么它只需要一個(gè)一級(jí)頁(yè)表和兩個(gè)二級(jí)頁(yè)表就行了,一級(jí)頁(yè)表中只需要使用兩項(xiàng)指向兩個(gè)二級(jí)頁(yè)表,兩個(gè)二級(jí)頁(yè)表填充滿,就可以表達(dá)8M虛擬內(nèi)存映射了,此時(shí)總共用了3個(gè)頁(yè)表,12K物理內(nèi)存,頁(yè)表的內(nèi)存占用大大減少了。所以在32位系統(tǒng)上,采取的是兩級(jí)頁(yè)表的方式,每級(jí)的一個(gè)頁(yè)表都是1024項(xiàng),32位虛擬地址正好可以分成三份,10、10、12,第一個(gè)10位可以用于在一級(jí)頁(yè)表中尋址,第二個(gè)10位在二級(jí)頁(yè)表中尋址,最后12位可以表達(dá)一個(gè)頁(yè)面中任何一個(gè)字節(jié)。

在64位系統(tǒng)上,一個(gè)頁(yè)面還是4K大小,一個(gè)頁(yè)表還是一個(gè)頁(yè)面,但是由于物理地址是64位的,所以一個(gè)頁(yè)表項(xiàng)變成了8個(gè)字節(jié),一個(gè)頁(yè)表就只有512個(gè)頁(yè)表項(xiàng)了,這樣一個(gè)頁(yè)表就只能表達(dá)2M虛擬內(nèi)存了。尋址512個(gè)頁(yè)表項(xiàng)只需要9位就夠了。在x86 64上,虛擬地址有64位,但是64位的地址空間實(shí)在是太大了,所以我們只需要用其中一部分就行了。x86 64上有兩種虛擬地址位數(shù)可選,48位和57位,分別對(duì)應(yīng)著四級(jí)頁(yè)表和五級(jí)頁(yè)表。為啥是四級(jí)頁(yè)表和五級(jí)頁(yè)表呢?因?yàn)?8=9+9+9+12,57=9+9+9+9+12,12可以尋址一個(gè)頁(yè)面內(nèi)的每一個(gè)字節(jié),9可以尋址一級(jí)頁(yè)表中的512個(gè)頁(yè)表項(xiàng)。

Linux內(nèi)核最多支持五級(jí)頁(yè)表,在五級(jí)頁(yè)表體系中,每一級(jí)頁(yè)表分別叫做PGD、P4D、PUD、PMD、PTE。如果頁(yè)表不夠五級(jí)的,從第二級(jí)開始依次去掉一級(jí)。

頁(yè)表項(xiàng)是下一級(jí)頁(yè)表或者最終頁(yè)幀的物理地址,頁(yè)表也是一個(gè)頁(yè)幀,頁(yè)幀的地址都是4K對(duì)齊的,所以頁(yè)表項(xiàng)中的物理地址的最后12位一定都是0,既然都是0,那么就沒必要表示出來了,我們就可以把這12位拿來做其它用途了。下面我們來看一下x86的頁(yè)表項(xiàng)格式。22513f90-15e5-11ed-ba43-dac502259ad0.png這是32位的頁(yè)表項(xiàng)格式,其中12-31位是物理地址。

P,此頁(yè)表項(xiàng)是否有效,1代表有效,0代表無效,為0時(shí)其它字段無意義。

R/W,0代表只讀,1代表可讀寫。

U/S,0代表內(nèi)核頁(yè)表,1代表用戶頁(yè)面。

PWT,Page-level write-through

PCD,Page-level cache disable

A,Accessed; indicates whether software has accessed the page

D,Dirty; indicates whether software has written to the page

PAT,If the PAT is supported, indirectly determines the memory type used to access the page

G,Global; determines whether the translation is global

64位系統(tǒng)的頁(yè)表項(xiàng)格式和這個(gè)是一樣的,只不過是物理地址擴(kuò)展到了硬件支持的最高物理地址位數(shù)。

6.2 MMU

MMU是通過遍歷頁(yè)表把虛擬地址轉(zhuǎn)換為物理地址的。其過程如下所示:226551a6-15e5-11ed-ba43-dac502259ad0.pngCR3是CPU的寄存器,存放的是PGD的物理地址。MMU首先通過CR3獲取PGD的物理地址,然后以虛擬地址的31-22位為index,在PGD中找到相應(yīng)的頁(yè)表項(xiàng),先檢測(cè)頁(yè)表項(xiàng)的P是否存在,R/W是否有讀寫權(quán)限,U/S是否有訪問權(quán)限,如果檢測(cè)都通過了,則進(jìn)入下一步,如果沒通過則觸發(fā)缺頁(yè)異常。關(guān)于中斷與異常的基本原理請(qǐng)參看《深入理解Linux中斷機(jī)制》。如果檢測(cè)通過了,頁(yè)表項(xiàng)的31-12位代表PTE的物理地址,取虛擬地址中的21-12位作為index,在PTE中找到對(duì)應(yīng)的頁(yè)表項(xiàng),也是先各種檢測(cè),如果沒通過則觸發(fā)缺頁(yè)異常。如果通過了,則31-12位代表最終頁(yè)幀的物理地址,然后把虛擬地址的11-0位作為頁(yè)內(nèi)偏移加上去,就找到了虛擬地址對(duì)應(yīng)的物理地址了,然后送到MC進(jìn)行訪問。64位系統(tǒng)的邏輯和32位是相似的,只不過是多了幾級(jí)頁(yè)表而已,就不再贅述了。

一個(gè)進(jìn)程的所有頁(yè)表通過頁(yè)表項(xiàng)的指向構(gòu)成了一個(gè)頁(yè)表樹,頁(yè)表樹的根節(jié)點(diǎn)是PGD,根指針是CR3。頁(yè)表樹中所有的地址都是物理地址,MMU在遍歷頁(yè)表樹時(shí)使用物理地址可以直接訪問內(nèi)存。一個(gè)頁(yè)表只有加入了某個(gè)頁(yè)表樹才有意義,孤立的頁(yè)表是沒有意義的。每個(gè)進(jìn)程都有一個(gè)頁(yè)表樹,切換進(jìn)程就會(huì)切換頁(yè)表樹,切換頁(yè)表樹的方法是給CR3賦值,讓其指向當(dāng)前進(jìn)程的頁(yè)表樹的根節(jié)點(diǎn)也就是PGD。進(jìn)程的虛擬內(nèi)存空間分為兩部分,內(nèi)核空間和用戶空間,所有進(jìn)程的內(nèi)核空間都是共享的,所以所有進(jìn)程的頁(yè)表樹根節(jié)點(diǎn)的內(nèi)核子樹都相同。22759fb6-15e5-11ed-ba43-dac502259ad0.png

6.3 缺頁(yè)異常

MMU在解析虛擬內(nèi)存時(shí)如果發(fā)現(xiàn)了讀寫錯(cuò)誤或者權(quán)限錯(cuò)誤或者頁(yè)表項(xiàng)無效,就會(huì)觸發(fā)缺頁(yè)異常讓內(nèi)核來處理。下面我們來看一下x86的缺頁(yè)異常處理的過程。linux-src/arch/x86/mm/fault.c

DEFINE_IDTENTRY_RAW_ERRORCODE(exc_page_fault)
{
unsignedlongaddress=read_cr2();
irqentry_state_tstate;

prefetchw(¤t->mm->mmap_lock);

if(kvm_handle_async_pf(regs,(u32)address))
return;

state=irqentry_enter(regs);

instrumentation_begin();
handle_page_fault(regs,error_code,address);
instrumentation_end();

irqentry_exit(regs,state);
}

static__always_inlinevoid
handle_page_fault(structpt_regs*regs,unsignedlongerror_code,
unsignedlongaddress)
{
trace_page_fault_entries(regs,error_code,address);

if(unlikely(kmmio_fault(regs,address)))
return;

/*Wasthefaultonkernel-controlledpartoftheaddressspace?*/
if(unlikely(fault_in_kernel_space(address))){
do_kern_addr_fault(regs,error_code,address);
}else{
do_user_addr_fault(regs,error_code,address);
/*
*Useraddresspagefaulthandlingmighthavereenabled
*interrupts.Fixingupallpotentialexitpointsof
*do_user_addr_fault()anditsleaffunctionsisjustnot
*doablew/ocreatinganunholymessorturningthecode
*upsidedown.
*/
local_irq_disable();
}
}

staticvoid
do_kern_addr_fault(structpt_regs*regs,unsignedlonghw_error_code,
unsignedlongaddress)
{
WARN_ON_ONCE(hw_error_code&X86_PF_PK);

#ifdefCONFIG_X86_32
if(!(hw_error_code&(X86_PF_RSVD|X86_PF_USER|X86_PF_PROT))){
if(vmalloc_fault(address)>=0)
return;
}
#endif

if(is_f00f_bug(regs,hw_error_code,address))
return;

/*Wasthefaultspurious,causedbylazyTLBinvalidation?*/
if(spurious_kernel_fault(hw_error_code,address))
return;

/*kprobesdon'twanttohookthespuriousfaults:*/
if(WARN_ON_ONCE(kprobe_page_fault(regs,X86_TRAP_PF)))
return;

bad_area_nosemaphore(regs,hw_error_code,address);
}

staticinline
voiddo_user_addr_fault(structpt_regs*regs,
unsignedlongerror_code,
unsignedlongaddress)
{
structvm_area_struct*vma;
structtask_struct*tsk;
structmm_struct*mm;
vm_fault_tfault;
unsignedintflags=FAULT_FLAG_DEFAULT;

tsk=current;
mm=tsk->mm;

if(unlikely((error_code&(X86_PF_USER|X86_PF_INSTR))==X86_PF_INSTR)){
/*
*Whoops,thisiskernelmodecodetryingtoexecutefrom
*usermemory.UnlessthisisAMDerratum#93,which
*corruptsRIPsuchthatitlookslikeauseraddress,
*thisisunrecoverable.Don'teventrytolookupthe
*VMAorlookforextableentries.
*/
if(is_errata93(regs,address))
return;

page_fault_oops(regs,error_code,address);
return;
}

/*kprobesdon'twanttohookthespuriousfaults:*/
if(WARN_ON_ONCE(kprobe_page_fault(regs,X86_TRAP_PF)))
return;

/*
*Reservedbitsareneverexpectedtobeseton
*entriesintheuserportionofthepagetables.
*/
if(unlikely(error_code&X86_PF_RSVD))
pgtable_bad(regs,error_code,address);

/*
*IfSMAPison,checkforinvalidkernel(supervisor)accesstouser
*pagesintheuseraddressspace.TheoddcasehereisWRUSS,
*which,accordingtothepreliminarydocumentation,doesnotrespect
*SMAPandwillhavetheUSERbitsetso,inallcases,SMAP
*enforcementappearstobeconsistentwiththeUSERbit.
*/
if(unlikely(cpu_feature_enabled(X86_FEATURE_SMAP)&&
!(error_code&X86_PF_USER)&&
!(regs->flags&X86_EFLAGS_AC))){
/*
*Noextableentryhere.Thiswasakernelaccesstoan
*invalidpointer.get_kernel_nofault()willnotgethere.
*/
page_fault_oops(regs,error_code,address);
return;
}

/*
*Ifwe'reinaninterrupt,havenousercontextorarerunning
*inaregionwithpagefaultsdisabledthenwemustnottakethefault
*/
if(unlikely(faulthandler_disabled()||!mm)){
bad_area_nosemaphore(regs,error_code,address);
return;
}

/*
*It'ssafetoallowirq'saftercr2hasbeensavedandthe
*vmallocfaulthasbeenhandled.
*
*User-moderegisterscountasauseraccessevenforany
*potentialsystemfaultorCPUbuglet:
*/
if(user_mode(regs)){
local_irq_enable();
flags|=FAULT_FLAG_USER;
}else{
if(regs->flags&X86_EFLAGS_IF)
local_irq_enable();
}

perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS,1,regs,address);

if(error_code&X86_PF_WRITE)
flags|=FAULT_FLAG_WRITE;
if(error_code&X86_PF_INSTR)
flags|=FAULT_FLAG_INSTRUCTION;

#ifdefCONFIG_X86_64
/*
*Faultsinthevsyscallpagemightneedemulation.The
*vsyscallpageisatahighaddress(>PAGE_OFFSET),butis
*consideredtobepartoftheuseraddressspace.
*
*Thevsyscallpagedoesnothavea"real"VMA,sodothis
*emulationbeforewegosearchingforVMAs.
*
*PKRUneverrejectsinstructionfetches,sowedon'tneed
*toconsiderthePF_PKbit.
*/
if(is_vsyscall_vaddr(address)){
if(emulate_vsyscall(error_code,regs,address))
return;
}
#endif

/*
*Kernel-modeaccesstotheuseraddressspaceshouldonlyoccur
*onwell-definedsingleinstructionslistedintheexception
*tables.But,anerroneouskernelfaultoccurringoutsideoneof
*thoseareaswhichalsoholdsmmap_lockmightdeadlockattempting
*tovalidatethefaultagainsttheaddressspace.
*
*Onlydotheexpensiveexceptiontablesearchwhenwemightbeat
*riskofadeadlock.Thishappensifwe
*1.Failedtoacquiremmap_lock,and
*2.Theaccessdidnotoriginateinuserspace.
*/
if(unlikely(!mmap_read_trylock(mm))){
if(!user_mode(regs)&&!search_exception_tables(regs->ip)){
/*
*Faultfromcodeinkernelfrom
*whichwedonotexpectfaults.
*/
bad_area_nosemaphore(regs,error_code,address);
return;
}
retry:
mmap_read_lock(mm);
}else{
/*
*Theabovedown_read_trylock()mighthavesucceededin
*whichcasewe'llhavemissedthemight_sleep()from
*down_read():
*/
might_sleep();
}

vma=find_vma(mm,address);
if(unlikely(!vma)){
bad_area(regs,error_code,address);
return;
}
if(likely(vma->vm_start<=?address))
??goto?good_area;
?if?(unlikely(!(vma->vm_flags&VM_GROWSDOWN))){
bad_area(regs,error_code,address);
return;
}
if(unlikely(expand_stack(vma,address))){
bad_area(regs,error_code,address);
return;
}

/*
*Ok,wehaveagoodvm_areaforthismemoryaccess,so
*wecanhandleit..
*/
good_area:
if(unlikely(access_error(error_code,vma))){
bad_area_access_error(regs,error_code,address,vma);
return;
}

/*
*Ifforanyreasonatallwecouldn'thandlethefault,
*makesureweexitgracefullyratherthanendlesslyredo
*thefault.SinceweneversetFAULT_FLAG_RETRY_NOWAIT,if
*wegetVM_FAULT_RETRYback,themmap_lockhasbeenunlocked.
*
*Notethathandle_userfault()mayalsoreleaseandreacquiremmap_lock
*(andnotreturnwithVM_FAULT_RETRY),whenreturningtouserlandto
*repeatthepagefaultlaterwithaVM_FAULT_NOPAGEretval
*(potentiallyafterhandlinganypendingsignalduringthereturnto
*userland).Thereturntouserlandisidentifiedwhenever
*FAULT_FLAG_USER|FAULT_FLAG_KILLABLEarebothsetinflags.
*/
fault=handle_mm_fault(vma,address,flags,regs);

if(fault_signal_pending(fault,regs)){
/*
*Quickpathtorespondtosignals.Thecoremmcode
*hasunlockedthemmforusifwegethere.
*/
if(!user_mode(regs))
kernelmode_fixup_or_oops(regs,error_code,address,
SIGBUS,BUS_ADRERR,
ARCH_DEFAULT_PKEY);
return;
}

/*
*Ifweneedtoretrythemmap_lockhasalreadybeenreleased,
*andifthereisafatalsignalpendingthereisnoguarantee
*thatwemadeanyprogress.Handlethiscasefirst.
*/
if(unlikely((fault&VM_FAULT_RETRY)&&
(flags&FAULT_FLAG_ALLOW_RETRY))){
flags|=FAULT_FLAG_TRIED;
gotoretry;
}

mmap_read_unlock(mm);
if(likely(!(fault&VM_FAULT_ERROR)))
return;

if(fatal_signal_pending(current)&&!user_mode(regs)){
kernelmode_fixup_or_oops(regs,error_code,address,
0,0,ARCH_DEFAULT_PKEY);
return;
}

if(fault&VM_FAULT_OOM){
/*Kernelmode?Handleexceptionsordie:*/
if(!user_mode(regs)){
kernelmode_fixup_or_oops(regs,error_code,address,
SIGSEGV,SEGV_MAPERR,
ARCH_DEFAULT_PKEY);
return;
}

/*
*Weranoutofmemory,calltheOOMkiller,andreturnthe
*userspace(whichwillretrythefault,orkillusifwegot
*oom-killed):
*/
pagefault_out_of_memory();
}else{
if(fault&(VM_FAULT_SIGBUS|VM_FAULT_HWPOISON|
VM_FAULT_HWPOISON_LARGE))
do_sigbus(regs,error_code,address,fault);
elseif(fault&VM_FAULT_SIGSEGV)
bad_area_nosemaphore(regs,error_code,address);
else
BUG();
}
}

缺頁(yè)異常首先從CR2寄存器中讀取發(fā)生異常的虛擬內(nèi)存地址。然后根據(jù)此地址是在內(nèi)核空間還是在用戶空間,分別調(diào)用do_kern_addr_fault和do_user_addr_fault來處理。使用vmalloc時(shí)會(huì)出現(xiàn)內(nèi)核空間的缺頁(yè)異常。用戶空間地址的缺頁(yè)異常在做完各種檢測(cè)處理之后會(huì)調(diào)用所有架構(gòu)都通用的函數(shù)handle_mm_fault來處理。下面我們來看一下這個(gè)函數(shù)是怎么處理的。linux-src/mm/memory.c

vm_fault_thandle_mm_fault(structvm_area_struct*vma,unsignedlongaddress,
unsignedintflags,structpt_regs*regs)
{
vm_fault_tret;

__set_current_state(TASK_RUNNING);

if(!arch_vma_access_permitted(vma,flags&FAULT_FLAG_WRITE,
flags&FAULT_FLAG_INSTRUCTION,
flags&FAULT_FLAG_REMOTE))
returnVM_FAULT_SIGSEGV;

if(flags&FAULT_FLAG_USER)
mem_cgroup_enter_user_fault();

if(unlikely(is_vm_hugetlb_page(vma)))
ret=hugetlb_fault(vma->vm_mm,vma,address,flags);
else
ret=__handle_mm_fault(vma,address,flags);

returnret;
}

staticvm_fault_t__handle_mm_fault(structvm_area_struct*vma,
unsignedlongaddress,unsignedintflags)
{
structvm_faultvmf={
.vma=vma,
.address=address&PAGE_MASK,
.flags=flags,
.pgoff=linear_page_index(vma,address),
.gfp_mask=__get_fault_gfp_mask(vma),
};
unsignedintdirty=flags&FAULT_FLAG_WRITE;
structmm_struct*mm=vma->vm_mm;
pgd_t*pgd;
p4d_t*p4d;
vm_fault_tret;

pgd=pgd_offset(mm,address);
p4d=p4d_alloc(mm,pgd,address);
if(!p4d)
returnVM_FAULT_OOM;

vmf.pud=pud_alloc(mm,p4d,address);

returnhandle_pte_fault(&vmf);
}

staticvm_fault_thandle_pte_fault(structvm_fault*vmf)
{
pte_tentry;

if(!vmf->pte){
if(vma_is_anonymous(vmf->vma))
returndo_anonymous_page(vmf);
else
returndo_fault(vmf);
}

if(!pte_present(vmf->orig_pte))
returndo_swap_page(vmf);

if(pte_protnone(vmf->orig_pte)&&vma_is_accessible(vmf->vma))
returndo_numa_page(vmf);

vmf->ptl=pte_lockptr(vmf->vma->vm_mm,vmf->pmd);
spin_lock(vmf->ptl);
entry=vmf->orig_pte;
if(unlikely(!pte_same(*vmf->pte,entry))){
update_mmu_tlb(vmf->vma,vmf->address,vmf->pte);
gotounlock;
}
if(vmf->flags&FAULT_FLAG_WRITE){
if(!pte_write(entry))
returndo_wp_page(vmf);
entry=pte_mkdirty(entry);
}
entry=pte_mkyoung(entry);
if(ptep_set_access_flags(vmf->vma,vmf->address,vmf->pte,entry,
vmf->flags&FAULT_FLAG_WRITE)){
update_mmu_cache(vmf->vma,vmf->address,vmf->pte);
}else{
if(vmf->flags&FAULT_FLAG_TRIED)
gotounlock;
if(vmf->flags&FAULT_FLAG_WRITE)
flush_tlb_fix_spurious_fault(vmf->vma,vmf->address);
}
unlock:
pte_unmap_unlock(vmf->pte,vmf->ptl);
return0;
}

staticvm_fault_tdo_fault(structvm_fault*vmf)
{
structvm_area_struct*vma=vmf->vma;
structmm_struct*vm_mm=vma->vm_mm;
vm_fault_tret;

if(!vma->vm_ops->fault){
if(unlikely(!pmd_present(*vmf->pmd)))
ret=VM_FAULT_SIGBUS;
else{
vmf->pte=pte_offset_map_lock(vmf->vma->vm_mm,
vmf->pmd,
vmf->address,
&vmf->ptl);
if(unlikely(pte_none(*vmf->pte)))
ret=VM_FAULT_SIGBUS;
else
ret=VM_FAULT_NOPAGE;

pte_unmap_unlock(vmf->pte,vmf->ptl);
}
}elseif(!(vmf->flags&FAULT_FLAG_WRITE))
ret=do_read_fault(vmf);
elseif(!(vma->vm_flags&VM_SHARED))
ret=do_cow_fault(vmf);
else
ret=do_shared_fault(vmf);

if(vmf->prealloc_pte){
pte_free(vm_mm,vmf->prealloc_pte);
vmf->prealloc_pte=NULL;
}
returnret;
}

可以看到handle_mm_fault最終會(huì)調(diào)用handle_pte_fault進(jìn)行處理。在handle_pte_fault中,會(huì)根據(jù)缺頁(yè)的內(nèi)存的類型進(jìn)行相應(yīng)的處理。

七、虛擬內(nèi)存空間

CPU開啟了分頁(yè)內(nèi)存機(jī)制之后,就只能通過虛擬內(nèi)存來訪問內(nèi)存了。內(nèi)核通過構(gòu)建頁(yè)表樹來創(chuàng)建虛擬內(nèi)存空間,一個(gè)頁(yè)表樹對(duì)應(yīng)一個(gè)虛擬內(nèi)存空間。虛擬內(nèi)存空間又分為兩部分,內(nèi)核空間和用戶空間。所有的頁(yè)表樹都共享內(nèi)核空間,它們內(nèi)核頁(yè)表子樹是相同的。內(nèi)核空間和用戶空間不僅在數(shù)量上不同,在權(quán)限上不同,在構(gòu)建方式上也不同。內(nèi)核空間在系統(tǒng)全局都只有一個(gè),不僅在UP上是如此,在SMP上也是只有一個(gè),多個(gè)CPU共享同一個(gè)內(nèi)核空間。內(nèi)核空間是特權(quán)空間,可以執(zhí)行所有的操作,也可以訪問用戶空間。用戶空間是非特權(quán)空間,很多操作不能做,也不能隨意訪問內(nèi)核,唯一能訪問內(nèi)核的方式就是通過系統(tǒng)調(diào)用。內(nèi)核空間和用戶空間最大的不同是構(gòu)建方式。內(nèi)核空間是在系統(tǒng)啟動(dòng)時(shí)就構(gòu)建好的,是完整構(gòu)建的,物理內(nèi)存和虛擬內(nèi)存是直接一次就映射好的,而且是不會(huì)銷毀的,因?yàn)橄到y(tǒng)運(yùn)行著內(nèi)核就要一直存在。用戶空間是在創(chuàng)建進(jìn)程時(shí)構(gòu)建的,但是并沒有完整構(gòu)建,虛擬內(nèi)存到物理內(nèi)存的映射是隨著進(jìn)程的運(yùn)行通過觸發(fā)缺頁(yè)異常一步一步構(gòu)建的,而且在內(nèi)存回收時(shí)還有可能被解除映射,最后隨著進(jìn)程的死亡,用戶空間還會(huì)被銷毀。下面我們看個(gè)圖:2284d9c2-15e5-11ed-ba43-dac502259ad0.png這個(gè)圖是在講進(jìn)程調(diào)度時(shí)畫的圖,但是也能表明內(nèi)核空間和用戶空間的關(guān)系。下面我們?cè)賮砜匆幌聠蝹€(gè)進(jìn)程角度下內(nèi)核空間與用戶空間的關(guān)系圖。2292dc3e-15e5-11ed-ba43-dac502259ad0.png在32位系統(tǒng)上默認(rèn)是內(nèi)核占據(jù)上面1G虛擬空間,進(jìn)程占據(jù)下面3G虛擬空間,有config選項(xiàng)可以選擇其它比列,所有CPU架構(gòu)都是如此。在64位系統(tǒng)上,由于64位的地址空間實(shí)在是太大了,Linux并沒有使用全部的虛擬內(nèi)存空間,而是只使用其中一部分位數(shù)。使用的方法是把用戶空間的高位補(bǔ)0,內(nèi)核空間的高位補(bǔ)1,這樣從64位地址空間的角度來看就是只使用了兩段,中間留空,方便以后往中間擴(kuò)展。中間留空的是非法內(nèi)存空間,不能使用。具體使用多少位,高位如何補(bǔ)0,不同架構(gòu)的選擇是不同的。ARM64在4K頁(yè)面大小的情況下有39位和48位兩種虛擬地址空間的選擇。X86 64有48位和57位兩種虛擬地址空間的選擇。ARM64是內(nèi)核空間和用戶空間都有這么多的地址空間,x86 64是內(nèi)核空間和用戶空間平分這么多的地址空間,上圖中的大小也可以反應(yīng)出這一點(diǎn)。

7.1 內(nèi)核空間

系統(tǒng)在剛啟動(dòng)時(shí)肯定不可能直接就運(yùn)行在虛擬內(nèi)存之上。系統(tǒng)是先運(yùn)行在物理內(nèi)存上,然后去建立一部分恒等映射,恒等映射就是虛擬內(nèi)存的地址和物理內(nèi)存的地址相同的映射。恒等映射的范圍不是要覆蓋全部的物理內(nèi)存,而是夠當(dāng)時(shí)內(nèi)核的運(yùn)行就可以了。恒等映射建立好之后就會(huì)開啟分頁(yè)機(jī)制,此時(shí)CPU就運(yùn)行在虛擬內(nèi)存上了。然后內(nèi)核再進(jìn)一步構(gòu)建頁(yè)表,把內(nèi)核映射到其規(guī)定好的地方。最后內(nèi)核跳轉(zhuǎn)到其目標(biāo)虛擬地址的地方運(yùn)行,并把之前的恒等映射取消掉,現(xiàn)在內(nèi)核就完全運(yùn)行在虛擬內(nèi)存上了。

由于內(nèi)核是最先運(yùn)行的,內(nèi)核會(huì)把物理內(nèi)存線性映射到自己的空間中去,而且是要把所有的物理內(nèi)存都映射到內(nèi)核空間。如果內(nèi)核沒有把全部物理內(nèi)存都映射到內(nèi)核空間,那不是因?yàn)椴幌?,而是因?yàn)樽霾坏?。在x86 32上,內(nèi)核空間只有1G,扣除一些其它用途保留的128M空間,內(nèi)核能線性映射的空間只有896M,而物理內(nèi)存可以多達(dá)4G,是沒法都映射到內(nèi)核空間的。所以內(nèi)核會(huì)把小于896M的物理內(nèi)存都映射到內(nèi)核空間,大于896M的物理內(nèi)存作為高端內(nèi)存,可以動(dòng)態(tài)映射到內(nèi)核的vmalloc區(qū)。對(duì)于64位系統(tǒng),就不存在這個(gè)煩惱了,虛擬內(nèi)存空間遠(yuǎn)遠(yuǎn)大于物理內(nèi)存的數(shù)量,所以內(nèi)核會(huì)一下子把全部物理內(nèi)存都映射到內(nèi)核空間。

大家在這里可能有兩個(gè)誤解:一是認(rèn)為物理內(nèi)存映射就代表使用,不使用就不會(huì)映射,這是不對(duì)的,使用時(shí)肯定要映射,但是映射了不代表在使用,映射了可以先放在那,只有被內(nèi)存分配器分配出去的才算是被使用;二是物理內(nèi)存只會(huì)被內(nèi)核空間或者用戶空間兩者之一映射,誰使用了就映射到誰的空間中去,這也是不對(duì)的,對(duì)于用戶空間,只有其使用了物理內(nèi)存才會(huì)去映射,但是對(duì)于內(nèi)核空間,內(nèi)核空間是管理者,它把所有物理內(nèi)存都映射到自己的空間比較方便管理,而且映射了不代表使用。

64位和32位還有一個(gè)很大的不同。32位上是把小于896M的物理內(nèi)存都線性映射到從3G開始的內(nèi)核空間中去,32位上只有一個(gè)線性映射區(qū)間。64位上有兩個(gè)線性映射區(qū)間,一是把內(nèi)核代碼和數(shù)據(jù)所在的物理內(nèi)存映射到一個(gè)固定的地址區(qū)間中去,二是把所有物理內(nèi)存都映射到某一段內(nèi)存區(qū)間中去,顯然內(nèi)核本身所占用的物理內(nèi)存被映射了兩次。下面我們畫圖來看一看內(nèi)核空間的布局。22a94fdc-15e5-11ed-ba43-dac502259ad0.png32位的內(nèi)核空間布局比較簡(jiǎn)單,前896M是直接映射區(qū),后面是8M的的隔離區(qū),然后是大約100多M的vmalloc區(qū),再后面是持久映射區(qū)和固定映射區(qū),其位置和大小是由宏決定的。

64位的內(nèi)核空間布局比較復(fù)雜,而且不同的架構(gòu)之間差異非常大,我們以x86 64 48位虛擬地址為例說一下。圖中一列畫不下,分成了兩列,我們從48位-1看起,首先是由一個(gè)8T的空洞,然后是LDT remap,然后是直接映射區(qū)有64T,用來映射所有的物理內(nèi)存,目前來說對(duì)于絕大部分計(jì)算機(jī)都?jí)蛴昧?,然后?.5T的空洞,然后是vmalloc和ioremap區(qū)有32T,然后是1T的空洞,然后是vmemmap區(qū)有1T,vmemmap就是我們前面所講的所有頁(yè)面描述符的數(shù)組,然后是1T的空洞,然后是KASAN的影子內(nèi)存有16T,緊接著再看48位-2,首先是2T的空洞,然后是cpu_entry_area,然后是0.5T的空洞,然后是%esp fixup stack,然后是444G的空洞,然后是EFI的映射區(qū)域,然后是2T的空洞,然后是內(nèi)核的映射區(qū)有512M,然后是ko的映射區(qū)有1520M,然后是fixmap和vsyscall,最后是2M的空洞。如果開啟了kaslr,內(nèi)核和映射區(qū)會(huì)增加512M,相應(yīng)的ko的映射區(qū)會(huì)減少512M。

64位的內(nèi)核空間中有直接映射區(qū)和內(nèi)核映射區(qū)兩個(gè)線性映射區(qū),這兩個(gè)區(qū)域都是線性映射,只不過是映射的起點(diǎn)不同。為什么要把內(nèi)核再單獨(dú)映射一遍呢?而且既然直接映射區(qū)已經(jīng)把所有的物理內(nèi)存都映射一遍了,那么為什么還有這么多的內(nèi)存映射區(qū)呢?直接映射區(qū)的存在是為了方便管理物理內(nèi)存,因?yàn)樗臀锢韮?nèi)存只差一個(gè)固定值。各種其它映射區(qū)的存在是為了方便內(nèi)核的運(yùn)行和使用。比如vmalloc區(qū)是為了方便進(jìn)行隨機(jī)映射,當(dāng)內(nèi)存碎片化比較嚴(yán)重,我們需要的內(nèi)存又不要求物理上必須連續(xù)時(shí),就可以使用vmalloc,它能把物理上不連續(xù)的內(nèi)存映射到連續(xù)的虛擬內(nèi)存上。vmemmap區(qū)域是為了在物理內(nèi)存有較大空洞時(shí),又能夠使得memmap在虛擬內(nèi)存上看起來是個(gè)完整的數(shù)組。這些都方便了內(nèi)核的操作。

對(duì)比32位和64位的虛擬內(nèi)存空間可以發(fā)現(xiàn),空間大了就是比較闊綽,動(dòng)不動(dòng)就來個(gè)1T、2T的空洞。

7.2 用戶空間

用戶空間的邏輯和內(nèi)核空間就完全不同了。首先用戶空間是進(jìn)程創(chuàng)建時(shí)動(dòng)態(tài)創(chuàng)建的。其次,對(duì)于內(nèi)核,虛擬內(nèi)存和物理內(nèi)存是提前映射好的,就算是vmalloc,也是分配時(shí)就映射好的,對(duì)于用戶空間,物理內(nèi)存的分配和虛擬內(nèi)存的分配是割裂的,用戶空間總是先分配虛擬內(nèi)存不分配物理內(nèi)存,物理內(nèi)存總是拖到最后一刻才去分配。而且對(duì)于進(jìn)程本身來說,它只能分配虛擬內(nèi)存,物理內(nèi)存的分配對(duì)它來說是不可見的,或者說是透明的。當(dāng)進(jìn)程去使用某一個(gè)虛擬內(nèi)存時(shí)如果發(fā)現(xiàn)還沒有分配物理內(nèi)存則會(huì)觸發(fā)缺頁(yè)異常,此時(shí)才會(huì)去分配物理內(nèi)存并映射上,然后再去重新執(zhí)行剛才的指令,這一切對(duì)進(jìn)程來說都是透明的,進(jìn)程感知不到。

管理進(jìn)程空間的結(jié)構(gòu)體是mm_struct,我們先來看一下(代碼有所刪減):linux-src/include/linux/mm_types.h

structmm_struct{
struct{
structvm_area_struct*mmap;/*listofVMAs*/
structrb_rootmm_rb;
u64vmacache_seqnum;/*per-threadvmacache*/
#ifdefCONFIG_MMU
unsignedlong(*get_unmapped_area)(structfile*filp,
unsignedlongaddr,unsignedlonglen,
unsignedlongpgoff,unsignedlongflags);
#endif
unsignedlongmmap_base;/*baseofmmaparea*/
unsignedlongmmap_legacy_base;/*baseofmmapareainbottom-upallocations*/

unsignedlongtask_size;/*sizeoftaskvmspace*/
unsignedlonghighest_vm_end;/*highestvmaendaddress*/
pgd_t*pgd;

atomic_tmm_users;
atomic_tmm_count;

#ifdefCONFIG_MMU
atomic_long_tpgtables_bytes;/*PTEpagetablepages*/
#endif
intmap_count;/*numberofVMAs*/
spinlock_tpage_table_lock;
structrw_semaphoremmap_lock;
structlist_headmmlist;

unsignedlonghiwater_rss;/*High-watermarkofRSSusage*/
unsignedlonghiwater_vm;/*High-watervirtualmemoryusage*/
unsignedlongtotal_vm;/*Totalpagesmapped*/
unsignedlonglocked_vm;/*PagesthathavePG_mlockedset*/
atomic64_tpinned_vm;/*Refcountpermanentlyincreased*/
unsignedlongdata_vm;/*VM_WRITE&~VM_SHARED&~VM_STACK*/
unsignedlongexec_vm;/*VM_EXEC&~VM_WRITE&~VM_STACK*/
unsignedlongstack_vm;/*VM_STACK*/
unsignedlongdef_flags;

unsignedlongstart_code,end_code,start_data,end_data;
unsignedlongstart_brk,brk,start_stack;
unsignedlongarg_start,arg_end,env_start,env_end;
unsignedlongsaved_auxv[AT_VECTOR_SIZE];/*for/proc/PID/auxv*/

structmm_rss_statrss_stat;
structlinux_binfmt*binfmt;
mm_context_tcontext;
unsignedlongflags;/*Mustuseatomicbitopstoaccess*/
structcore_state*core_state;/*coredumpingsupport*/
structuser_namespace*user_ns;

/*storereftofile/proc//exesymlinkpointsto*/
structfile__rcu*exe_file;

}__randomize_layout;

unsignedlongcpu_bitmap[];
};

可以看到mm_struct有很多管理數(shù)據(jù),其中最重要的兩個(gè)是mmap和pgd,它們一個(gè)代表虛擬內(nèi)存的分配情況,一個(gè)代表物理內(nèi)存的分配情況。pgd就是我們前面所說的頁(yè)表樹的根指針,當(dāng)要運(yùn)行我們的進(jìn)程時(shí)就需要把pgd寫到CR3上,這樣MMU用我們頁(yè)表樹來解析虛擬地址就能訪問到我們的物理內(nèi)存了。不過pgd的值是虛擬內(nèi)存,CR3需要物理內(nèi)存,所以把pgd寫到CR3上時(shí)還需要把pgd轉(zhuǎn)化為物理地址。mmap是vm_area_struct(vma)的鏈表,它代表的是用戶空間虛擬內(nèi)存的分配情況。用戶空間只能分配虛擬內(nèi)存,物理內(nèi)存的分配是自動(dòng)的透明的。用戶空間想要分配虛擬內(nèi)存,最終的唯一的方法就是調(diào)用函數(shù)mmap來生成一個(gè)vma,有了vma就代表虛擬內(nèi)存分配了,vma會(huì)記錄虛擬內(nèi)存的起點(diǎn)、大小和權(quán)限等信息。有了vma,缺頁(yè)異常在處理時(shí)就有了依據(jù)。如果造成缺頁(yè)異常的虛擬地址不再任何vma的區(qū)間中,則說明這是一個(gè)非法的虛擬地址,缺頁(yè)異常就會(huì)給進(jìn)程發(fā)SIGSEGV。如果異常地址在某個(gè)vma區(qū)間中并且權(quán)限也對(duì)的話,那么說明這個(gè)虛擬地址進(jìn)程已經(jīng)分配了,是個(gè)合法的虛擬地址,此時(shí)缺頁(yè)異常就會(huì)去分配物理內(nèi)存并映射到虛擬內(nèi)存上。

調(diào)用函數(shù)mmap生成vma的方式有兩種,一是內(nèi)核為進(jìn)程調(diào)用,就是在內(nèi)核里直接調(diào)用了,二是進(jìn)程自己調(diào)用,那就是通過系統(tǒng)調(diào)用來調(diào)用mmap了。生成的vma也有兩種類型,文件映射vma和匿名映射vma,哪種類型取決于mmap的參數(shù)。文件映射vma,在發(fā)生缺頁(yè)異常時(shí),分配的物理內(nèi)存要用文件的內(nèi)容來初始化,其物理內(nèi)存也被叫做文件頁(yè)。匿名映射vma,在發(fā)生缺頁(yè)異常時(shí),直接分配物理內(nèi)存并初始化為0,其物理內(nèi)存也被叫做匿名頁(yè)。

一個(gè)進(jìn)程的text段、data段、堆區(qū)、棧區(qū)都是vma,這些vma都是內(nèi)核為進(jìn)程調(diào)用mmap生成的。進(jìn)程自己也可以調(diào)用mmap來分配虛擬內(nèi)存。堆區(qū)和棧區(qū)是比較特殊的vma,棧區(qū)的vma會(huì)隨著棧的增長(zhǎng)而自動(dòng)增長(zhǎng),堆區(qū)的vma則需要進(jìn)程用系統(tǒng)調(diào)用brk或者sbrk來增長(zhǎng)。不過我們?cè)诜峙涠褍?nèi)存的時(shí)候都不是直接使用的系統(tǒng)調(diào)用,而是使用libc給我們提供的malloc接口,有了malloc接口,我們分配釋放堆內(nèi)存就方便多了。Malloc接口的實(shí)現(xiàn)叫做malloc庫(kù),目前比較流行的malloc庫(kù)有ptmalloc、jemalloc、scudo等。

八、內(nèi)存統(tǒng)計(jì)

暫略

8.1 總體統(tǒng)計(jì)

8.2 進(jìn)程統(tǒng)計(jì)

九、總結(jié)回顧

前面我們講了這么多的東西,現(xiàn)在再來總結(jié)回顧一下。首先我們?cè)僦匦驴匆幌翷inux的內(nèi)存管理體系圖,我們邊看這個(gè)圖邊進(jìn)行總結(jié)。21746c96-15e5-11ed-ba43-dac502259ad0.png首先要強(qiáng)調(diào)的一點(diǎn)是,這么多的東西,都是在內(nèi)核里進(jìn)行管理的,內(nèi)核是可以操作這一切的。但是對(duì)進(jìn)程來說這些基本都是透明的,進(jìn)程只能看到自己的虛擬內(nèi)存空間,只能在自己空間里分配虛擬內(nèi)存,其它的,進(jìn)程什么也看不見、管不著。

目前絕大部分的操作系統(tǒng)采用的內(nèi)存管理模式都是以分頁(yè)內(nèi)存為基礎(chǔ)的虛擬內(nèi)存機(jī)制。虛擬內(nèi)存機(jī)制的中心是MMU和頁(yè)表,MMU是需要硬件提供的,頁(yè)表是需要軟件來操作的。虛擬內(nèi)存左邊連著物理內(nèi)存管理,右邊連著虛擬內(nèi)存空間,左邊和右邊有著復(fù)雜的關(guān)系。物理內(nèi)存管理中,首先是對(duì)物理內(nèi)存的三級(jí)區(qū)劃,然后是對(duì)物理內(nèi)存的三級(jí)分配體系,最后是物理內(nèi)存的回收。虛擬內(nèi)存空間中,首先可以分為內(nèi)核空間和用戶空間,兩者在很多方面都有著顯著的不同。內(nèi)核空間是內(nèi)核運(yùn)行的地方,只有一份,永久存在,有特權(quán),而且其內(nèi)存映射是提前映射、線性映射,不會(huì)換頁(yè)。用戶空間是進(jìn)程運(yùn)行的地方,有N份,隨著進(jìn)程的誕生而創(chuàng)建、進(jìn)程的死亡而銷毀。用戶空間中虛擬內(nèi)存的分配和物理內(nèi)存的分配是分開的,進(jìn)程只能分配虛擬內(nèi)存,物理內(nèi)存的分配是在進(jìn)程運(yùn)行過程中動(dòng)態(tài)且透明地分配的。用戶空間的物理內(nèi)存可以分為文件頁(yè)和匿名頁(yè),頁(yè)幀回收的主要邏輯就是圍繞文件頁(yè)和匿名頁(yè)展開的。

審核編輯:彭靜
聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • Linux
    +關(guān)注

    關(guān)注

    87

    文章

    11350

    瀏覽量

    210460
  • 軟件
    +關(guān)注

    關(guān)注

    69

    文章

    5021

    瀏覽量

    88111
  • 內(nèi)存管理
    +關(guān)注

    關(guān)注

    0

    文章

    168

    瀏覽量

    14193

原文標(biāo)題:深入理解Linux內(nèi)存管理

文章出處:【微信號(hào):LinuxDev,微信公眾號(hào):Linux閱碼場(chǎng)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    Linux內(nèi)存管理是什么,Linux內(nèi)存管理詳解

    Linux內(nèi)存管理 Linux內(nèi)存管理是一個(gè)非常復(fù)雜的過程,主要分成兩個(gè)大的部分:內(nèi)核的
    的頭像 發(fā)表于 05-11 17:54 ?6164次閱讀
    <b class='flag-5'>Linux</b>的<b class='flag-5'>內(nèi)存</b><b class='flag-5'>管理</b>是什么,<b class='flag-5'>Linux</b>的<b class='flag-5'>內(nèi)存</b><b class='flag-5'>管理</b>詳解

    深度解析Linux內(nèi)存管理體系

    Linux內(nèi)存管理的整體模式是虛擬內(nèi)存管理(分頁(yè)內(nèi)存管理
    發(fā)表于 08-06 16:55 ?1782次閱讀

    關(guān)于Linux內(nèi)存管理的詳細(xì)介紹

    Linux內(nèi)存管理是指對(duì)系統(tǒng)內(nèi)存的分配、釋放、映射、管理、交換、壓縮等一系列操作的管理。在
    發(fā)表于 03-06 09:28 ?1092次閱讀

    美國(guó)GPS管理體系

      文 初一  一、GPS管理體系及職責(zé)  1. GPS管理現(xiàn)行體系  2004年美國(guó)國(guó)家天基導(dǎo)航、定位、授時(shí)(PNT)政策中要求成立國(guó)家天基PNT執(zhí)行委員會(huì),以代替已有的部際GPS聯(lián)席執(zhí)行委員會(huì)
    發(fā)表于 07-16 08:23

    喜訊!熱烈祝賀武漢芯源半導(dǎo)體順利通過CQC質(zhì)量管理體系認(rèn)證

    近日,經(jīng)過CQC中國(guó)質(zhì)量認(rèn)證中心全面、嚴(yán)格、系統(tǒng)的審查考核,武漢芯源半導(dǎo)體順利通過ISO 14001:2015環(huán)境管理體系認(rèn)證、ISO 45001:2018職業(yè)健康安全管理體系認(rèn)證、ISO 9001
    發(fā)表于 01-10 14:43

    基于知識(shí)流的政府信息發(fā)布知識(shí)管理體系

    針對(duì)政府信息發(fā)布實(shí)施知識(shí)管理的薄弱環(huán)節(jié),從系統(tǒng)工程角度出發(fā),在簡(jiǎn)述政府信息發(fā)布知識(shí)管理體系基本功能的基礎(chǔ)上,提出基于知識(shí)流的知識(shí)管理體系框架和主要設(shè)計(jì)原則。
    發(fā)表于 01-15 14:38 ?8次下載

    基于績(jī)效管理管理體系審核

    基于績(jī)效管理管理體系審核 隨著管理體系認(rèn)證市場(chǎng)發(fā)展的理性回歸,越來越多的組織不再純粹出于獲得一紙證書而尋求或保持認(rèn)證,而是希望借助外部第三方審核來發(fā)現(xiàn)
    發(fā)表于 04-23 09:20 ?20次下載

    什么是HSE管理體系

    什么是HSE管理體系 健康、安全與環(huán)境管理體系簡(jiǎn)稱為HSE管理體系,或簡(jiǎn)單地用HSE MS(Health Safety and Enviromen Management System)表示 。HSE MS是近幾年出現(xiàn)的國(guó)際石油
    發(fā)表于 04-10 12:33 ?4686次閱讀

    ISO14000環(huán)境管理體系知識(shí)介紹

    ISO14000環(huán)境管理體系知識(shí)介紹 一、什么是ISO(國(guó)際標(biāo)準(zhǔn)化組織)
    發(fā)表于 10-22 10:17 ?2062次閱讀

    linux內(nèi)存管理機(jī)制淺析

    本內(nèi)容介紹了arm linux內(nèi)存管理機(jī)制,詳細(xì)說明了linux內(nèi)核內(nèi)存
    發(fā)表于 12-19 14:09 ?73次下載
    <b class='flag-5'>linux</b><b class='flag-5'>內(nèi)存</b><b class='flag-5'>管理</b>機(jī)制淺析

    如何在PCBA中貫徹質(zhì)量管理體系

    很多公司可以輕松的拿到ISO9001質(zhì)量管理體系認(rèn)證。但是,證書并不能代表公司內(nèi)部的管理體系符合規(guī)范。如何真正意義上將ISO9001質(zhì)量管理體系,在整個(gè)PCBA加工制程中貫徹實(shí)施,實(shí)現(xiàn)對(duì)品質(zhì)效率和
    發(fā)表于 03-15 11:36 ?1852次閱讀

    Linux 內(nèi)存管理總結(jié)

    一、Linux內(nèi)存管理概述 Linux內(nèi)存管理是指對(duì)系統(tǒng)內(nèi)存
    的頭像 發(fā)表于 11-10 14:58 ?596次閱讀
    <b class='flag-5'>Linux</b> <b class='flag-5'>內(nèi)存</b><b class='flag-5'>管理</b>總結(jié)

    本源量子獲得質(zhì)量管理體系認(rèn)證證書

    近期,本源量子計(jì)算科技(合肥)股份有限公司繼2021年首次取得質(zhì)量管理體系認(rèn)證證書后,再次通過了ISO9001:2015質(zhì)量管理體系認(rèn)證,并成功獲得了質(zhì)量管理體系認(rèn)證證書。此次質(zhì)量管理體系
    的頭像 發(fā)表于 10-25 08:06 ?414次閱讀
    本源量子獲得質(zhì)量<b class='flag-5'>管理體系</b>認(rèn)證證書

    設(shè)備管理體系實(shí)施指南

    設(shè)備管理體系在保障生產(chǎn)運(yùn)營(yíng)基石、搭建交流學(xué)習(xí)平臺(tái)以及助力打造標(biāo)桿典范等方面具有重要意義。組織應(yīng)全面實(shí)施設(shè)備管理體系,提高設(shè)備管理效率和效果。
    的頭像 發(fā)表于 12-18 10:38 ?248次閱讀
    設(shè)備<b class='flag-5'>管理體系</b>實(shí)施指南

    華為通過BSI全球首批漏洞管理體系認(rèn)證

    近日,華為通過全球權(quán)威標(biāo)準(zhǔn)機(jī)構(gòu)BSI漏洞管理體系認(rèn)證,涵蓋了ISO/IEC 27001信息安全管理、ISO/IEC 29147漏洞披露及ISO/IEC 30111漏洞處理流程三大國(guó)際標(biāo)準(zhǔn)。華為憑借其
    的頭像 發(fā)表于 01-16 11:15 ?230次閱讀
    華為通過BSI全球首批漏洞<b class='flag-5'>管理體系</b>認(rèn)證