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

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

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

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

Linux閱碼場 ? 來源:Linux閱碼場 ? 作者:Linux閱碼場 ? 2022-08-08 09:28 ? 次閱讀

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

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

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

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

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

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

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

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

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

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

2.5 三級區(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)的實現(xiàn)

3.2 Slab Allocator

3.2.1 Slab接口

3.2.2 Slab實現(xiàn)

3.2.3 Slob實現(xiàn)

3.2.4 Slub實現(xiàn)

3.3 Kmalloc

3.4 Vmalloc

3.5 CMA

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

4.2 頁幀回收

4.3 交換區(qū)

4.4 OOM Killer

5.1 ZRAM

5.2 ZSwap

5.3 ZCache

6.1 頁表

6.2 MMU

6.3 缺頁異常

7.1 內(nèi)核空間

7.2 用戶空間

8.1 總體統(tǒng)計

8.2 進程統(tǒng)計

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

下面我們先來看一下節(jié)點描述符的定義。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;

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

#ifndefCONFIG_NUMA
structpglist_data__refdatacontig_page_data;
EXPORT_SYMBOL(contig_page_data);
#endif

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

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

對于NUMA,內(nèi)核會定義內(nèi)存節(jié)點指針數(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é)點的代碼如下:linux-src/arch/x86/include/asm/mmzone_64.h

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

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

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

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

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

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

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

ZONE_HIGHMEM:高端內(nèi)存,由配置項CONFIG_HIGHMEM決定是否存在。只在32位系統(tǒng)上有,這是因為32位系統(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)存進行特殊處理。對于64位系統(tǒng),從理論上來說,內(nèi)核空間最大263-1,物理內(nèi)存最大264,好像內(nèi)核空間還是不夠用。但是從現(xiàn)實來說,內(nèi)核空間的一般配置為247,高達128T,物理內(nèi)存暫時還遠遠沒有這么多。所以從現(xiàn)實的角度來說,64位系統(tǒng)是不需要高端內(nèi)存區(qū)域的。

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

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

下面我們先來看一下這幾個內(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
};

我們再來看一下區(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)體中各個字段的含義我們在用到的時候再進行解釋。

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

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

物理內(nèi)存頁面也叫做頁幀。物理內(nèi)存從開始起每4K、4K的,構(gòu)成一個個頁幀,這些頁幀的編號依次是0、1、2、3......。頁幀的編號也叫做pfn(page frame number)。很顯然,一個頁幀的物理地址和它的pfn有一個簡單的數(shù)學關(guān)系,那就是其物理地址除以4K就是其pfn,其pfn乘以4K就是其物理地址。由于4K是2的整數(shù)次冪,所以這個乘除運算可以轉(zhuǎ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的值在大部分平臺上都是等于12,2的12次方冪正好就是4K。

下面我們來看一下頁面描述符的定義。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;

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

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

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

最開始的時候是沒有內(nèi)存模型的,后來有了其它的內(nèi)存模型,這個最開始的情況就被叫做平坦內(nèi)存模型(Flat Memory)。平坦內(nèi)存模型看到的物理內(nèi)存就是連續(xù)的沒有空洞的內(nèi)存。后來為了處理物理內(nèi)存有空洞的情況以及內(nèi)存熱插拔問題,又開發(fā)出了離散內(nèi)存模型(Discontiguous Memory)。但是離散內(nèi)存模型的實現(xiàn)復用了NUMA的代碼,導致NUMA和內(nèi)存模型的耦合,實際上二者在邏輯上是正交的。內(nèi)核后來又開發(fā)了稀疏內(nèi)存模型(Sparse Memory),其實現(xiàn)和NUMA不再耦合在一起了,而且稀疏內(nèi)存模型能同時處理平坦內(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)變成了唯一的可選項了,也就是必選內(nèi)存模型。

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

我們來看一下頁面描述符數(shù)組指針的定義和指針與頁幀號之間的轉(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 三級區(qū)劃關(guān)系

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

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

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

當我們把物理內(nèi)存區(qū)劃弄明白之后,再來學習物理內(nèi)存分配就比較容易了。物理內(nèi)存分配最底層的是頁幀分配。頁幀分配的分配單元是區(qū)域,分配粒度是頁面。如何進行頁幀分配呢?Linux采取的算法叫做伙伴系統(tǒng)(buddy system)。只有伙伴系統(tǒng)還不行,因為伙伴系統(tǒng)進行的是大粒度的分配,我們還需要批發(fā)與零售,于是便有了slab allocator和kmalloc。這幾種內(nèi)存分配方法分配的都是線性映射的內(nèi)存,當系統(tǒng)連續(xù)內(nèi)存不足的時候,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)存接口,還能同時減少外部碎片和內(nèi)部碎片。

3.1 Buddy System

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

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

伙伴系統(tǒng)管理的內(nèi)存并不是全部的物理內(nèi)存,而是內(nèi)核在完成初步的初始化之后的未使用內(nèi)存。內(nèi)核在剛啟動的時候有一個簡單的早期內(nèi)存管理器,它會記錄系統(tǒng)的所有物理內(nèi)存以及在它之前就被占用的內(nèi)存,并為內(nèi)核提供早期的內(nèi)存分配服務(wù)。當內(nèi)核的基礎(chǔ)初始化完成之后,它就會把所有剩余可用的物理內(nèi)存交給伙伴系統(tǒng)來管理,然后自己就退出歷史舞臺了。早期內(nèi)存管理器會首先嘗試把頁幀以10階頁塊的方式加入伙伴系統(tǒng),不夠10階的以9階頁塊的方式加入伙伴系統(tǒng),以此類推,直到以0階頁塊的方式把所有可用頁幀都加入到伙伴系統(tǒng)。顯而易見,內(nèi)核剛啟動的時候高階頁塊比較多,低階頁塊比較少。早期內(nèi)存管理器以前是bootmem,后來是bootmem和memblock共存,可以通過config選擇使用哪一個,現(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,因為從0到10有11個數(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的定義非常簡單,就是由MIGRATE_TYPES個鏈表組成,鏈表連接的是同一個階的遷移類型相同的頁幀。遷移類型是內(nèi)核為了減少內(nèi)存碎片而提出的技術(shù),不同區(qū)域的頁塊有不同的默認遷移類型,比如DMA、NORMAL默認都是不可遷移(MIGRATE_UNMOVABLE)的頁塊,HIGHMEM、MOVABLE區(qū)域默認都是可遷移(MIGRATE_UNMOVABLE)的頁塊。我們申請的內(nèi)存有時候是不可移動的內(nèi)存,比如內(nèi)核線性映射的內(nèi)存,有時候是可以移動的內(nèi)存,比如用戶空間缺頁異常分配的內(nèi)存。我們把不同遷移類型的內(nèi)存分開進行分配,在進行內(nèi)存碎片整理的時候就比較方便,不會出現(xiàn)一片可移動內(nèi)存中夾著一個不可移動的內(nèi)存(這種情況就很礙事)。如果要分配的遷移類型的內(nèi)存不足時就需要從其它的遷移類型中進行盜頁了。內(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
};

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

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

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

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

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

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

下面我們來看一下伙伴系統(tǒng)的接口。伙伴系統(tǒng)提供了兩類接口,一類是返回頁表描述符的,一類是返回虛擬內(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)

釋放的接口很簡單,只需要一個頁表描述符指針加一個階數(shù)。分配的接口中,有的會指定nodeid,就從那個節(jié)點中分配內(nèi)存。不指定nodeid的接口,如果是在UMA中,那就從唯一的節(jié)點中分配內(nèi)存,如果是NUMA,會按照一定的策略選擇在哪個節(jié)點中分配內(nèi)存。最復雜的參數(shù)是gfp,gfp是標記參數(shù),可以分為兩類標記,一類是指定分配區(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個是指定分配區(qū)域的,內(nèi)核里一共定義了6類區(qū)域,為啥只有4個指示符呢?因為ZONE_DEVICE有特殊用途,不在一般的內(nèi)存分配管理中,當不指定區(qū)域類型時默認就是ZONE_NORMAL,所以4個就夠了。是不是指定了哪個區(qū)域就只能在哪個區(qū)域分配內(nèi)存呢,不是的。每個區(qū)域都有后備區(qū)域,當其內(nèi)存不足時,會從其后備區(qū)域中分配內(nèi)存。后備區(qū)域是在節(jié)點描述符中定義,我們來看一下: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ū)域只有一個鏈表,就是本節(jié)點內(nèi)的后備區(qū)域,在NUMA中后備區(qū)域有兩個鏈表,包括本節(jié)點內(nèi)的后備區(qū)域和其它節(jié)點的后備區(qū)域。這些后備區(qū)域是在內(nèi)核啟動時初始化的。對于本節(jié)點的后備區(qū)域,是按照區(qū)域類型的id排列的,高id的排在前面,低id的排在后面,后面的是前面的后備,前面的區(qū)域內(nèi)存不足時可以從后面的區(qū)域里分配內(nèi)存,反過來則不行。比如MOVABLE區(qū)域的內(nèi)存不足時可以從NORMAL區(qū)域來分配,NORMAL區(qū)域的內(nèi)存不足時可以從DMA區(qū)域來分配,反過來則不行。對于其它節(jié)點的后備區(qū)域,除了會符合前面的規(guī)則之外,還會考慮后備區(qū)域是按照節(jié)點優(yōu)先的順序來排列還是按照區(qū)域類型優(yōu)先的順序來排列。

下面我們再來看一下分配行為的flag都是什么含義。

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

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

__GFP_IO:可以進行磁盤IO操作。

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

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

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

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

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

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

__GFP_ZERO:把要分配的頁清零。

還有一些其它的flag就不再一一進行介紹了。

如果我們每次分配內(nèi)存都把這些flag一一進行組合,那就太麻煩了,所以系統(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。

我們再來看一下直接返回虛擬內(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)存,因為HIGHMEM中的內(nèi)存不是直接映射到內(nèi)核空間中去的。除此之外這個接口和前面的沒有區(qū)別,其參數(shù)函數(shù)也跟前面的一樣,就不再贅述了。

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

下面我們再來看一下伙伴系統(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)的所有分配接口最終都會使用__alloc_pages這個函數(shù)來進行分配。對這個函數(shù)進行刪減之后,其邏輯也比較簡單清晰,先使用函數(shù)get_page_from_freelist直接從free_area中進行分配,如果分配不到就使用函數(shù) __alloc_pages_slowpath進行內(nèi)存回收。內(nèi)存回收的內(nèi)容在下一章里面講。

3.2 Slab Allocator

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

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

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*);

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

3.2.2 Slab實現(xiàn)

暫略

3.2.3 Slob實現(xiàn)

暫略

3.2.4 Slub實現(xiàn)

暫略

3.3 Kmalloc

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

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

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

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

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

3.4 Vmalloc

暫略

3.5 CMA

暫略

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

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

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

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

4.2 頁幀回收

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

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

在回收文件頁和匿名頁的時候是需要把它們的虛擬內(nèi)存映射給解除掉的。由于一個物理頁幀可能會同時映射到多個虛擬內(nèi)存上,包括映射到多個進程或者同一個進程的不同地址上,所以我們需要找到一個物理頁幀所映射的所有虛擬內(nèi)存。如何找到物理內(nèi)存所映射的虛擬內(nèi)存呢,這個過程就叫做反向映射(rmap)。之所以叫反向映射是因為正常的映射都是從虛擬內(nèi)存映射到物理內(nèi)存。

4.3 交換區(qū)

暫略

4.4 OOM Killer

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

下面我們來看一下out_of_memory函數(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ù)的代碼邏輯還是非常簡單清晰的,總共有兩步,1.先選擇一個要殺死的進程,2.殺死它。oom_kill_process函數(shù)的目的很簡單,但是實現(xiàn)過程也有點復雜,這里就不展開分析了,大家可以自行去看一下代碼。我們重點分析一下select_bad_process函數(shù)的邏輯,select_bad_process主要是依靠oom_score來進行進程選擇的。我們先來看一下和oom_score有關(guān)的三個文件。

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

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

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

下面我們來分析一下select_bad_process函數(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值,這個值是用來比較所有的oom_score值,最后誰的值最大就選中哪個進程。然后函數(shù)已經(jīng)遍歷所有進程,計算其oom_score,并更新chosen_points和被選中的task,有點類似于選擇排序。我們繼續(xù)看oom_evaluate_task函數(shù)是如何評估每個進程的函數(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ù)首先會跳過所有不適合kill的進程,如init進程、內(nèi)核線程、OOM_DISABLE進程等。然后通過select_bad_process算出此進程的得分points 也就是oom_score,并和上一次的勝出進程進行比較,如果小的會話就會goto next 返回,如果大的話就會更新oc->chosen 的task 和 chosen_points 也就是目前最高的oom_score。那么 oom_badness是如何計算的呢?

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

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

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

暫略

5.1 ZRAM

5.2 ZSwap

5.3 ZCache

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

開啟分頁內(nèi)存機制之后,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里面有兩個部件,TLB和PTW。TLB可以意譯地址轉(zhuǎn)換緩存器,它是緩存虛擬地址解析結(jié)果的地方。PTW可以意譯為虛擬地址解析器,它負責解析頁表,把虛擬地址轉(zhuǎn)換為物理地址,然后再送去MC進行訪問。同時其轉(zhuǎn)換結(jié)果也會被送去TLB進行緩存,下次再訪問相同虛擬地址的時候就不用再去解析了,可以直接用緩存的結(jié)果。22447ddc-15e5-11ed-ba43-dac502259ad0.png

6.1 頁表

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

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

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

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

P,此頁表項是否有效,1代表有效,0代表無效,為0時其它字段無意義。

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

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

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)的頁表項格式和這個是一樣的,只不過是物理地址擴展到了硬件支持的最高物理地址位數(shù)。

6.2 MMU

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

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

6.3 缺頁異常

MMU在解析虛擬內(nèi)存時如果發(fā)現(xiàn)了讀寫錯誤或者權(quán)限錯誤或者頁表項無效,就會觸發(fā)缺頁異常讓內(nèi)核來處理。下面我們來看一下x86的缺頁異常處理的過程。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();
}
}

缺頁異常首先從CR2寄存器中讀取發(fā)生異常的虛擬內(nèi)存地址。然后根據(jù)此地址是在內(nèi)核空間還是在用戶空間,分別調(diào)用do_kern_addr_fault和do_user_addr_fault來處理。使用vmalloc時會出現(xiàn)內(nèi)核空間的缺頁異常。用戶空間地址的缺頁異常在做完各種檢測處理之后會調(diào)用所有架構(gòu)都通用的函數(shù)handle_mm_fault來處理。下面我們來看一下這個函數(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最終會調(diào)用handle_pte_fault進行處理。在handle_pte_fault中,會根據(jù)缺頁的內(nèi)存的類型進行相應(yīng)的處理。

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

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

7.1 內(nèi)核空間

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

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

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

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

64位的內(nèi)核空間布局比較復雜,而且不同的架構(gòu)之間差異非常大,我們以x86 64 48位虛擬地址為例說一下。圖中一列畫不下,分成了兩列,我們從48位-1看起,首先是由一個8T的空洞,然后是LDT remap,然后是直接映射區(qū)有64T,用來映射所有的物理內(nèi)存,目前來說對于絕大部分計算機都夠用了,然后是0.5T的空洞,然后是vmalloc和ioremap區(qū)有32T,然后是1T的空洞,然后是vmemmap區(qū)有1T,vmemmap就是我們前面所講的所有頁面描述符的數(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ū)會增加512M,相應(yīng)的ko的映射區(qū)會減少512M。

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

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

7.2 用戶空間

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

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

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

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

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

暫略

8.1 總體統(tǒng)計

8.2 進程統(tǒng)計

九、總結(jié)回顧

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

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

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

    關(guān)注

    87

    文章

    11304

    瀏覽量

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

    關(guān)注

    69

    文章

    4944

    瀏覽量

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

    關(guān)注

    0

    文章

    168

    瀏覽量

    14139

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

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

收藏 人收藏

    評論

    相關(guān)推薦

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

    Linux內(nèi)存管理 Linux內(nèi)存管理是一個非常復雜的過程,主要分成兩個大的部分:內(nèi)核的
    的頭像 發(fā)表于 05-11 17:54 ?6057次閱讀
    <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)存管理(分頁內(nèi)存管理
    發(fā)表于 08-06 16:55 ?1738次閱讀

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

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

    ISO14001管理體系基礎(chǔ)知識(43頁PPT)

    ISO14001管理體系基礎(chǔ)知識
    發(fā)表于 09-01 10:10

    美國GPS管理體系

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

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

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

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

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

    基于績效管理管理體系審核

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

    什么是HSE管理體系

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

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

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

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

    本內(nèi)容介紹了arm linux內(nèi)存管理機制,詳細說明了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>機制淺析

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

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

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

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

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

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

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

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