Linux內核內存管理:內存分配機制


讓我們看一下下面的圖,它向我們展示了存在於基於linux的系統上的不同內存分配器,稍后討論它。

 

                                                內核內存分配器概述

有一種分配機制可以滿足任何類型的內存請求。根據你需要什么樣的內存,你可以選擇一個最接近你的目標。主要的分配器是頁分配器,它只處理頁(頁是它能交付的最小內存單元)。然后是SLAB分配器,它構建在頁面分配器之上,從它獲取頁面並返回較小的內存實體(通過SLAB和緩存)。這是kmalloc分配器所依賴的分配器。

頁分配器

頁分配器是Linux系統中最低級別的分配器,是其他分配器所依賴的。系統的物理內存由固定大小的塊(稱為頁幀)組成。在內核中,頁幀(page frame)在內核里表示為結構體  struct page 的實例。一頁是操作系統能給予任何低級別內存請求的最小內存單位。

頁分配 API

我們知道內核頁面分配器使用 buddy 算法來分配和釋放頁面塊。頁面以大小為2的冪的塊分配(為了從buddy算法中得到最好的結果)。這意味着它可以分配1頁、2頁、4頁、8頁、16頁等等:

1. alloc_pages(mask, order)申請2的order次冪個頁, 並返回struct page結構體的實例,指向申請到的block的第一頁。如果只申請一頁內存,order的值應該為0。以下是alloc_page(mask)實現:

struct page *alloc_pages(gfp_t mask, unsigned int order)
#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)

__free_pages()用於釋放由alloc_pages()函數分配的內存。它接受一個指向已分配頁面的指針作為參數,其順序與分配時相同:

void __free_pages(struct page *page, unsigned int order);

2. 還有其他函數以同樣的方式工作,但不是struct page的實例,它們返回保留塊的地址(虛擬地址)。比如 __get_free_pages(mask, order) 和 __get_free_page(mask):

unsigned long __get_free_pages(gfp_t mask, unsigned int order);
unsigned long get_zeroed_page(gfp_t mask);

free_pages()用於釋放用__get_free_pages()分配的頁面。地址addr參數表示被分配頁面的起始區域,以及參數order,應該與分配時的相同:

free_pages(unsigned long addr, unsigned int order);

在上面兩種情況下,mask 指定有關請求的詳細信息,即內存區域和分配器的行為。mask可選值如下:

  • GFP_USER: 用於用戶內存分配。
  • GFP_KERNEL: 內核內存分配的常用標志。
  • GFP_HIGHMEM: 從HIGH_MEM區域請求內存。
  • GFP_ATOMIC: 以不能休眠的原子方式分配內存。當需要從中斷上下文分配內存時使用。

使用GFP_HIGHMEM時需要注意,不應該與__get_free_pages() (或者 __get_free_page())一起使用,因為HIGHMEM內存不能保證是連續的,所以不能返回從該區域分配的內存地址。全局來說,只有GFP_*的一個子集被允許在內存相關的函數中:

 1 unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
 2 {
 3  struct page *page;
 4  /*
 5  * __get_free_pages() returns a 32-bit address, which cannot represent
 6  * a highmem page
 7  */
 8  VM_BUG_ON((gfp_mask & __GFP_HIGHMEM) != 0);
 9  page = alloc_pages(gfp_mask, order);
10  if (!page)
11  return 0;
12  return (unsigned long) page_address(page);
13 }
alloc_pages() /__get_free_pages() 可以分配的最大頁面數是1024。這意味着在一個4KB大小的系統上,您最多可以分配1024 * 4KB = 4MB。kmalloc也是一樣。

轉換函數

page_to_virt()函數用於將struct page(例如alloc_pages()返回的頁面)轉換為內核地址。virt_to_page()接受內核虛擬地址並返回其關聯的struct page實例(就像使用alloc_pages()函數分配的一樣)。virt_to_page() 和 page_to_virt() 都定義在 <asm/page.h>:

struct page *virt_to_page(void *kaddr);
void *page_to_virt(struct page *pg);

page_address() 宏返回的虛擬地址對應於 struct page 實例的起始地址(邏輯地址):

void *page_address(const struct page *page);

我們可以在get_zeroed_page()函數中看到它是如何使用的:

 1 unsigned long get_zeroed_page(unsigned int gfp_mask)
 2 {
 3   struct page * page;
 4   page = alloc_pages(gfp_mask, 0);
 5   if (page) {
 6     void *address = page_address(page);
 7     clear_page(address);
 8     return (unsigned long) address;
 9   }
10   return 0;
11 }

__free_pages() 和 free_pages() 容易混淆。它們之間的主要區別是 free_pages() 接受一個虛地址作為參數,而__free_pages()接受一個struct page 結構作為參數。

slab分配器

slab 分配器是 kmalloc() 所依賴的。它的主要目的是消除在內存分配較小的情況下由buddy系統引起的內存分配/釋放造成的碎片,並加快常用對象的內存分配。

buddy 算法

內存分配請求的大小被四舍五入到2的冪,然后 buddy 分配器搜索相應的列表。如果不存在請求的條目,則下一個上級列表(其塊的大小是上一個列表的兩倍)中的條目被分成兩部分(稱為buddies)。分配器使用前半部分,而另一部分添加到下一個列表中。這是一種遞歸方法,當 buddy 分配器成功地找到可以拆分的塊時,或者當塊達到最大大小且沒有可用的空閑塊時,該方法就會停止。

舉個例子,如果最小分配大小是1 KB,內存大小是1 MB,buddy 分配器將創建一個空列表1 KB洞,空列表2 KB的洞,一個4 KB洞,8 KB、16 KB, 32 KB、64 KB、128 KB、256 KB、512 KB、和一個列表1 MB洞。它們最初都是空的,除了1MB的列表,它只有一個洞。讓我們假如我們想要分配一個70K大小的塊。buddy 分配器將它四舍五入到128K,最終將這1MB分成兩個512K塊,然后是256K,最后是128K,然后它將把其中一個128K塊分配給用戶。以下是該場景的概述:

 

 

      使用buddy算法分配

釋放和分配一樣快。下圖總結了回收算法:

 

 

              使用buddy算法進行回收

slab 分配器分析

  • Slab: 這是一個由幾個頁幀組成的連續的物理內存。每個slab被划分為相同大小的相等塊,用於存儲特定類型的內核對象,如索引節點、互斥對象等。每個slab是一個對象的數組。
  • Cache: 它由鏈表中的一個或多個slab組成,它們在內核中表示為 struct kmem_cache_t 結構的實例。cache 只存儲相同類型的對象(例如,僅存儲inodes,或僅存儲地址空間結構).

Slabs可能處於以下狀態之一:

  • Empty: 這是 slab 上的所有 objects(chunks) 被標記為 free 的地方。
  • Partial: used 和 free 的 objects 同時存在於 slab 中。
  • Full: slab 上所有的 objects 被標記為 used。

構建 caches 取決於內存分配器,最初,每個slab被標記為空。當你的代碼為內核對象分配內存時,系統會在緩存的 partial/free slab 中為該類型的對象尋找空閑位置。如果沒有找到,系統將分配一個新的slab並將其添加到 cache 中。從這個slab中分配新對象,並且slab被標記為partial。當內存使用完(釋放)時,對象被簡單地返回到初始化狀態的slab緩存。

這就是為什么內核還提供幫助函數來獲取初始化為零的內存,以消除以前的內容。slab保持有多少對象被使用的引用計數,所以當緩存中的所有slab都滿了,並且請求另一個對象時,slab分配器負責添加新的slab:

 

 

                                                          Slab cache概述

這有點像創建一個 per-object 分配器,系統為每種類型的對象分配一個緩存,並且只有相同類型的對象可以存儲在同一個緩存中(例如,只有 task_struct 結構體)。

內核中有不同類型的slab分配器,取決於是否需要緊湊性、緩存友好性或原始速度:

  • SLOB,它是盡可能緊湊的
  • SLAB,它是盡可能有利於緩存的
  • SLUB,非常簡單,需要較少的指令開銷計數

kmalloc

kmalloc是一個內核內存分配函數,如用戶空間中的malloc()。kmalloc返回的內存在物理內存和虛擬內存中是連續的:

 

 

 kmalloc分配器是內核中通用的高級內存分配器,它依賴於SLAB分配器。kmalloc返回的內存有一個內核邏輯地址,因為它是從 LOW_MEM 區域分配的,除非指定了 HIGH_MEM。它在<linux/slab.h>中聲明,在驅動程序中使用kmalloc時要包含這個頭文件。以下是原型:

void *kmalloc(size_t size, int flags);

size指定要分配的內存大小(以字節為單位)。flags 決定如何分配內存以及在哪里分配內存。可用 flags 與 page分配器的 flags 相同(GFP_KERNEL, GFP_ATOMIC, GFP_DMA,等等):

  • GFP_KERNEL: 我們不能在中斷處理程序中使用這個標志,因為它的代碼可能會休眠。它總是從 LOM_MEM 區域返回內存(因此是一個邏輯地址)。
  • GFP_ATOMIC:  這保證了分配的原子性。在中斷上下文中使用的唯一標志。請不要濫用它,因為它使用一個應急內存池。
  • GFP_USER: :這將內存分配給用戶空間進程。與分配給內核的內存是截然不同的。
  • GFP_HIGHUSER:  這將從HIGH_MEMORY區域分配內存。
  • GFP_DMA: 從DMA_ZONE中分配內存。

在成功分配內存時,kmalloc返回分配的塊的虛擬地址,保證是物理連續的。如果出錯,它將返回NULL。

kmalloc在分配小容量內存時依賴SLAB緩存。在這種情況下,內核將分配的區域大小舍入到能夠容納它的最小SLAB緩存的大小。始終使用它作為您的默認內存分配器。在 ARM 和 x86 架構中,每次分配的最大大小是4MB,總分配的最大大小是128MB。

kfree函數用於釋放kmalloc分配的內存。以下是kfree()的原型:

void kfree(const void *ptr)

例子:

 1 #include <linux/init.h>
 2 #include <linux/module.h>
 3 #include <linux/slab.h>
 4 #include <linux/mm.h>
 5 
 6 void *ptr;
 7 static int alloc_init(void)
 8 {
 9   size_t size = 1024; /* allocate 1024 bytes */
10   ptr = kmalloc(size, GFP_KERNEL);
11   if(!ptr) {
12     /* handle error */
13     pr_err("memory allocation failed\n");
14     return -ENOMEM;
15   } else {
16     pr_info("Memory allocated successfully\n");
17   }
18 
19   return 0;
20 }
21 static void alloc_exit(void)
22 {
23   kfree(ptr);
24   pr_info("Memory freed\n");
25 }
26 module_init(alloc_init);
27 module_exit(alloc_exit);
28 MODULE_LICENSE("GPL");
29 MODULE_AUTHOR("xxx");

其他類似的函數有:

1 void kzalloc(size_t size, gfp_t flags);
2 void kzfree(const void *p);
3 void *kcalloc(size_t n, size_t size, gfp_t flags);
4 void *krealloc(const void *p, size_t new_size, gfp_t flags);

krealloc() 是內核中的用戶空間 realloc() 函數。由於 kmalloc() 返回的內存保留了以前的內容,如果將其暴露給用戶空間,就可能存在安全風險。要獲得值全為零的內存,您應該使用 kzalloc。kzfree() 是 kzalloc() 的釋放函數,而kcalloc()為數組分配內存,其參數n 和 size 分別表示數組中元素的數量和元素的大小。

由於kmalloc()返回內核永久映射中的內存區域(這意味着物理上連續),可以使用 virt_to_phys() 將內存地址轉換為物理地址,或者使用 virt_to_bus() 將內存地址轉換為IO總線地址。這些宏內部調用 __pa() 或 __va() 中任何一個(如有必要)。物理地址(virt_to_phys(kmalloc'ed address)),通過PAGE_SHIFT向下移動,將生成所分配的塊的第一個頁面的PFN。

vmalloc

vmalloc() 申請的內存只在虛擬地址上連續,在物理地址上不連續。

 

 

 返回的內存總是來自HIGH_MEM區域。返回的地址不能被轉換成物理地址或總線地址,因為你不能斷言內存是物理上連續的。這意味着vmalloc()返回的內存不能在微處理器之外使用(您不能輕松地將其用於DMA目的)。使用vmalloc()為只存在於軟件(例如,網絡緩沖區)中的大型序列(例如,使用它來分配一個頁面沒有意義)分配內存是正確的。需要注意的是,vmalloc()比kmalloc()或頁分配器函數慢,因為它必須檢索內存,構建頁表,甚至重新映射到一個虛擬地址連續的范圍,而kmalloc()從不這樣做。

在使用vmalloc API之前,應該在代碼中包含這個頭文件:

#include <linux/vmalloc.h>

以下是vmalloc家族原型:

1 void *vmalloc(unsigned long size);
2 void *vzalloc(unsigned long size);
3 void vfree( void *addr);

size是您需要分配的內存大小。成功分配內存后,它返回已分配內存塊的第一個字節的地址。如果失敗,它將返回NULL。vfree函數用於釋放 vmalloc() 分配的內存。

vmalloc的示例如下:

 1 #include<linux/init.h>
 2 #include<linux/module.h>
 3 #include <linux/vmalloc.h>
 4 void *ptr;
 5 static int my_vmalloc_init(void)
 6 {
 7     unsigned long size = 8192;
 8     ptr = vmalloc(size);
 9     if(!ptr) {
10         /* handle error */
11         printk("memory allocation failed\n");
12         return -ENOMEM;
13     } else {
14         pr_info("Memory allocated successfully\n");
15     }    
16     return 0;
17 }
18 static void my_vmalloc_exit(void) /* function called at the time of 
19 
20 */
21 {
22     vfree(ptr); //free the allocated memory
23     printk("Memory freed\n");
24 }
25 module_init(my_vmalloc_init);
26 module_exit(my_vmalloc_exit);
27 MODULE_LICENSE("GPL");
28 MODULE_AUTHOR("xxx");            

可以使用 /proc/vmallocinfo 顯示系統中 vmalloc 使用的所有內存。VMALLOC_START 和 VMALLOC_END 是兩個分隔 vmalloc 地址范圍的符號。它們依賴於體系結構,在<asm/pgtable.h>中定義。

內部處理內存分配

讓我們關注更底層的分配器,它分配內存頁。內核將報告框架頁(物理頁)的分配,直到真正需要時(當這些頁通過讀或寫被實際訪問時)。這種按需分配稱為惰性分配,消除了分配永遠不會使用的頁面的風險。

每當請求一個頁時,只更新頁表,在大多數情況下會創建一個新條目,這意味着只分配虛擬內存。只有當您訪問該頁面時,才會引發稱為頁面錯誤的中斷。這個中斷有一個專用的處理程序,稱為頁面錯誤處理程序,當嘗試訪問沒有立即成功的虛擬內存時,MMU會調用這個處理程序。

實際上,對於頁表中的條目沒有設置允許這種訪問類型的適當權限位的頁,無論其訪問類型是什么(讀、寫、執行),都會引發頁錯誤中斷。對該中斷的響應可分為以下三種方式之一:

  • hard fault: 頁面不駐留在任何地方(既不在物理內存中,也不在內存映射文件中),這意味着處理程序不能立即解決故障。處理程序將執行I/O操作,以准備解決故障所需的物理頁,並可能在系統工作以解決問題時掛起中斷的進程並切換到另一個進程。
  • soft fault: 頁面駐留在內存的其他地方(在另一個進程的工作集中)。這意味着錯誤處理程序可以立即將物理內存的一個頁附加到適當的頁表項上,調整頁表項,並恢復被中斷的指令,從而解決故障。
  • 無法解決的 fault :  這將導致總線錯誤或segv。SIGSEGV被發送到出錯的進程,終止它(默認行為),除非SIGSEV已經安裝了一個信號處理程序來改變默認行為。

內存映射通常一開始不附加任何物理頁,而是在不關聯任何物理內存的情況下定義虛擬地址范圍。當訪問內存時,實際的物理內存稍后被分配,以響應頁面錯誤異常,因為內核提供了一些標志來確定嘗試的訪問是否合法,並指定了頁面錯誤處理程序的行為。因此,用戶空間brk()、mmap() 和 類似的分配(虛擬)空間,但是物理內存稍后附加。

在中斷上下文中出現的頁面錯誤會導致雙重錯誤中斷,這通常會使內核感到恐慌(調用panic()函數)。這就是為什么在中斷上下文中分配的內存是從內存池中獲取的,這不會引發頁錯誤中斷。處理雙重故障時發生中斷,會產生三重故障異常,導致CPU關閉,操作系統立即重啟。這種行為實際上是 arc-dependent 的。

copy-on-write (CoW)

CoW(在fork()中大量使用)是一個內核特性,它不會為兩個或多個進程共享的數據分配幾倍的內存,直到一個進程使用到它(寫入它);在這種情況下,內存被分配給它的私有副本。下面展示了頁面錯誤處理程序如何管理CoW(單頁案例研究):

  1. 將PTE添加到進程頁表,並標記為不可寫。
  2. 映射將導致在流程VMA列表中創建VMA。該頁面被添加到該VMA,該VMA被標記為可寫。
  3. 在頁訪問(第一次寫入時),錯誤處理程序注意到差異,這意味着這是一個CoW。然后,它將分配一個物理頁(分配給之前添加的PTE),更新PTE標志,刷新TLB項,並執行do_wp_page()函數,該函數可以將內容從共享地址復制到新位置。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM