linux記憶體管理學習總結

小小的番茄發表於2024-11-04

一、記憶體定址

1.1 邏輯地址、線性地址、實體地址的概念

1.2 邏輯地址轉換線性地址步驟

1.3 線性地址到實體地址的轉換

二、記憶體管理

2.1 引導記憶體分配器階段

2.2 記憶體管理子系統

2.3 32位架構的地址空間劃分

2.4 64位架構的地址空間劃分

2.5 核心態的記憶體管理

2.6 使用者態記憶體管理

2.7 一些記憶體的特殊用法

一、 記憶體定址

  本節介紹linux核心如何進行晶片級的記憶體定址;

  當處理器執行在真實模式時,處理器執行指令直接使用實體地址;

  本節介紹在保護模式下,處理器執行linux核心時,是如何對記憶體進行定址的;在保護模式下,處理器執行指令時首先看到的是邏輯地址,邏輯地址需要先轉換成線性地址,線性地址再轉換成實體地址,才能對記憶體進行訪問;這個過程除了需要作業系統的處理,還需要藉助硬體單元,比如邏輯地址到線性地址的轉換,需要linux核心建立一個段描述符表,分段單元藉助這個段描述符表把邏輯地址轉換成線性地址;對於線性地址到實體地址的轉換,軟體上需要建立頁表,硬體上需要MMU硬體單元,然後在訪問記憶體的過程中,為了提高程式的訪存效率,硬體上還引入了快取記憶體和TLB快取,本節介紹在這樣的硬體結構下,執行linux核心時,處理器是如何進行記憶體訪問的;

1.1 邏輯地址、線性地址、實體地址的概念

  邏輯地址,機器語言指令中使用的地址,也就是可執行檔案中一條指令或者一個運算元的地址,cpu在取指令、取運算元時首先看到的就是邏輯地址;可執行檔案被分為一個一個段,比如程式碼段、資料段、棧段、任務狀態段、區域性執行緒儲存段等,因此一個邏輯地址就由一個16位的段識別符號和32位偏移量組成;

  線性地址,作業系統使用的地址,在程式碼層面看到的指令地址和記憶體地址,就是線性地址;

  實體地址,最終訪問儲存單元,從地址匯流排發出去的就是實體地址;

1.2 邏輯地址轉換線性地址步驟

  邏輯地址由一個16位的段識別符號和32位偏移量組成,如下圖所示,首先硬體把16位段選擇符裝入段暫存器(處理器為每個段都提供了段暫存器,cs、ss、ds、es、fs、gs),硬體透過段選擇符找到對應的段描述符,這個段描述符儲存在全域性描述符表GDT或者區域性描述符表LDT裡面(描述符表是核心建立的,儲存在記憶體的cpu_gdt_table陣列裡面,每個cpu有一個GDT,GDT的物理首地址放在gdtr控制暫存器中,當前正被使用的LDT地址和大小放在ldtr控制暫存器中),段描述符被硬體載入到cpu的非程式設計暫存器;段描述符裡面包含段的首位元組對應的線性地址,首位元組的線性地址加上32位偏移量就得到這個邏輯地址對應的線性地址;需要說明的是在linux中,各段首對應的線性地址都是0,因此,在linux下邏輯地址總是和線性地址一致;

1.3 線性地址到實體地址的轉換

  線性地址轉換成實體地址的步驟,這個步驟透過查詢頁表來完成的,作業系統把線性地址劃分成一個一個頁,然後把實體記憶體也分成頁大小一樣的一個一個頁框,頁的內容可以儲存在任意頁框裡面,頁儲存在哪個頁框就記錄在頁表裡面;頁表由作業系統建立,每個程序都有一個頁表,這個頁表儲存在記憶體裡面,頁表的首地址會儲存在處理器的暫存器中,x86是儲存在cr3暫存器,arm架構是儲存在cp15協處理器的暫存器裡面;核心提供了一組宏用於操作頁表項,如pdg_index(addr)、pgd_offset(mm, addr)、pgd_page(pgd)等;

  由於線性地址龐大,為了減小頁表的大小,作業系統一般會把頁表組織成頁目錄的結構,比如linux的4級目錄結構包括頁全域性目錄、頁上級目錄、頁中間目錄、頁表,對應的線性地址也會被分為4段,頁全域性目錄索引、頁上級目錄索引、頁中間目錄索引、頁表索引,查詢過程大致是,首先從cpu的暫存器取出頁全域性目錄的首地址,然後透過頁全域性目錄的索引找到對應的頁全域性目錄表項,這個表項裡面裡面儲存了頁上級目錄的地址,找到頁上級目錄後,透過頁上級目錄索引,找到頁上級目錄中對應的表項,這個表項儲存了頁中間目錄的地址,找到頁中間目錄後,透過頁中間目錄索引,找到頁中間目錄中對應的表項,這個表項儲存了頁表的地址,找到頁表地址後,透過頁表索引找到對應的頁表項,這個頁表項裡面儲存了對應頁框的物理首地址,這個首地址加上線性地址中的偏移欄位,就是線性地址對應的實體地址;需要指出的是,這些表項除了儲存下一級的首地址,還儲存了這個頁對應的屬性,包括present標誌(頁是否在主存中)、read/write標誌(頁或頁表的存取許可權)、user/supervisor標誌(訪問該頁或頁表需要的特權級)、PCD和PWT標誌(控制硬體快取記憶體處理頁或頁表的方式);

  由於頁表儲存在記憶體,每次查表都訪問記憶體效率低,因此硬體上引入了頁表的快取記憶體TLB,類似於記憶體的快取記憶體,把最近使用的頁表快取到TLB,處理器訪問TLB的開銷比訪問記憶體小;

二、 記憶體管理

2.1 引導記憶體分配器階段

  在核心的記憶體管理子系統還沒初始化之前,分配記憶體的工作由引導記憶體分配器完成,早期使用的引導分配器是bootmem分配器,目前大部分使用的是memblock分配器;

  memblock分配器維護了一個資料結構,這個資料結構描述了哪些實體記憶體已經被使用,哪些實體記憶體可以被核心使用,資料結構如下,其中memory成員描述了核心可以訪問的實體記憶體,reserved成員描述的是已經被使用的實體記憶體;

#include /linux/memblock.h
struct memblock {
    bool bottom_up;  /* is bottom up direction? */
    phys_addr_t current_limit;
    struct memblock_type memory;
    struct memblock_type reserved;
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
    struct memblock_type physmem;
#endif
};

  memblock分配器的初始化函式是arm64_memblock_init,這個函式會解析裝置樹的記憶體資訊節點/memory,這個節點描述了哪些實體記憶體可以被核心使用,解析這個節點的資訊填充到memory成員中;從裝置樹讀取保留記憶體的資訊儲存到reserved成員中,保留記憶體對應的節點是/memreserve和/reserved-memory;把記憶體映象佔用的實體記憶體範圍新增到reserved成員中,這段記憶體可以被核心訪問,但是已經被佔用,所以memory和reserved兩個成員都要新增;

memblock分配器對外提供的介面有memblock_add、memblock_remove、memblock_alloc、memblock_free;

2.2 記憶體管理子系統

記憶體管理子系統使用節點、區域和頁框三級結構描述實體記憶體;

  節點:在儲存系統中,cpu訪問不同區域的實體記憶體開銷是不一樣的,這種儲存系統叫做非一致記憶體訪問NUMA,記憶體管理子系統將實體記憶體分為不同的節點,同一個節點cpu訪問記憶體的開銷是相同的,節點使用pglist_data結構體描述該節點的記憶體佈局,裡面描述了這個節點包含的區域資訊,以及這個節點包含的物理頁框資訊;

  區域:計算機體系結構有硬體的制約,有的外設使用DMA只能對RAM的前16M定址,32位系統裡面,核心空間的虛擬地址只有1G大小,如果RAM大於1G,那麼核心的1G線性地址空間就無法覆蓋所有RAM區域,基於這個考慮,記憶體管理系統將節點裡面的記憶體分為DMA區域、NORMAL區域、高階記憶體區域,DMA區域:包含低於16MB的物理頁框,NORMAL區域:直接對映到核心虛擬地址空間的區域,虛擬地址和實體地址是線性對映的關係,只差一個偏移,是否使用頁表,不同處理器實現不同,MIPS處理器不需要頁表,ARM處理器需要使用頁表;高階記憶體區域:32位處理器才有這個區域,這個區域是由於RAM大於1G,導致核心1G的虛擬地址無法覆蓋到,64位處理器就沒有這個區域,因為核心的虛擬地址空間足夠大;描述區域的資料結構是;

  頁框:記憶體管理子系統以頁框為單位對實體記憶體進行管理,描述頁框的資料結構是page,記錄頁框的當前狀態,比如該頁框屬於哪個程序的頁、是否空閒、屬於哪個區域、是否包含核心程式碼還是核心資料:

  分割槽頁框分配器:

 

  核心管理子系統使用頁分配器管理物理頁,當前使用的頁分配器是夥伴分配器;夥伴分配器就是把物理頁框分成11個塊連結串列組成,每個連結串列塊大小不一樣,連結串列中塊大小是一樣的,各連結串列塊大小都是2的冪次方大小1、2、4、8、16,每個塊中物理頁框都是實體地址連續的;請求和釋放頁框以操作連結串列為單位,請求塊過程中如果操作的塊比請求的頁框大,那麼會將塊進行拆分,拆分後剩餘的頁框會插入另一個小塊連結串列中,如果拆分的塊可以和其他塊構成夥伴關係,那麼還會進行塊的合併操作,釋放塊時也會進行夥伴塊合併已經塊遷移連結串列的動作;請求和釋放頁框常用的函式有alloc_pages(gfp_mask, order)、alloc_page(gfp_mask)、__get_free_pages(gfp_mask, 0)、__get_free_page(gfp_mask)、__get_zeroed_page(gfp_mask)、__get_dma_pages(gfp_mask, order);

2.3 32位架構的地址空間劃分

32位架構可使用的虛擬地址空間是4GB,前0~3GB是使用者空間使用,3~4GB是核心空間使用;3GB+896MB這段地址是線性對映區,也就是低端記憶體區(包括DMA和NORMAL區),其中核心頁表swapper_pg_dir和核心可執行檔案存在這個區域開始的地方0xc0000000;接著是高階記憶體區,高階記憶體區裡面有非連續記憶體對映區vmalloc、高階記憶體永久對映區、高階記憶體固定對映區;

2.4 64位架構的地址空間劃分

目前應用程式不需要64位那麼大的記憶體需求,arm64處理器不支援完全的64位虛擬地址;實際虛擬地址的最大寬度是48位,使用者空間訪問是0x0000 0000 0000 0000~0x0000 FFFF FFFF FFFF,核心地址範圍是0xFFFF 0000 0000 0000~0xFFFF FFFF FFFF FFFF;

  ARM64架構的核心地址空間佈局如下圖所示,最上面是線性對映區,長度是核心虛擬地址空間的一半,這部分割槽域虛擬地址和實體地址是線性關係;vmemmap區域是稀疏記憶體的page結構體陣列的虛擬地址空間;PCI I/O區域是PCI裝置的I/O地址空間;固定對映區域是編譯時的特殊虛擬地址,編譯的時候是一個常量,在核心初始化的時候對映到實體地址;vmalloc區域是非線性對映區,核心使用vmalloc分配該區域的記憶體,核心映象也在這個區域;核心模組區域是核心模組使用的虛擬地址空間;最後是KASAN記憶體檢測工具使用的區域;

2.5 核心態的記憶體管理

Linux作業系統的記憶體管理子系統是執行在核心態的,使用者態申請記憶體最終分配物理頁框時也是在核心態完成的;

  從請求記憶體的大小來分,記憶體管理子系統提供兩種記憶體分配方式,一種是以頁框大小倍數為單位申請和釋放記憶體,這種方式基於分割槽頁框分配器(當前用的是夥伴管理系統)實現;另一種是任意位元組大小記憶體的分配,這種方式基於slab分配器實現,slab分配器使用夥伴分配器獲得一塊連續的物理頁框,然後把這個頁框進行切割,分成一個個固定大小的記憶體物件,請求記憶體時,將大小適合的記憶體物件分配給請求者;

  從請求的實體記憶體是否連續來區分,直接使用夥伴系統請求的頁框是實體地址連續的,使用slab分配器申請的記憶體也是實體地址連續的;使用vmalloc申請的記憶體是不保證實體地址連續的;

  需要說明的是,記憶體管理子系統將實體記憶體分為三個區域:DMA區、NORMAL區以及高階記憶體區;使用夥伴系統申請連續頁框時,可以使用標誌指定從哪個區域請求頁框;使用slab分配器請求頁框也是一樣的,可以使用標誌指定從哪個區域請求記憶體;__GFP_DMA、__GFP_HIGHMEM;

  slab分配器:核心思想是把申請和釋放的記憶體當作物件來處理,如下圖所示slab分配器把整塊記憶體分為一個個小的記憶體物件,並且採用物件導向的思想,為這些對應提供對應的構造和解構函式;物件型別比如核心專用的物件有程序描述符、記憶體描述符、檔案描述符等,這些都是常用物件,slab分配器預先分配好這些物件,當記憶體請求這些物件時,就可以直接提供,提高了記憶體申請效率;還有通用的記憶體物件,這些物件以2的冪次方大小提供,從普通區域分配頁的記憶體物件名稱是kmalloc-<size>,從DMA區域分配頁的物件名稱是dma-kmalloc-<size>,透過命令cat /proc/slabinfo可以查詢這些通用物件;

  基於slab分配器衍生出來的還有slub分配器和slob分配器,slab分配器由於資料結構複雜,本身的記憶體開銷大,在實體記憶體使用量大的時候記憶體開銷大,因此設計了slub分配器;由於slab分配器程式碼多、實現複雜,因此針對小記憶體的嵌入式系統設計了精簡的slob分配器;

  非連續記憶體區管理:核心態的線性地址空間有一段vmalloc區間,這段區間用於對映非連續實體記憶體,呼叫vmalloc介面申請記憶體時,在這個區間找到一塊大小合適的線性地址區間,然後記憶體管理子系統找到合適的物理頁框,建立頁表把線性地址和物理頁框對映起來;每次呼叫vmalloc都呼叫get_vm_area()函式建立一個非連續記憶體區描述符,然後多次呼叫alloc_page介面從分割槽頁框分配器申請物理頁框,接著呼叫map_vm_area函式把線性地址和物理頁框對應的實體地址對映起來;需要注意的是vmalloc介面優先從高階記憶體區請求頁框,每次請求實體記憶體都是以頁框為單位,也就是不管申請的記憶體大小是多少,至少都是4096位元組大小,因此建議需要申請的記憶體大於一個頁時才使用vmalloc;

2.6 使用者態記憶體管理

程序的使用者虛擬地址空間包含以下區域:

  1、 程式碼段、資料段和未初始化資料段;

  2、 動態庫的程式碼段、資料段和未初始化資料段;

  3、 動態記憶體:堆;

  4、 棧;

  5、 存放在棧底部的環境變數和引數字串;

  6、把檔案對映到虛擬地址空間的記憶體對映區;

  Linux透過線性區的方式來管理程序的使用者態虛擬地址空間;比如堆是一個線性區、棧是一個線性區、程式碼段是一個線性區、資料段是一個線性區、對一個檔案執行記憶體對映建立線性區、共享記憶體線性區等;

線性區使用資料結構vm_area_struct表示,線性區資料結構包含在程序的記憶體描述符裡面mm_struct;

線性區對映的實體記憶體採用延遲分配的原則,在使用時才觸發缺頁異常處理來分配實際的物理頁,比如程式碼段和資料段的訪問,程式碼段和資料段一般是儲存在硬碟這樣的儲存介質裡面,程序透過檔案對映的方式把可執行檔案的程式碼段和資料段對映到程序的線性區,當cpu實際訪問程式碼段和資料段的時候,核心才給程式碼段和資料段對映物理頁框,然後把程式碼段和資料段從硬碟讀到物理頁框;

2.7 一些記憶體的特殊用法

  待總結~

參考部落格:

https://blog.csdn.net/u012489236/article/details/106109251?spm=1001.2014.3001.5501

《ARM體系結構與程式設計》

《linux核心深度解析》

《深入理解linux核心》

本文僅學習總結以便更好地理解linux的記憶體管理,還有部分部落格沒有一一列上,如有侵權請聯絡刪除

相關文章