《趣談Linux》總結四:內存管理

16 內存管理(上)-規劃進程內存空間佈局

每個進程應該有自己的內存空間。
內存空間都是獨立的、相互隔離的。
對於每個進程來講,看起來應該都是獨佔的。

16.1 獨享內存空間的原理

內存都被分成一塊一塊兒的,都編好了號,這些一塊一塊的地址是實實在在的地址,通過這個地址我們就能夠定位到物理內存的位置;
如果所有進程都使用這些地址,同時發生同一個位置的寫操作時很容易起衝突

所以使用虛擬地址:物理地址對於進程不可見,誰也不能直接訪問這個物理地址。
操作系統會給進程分配一個虛擬地址。
所有進程看到的這個地址都是一樣的,裏面的內存都是從0開始編號。
在程序裏面,指令寫入的地址是虛擬地址。
操作系統會提供一種機制,將不同進程的虛擬地址和不同內存的物理地址映射起來。
當程序要訪問虛擬地址的時候,由內核的數據結構進行轉換,轉換成不同的物理地址,這樣不同的進程運行
的時候,寫入的是不同的物理地址,這樣就不會衝突了。

16.2 規劃虛擬地址空間

通過16.1的原理可以看出,操作系統的內存管理主要分爲三個方面:
第一,物理內存的管理
第二,虛擬地址的管理
第三,虛擬地址和物理地址如何映射

用戶態的進程使用虛擬地址,內核態的也基本都是使用虛擬地址,只有內存管理系統才能使用物理地址;

16.2.1 用戶態(普通進程)的角度

  • 需求:

代碼需要放在內存裏面;
全局變量,例如max_length;
常量字符串"Input the string length : ";
函數棧,例如局部變量num是作爲參數傳給generate函數的,這裏面涉及了函數調用,局部變量,函數參
數等都是保存在函數棧上面的;
堆,malloc分配的內存在堆裏面;
這裏面涉及對glibc的調用,所以glibc的代碼是以so文件的形式存在的,也需要放在內存裏面。

  • 實現:通過分段

(0號到29號會議室)
在這裏插入圖片描述
Text Segment是存放二進制可執行代碼的位置,Data Segment存放靜態常量,BSS Segment存放未初始化的靜態變量;
在二進制執行文件裏面,就有這三個部分,這裏就是把二進制執行文件的三個部分加載到內存裏面。

接下來是堆(Heap)段。堆是往高地址增長的,是用來動態分配內存的區域,malloc就是在這裏面分配
的。

接下來的區域是Memory Mapping Segment。這塊地址可以用來把文件映射進內存用的,如果二進制的執
行文件依賴於某個動態鏈接庫,就是在這個區域裏面將so文件映射到了內存中。

再下面就是棧(Stack)地址段。主線程的函數調用的函數棧就是用這裏的。

如果普通進程還想進一步訪問內核空間,是沒辦法的。
如果需要進行更高權限的工作,就需要調用系統調用,進入內核。

16.2.2 內核的角度

  • 需求:

內核的代碼要在內存裏面;
內核中也有全局變量;
每個進程都要有一個task_struct;
每個進程還有一個內核棧;
在內核裏面也有動態分配的內存;
虛擬地址到物理地址的映射表放在哪裏?

  • 實現:

一旦進入了內核,就換了一副視角。

剛纔是普通進程的視角,覺着整個空間是它獨佔的,沒有其他進程存在。
當然另一個進程也這樣認爲,因爲它們互相看不到對方。
這也就是說,不同進程在相同地址上放的東西都不一樣。

但是到了內核裏面,無論是從哪個進程進來的,看到的都是同一個內核空間,看到的都是同一個進程列表。
雖然內核棧是各用個的,但是如果想知道的話,還是能夠知道每個進程的內核棧在哪裏的。
所以,如果要訪問一些公共的數據結構,需要進行鎖保護
也就是說,不同的進程進入到內核後,進入的內存區域是相同的(30號到39號會議室是同一批會議室。)
在這裏插入圖片描述
內核的代碼訪問內核的數據結構,大部分的情況下都是使用虛擬地址的,雖然內核代碼權限很大,但是能夠
使用的虛擬地址範圍也只能在內核空間,也即內核代碼訪問內核數據結構。
只能用30號到39號這些編號,不能用0到29號,因爲這些是被進程空間佔用的。
而且,進程有很多個。你現在在內核,但是你不知道當前指的0號是哪個進程的0號。
在內核裏面也會有內核的代碼,同樣有Text Segment、Data Segment和BSS Segment:內核啓動的時候,內核代碼也是ELF格式的。

17 內存管理(下):物理地址和虛擬地址的映射

可以使用分段機制:
在這裏插入圖片描述
分段機制下的虛擬地址由兩部分組成:段選擇子和段內偏移量。

段選擇子保存在段寄存器裏面。
段選擇子裏面最重要的是段號,用作段表的索引。
段表裏面保存的是這個段的基地址、段的界限和特權等級等。

虛擬地址中的段內偏移量應該位於0和段界限之間。如果段內偏移量是合法的,就將段基地址加上段內偏移量得到物理內存地址。

案例:將每個進程的虛擬空間分成以下4個段,用0~3來編號(如圖左邊)。
每個段在段表中有一個項(如圖中間),在物理空間中,段的排列如圖右邊所示:
在這裏插入圖片描述
如果要訪問段2中偏移量600的虛擬地址,我們可以計算出物理地址爲,段2基地址2000 + 偏移量600 =2600。

在Linux裏面,段表全稱段描述符表(segment descriptors),放在全局描述符表GDT(Global Descriptor
Table)裏面,段描述符表裏面存儲着一個個的段表項

一個段表項由段基地址base、段界限limit,還有一些標識符組成;
對於64位的和32位的系統,都定義了內核代碼段、內核數據段、用戶代碼段和用戶數據段。

另外,還會定義四個段選擇子,指向段描述符表項

在Linux中,所有的段的起始地址都是一樣的,都是0,並沒有分段,這可以看出並沒有使用到全部的分段功能;
而分段的作用,則是可以做權限審覈,例如用戶態DPL是3,內核態DPL是0。當用戶態試圖訪問內核態的時候,會因爲權限不足而報錯。

Linux傾向於另外一種從虛擬地址到物理地址的轉換方式,稱爲分頁(Paging)。

對於物理內存,操作系統把它分成一塊一塊大小相同的頁,這樣更方便管理;
例如有的內存頁面長時間不用了,可以暫時寫到硬盤上,稱爲換出
一旦需要的時候,再加載進來,叫作換入
這樣可以擴大可用物理內存的大小,提高物理內存的利用率。

換入和換出都是以頁爲單位的;
頁面的大小一般爲4KB;
爲了能夠定位和訪問每個頁,需要有個頁表,保存每個頁的起始地址,再加上在頁內的偏移量,組成線性地址,就能對於內存中的每個位置進行訪問了:
在這裏插入圖片描述
虛擬地址分爲兩部分:頁號和頁內偏移。
頁號作爲頁表的索引,頁表包含物理頁每頁所在物理內存的基地址。這個基地址與頁內偏移的組合就形成了物理內存地址。

案例:虛擬內存中的頁通過頁表映射爲了物理內存中的頁
在這裏插入圖片描述
32位環境下,虛擬地址空間共4GB。(4194304kb)(kb/kb=b)
如果分成4KB一個頁,那就是1M個頁。(4096kb)
每個頁表項需要4個字節(4B)來存儲,即每頁額外用了4個字節,那麼整個4GB空間的映射就需要4MB的內存來存儲映射表。
如果每個進程都有自己的映射表,100個進程就需要400MB的內存。
對於內核來講,有點大了 。

且頁表中所有頁表項必須提前建好,並且要求是連續的;
如果不連續,就沒有辦法通過虛擬地址裏面的頁號找到對應的頁表項了;
這樣就不能等到需要時再建,也不能使用鏈表的方式來保證內存不被浪費

那怎麼辦呢?
可以將頁表再分頁
4G的空間需要4M的頁表來存儲映射,把這4M分成1K(1024)個4K,每個4K又能放在一頁裏面,這樣1K個4K就是1K個頁;
這1K個頁也需要一個表進行管理,我們稱爲頁目錄表,這個頁目錄表裏面有1K項,每項4個字節,頁目錄表大小也是4K。
頁目錄有1K項,用10位就可以表示訪問頁目錄的其中一項;
這一項其實對應的是一整頁的頁表項,也即4K的頁表項;
每個頁表項也是4個字節,因而一整頁的頁表項是1K個,再用10位就可以表示訪問頁表項的哪一項,頁表項中的一項對應的就是一個頁,是存放數據的頁,這個頁的大小是4K,用12位可以定位這個頁內的任何一個位置。

這樣加起來正好32位,也就是用前10位定位到頁目錄表中的一項,將這一項對應的頁表取出來共1k項,再用中間10位定位到頁表中的一項,將這一項對應的存放數據的頁取出來,再用最後12位定位到頁中的具體位置訪問數據。
在這裏插入圖片描述
如果這樣的話,映射4GB地址空間就需要4MB+4KB的內存;
但是,我們往往不會爲一個進程分配那麼多內存;
比如說,上面圖中,我們假設只給這個進程分配了一個數據頁;
如果只使用頁表,也需要完整的1M個頁表項共4M的內存;
但是如果使用了頁目錄,頁目錄需要1K個全部分配,佔用內存4K,但是裏面只有一項使用了;
到了頁表項,只需要分配能夠管理那個數據頁的頁表項頁就可以了,也就是說,最多4K,這樣內存就節省多了。

對於64位的系統,兩級不夠,就變成了四級目錄;
分別是全局頁目錄項PGD(Page Global Directory)、上層頁目錄項PUD(Page Upper Directory)、中間頁目錄項PMD(Page Middle Directory)和頁表項PTE(Page Table Entry):
在這裏插入圖片描述

  • 總結

內存管理系統可以精細化爲下面三件事情:
第一,虛擬內存空間的管理,將虛擬內存分成大小相等的頁;
第二,物理內存的管理,將物理內存分成大小相等的頁;
第三,內存映射,將虛擬內存也和物理內存也映射起來,並且在內存緊張的時候可以換出到硬盤中。
在這裏插入圖片描述

18 進程空間管理

內存管理有三個方面:虛擬內存空間的管理、物理內存的管理以及內存映射;
本節詳細介紹第一個方面:進程的虛擬內存空間是如何管理的

18.1 用戶態和內核態的劃分

task_struct裏面有一個struct mm_struct結構來管理內存。

裏面的unsigned long task_size是用戶態地址空間和內核態地址空間的分界線

對於32位系統,最大能夠尋址2^32=4G,其中用戶態虛擬地址空間是3G,內核態是1G。

對於64位系統,虛擬地址使用了48位,共128T,內核空間也是128T。內核空間和用戶空間之間隔着很大的空隙,以此來進行隔離
在這裏插入圖片描述

18.2 用戶態虛擬空間的佈局

struct mm_struct裏面定義了代碼、全局變量、堆、棧、內存映射區等區域的統計信息和位置,以所佔用的頁的數量爲單位,如下圖,16.2.1中可以找到其存放的內容
在這裏插入圖片描述
這些區域是通過struct mm_struct裏面的一個結構vm_area_struct來描述的,這是一個單鏈表,用於將這些區域串起來;
另外還有一個紅黑樹,用於快速查找一個內存區域,並在需要改變的時候,能夠快速修改。

虛擬內存區域可以映射到物理內存,也可以映射到文件,映射到物理內存的時候稱爲匿名映射;
映射到文件就需要有vm_file指定被映射的文件。(mmap指的是具體內存對象的映射,而之前說的虛擬內存到物理內存的映射是指整塊的,mmap就是在這分配的整塊裏面再進行分配的映射)

struct mm_struct的vm_area_struct到上述區域的內存映射圖:
在這裏插入圖片描述
映射完畢後,什麼情況下會被修改:
1 第一種情況是函數的調用,涉及函數棧的改變,主要是改變棧頂指針
2 第二種情況是通過malloc申請一個堆內的空間,底層要麼執行brk,要麼執行mmap。

18.2.1 brk解析

堆是從低地址向高地址增長的

流程開始:將原來的堆頂和現在的堆頂,都按照頁對齊地址,然後比較大小

如果兩者相同,說明這次增加的堆的量很小,還在一個頁裏面,不需要另行分配頁。

如果發現新舊堆頂不在一個頁裏面,則需要跨頁;如果發現新堆頂小於舊堆頂,這說明不是新分配內存了,而是釋放內存了,而且是至少釋放了一頁;

如果堆將要擴大,找到原堆頂所在的vm_area_struct的下一個vm_area_struct,看當前的堆頂和下一個vm_area_struct之間還能不能分配一個完整的頁;
如果不能,內存空間都被佔滿了,直接退出返回;
如果還有空間,就調用do_brk進一步分配堆空間,從舊堆頂開始,分配計算出的新舊堆頂之間的頁數。

18.3 內核態虛擬空間的佈局

內核態的虛擬空間和某一個進程沒有關係,所有進程通過系統調用進入到內核之後,看到的虛擬地址空間都是一樣的。

32位的內核態的佈局(1G):
在這裏插入圖片描述
前896M稱爲直接映射區
這一塊空間是連續的,和物理內存是非常簡單的映射關係:就是虛擬內存地址減去3G,就得到物理內存的位置;
在物理內存的開始的896M的空間,會被直接映射到3G至3G+896M的虛擬地址

896M分解爲:
1 在系統啓動的時候,物理內存的前1M已經被佔用了
2 從1M開始加載內核代碼段,然後加載內核的全局變量、BSS等,也是ELF裏面涵蓋的,即內核的代碼段,全局變量,BSS會被映射到3G後的虛擬地址空間裏面

物理內存896M以上的是高端內存;
在內核中,除了內存管理模塊直接操作物理地址之外,內核的其他模塊,仍然要操作虛擬地址,而虛擬地址是需要內存管理模塊分配和映射好的

剩下的虛擬內存地址:

  • 在896M到VMALLOC_START之間有8M的空間。

  • VMALLOC_START到VMALLOC_END之間稱爲內核動態映射空間,也即內核想像用戶態進程一樣malloc申
    請內存,在內核裏面可以使用vmalloc。假設物理內存裏面,896M到1.5G之間已經被用戶態進程佔用了,
    並且映射關係放在了進程的頁表中,內核vmalloc的時候,只能從分配物理內存1.5G開始,就需要使用這
    一段的虛擬地址進行映射,映射關係放在專門給內核自己用的頁表裏面。

  • PKMAP_BASE到FIXADDR_START的空間稱爲持久內核映射。使用alloc_pages()函數的時候,在物理內存的
    高端內存得到struct page結構,可以調用kmap將其在映射到這個區域。

  • FIXADDR_START到FIXADDR_TOP(0xFFFF F000)的空間,稱爲固定映射區域,主要用於滿足特殊需求。

  • 在最後一個區域可以通過kmap_atomic實現臨時內核映射。
    假設用戶態的進程要映射一個文件到內存中,先要映射用戶態進程空間的一段虛擬地址到物理內存,即將用戶態進程虛擬地址和物理內存的映射關係放在用戶態進程的頁表中,然後將文件內容寫入這個物理內存供用戶態進程訪問。
    分配完畢後,用戶態進程可以通過用戶態的虛擬地址,也即0至3G的部分,經過頁表映射後訪問物理內存,並不需要內核態的虛擬地址裏面也劃出一塊來,映射到這個物理內存頁。
    但是因爲要把文件內容寫入物理內存,這件事情要內核來幹,所以可以通過kmap_atomic做一個臨時映射,寫入物理內存完畢後,再kunmap_atomic來解映射即可。

64位的內核佈局:64位的內核佈局反而簡單,因爲虛擬空間很大,不需要“高端內存”這個概念,因爲內核是128T,根本不可能有物理內存超過這個值:
在這裏插入圖片描述
從0xffff800000000000開始就是內核的部分,一開始有8T的空檔區域。

從 __ PAGE_OFFSET_BASE(0xffff880000000000)開始的64T的虛擬地址空間是直接映射區域,也就是減去PAGE_OFFSET就是物理地址。

虛擬地址和物理地址之間的映射在大部分情況下還是會通過建立頁表的方式進行映射。

從VMALLOC_START(0xffffc90000000000)開始到VMALLOC_END(0xffffe90000000000)的32T的空間是
給vmalloc的。

從VMEMMAP_START(0xffffea0000000000)開始的1T空間用於存放物理頁面的描述結構struct page的。

從 __ START_KERNEL_map(0xffffffff80000000)開始的512M用於存放內核代碼段、全局變量、BSS等;
這裏對應到物理內存開始的位置,減去__START_KERNEL_map就能得到物理內存的地址;
這裏和直接映射區有點像,但是不矛盾,因爲直接映射區之前有8T的空當區域,早就過了內核代碼在物理內存中加載的位置。

18.4 總結

一個進程要運行起來需要以下的內存結構:

  • 用戶態:

代碼段、全局變量、BSS
函數棧

內存映射區

  • 內核態:

內核的代碼、全局變量、BSS
內核數據結構例如task_struct
內核棧
內核中動態分配的內存

到了這裏都已經有了相應的內存塊了

進程運行狀態在32位下對應關係:
在這裏插入圖片描述
64:
在這裏插入圖片描述

19 物理內存管理

19.1 物理內存的組織形式之頁

平坦內存模型(Flat MemoryModel):
把內存想象成它是由連續的一頁一頁的塊組成的。
可以從0開始對物理頁編號,這樣每個物理頁都會有個頁號。
由於物理地址是連續的,頁也是連續的,每個頁大小也是一樣的,因而對於任何一個地址,只要直接除一下
每頁的大小,很容易直接算出在哪一頁。
每個頁有一個結構struct page表示,這個結構也是放在一個數組裏面,這樣根據頁號,很容易通過下標找到相應的struct page結構。

經典的內存使用方式:CPU通過總線去訪問內存
在這裏插入圖片描述
在這種模式下,CPU會有多個,在總線的一側。
而所有的內存條組成一大片內存,在總線的另一側,所有的CPU訪問內存都要過總線,而且距離都是一樣的,這種模式稱爲SMP(Symmetric multiprocessing),即對稱多處理器。
缺點:總線會成爲瓶頸,因爲數據都要通過它來流通:
在這裏插入圖片描述
爲了提高性能和可擴展性,出現了NUMA(Non-uniform memory access):非一致內存訪問。
在這種模式下,內存不是一整塊。
每個CPU都有自己的本地內存,CPU訪問本地內存不用過總線,因而速度要快很多,每個CPU和內存在一起,稱爲一個NUMA節點。
但是,在本地內存不足的情況下,每個CPU都可以去另外的NUMA節點申請內存,這個時候訪問延時就會比較長。這樣,內存被分成了多個節點,每個節點再被分成一個一個的頁面。
由於頁需要全局唯一定位,頁還是需要有全局唯一的頁號的。
但是由於物理內存不是連起來的了,頁號也就不再連續了。於是內存模型就變成了非連續內存模型,管理起來就複雜一些。

NUMA往往是非連續內存模型。而非連續內存模型不一定就是NUMA,有時候一大片內存的情況下,也會有物理內存地址不連續的情況。

後來內存技術慢慢發展,可以支持熱插拔了。這個時候,不連續成爲常態,於是就有了稀疏內存模型
在這裏插入圖片描述

  • 物理內存的組織:

從節點到區域到頁到小塊

  • 物理內存的分配:

對於要分配比較大的內存,例如到分配頁級別的,可以使用夥伴系統(Buddy System):
Linux中的內存管理的“頁”大小爲4KB。
把所有的空閒頁分組爲11個頁塊鏈表,每個塊鏈表分別包含很多個大小的頁塊,有1、2、4、8、16、32、64、128、256、512和1024個連續頁的頁塊。
最大可以申請1024個連續頁,對應4MB大小的連續內存。每個頁塊的第一個頁的物理地址是該頁塊大小的整數倍:
在這裏插入圖片描述
當向內核請求分配(2(i-1),2i]數目的頁塊時,按照2^i頁塊請求處理。如果對應的頁塊鏈表中沒有空閒頁塊,那我們就在更大的頁塊鏈表中去找。
當分配的頁塊中有多餘的頁時,夥伴系統會根據多餘的頁塊大小插入到對應的空閒頁塊鏈表中。

例如,要請求一個128個頁的頁塊時,先檢查128個頁的頁塊鏈表是否有空閒塊。
如果沒有,則查256個頁的頁塊鏈表;如果有空閒塊的話,則將256個頁的頁塊分成兩份,一份使用,一份插入128個頁的頁塊鏈表中。
如果還是沒有,就查512個頁的頁塊鏈表;如果有的話,就分裂爲128、128、256三個頁塊,一個128的使用,剩餘兩個插入對應頁塊鏈表。

  • 總結:分配系統的實現

如果有多個CPU,那就有多個節點。
每個節點用struct pglist_data表示,放在一個數組裏面。
每個節點分爲多個區域,每個區域用struct zone表示,也放在一個數組裏面。
每個區域分爲多個頁。爲了方便分配,空閒頁放在struct free_area裏面,使用夥伴系統進行管理和分配,每
一頁用struct page表示。
在這裏插入圖片描述

19.2 物理內存的組織形式之小對象

19.2.1 小內存的分配

如果遇到小的對象,會使用slub分配器進行分配。

工作原理:
調用了kmem_cache_alloc_node函數,在task_struct的緩存區域kmem_cache *task_struct_cach分配了一塊內存。

kmem_cache_alloc_node方法的作用:每次創建task_struct的時候,不用到內存裏面去分配,而是先在緩存區kmem_cache裏面看看有沒有直接可用的

kmem_cache_free方法的作用:當一個進程結束,task_struct不用直接被銷燬,而是放回到緩存區kmem_cache中

所有的緩存最後都會放在一個鏈表LIST_HEAD(slab_caches)裏面:task_struct、mm_struct、fs_struct等的緩存

緩存結構kmem_cache有三個kmem_cache_order_objects類型的變量。
這裏面的order,就是2的order次方個頁面的大內存塊,objects就是能夠存放的緩存對象的數量。

對於緩存來講,其實就是分配了連續幾頁的大內存塊,然後根據緩存對象的大小,切成小內存塊:
在這裏插入圖片描述
這樣就將所有的空閒對象鏈成一條鏈,使用數組實現

那這些緩存對象哪些被分配了、哪些在空着,什麼情況下整個大內存塊都被分配完了,需要向夥伴系統申請
幾個頁形成新的大內存塊?這些信息該由誰來維護呢?
解決這些問題使用到的變量是kmem_cache_cpu和kmem_cache_node,每個NUMA節點上都各有一個。

如圖是分配機制相關的圖:
在這裏插入圖片描述
在分配緩存塊的時候,要分兩種路徑,fast path和slow path,也就是快速通道和普通通道。
其中kmem_cache_cpu就是快速通道,kmem_cache_node是普通通道。
每次分配的時候,要先從kmem_cache_cpu進行分配:page有則page,若沒有,但partial不爲空,則把page的內存替換爲partial的內存,然後重新嘗試分配,還是失敗,則繼續;
如果kmem_cache_cpu裏面沒有空閒的塊,那就到kmem_cache_node中進行分配;
如果還是沒有空閒的塊,纔去夥伴系統分配新的頁。

在這裏,page指向大內存塊的第一個頁,緩存塊就是從裏面分配的。freelist指向大內存塊裏面第一個空閒的項。按照上上圖說的,這一項會有指針指向下一個空閒的項,最終所有空閒的項會形成一個鏈表。

19.2.2 頁面換出

無論是32位還是64位,虛擬地址空間都非常大,物理內存不可能有這麼多的空間放得下。
所以,一般情況下,頁面只有在被使用的時候,纔會放在物理內存中。如果過了一段時間不被使用,即便用戶進程並沒有釋放它,物理內存管理也有責任做一定的干預。
例如,將這些物理內存中的頁面換出到硬盤上去;將空出的物理內存,交給活躍的進程去使用。

觸發的時機:
1 最常見的情況:分配內存的時候,發現沒有地方了,就試圖回收一下;
2 主動去檢測:內核線程kswapd在系統初始化的時候就被創建。它會進入一個無限循環,直到系統停止。
在這個循環中,如果內存使用沒有那麼緊張,那它就暫時閒着;如果內存緊張了,就需要去檢查一下內存,看看是否需要換出一些內存頁。

策略:LRU,最近最少使用。也就是說,所有的頁面都被掛在LRU列表lru_list中,這個列表裏面會按照活躍程度進行排序,這樣就容易把不怎麼用的內存頁拿出來做處理。
內存頁總共分兩類,一類是匿名頁,和虛擬地址空間進行關聯;一類是內存映射,不但和虛擬地址空間關聯,還和文件管理關聯。
每一類都有兩個列表,一個是active,一個是inactive,存放在lru_list中
active就是比較活躍的,inactive就是不怎麼活躍的。
這兩個裏面的頁會變化,比如過一段時間,活躍的可能變爲不活躍,不活躍的可能變爲活躍。
如果要換出內存,那就是從不活躍的列表中找出最不活躍的,換出到硬盤:

enum lru_list {
    LRU_INACTIVE_ANON = LRU_BASE,
    LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE,
    LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE,
    LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE,
    LRU_UNEVICTABLE,
    NR_LRU_LISTS
};
#define for_each_evictable_lru(lru) for (lru = 0; lru <= LRU_ACTIVE_FILE; lru++)
static unsigned long shrink_list(enum lru_list lru, unsigned long nr_to_scan,
    struct lruvec *lruvec, struct mem_cgroup *memcg,
    struct scan_control *sc)
{
    if (is_active_lru(lru)) {
    if (inactive_list_is_low(lruvec, is_file_lru(lru),
    memcg, sc, true))
    shrink_active_list(nr_to_scan, lruvec, sc, lru);
    return 0;
}
return shrink_inactive_list(nr_to_scan, lruvec, sc, lru);

即,shrink_list會先縮減活躍頁面列表,再壓縮不活躍的頁面列表。
對於不活躍列表的縮減,shrink_inactive_list就需要對頁面進行回收;

對於匿名頁來講,需要分配swap,將內存頁寫入文件系統;
對於內存映射關聯了文件的,我們需要將在內存中對於文件的修改寫回到文件中。

  • 總結

對於物理內存來講,從下層到上層的關係及分配模式如下:

物理內存分NUMA節點,分別進行管理;

每個NUMA節點分成多個內存區域;

每個內存區域分成多個物理頁面;

夥伴系統將多個連續的頁面作爲一個大的內存塊分配給上層;

kswapd負責物理頁面的換入換出;

Slub Allocator將從夥伴系統申請的大內存塊切成小塊,分配給其他系統。
在這裏插入圖片描述

20 用戶態內存映射

學習了虛擬內存空間如何組織的,也學習了物理頁面如何管理的,接下來就是學習如何將虛擬內存空間和物理頁面關聯起來?使用哪些數據結構?

20.1 mmap原理

每一個進程都有一個列表vm_area_struct,指向虛擬地址空間的不同的內存塊,這個變量的名字叫mmap。
在這裏插入圖片描述
要申請小塊內存,就用brk;要申請一大塊內存,就用mmap

如果一個進程想映射一個文件到自己的虛擬內存空間,也要通過mmap系統調用。這個時候mmap是映射內存空間到物理內存再到文件。

經過mmap的調用後,虛擬內存的映射就建立起來了,當然這個時候,內存管理並不直接分配物理內存,因
爲物理內存相對於虛擬地址空間太寶貴了,只有等真正用的那一刻纔會開始分配。

20.2 用戶態缺頁異常

一旦開始訪問虛擬內存的某個地址,如果發現並沒有對應的物理頁,那就觸發缺頁中斷,調用
do_page_fault。

經過此步處理,物理內存中有了頁面,頁表也建立好了映射。
接下來,用戶程序在虛擬內存空間裏面,可以通過虛擬地址經過頁表映射的訪問物理頁面上的數據了。

爲了加快映射速度,我們不需要每次從虛擬地址到物理地址的轉換都走一遍頁表;
頁表一般都很大,只能存放在內存中。操作系統每次訪問內存都要兩步:先通過查詢頁表得到物理地址,然後訪問該物理地址讀取指令、數據。
爲了提高映射速度,引入了TLB(Translation Lookaside Buffer),經常稱爲快表,專門用來做地址映射的硬件設備。
它不在內存中,可存儲的數據比較少,但是比內存要快。
所以可以想象爲TLB就是頁表的Cache,其中存儲了當前最可能被訪問到的頁表項,其內容是部分頁表項的一個副本。
有了TLB之後,地址映射的過程就如圖:
先查快表,快表中有映射關係,然後直接轉換爲物理地址。如果在TLB查不到映射關係時,纔會到內存中查詢頁表。

這個頁表的最頂級的pgd存放在task_struct中的mm_struct的pgd變量裏面。
在這裏插入圖片描述

20.3 總結

用戶態的內存映射機制包含以下幾個部分:

1 用戶態內存映射函數mmap,包括用它來做匿名映射和文件映射。

2 用戶態的頁表結構,存儲位置在mm_struct中。

3 在用戶態訪問沒有映射的內存會引發缺頁異常:分配物理頁表、補齊頁表。
如果是匿名映射則分配物理內存;如果是swap,則將swap文件讀入;如果是文件映射,則將文件讀入。
在這裏插入圖片描述

21 內核態內存映射

內核態的內存映射機制,主要包含以下幾個部分:

1 內核態內存映射函數vmalloc、kmap_atomic是如何工作的;
2 內核態頁表是放在哪裏的,如何工作的?swapper_pg_dir是怎麼回事;
3 出現了內核態缺頁異常應該怎麼辦?

21.1 內核頁表

每個進程的“進程頁表”中內核態地址相關的頁表項都是“內核頁表”的一個拷貝。

和用戶態頁表不同,在系統初始化的時候,我們就要創建內核頁表了:
在這裏插入圖片描述
XXX_ident_pgt對應的是直接映射區,XXX_kernel_pgt對應的是內核代碼區,XXX_fixmap_pgt對應的是固定映射區。

init_top_pgt有三項,上來先有一項,指向的是level3_ident_pgt,也即直接映射區頁表的三級目錄。

第二項也指向level3_ident_pgt,直接映射區。

第三項指向level3_kernel_pgt,內核代碼區。

內核頁表定義完了,一開始這裏面的頁表能夠覆蓋的內存範圍比較小。
例如,內核代碼區512M,直接映射區1G。
這個時候,其實只要能夠映射基本的內核代碼和數據結構就可以了。
可以看出,裏面還空着很多項,可以用於將來映射巨大的內核虛擬地址空間,等用到的時候再進行映射。

定義完了內核頁表,接下來是初始化內核頁表:在系統啓動的時候start_kernel會調用setup_arch。

21.2 vmalloc和kmap_atomic原理

在虛擬地址空間裏面,有個vmalloc區域,從VMALLOC_START開始到VMALLOC_END,可以用於映射一段物
理內存。

再來看內核的臨時映射函數kmap_atomic的實現:
如果是32位有高端地址的,就需要調用set_pte通過內核頁表進行臨時映射;
如果是64位沒有高端地址的,就調用page_address,裏面會調用lowmem_page_address。
其實低端內存的映射,會直接使用__va進行臨時映射。

可以看出,kmap_atomic和vmalloc不同:
kmap_atomic發現,沒有頁表的時候,就直接創建頁表進行映射了;
而vmalloc沒有,它只分配了內核的虛擬地址。所以,訪問它的時候,會產生缺頁異常。

  • 內核態缺頁異常

調用do_page_fault,主要用於關聯內核頁表項。

21.3 總結

將整個內存管理的體系串起來:

物理內存根據NUMA架構分節點;
每個節點裏面再分區域;
每個區域裏面再分頁;
物理頁面通過夥伴系統進行分配。

分配的物理頁面要變成虛擬地址讓上層可以訪問,kswapd可以根據物理頁面的使用情況對頁面進行換入換出。

對於內存的分配需求,可能來自內核態,也可能來自用戶態:

對於內核態的內存分配,kmalloc在分配大內存、以及vmalloc分配不連續物理頁的時候,直接使用夥伴系統,分配後轉換爲虛擬地址,訪問的時候需要通過內核頁表進行映射;
對於kmem_cache以及kmalloc分配小內存,則使用slub分配器,將夥伴系統分配出來的大塊內存切成一小
塊一小塊進行分配。
kmem_cache和kmalloc的部分不會被換出,因爲用這兩個函數分配的內存多用於保持內核關鍵的數據結
構;
內核態中vmalloc分配的部分會被換出,因而當訪問的時候,發現不在,就會調用do_page_fault。

對於用戶態的內存分配,或者直接調用mmap系統調用分配,或者調用malloc;
調用malloc的時候,如果分配小的內存,就用sys_brk系統調用;
如果分配大的內存,還是用sys_mmap系統調用。
正常情況下,用戶態的內存都是可以換出的,因而一旦發現內存中不存在,就會調用do_page_fault。
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章