1. 程式人生 > >linux核心記憶體分配

linux核心記憶體分配

核心中的記憶體分配通常通過kmalloc/kfree來進行,但是也有其它的方式來獲取記憶體,所有這些方式共同提供了核心中分配、釋放記憶體的介面。

一、kmalloc/kfree

類似於標準C中的malloc/free,kmalloc/kfree是核心中的用於常規記憶體分配的介面。

kmalloc/kfree是工作在slab分配器的基礎上的,在系統啟動時會呼叫kmem_cache_init,該函式會建立多個通用緩衝池,這些緩衝池中的slab物件的大小都是2的整數倍,並且依次增大,最小的大小為1<<KMALLOC_SHIFT_LOW,最大的大小為1<<KMALLOC_SHIFT_HIGH,緩衝池的數目為KMALLOC_SHIFT_HIGH - KMALLOC_SHIFT_LOW+ 1。這些緩衝池統稱為通用緩衝池,被用於kmalloc和kfree。從其實現機制可以看出,我們無法使用kmalloc申請大小大於1<< KMALLOC_SHIFT_HIGH的記憶體塊。

當通過kmalloc申請記憶體時,核心會根據所請求的大小來從通用緩衝池中選擇最合適的緩衝池進行記憶體分配,所謂的最合適就是大於等於申請大小的所有緩衝池中slab物件大小最小的那一個。

在分配時可以指定一些標記來指定分配時的行為,最常用的是GFP_KERNEL,使用該標記時,可能會休眠,另外一個是GFP_ATOMIC,分配不會休眠,用於原子上下文。還有很多不同的標記,可以參考檔案“include/linux/gfp.h”該檔案包含了分配標記及其含義。

二、專用緩衝池

通常情況下,通用緩衝池是夠用的,但是對於有的核心部件來說,它可能需要反覆申請、釋放固定大小的記憶體塊,這時也可以選擇建立自己的專用緩衝池,然後從該專用池中進行申請、釋放。這涉及到三個API:

2.1 建立專用緩衝池

typedef struct mempool_s {
	spinlock_t lock;
	int min_nr;		/* nr of elements at *elements */
	int curr_nr;		/* Current nr of elements at *elements */
	void **elements;

	void *pool_data;
	mempool_alloc_t *alloc;
	mempool_free_t *free;
	wait_queue_head_t wait;
} mempool_t;

記憶體池的設計思想是:在建立記憶體池時,首先申請指定數目的記憶體物件,並將其儲存在記憶體池中,隨後在進行分配時,首先嚐試常規的分配,如果無法申請到記憶體,就從記憶體池預留的記憶體物件中取出一個;在釋放記憶體時,如果記憶體池預留的記憶體物件數目小於指定的數目,則將要釋放的記憶體放入預留記憶體中而不做實際的釋放,如果記憶體池中預留記憶體物件的數目等於指定的數目則進行真正的釋放操作。

記憶體池所預留的記憶體實際上是被浪費了,因而最好不要用它。

3.1 建立記憶體池

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

該函式被用於建立記憶體池,在建立時會首先使用alloc_fn申請min_nr個記憶體物件,並將其儲存在記憶體池中。

  • min_nr:保證記憶體池中最少有這麼多個記憶體物件
  • alloc_fn:用於自定義的進行真正的記憶體分配的函式,一般可用mempool_alloc_slab
  • free_fn:用於自定義的用於真正的記憶體釋放的函式,一般可用mempool_free_slab
  • pool_data: 被傳遞給使用者自定義函式(alloc_fn,free_fn)的引數

不在使用的記憶體池可以用voidmempool_destroy(mempool_t *pool)來銷燬。

3.2 從記憶體池申請記憶體/向記憶體池釋放記憶體

void *mempool_alloc(mempool_t *pool, intgfp_mask);

它用於使用標記gfp_mask來從記憶體池pool中申請記憶體,如果常規的申請失敗(即呼叫建立該記憶體池時提供的使用者自定義分配函式分配失敗),則從預留的記憶體物件中返回一個。gfp_mask類似於kmalloc的flags。

void mempool_free(void *element, mempool_t*pool);

將記憶體element返回給記憶體池pool,如果pool中當前的預留記憶體物件數目小於記憶體池的min_nr,則記憶體被歸還懂啊記憶體池的預留記憶體物件中,否則呼叫真正的釋放函式(即呼叫建立該記憶體池時提供的使用者自定義釋放函式)

四、分配大塊記憶體

如果一個核心部件需要大塊的記憶體,則可以使用面向頁面的技術(kmalloc對申請的最大大小有限制)

如果一個模組需要分配大塊的記憶體, 它常常最好是使用一個面向頁的技術

4.1 分配、釋放頁(使用地址指標)

get_zeroed_page(gfp_t gfp_mask);

返回一個指向新頁的指標並且用零填充了該頁.

__get_free_page(gfp_t gfp_mask);

類似於get_zeroed_page, 但是沒有清零該頁.

__get_free_pages(unsigned int gfp_mask,unsigned int order);

分配並返回一個指向一個記憶體區第一個位元組的指標, 記憶體區可能是幾個(物理上連

續)頁長但是沒有清零。

Order:為冪指數,即它指定分配多少個頁,比如order為0,表示分配2的0次冪即1頁。

gfp_mask:和kmalloc的flags相同

分配可能失敗,因而呼叫者必須處理失敗。

void free_page(unsigned long addr);

void free_pages(unsigned long addr,unsigned long order);

這兩個函式用於釋放頁,注意如果指定了order,則申請時和釋放時必須使用相同的值。需要注意的是這裡的api無法用於分配高階記憶體。

4.2 分配、釋放頁(使用page)

static inline struct page*alloc_pages(gfp_t gfp_mask, unsigned int order)

#define alloc_page(gfp_mask)alloc_pages(gfp_mask, 0)

這兩個函式也用於分配頁,但是它們返回的是一個指向page資料結構的指標,而不是頁面的起始地址。gfp_mask類似於kmalloc的flags。order類似於__get_free_pages的order引數。

使用它們分配的記憶體頁應該使用下面的介面歸還給系統:

void __free_page(struct page *page);

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

void free_hot_page(struct page *page);

void free_cold_page(struct page *page);

這裡的API適用於高階記憶體。

4.3 vmalloc/vfree

vmalloc用於從虛擬記憶體空間分配一塊連續的記憶體區,儘管這些頁在實體記憶體中不連續 (使用alloc_page來獲得每個頁),但是核心將它們作為一個連續的地址範圍來看待。

從 vmalloc 獲得的記憶體用起來稍微低效些,因此不建議使用它。另外由vmalloc返回的地址必須經過頁表才能找到真正的實體地址,因而如果核心部件需要使用真正的實體地址,則不能使用它來分配。

void *vmalloc(unsigned long size);用來申請記憶體

void vfree(void * addr);用來釋放用vmalloc申請的記憶體

4.3.1 vmalloc和kmalloc以及__get_free_pages的區別:

vmalloc返回的是虛擬地址,但是實際上kmalloc和__get_free_pages及相關函式返回的也是虛擬地址,那麼為什麼vmalloc的效率很低,而其它兩個不低呢,這是因為雖然kmalloc和__get_free_pages及相關函式雖然也返回虛擬地址,但是它們所返回的虛擬地址是不同的。

  • vmalloc所返回的地址範圍在VMALLOC_START和VMALLOC_END之間,對於這部分地址需要在使用時建立頁表,並且這部分地址對應的實體地址可能是不連續的,必須通過頁表來訪問;而kmalloc和__get_free_pages返回的虛擬地址則屬於常規的核心虛擬地址空間,這部分虛擬地址空間的特點在於它和真實的實體地址之間就只有一個偏移量的差異,也就是說它們和真實的實體地址之間是一一對應的。
  • 另一方面也容易看出雖然__get_free_pages和vmalloc都可以返回大塊的記憶體,但是vmalloc返回的記憶體可能是由多個不連續的物理頁組成的,而且需要建立頁表(vmalloc分配過程中會在用alloc_page(s)之後用map_vm_area來建立頁表),而__get_free_pages則是返回連續的已經建立了頁表的常規記憶體頁。
vmalloc不能在原子上下文使用,因為它要建立頁表,因而需要使用kmalloc來為頁表來分配記憶體空間,這個過程可能會休眠。

4.4 ioremap

類似於vmalloc,使用ioremap 時也要建立新頁表,不同於 vmallocd的是它實際上不分配任何記憶體,ioremap 的返回值是一個特殊的虛擬地址,該地址用於存取特定的實體地址範圍。使用它獲取的地址要用iounmap 來釋放。對於ioremap返回的地址在使用時最好使用/IO讀寫函式而不是直接訪問。

五、 獲得大塊連續的實體記憶體

在核心啟動後,尤其是執行一段時間後,就很難通過上述方法來獲取大塊物理上連續的記憶體區域,因為使用上述方法可以獲取大塊連續的實體記憶體的方法就是呼叫__get_free_pages,但是在核心執行一段時間後,可能就很難找到大塊物理上連續的記憶體了。如果一個核心部件確實需要大塊物理上連續的記憶體,那麼最好的方法是在啟動過程中就進行分配,然後保留給自己使用。

啟動過程中分配並保留記憶體的方法是呼叫如下API:

#include <linux/bootmem.h>

void *alloc_bootmem(unsigned long size);

該函式用於分配指定大小的記憶體區域。

void *alloc_bootmem_low(unsigned long size);

該函式用於在低端地址區域分配指定大小的記憶體。低端地址區域指的是小於ARCH_LOW_ADDRESS_LIMIT的地址。

void *alloc_bootmem_pages(unsigned long size);

該函式用於分配指定大小的記憶體區域,但是分配的地址會對其到page上。

void *alloc_bootmem_low_pages(unsigned long size);

該函式用於在低端地址區域分配指定大小的記憶體。低端地址區域指的是小於ARCH_LOW_ADDRESS_LIMIT的地址。但是分配的地址會對其到page上。

需要注意的是這種分配是有限制的,即使用這種分配的程式碼必須在系統啟動時就被載入執行,模組是不可能使用這種技術的。另外使用該技術分配的記憶體對記憶體管理子系統來說是不可見的,因而它會減少系統的可用記憶體。

使用該技術分配的記憶體可以用free_bootmem釋放,但是這種釋放並不能把記憶體釋放給記憶體管理子系統(除非你在記憶體管理子系統進行初始化之前將其歸還給了系統)。

除了使用bootmem之外,在較新的核心中還引入了一種新的機制來在啟動階段分配預留記憶體,這就是memblock。memblock這個部件在初始化階段會獲取系統的所有實體記憶體的資訊,並將它們分為兩類,常規記憶體和保留記憶體,在記憶體剛被發現時它都是常規記憶體,核心部件可以通過memblock_alloc這個API來申請並預留一片記憶體區域,通過memblock_free可以釋放相應的記憶體區域,它們的工作機制類似於bootmem。不同於bootmem的是:

  1. bootmem需要在bootmem分配器初始化完成後才能呼叫,從程式碼上來說,bootmem在start_kernel->setup_arch->do_init_bootmem中進行初始化,也就是直到這一步之後它才能使用。而memblock的初始化則在:early_setup->early_init_devtree->early_init_dt_scan_memory_ppc(PPC為例),在PPC中,early_setup是在start_kernel之前的。從兩者的功能上來說,也可以看出其先後,bootmem是一個記憶體分配器,它本身就需要使用實體記憶體,而memblock則完成實體記憶體的檢測,因而bootmem可用的時間點必定在memblock之後
  2. memblock用連結串列來維護保留區域以及常規記憶體,而bootmem的管理則使用點陣圖來管理,相對而言memblock更靈活(bootmem每次分配進行搜尋時可能會從上一次分配結束的地址開始找到一塊滿足分配的連續區域,因而可能引入碎片,而且如果有大量分配,搜尋點陣圖也比較慢,這也是這個簡單的分配器的一個弊端,具體的細節可以參考程式碼)。