詳細解讀 linux 內核中的各種內存分配函數

本文敘述了在 Linux 內核中常見的幾種內存分配函數及其異同,對理解 linux 底層內存分配機制有個較好理解。

1、kmalloc()

kmalloc() 函數類似與我們常見的 malloc() 函數,前者用於內核態的內存分配,後者用於用戶態。

kmalloc() 函數在物理內存中分配一塊連續的存儲空間,且和 malloc() 函數一樣,不會清除裏面的原始數據,如果內存充足,它的分配速度很快。其原型如下:

static inline void *kmalloc(size_t size, gfp_t flags);

除了上述表格所列標誌外,還包括如下:

2、vmalloc()

vmalloc() 一般用在爲只存在於軟件中(沒有對應的硬件意義)的較大的順序緩衝區分配內存,當內存沒有足夠大的連續物理空間可以分配時,可以用該函數來分配虛擬地址連續但物理地址不連續的內存。由於需要建立新的頁表,所以它的開銷要遠遠大於 kmalloc 及後面將要講到的__get_free_pages() 函數。且 vmalloc() 不能用在原子上下文中,因爲它的內部實現使用了標誌爲 GFP_KERNEL 的 kmalloc()。其函數原型如下:

void *vmalloc(unsigned long size);
void vfree(const void *addr);

static int xxx(...)
{
  ...
  cpuid_entries = vmalloc(sizeof(struct kvm_cpuid_entry) * cpuid->nent);
  if(!cpuid_entries)
  goto out;
  if(copy_from_user(cpuid_entries, entries, cpuid->nent * sizeof(struct kvm_cpuid_entry)))
    goto out_free;
  for(i=0; i<cpuid->nent; i++){
    vcpuid->arch.cpuid_entries[i].eax = cpuid_entries[i].eax;
    ...
    vcpuid->arch.cpuid_entries[i].index = 0;
  }
  ...
out_free:
  vfree(cpuid_entries);
out:
  r

3、頁分配函數

在 linux 中,內存分配是以頁爲單位的,32 位機中一頁爲 4KB,64 位機中,一頁爲 8KB,但具體還有根據平臺而定。

根據返回值類型的不同,頁分配函數分爲兩類,一是返回物理頁地址,二是返回虛擬地址。虛擬地址和物理地址起始相差一個固定的偏移量。


#define __pa(x) ((x) - PAGE_OFFSET)
static inline unsigned long virt_to_phys(volatile void *address)
{
  return __pa((void *)address);
}

#define __va(x) ((x) + PAGE_OFFSET)
static inline  void* phys_to_virt(unsigned long address)
{
  return __va(address);
}

根據返回頁面數目分類,分爲僅返回單頁面的函數和返回多頁面的函數。

3.1 alloc_page() 和 alloc_pages() 函數

該函數定義在頭文件 /include/linux/gfp.h 中,它既可以在內核空間分配,也可以在用戶空間分配,它返回分配的第一個頁的描述符而非首地址,其原型爲:


#define alloc_page(gfp_mask)  alloc_pages(gfp_mask, 0)
#define alloc_pages(gfp_mask, order) alloc_pages_node(numa_node_id(), gfp_mask, order)  //分配連續2^order個頁面
static inline struct page *alloc_pages_node(int nid, gfp_t gfp_mask, unsigned int order) 
{
  if(unlikely(order >= MAX_ORDER))
    return NULL;
  if(nid < 0)
    nid = numa_node_id();
  return __alloc_pages(gfp_mask, order, noed_zonelist(nid, gfp_mask));
}

3.2 __get_free_pages() 系列函數

它是 kmalloc 函數實現的基礎,返回一個或多個頁面的虛擬地址。該系列函數 / 宏包括 get_zeroed_page()、_ _get_free_page() 和_ _get_free_pages()。在使用時,其申請標誌的值及含義與 kmalloc() 完全一樣,最常用的是 GFP_KERNEL 和 GFP_ATOMIC。


/*分配多個頁並返回分配內存的首地址,分配的頁數爲2^order,分配的頁不清零。
order 允許的最大值是 10(即 1024 頁)或者 11(即 2048 頁),依賴於具體
的硬件平臺。*/
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
  struct page *page;
  page = alloc_pages(gfp_mask, order);
  if(!page)
    return 0;
  return (unsigned long)page_address(page);
}

#define __get_free_page(gfp_mask)  __get_free_pages(gfp_mask, 0)

/*該函數返回一個指向新頁的指針並且將該頁清零*/
unsigned long get_zeroed_page(unsigned int flags);

使用_ _get_free_pages() 系列函數 / 宏申請的內存應使用 free_page(addr) 或 free_pages(addr, order) 函數釋放:


#define __free_page(page) __free_pages((page), 0)
#define free_page(addr) free_pages((addr), 0)

void free_pages(unsigned long addr, unsigned int order)
{
  if(addr != 0){
    VM_BUG_ON(!virt_addr_valid((void*)addr));
    __free_pages(virt_to_page((void *)addr), order);
  }
}

void __free_pages(struct page *page, unsigned int order)
{
  if(put_page_testzero(page)){
    if(order == 0)
      free_hot_page(page);
    else
      __free_pages_ok(page, order);
  }
}

free_pages() 函數是調用__free_pages() 函數完成內存釋放的。

4、slab 緩存

4.1 創建 slab 緩存區

該函數創建一個 slab 緩存(後備高速緩衝區),它是一個可以駐留任意數目全部同樣大小的後備緩存。其原型如下:

struct kmem_cache *kmem_cache_create(const char *name, size_t size, \
                   size_t align, unsigned long flags,\
                   void (*ctor)(void *, struct kmem_cache *, unsigned long),\
                   void (*dtor)(void *, struct kmem_cache *, unsigned ong)));

其中:

name:創建的緩存名;

size:可容納的緩存塊個數;

align:後備高速緩衝區中第一個內存塊的偏移量(一般置爲 0);

flags:控制如何進行分配的位掩碼,包括 SLAB_NO_REAP(即使內存緊缺也不自動收縮這塊緩存)、SLAB_HWCACHE_ALIGN ( 每 個 數 據 對 象 被 對 齊 到 一 個 緩 存 行 )、SLAB_CACHE_DMA(要求數據對象在 DMA 內存區分配)等);

ctor:是可選的內存塊對象構造函數(初始化函數);

dtor:是可選的內存對象塊析構函數(釋放函數)。

4.2 分配 slab 緩存函數

一旦創建完後備高速緩衝區後,就可以調用 kmem_cache_alloc() 在緩存區分配一個內存塊對象了,其原型如下:

void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags);

cachep 指向開始分配的後備高速緩存,flags 與傳給 kmalloc 函數的參數相同,一般爲 GFP_KERNEL。

4.3 釋放 slab 緩存

該函數釋放一個內存塊對象:

void *kmem_cache_free(struct kmem_cache *cachep, void *objp);

4.4 銷燬 slab 緩存

與 kmem_cache_create 對應的是銷燬函數,釋放一個後備高速緩存:

int kmem_cache_destroy(struct kmem_cache *cachep);

它必須等待所有已經分配的內存塊對象被釋放後才能釋放後備高速緩存區。

4.5 slab 緩存使用舉例

創建一個存放線程結構體(struct thread_info)的後備高速緩存,因爲在 linux 中涉及頻繁的線程創建與釋放,如果使用__get_free_page() 函數會造成內存的大量浪費,效率也不高。所以在 linux 內核的初始化階段就創建了一個名爲 thread_info 的後備高速緩存,代碼如下:


/* 創建slab緩存 */
static struct kmem_cache *thread_info_cache;
thread_info_cache = kmem_cache_create("thread_info", sizeof(struct thread_info), \
                    SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL, NULL);

/* 分配slab緩存 */
struct thread_info *ti;
ti = kmem_cache_alloc(thread_info_cache, GFP_KERNEL);

/* 使用slab緩存 */
...
/* 釋放slab緩存 */
kmem_cache_free(thread_info_cache, ti);
kmem_cache_destroy(thread_info_cache);

5、內存池

在 Linux 內核中還包含對內存池的支持,內存池技術也是一種非常經典的用於分配大量小對象的後備緩存技術。

5.1 創建內存池


mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn, \
               mempool_free_t*free_fn, void *pool_data);

mempool_create() 函數用於創建一個內存池,min_nr 參數是需要預分配對象的數目,alloc_fn 和 free_fn 是指向內存池機制提供的標準對象分配和回收函數的指針,其原型分別爲:

typedef void *(mempool_alloc_t)(int gfp_mask, void *pool_data); 

typedef void (mempool_free_t)(void *element, void *pool_data);

pool_data 是分配和回收函數用到的指針,gfp_mask 是分配標記。只有當_ _GFP_WAIT 標記被指定時,分配函數纔會休眠。

5.2 分配和回收對象

在內存池中分配和回收對象需由以下函數來完成:


void *mempool_alloc(mempool_t *pool, int gfp_mask); 
void mempool_free(void *element, mempool_t *pool);

mempool_alloc() 用來分配對象,如果內存池分配器無法提供內存,那麼就可以用預分配的池。

5.3 銷燬內存池

void mempool_destroy(mempool_t *pool);

mempool_create() 函數創建的內存池需由 mempool_destroy() 來回收。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Fi8KH6bR6m3Kb8kgofMiGg