1.原理說明
Linux核心中采 用了一種同時適用於32位和64位系統的內 存分頁模型,對於32位系統來說,兩級頁表足夠用了,而在x86_64系 統中,用到了四級頁表,2-1所示。四級頁表分別為:
* 頁全域目錄(Page Global Directory)
* 頁上級目錄(Page Upper Directory)
* 頁中間目錄(Page Middle Directory)
* 頁表(Page Table)
頁全域目錄包含若干頁上級目錄的地址,頁上級目錄又依次包含若干頁中間目錄的地址,而頁中間目錄又包含若干頁表的地址,每一個頁表項指 向一個頁框。Linux中採用4KB大小的 頁框作為標準的記憶體配置單元。
多級分頁目錄結構
1.1.夥伴系統演算法
在實際應用中,經常需要分配一組連續的頁框,而頻繁地申請和釋放不同大小的連續頁框,必然導致在已指派頁框的記憶體塊中分散了許多小塊的 空閑頁框。這樣,即使這些頁框是閒置,其他需要分配連續頁框的應用也很難得到滿足。
為了避免出現這種情況,Linux核心中引入了夥伴系統演算法(buddy system)。把所有的空閑頁框分組為11個 塊鏈表,每個塊鏈表分別包含大小為1,2,4,8,16,32,64,128,256,512和1024個連續頁框的頁框塊。最大可以申請1024個連 續頁框,對應4MB大小的連續記憶體。每個頁框塊的第一個頁框的物理地址是該塊大小的整數倍。
假設要申請一個256個頁框的塊,先從256個頁框的鏈表中尋找空閑塊,如果沒有,就去512個 頁框的鏈表中找,找到了則將頁框塊分為2個256個 頁框的塊,一個分配給應用,另外一個移到256個頁框的鏈表中。如果512個頁框的鏈表中仍沒有空閑塊,繼續向1024個頁 框的鏈表尋找,如果仍然沒有,則返回錯誤。
頁框塊在釋放時,會主動將兩個連續的頁框塊合并為一個較大的頁框塊。
1.2.slab分 配器
slab分配器源於 Solaris 2.4 的 分配演算法,工作於實體記憶體頁框分配器之上,管理特定大小對象的緩衝,進行快速而高效的記憶體配置。
slab分配器為每種使用的核心對象建立單獨的緩衝區。Linux 核心已經採用了夥伴系統管理實體記憶體頁框,因此 slab分配器直接工作於夥伴系 統之上。每種緩衝區由多個 slab 組成,每個 slab就是一組連續的實體記憶體頁框,被劃分成了固定數目的對象。根據對象大小的不同,預設情況下一個 slab 最多可以由 1024個頁框構成。出於對齊 等其它方面的要求,slab 中分配給對象的記憶體可能大於使用者要求的對象實際大小,這會造成一定的 記憶體浪費。
2.常用記憶體配置函數
2.1.__get_free_pages
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
__get_free_pages函數是最原始的記憶體配置方式,直接從夥伴系統中擷取原始頁框,傳回值為第一個頁框的起始地址。__get_free_pages在實現上只是封裝了alloc_pages函 數,從程式碼分析,alloc_pages函數會分配長度為1<<order的 連續頁框塊。order參數的最大值由include/Linux/Mmzone.h文 件中的MAX_ORDER宏決定,在預設的2.6.18內 核版本中,該宏定義為10。也就是說在理論上__get_free_pages函 數一次最多能申請1<<10
* 4KB也就是4MB的 連續實體記憶體。但是在實際應用中,很可能因為不存在這麼大量的連續空閑頁框而導致分配失敗。在測試中,order為10時分配成功,order為11則返回錯誤。
2.2.kmem_cache_alloc
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 long))
void *kmem_cache_alloc(struct kmem_cache *c, gfp_t flags)
kmem_cache_create/ kmem_cache_alloc是基於slab分配器的一種記憶體配置方式,適用於反覆分配釋放同一大小記憶體塊的場合。首先用kmem_cache_create建立一個快取地區,然後用kmem_cache_alloc從 該快取地區中擷取新的記憶體塊。 kmem_cache_alloc一次能分配的最大記憶體由mm/slab.c檔案中的MAX_OBJ_ORDER宏 定義,在預設的2.6.18核心版本中,該宏定義為5, 於是一次最多能申請1<<5 * 4KB也就是128KB的
連續實體記憶體。分析核心源碼發現,kmem_cache_create函數的size參數大於128KB時會調用BUG()。測試結果驗證了分析結果,用kmem_cache_create分 配超過128KB的記憶體時使核心崩潰。
2.3.kmalloc
void *kmalloc(size_t size, gfp_t flags)
kmalloc是核心中最常用的一種記憶體配置方式,它通過調用kmem_cache_alloc函 數來實現。kmalloc一次最多能申請的記憶體大小由include/Linux/Kmalloc_size.h的 內容來決定,在預設的2.6.18核心版本中,kmalloc一 次最多能申請大小為131702B也就是128KB字 節的連續實體記憶體。測試結果表明,如果試圖用kmalloc函數分配大於128KB的記憶體,編譯不能通過。
2.4.vmalloc
void *vmalloc(unsigned long size)
前面幾種記憶體配置方式都是物理連續的,能保證較低的平均訪問時間。但是在某些場合中,對記憶體區的請求不是很頻繁,較高的記憶體訪問時間也 可以接受,這是就可以分配一段線性連續,物理不連續的地址,帶來的好處是一次可以分配較大塊的記憶體。圖3-1表 示的是vmalloc分配的記憶體使用量的位址範圍。vmalloc對 一次能分配的記憶體大小沒有明確限制。出於效能考慮,應謹慎使用vmalloc函數。在測試過程中, 最大能一次分配1GB的空間。
Linux核心部分記憶體分布
2.5.dma_alloc_coherent
void *dma_alloc_coherent(struct device *dev, size_t size,
ma_addr_t *dma_handle, gfp_t gfp)
DMA是一種硬體機制,允許外圍裝置和主存之間直接傳輸IO資料,而不需要CPU的參與,使用DMA機制能大幅提高與裝置通訊的 輸送量。DMA操作中,涉及到CPU高速緩 存和對應的記憶體資料一致性的問題,必須保證兩者的資料一致,在x86_64體繫結構中,硬體已經很 好的解決了這個問題, dma_alloc_coherent和__get_free_pages函數實現差別不大,前者實際是調用__alloc_pages函 數來分配記憶體,因此一次分配記憶體的大小限制和後者一樣。__get_free_pages分配的內
存同樣可以用於DMA操作。測試結果證明,dma_alloc_coherent函 數一次能分配的最大記憶體也為4M。
2.6.ioremap
void * ioremap (unsigned long offset, unsigned long size)
ioremap是一種更直接的記憶體“分配”方式,使用時直接指定物理起始地址和需要分配記憶體的大小,然後將該段 物理地址映射到核心地址空間。ioremap用到的物理地址空間都是事先確定的,和上面的幾種記憶體 分配方式並不太一樣,並不是分配一段新的實體記憶體。ioremap多用於裝置驅動,可以讓CPU直接存取外部裝置的IO空間。ioremap能映射的記憶體由原有的實體記憶體空間決定,所以沒有進行測試。
2.7.Boot Memory
如果要分配大量的連續實體記憶體,上述的分配函數都不能滿足,就只能用比較特殊的方式,在Linux內 核引導階段來預留部分記憶體。
2.7.1.在核心引導時分配記憶體
void* alloc_bootmem(unsigned long size)
可以在Linux核心引導過程中繞過夥伴系統來分配大塊記憶體。使用方法是在Linux核心引導時,調用mem_init函數之前 用alloc_bootmem函數申請指定大小的記憶體。如果需要在其他地方調用這塊記憶體,可以將alloc_bootmem返回的記憶體首地址通過EXPORT_SYMBOL導 出,然後就可以使用這塊記憶體了。這種記憶體配置方式的缺點是,申請記憶體的代碼必須在連結到核心中的代碼裡才能使用,因此必須重新編譯核心,而且記憶體管理系統 看不到這部分記憶體,需要使用者自行管理。測試結果表明,重新編譯核心後重啟,能夠訪問引導時分配的記憶體塊。
2.7.2.通過核心引導參數預留頂部記憶體
在Linux核心引導時,傳入參數“mem=size”保留頂部的記憶體區間。比如系統有256MB內 存,參數“mem=248M”會預留頂部的8MB記憶體,進入系統後可以調用ioremap(0xF800000,0x800000)來申請這段記憶體。
3.幾種分配函數的比較
分配原理 |
最大記憶體 |
其他 |
__get_free_pages |
直接對頁框進行操作 |
4MB |
適用於分配較大量的連續實體記憶體 |
kmem_cache_alloc |
基於slab機制實現 |
128KB |
適合需要頻繁申請釋放相同大小記憶體塊時使用 |
kmalloc |
基於kmem_cache_alloc實現 |
128KB |
最常見的分配方式,需要小於頁框大小的記憶體時可以使用 |
vmalloc |
建立非連續實體記憶體到虛擬位址的映射 |
物理不連續,適合需要大記憶體,但是對地址連續性沒有要求的場合 |
dma_alloc_coherent |
基於__alloc_pages實現 |
4MB |
適用於DMA操 作 |
ioremap |
實現已知物理地址到虛擬位址的映射 |
適用於物理地址已知的場合,如裝置驅動 |
alloc_bootmem |
在啟動kernel時,預留一段記憶體,核心看不見 |
小於實體記憶體大小,記憶體管理要求較高 |
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Linux核心中記憶體的管理不像在核心外這麼簡單。和使用者空間最大的不同是核心的內從空間不像使用者空間那麼容易得到,並不是總能輕易的得到想要的記憶體。
頁:
核心最基本的記憶體管理單元就是頁(page),因為MMU管理的記憶體基本單位是page,其維護著提供虛擬位址到物理地址轉換的頁表。
核心使用如下資料結構表述page:
struct page {
unsigned long flags; //the status of the page,eg if the page is dirty,is locked
atomic_t _count; //the references to this page
atomic_t _mapcount;
unsigned long private;
struct address_space *mapping;
pgoff_t index;
struct list_head lru;
void *virtual;//the page's virtual space
};
需要注意的page資料結構是對機器中所有實體記憶體的描述並關心頁中具體的資料,它只關心諸如page是否空閑,誰在佔用這個page等等。
區:
核心將整個記憶體分為三個區(zone)。
ZONE_DMA--可進行DMA的記憶體空間,在x86中是低於16M的地址空間
ZONE_NOMAL--在x86中是16M-896M的空間,這部分空間是永久直接映射到核心地址空間的
ZONE_HIGHMEM--動態映射的地址空間
為什麼要分區呢?因為在一些體系(如x86)中只有部分記憶體是可以直接映射的,對x86在896M以上的記憶體都是動態映射的,所以核心對記憶體分區描述顯得必要,DMA分區是能夠進行DMA操作的記憶體空間。
核心頁分配介面:
核心提供若干以page為配置單位的記憶體配置函數。
struct page * alloc_pages(gfp_t gfp_mask, unsigned int order);
分配2的order次方個連續的頁面,返回指向第一個page的指標。當然在實際中我們一般針對分配的記憶體的虛擬位址進行操作,下面這個
void * page_address(struct page *page);
返回page的邏輯地址指標。
當然為了方便核心也提供直接返回虛擬位址指標的函數,實際就是封裝了這兩個函數:
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);
還有直接分配單個page的函數,實際就是order參數為0的版本:
struct page * alloc_page(gfp_t gfp_mask) ;
unsigned long __get_free_page(gfp_t gfp_mask);
如果你想獲得的頁面全部被初始化為0,核心提供這個調用:
get_zeroed_page(gfp_mask);
工作方式和__get_free_pages相同,除了該函數會將核心空間初始化為0。
這些函數都有相對應的頁面釋放函數,原型如下:
void __free_pages(struct page *page, unsigned int order) ;
void free_pages(unsigned long addr, unsigned int order) ;
void free_page(unsigned long addr);
更常用的分配介面:
除了原始的以頁為單元的記憶體配置函數,核心還提供更為方便的和C語言中介面類似的分配介面,最常用的就是kmalloc函數:
void * kmalloc(size_t size, gfp_t flags);
該介面返回至少size位元組的物理連續的記憶體空間。用法和c語言malloc函數一樣。
當然還有類似free的記憶體釋放函數
void kfree(const void *ptr);
核心還提供vmalloc函數,用法和kmalloc一樣,唯一的不同的vmalloc不保證分配的記憶體是物理連續的。
怎麼選擇用哪個分配函數呢?大多數情況下,我們需要分配的記憶體並沒有需要是物理連續的,除了外設操作的記憶體需要記憶體連續,因為對很多外設來說並沒有邏輯記憶體的概念,他只能看到和操作實體記憶體。其他情況下,我們並沒有一定要使用連續實體記憶體的需要。然而事實是,大多是情況下,核心中還是使用kmalloc來分配記憶體,這基本上是基於效率考慮。因為vmalloc調用為了使不連續實體記憶體的邏輯地址連續會有很多附加對頁表的操作,所以除了必須(需要使用相當大的記憶體空間),一般情況直接使用kmalloc函數。
vmalloc也有對應的vfree函數。
gfp_mask參數:
gfp_mask參數可以設定很多值,一下是各個取值的用處(直接引用至LKD):
GFP_ATOMIC The allocation is high priority and must not sleep. This is the flag to use in interrupt handlers, in bottom halves, while holding a spinlock, and in other situations where you cannot sleep.
GFP_NOWAIT Like GFP_ATOMIC, except that the call will not fallback on emergency memory pools. This increases the liklihood of the memory allocation failing.
GFP_NOIO This allocation can block, but must not initiate disk I/O. This is the flag to use in block I/O code when you cannot cause more disk I/O, which might lead to some unpleasant recursion.
GFP_NOFS This allocation can block and can initiate disk I/O, if it must, but it will not initiate a filesystem operation. This is the flag to use in filesystem code when you cannot start another filesystem operation.
GFP_KERNEL This is a normal allocation and might block. This is the flag to use in process context code when it is safe to sleep. The kernel will do whatever it has to do to obtain the memory requested by the caller. This flag should be
your default choice.
GFP_USER This is a normal allocation and might block. This flag is used to allocate memory for user-space processes.
GFP_HIGHUSER This is an allocation from ZONE_HIGHMEM and might block. This flag is used to allocate memory for user-space processes.
GFP_DMA This is an allocation from ZONE_DMA. Device drivers that need DMA-able memory use this flag, usually in combination with one of the preceding flags.
使用SLAB緩衝分配:
當需要頻繁的針對特定資料結構分配和釋放記憶體時,為了避免反覆記憶體配置和釋放的開銷,Linux核心提供了強大的記憶體緩衝分配策略,即SLAB緩衝分配。(實際上kmalloc函數就是建立在slab分配器上的!)
每個slab包含一定數量的對象,即需要被緩衝的資料結構,每個slab都有三個狀態:full,partial,empty。當試圖從slab中分配一個object時,核心優先從partial的slab中擷取對象,不行則從empty slab中,如果全部是full則建立一個新的slab。核心中管理slab緩衝的組成主要有cache,slab,object,他們的關係可以用描述。
cache在核心中使用 kmem_cache結構來描述,包含三個鏈表--—slabs_full, slabs_partial, 以及slabs_empty。這些list中就包含相應狀態的slab,slab在核心中的描述如下:
struct slab {
struct list_head list; /* full, partial, or empty list */
unsigned long colouroff; /* offset for the slab coloring */
void *s_mem; /* first object in the slab */
unsigned int inuse; /* allocated objects in the slab */
kmem_bufctl_t free; /* first free object, if any */
};
下列函數可以建立一個新的cache:
struct kmem_cache * kmem_cache_create(const char *name, size_t size,
size_t align, unsigned long flags, void (*ctor)(void *));
各個參數說明如下:
const char *name:cache的名稱。
size_t size:cache中需要緩衝的object的大小。
size_t align:slab中第一個object的位移,一般0即可,使用標準對齊。
unsigned long flags:可以為緩衝分配執行一些附加操作,沒有的話直接0即可。
void (*ctor)(void *):cache的建構函式,你可以賦值一個函數地址,當新的page加入cache後一定會執行該建構函式。可以複賦值為NULL。
銷毀一個cache時使用
int kmem_cache_destroy(struct kmem_cache *cachep);
核心提供兩個函數負責從指定的cache擷取和交還記憶體:
void * kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags);
void kmem_cache_free(struct kmem_cache *cachep, void *objp);