內(nèi)存管理,不用多說(shuō),言簡(jiǎn)意賅。在內(nèi)核里分配內(nèi)存還真不是件容易的事情,根本上是因?yàn)閮?nèi)核不能想用戶空間那樣奢侈的使用內(nèi)存。
先來(lái)說(shuō)說(shuō)內(nèi)存管理。內(nèi)核把物理頁(yè)作為內(nèi)存管理的基本單位。盡管處理器的最小可尋址單位通常是字,但是,內(nèi)存管理單元MMU通常以頁(yè)為單位進(jìn)行處理。因此,從虛擬內(nèi)存的交代來(lái)看,頁(yè)就是最小單位。內(nèi)核用struct page(linux/mm.h)結(jié)構(gòu)表示系統(tǒng)中的每個(gè)物理頁(yè):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct page {
unsigned long flags;
atomic_t count;
unsigned int mapcount;
unsigned long private;
struct address_space *mapping;
pgoff_t index;
struct list_head lru;
union{
struct pte_chain;
pte_addr_t;
}
void *virtual;
};
flag用來(lái)存放頁(yè)的狀態(tài),每一位代表一種狀態(tài),所以至少可以同時(shí)表示出32中不同的狀態(tài),這些狀態(tài)定義在linux/page-flags.h中。count記錄了該頁(yè)被引用了多少次。mapping指向與該頁(yè)相關(guān)的address_space對(duì)象。virtual是頁(yè)的虛擬地址,它就是頁(yè)在虛擬內(nèi)存中的地址。要理解的一點(diǎn)是page結(jié)構(gòu)與物理頁(yè)相關(guān),而并非與虛擬頁(yè)相關(guān)。因此,該結(jié)構(gòu)對(duì)頁(yè)的描述是短暫的。內(nèi)核僅僅用這個(gè)結(jié)構(gòu)來(lái)描述當(dāng)前時(shí)刻在相關(guān)的物理頁(yè)中存放的東西。這種數(shù)據(jù)結(jié)構(gòu)的目的在于描述物理內(nèi)存本身,而不是描述包含在其中的數(shù)據(jù)。
在linux中,內(nèi)核也不是對(duì)所有的也都一視同仁,內(nèi)核而是把頁(yè)分為不同的區(qū),使用區(qū)來(lái)對(duì)具有相似特性的頁(yè)進(jìn)行分組。Linux必須處理如下兩種硬件存在缺陷而引起的內(nèi)存尋址問(wèn)題:
1.一些硬件只能用某些特定的內(nèi)存地址來(lái)執(zhí)行DMA
2.一些體系結(jié)構(gòu)其內(nèi)存的物理尋址范圍比虛擬尋址范圍大的多。這樣,就有一些內(nèi)存不能永久地映射在內(nèi)核空間上。
為了解決這些制約條件,Linux使用了三種區(qū):
1.ZONE_DMA:這個(gè)區(qū)包含的頁(yè)用來(lái)執(zhí)行DMA操作。
2.ZONE_NOMAL:這個(gè)區(qū)包含的都是能正常映射的頁(yè)。
3.ZONE_HIGHEM:這個(gè)區(qū)包"高端內(nèi)存",其中的頁(yè)能不永久地映射到內(nèi)核地址空間。
區(qū)的實(shí)際使用與體系結(jié)構(gòu)是相關(guān)的。linux 把系統(tǒng)的頁(yè)劃分區(qū),形成不同的內(nèi)存池,這樣就可以根據(jù)用途進(jìn)行分配了。需要說(shuō)明的是,區(qū)的劃分沒(méi)有任何物理意義,只不過(guò)是內(nèi)核為了管理頁(yè)而采取的一種邏輯上的分組。盡管某些分配可能需要從特定的區(qū)中獲得頁(yè),但這并不是說(shuō),某種用途的內(nèi)存一定要從對(duì)應(yīng)的區(qū)來(lái)獲取,如果這種可供分配的資源不夠用了,內(nèi)核就會(huì)占用其他可用去的內(nèi)存。下表給出每個(gè)區(qū)及其在X86上所占的列表:
每個(gè)區(qū)都用定義在linux/mmzone.h中的struct zone表示,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct zone {
spinlock_t lock;
unsigned long free_pages;
unsigned long pages_min, pages_low, pages_high;
unsigned long protection[MAX_NR_ZONES];
spinlock_t lru_lock;
struct list_head active_list;
struct list_head inactive_list;
unsigned long nr_scan_active;
unsigned long nr_scan_inactive;
unsigned long nr_active;
unsigned long nr_inactive;
int all_unreclaimable;
unsigned long pages_scanned;
struct free_area free_area[MAX_ORDER];
wait_queue_head_t * wait_table;
unsigned long wait_table_size;
unsigned long wait_table_bits;
struct per_cpu_pageset pageset[NR_CPUS];
struct pglist_data *zone_pgdat;
struct page *zone_mem_map;
unsigned long zone_start_pfn;
char *name;
unsigned long spanned_pages;
unsigned long present_pages;
};
其中的lock域是一個(gè)自旋鎖,這個(gè)域只保護(hù)結(jié)構(gòu),而不是保護(hù)駐留在這個(gè)區(qū)中的所有頁(yè)。沒(méi)有特定的鎖來(lái)保護(hù)單個(gè)頁(yè)。free_pages域是這個(gè)區(qū)中空閑頁(yè)的個(gè)數(shù)。內(nèi)核盡可能的保護(hù)有pages_min個(gè)空閑頁(yè)可用。name域是一個(gè)以NULL結(jié)束的字符串,表示這個(gè)區(qū)的名字。內(nèi)核啟動(dòng)期間初始化這個(gè)值,其代碼位于mm/page_alloc.h中,三個(gè)區(qū)的名字分別是"DMA","Normal","HighMem"。
內(nèi)核提供了一種請(qǐng)求內(nèi)層的底層機(jī)制,并提供了對(duì)它進(jìn)行訪問(wèn)的幾個(gè)接口。所有這些接口都是以頁(yè)為單位進(jìn)行操作的。下表給出所有底層的頁(yè)分配方法:
當(dāng)你不再需要頁(yè)時(shí)可以用下列函數(shù)釋放它們,只是提醒:僅能釋放屬于你的頁(yè),否則可能導(dǎo)致系統(tǒng)崩潰。內(nèi)核是完全信任自己的,如果有非法操作,內(nèi)核會(huì)開(kāi)心的把自己掛起來(lái),停止運(yùn)行。列表如下:
上面提到都是以頁(yè)為單位的分配方式,那么對(duì)于常用的以字節(jié)為單位的分配來(lái)說(shuō),內(nèi)核通供的函數(shù)是kmalloc(),和mallloc很像吧,其實(shí)還真是這樣,只不過(guò)多了一個(gè)flags參數(shù)。用它可以獲得以字節(jié)為單位的一塊內(nèi)核內(nèi)存。如果需要的是頁(yè)----尤其是在你的需求總量接近2的冪次方的時(shí)候----那么,前面討論的頁(yè)分配接口可能是更好的選擇。
接下來(lái),注意的話,可能會(huì)發(fā)現(xiàn)無(wú)論是頁(yè)分配接口還是kmalloc都有一個(gè)分配器標(biāo)志(如GFP_KERNEL這樣的)。這些標(biāo)志可分為三類:行為修飾符,區(qū)修飾符及類型.下面就來(lái)討論個(gè)問(wèn)題.
1.行為修飾符(linux/gfp.h):表示內(nèi)核應(yīng)當(dāng)如何分配所需的內(nèi)存。在某些特定的情況下,只能使用某些特定的方法分配內(nèi)存??梢酝瑫r(shí)使用這些標(biāo)志,用|鏈接。列表如下:
2.區(qū)分配符:它只關(guān)心去應(yīng)當(dāng)從何處分配。通常,分配可以從任何區(qū)開(kāi)始。不過(guò),內(nèi)核優(yōu)先從ZONE_NORMAL開(kāi)始,這樣可以確保其他區(qū)在需要時(shí)有足夠的空閑頁(yè)可以使用。區(qū)修飾符如下:
不能給_get_free_pages()指定ZONE_HIGHMEM,因?yàn)檫@個(gè)函數(shù)返回都是邏輯地址,而不是page結(jié)構(gòu)。這兩個(gè)函數(shù)分配的內(nèi)存當(dāng)前可能有可能還沒(méi)有映射到內(nèi)核的虛擬地址空間,因此,也可能根本就沒(méi)有邏輯地址。只有alloc_pages()才能分配高端內(nèi)存。實(shí)際上,大多數(shù)ZONE_NORMAL就已經(jīng)足夠了。
3.類型標(biāo)志:指定所需的行為和區(qū)描述符以完成特殊類型的處理。正因?yàn)檫@點(diǎn),內(nèi)核代碼趨向于使用正確的類型標(biāo)志,而不是一味地指定它可能需要用到的多個(gè)描述符。下面兩個(gè)表分別給出了類型標(biāo)志的列表和每個(gè)類型標(biāo)志與哪些修飾符相關(guān)聯(lián):
上表中,左邊是類型標(biāo)志,右邊是每種類型標(biāo)志后隱含的修飾符列表。在編寫(xiě)的大多數(shù)代碼中,用到的要么是GFP_KERNEL,要么是GFP_ATOMIC。下表是通常情形和所用標(biāo)志的列表,不管使用那種分配類型,你都必須進(jìn)行檢查,并對(duì)錯(cuò)誤進(jìn)行處理:
有了kmalloc,當(dāng)然就有kfree()(linux/slab.h),釋放由kmalloc()分配出來(lái)的內(nèi)存塊。如果想要釋放的內(nèi)存不是由kmalloc()分配的,或者想要釋放的內(nèi)存早就被釋放了,在這種情況下調(diào)用這個(gè)函數(shù)會(huì)導(dǎo)致嚴(yán)重的后果。特別說(shuō)明kfree(NULL)是安全的。
vmalloc()和kmalloc是一樣的作用,不同在于前者分配的內(nèi)存虛擬地址是連續(xù)的,而物理地址則無(wú)需連續(xù)。這也是用戶空間分配函數(shù)的工作方式,如malloc().kmalloc()可以保證在物理地址上都是連續(xù)的(當(dāng)然,虛擬地址當(dāng)然也是連續(xù)的)。vmalloc()函數(shù)只確保頁(yè)在虛擬機(jī)地址空間內(nèi)是連續(xù)的。它通過(guò)分配非聯(lián)系的物理內(nèi)存塊,再“修正”頁(yè)表,把內(nèi)存映射到邏輯地址空間的連續(xù)區(qū)域中,就能做到這點(diǎn)。但很顯然這樣會(huì)降低處理性能,因?yàn)閮?nèi)核不得不做“拼接”的工作。所以這也是為什么不得已才使用vmalloc()的原因(比如獲得大內(nèi)存時(shí))。大多數(shù)情況下,只有硬件設(shè)備需要得到物理地址連續(xù)的內(nèi)存。硬件設(shè)備存在于內(nèi)存管理單元以外,它根本不懂什么是虛擬地址。因此,硬件設(shè)備用到的任何內(nèi)存區(qū)都必須是物理上連續(xù)的塊,而不僅僅是虛地址連續(xù)的塊。最后需要說(shuō)明的是,vmalloc()可能睡眠,不能從中斷上下文中進(jìn)行調(diào)用,也不能從其他不允許阻塞的情況下進(jìn)行調(diào)用。釋放時(shí)必須使用vfree().
分配和釋放數(shù)據(jù)結(jié)構(gòu)是所有內(nèi)核中最普遍的操作之一。為了便于數(shù)據(jù)的頻繁分配和回收,常常會(huì)用到一個(gè)空間鏈表。它就相當(dāng)于對(duì)象高速緩存以便快速存儲(chǔ)頻繁使用的對(duì)象類型。在內(nèi)核中,空閑鏈表面臨的主要問(wèn)題之一是不能全局控制。當(dāng)可用內(nèi)存變得緊張的時(shí)候,內(nèi)核無(wú)法通知每個(gè)空閑鏈表,讓其收縮緩存的大小以便釋放一些內(nèi)存來(lái)。實(shí)際上,內(nèi)核根本不知道有這樣的空閑離岸邊。為了彌補(bǔ)這一缺陷,也為了是代碼更加穩(wěn)固,linux內(nèi)核提供了slab層(也就是所謂的slab分類器),slab分類器扮演了通用數(shù)據(jù)結(jié)構(gòu)緩存層的角色。slab分配器試圖在如下幾個(gè)原則中尋求一種平衡:
1.頻繁使用的數(shù)據(jù)結(jié)構(gòu)也會(huì)頻繁分配和釋放,因此應(yīng)當(dāng)緩存它們。
2.頻繁分配和回收必然會(huì)導(dǎo)致內(nèi)存碎片。為了避免這種情況,空閑鏈表的緩存會(huì)連續(xù)地存放。因?yàn)橐厌尫诺臄?shù)據(jù)結(jié)構(gòu)又會(huì)放回空閑鏈表,不會(huì)導(dǎo)致碎片。
3.回收的對(duì)象可以立即投入下一次分配,因此,對(duì)于頻繁的分配和釋放,空閑鏈表能夠提高其性能。
4.如果讓部分緩存專屬于單個(gè)處理器,那么,分配和釋放就可以在不加SMP鎖的情況下進(jìn)行。
5.對(duì)存放的對(duì)象進(jìn)行著色,以防止多個(gè)對(duì)象映射到相同的高速緩存行。
slab層把不同的對(duì)象劃分為所謂的高速緩存組,其中每個(gè)高速緩存都存放不同類型的對(duì)象,每種對(duì)象類型對(duì)應(yīng)一個(gè)高速緩存。kmalloc()接口建立在slab層上,使用了一組通用高速緩存。這些緩存又被分為slabs,slab由一個(gè)或多個(gè)物理上連續(xù)的頁(yè)組成,一般情況下,slab也就僅僅由一頁(yè)組成。每個(gè)高速緩存可以由多個(gè)slab組成。每個(gè)slab都包含一些對(duì)象成員,這里的對(duì)象指的是被緩存的數(shù)據(jù)結(jié)構(gòu),每個(gè)slab處于三種狀態(tài)之一:滿,部分滿,空。當(dāng)內(nèi)核的某一部分需要一個(gè)新的對(duì)象時(shí),先從部分滿的slab中進(jìn)行分配。如果沒(méi)有部分滿的slab,就從空的slab中進(jìn)行分配。如果沒(méi)有空的slab,就要?jiǎng)?chuàng)建一個(gè)slab了。下圖給出高速緩存,slab及對(duì)象之間的關(guān)系:
上圖中的每個(gè)cache由kmem_cache_s結(jié)構(gòu)表示,這個(gè)結(jié)構(gòu)包含三個(gè)鏈表slabs_full,slab_partial和slabs_empty,均存放在kmem_list3結(jié)構(gòu)內(nèi),這些鏈表包含高速緩存中的所有slab,slab描述符struct slab:
1
2
3
4
5
6
7
struct slab {
struct list_head list; /*滿,部分滿或空鏈表*/
unsigned long colouroff; /*slab著色的偏移量*/
void *s_mem; /*在slab中的第一個(gè)對(duì)象*/
unsigned int inuse; /*已分配的對(duì)象數(shù)*/
kmem_bufctl_t free; /*第一個(gè)空閑對(duì)象*/
};
slab描述符要么在slab之外另行分配,要么就在slab自身最開(kāi)始的地方。如果slab很小或者slab內(nèi)核有足夠的空間容納slab描述符,那么描述符就存放在slab里面.slab分配器創(chuàng)建新的slab是通過(guò)__get_free_pages()低級(jí)內(nèi)存分配器進(jìn)行的:
1
2
3
4
5
6
7
static inline void * kmem_getpages(kmem_cache_t *cachep, unsigned long flags)
{
void *addr;
flags |= cachep->gfpflags;
addr = (void*)__get_free_pages(flags, cachep->gfporder);
return addr;
}
上面的是一個(gè)描述原理的簡(jiǎn)化版。接著,調(diào)用kmem_freepages()釋放內(nèi)存,而對(duì)給定的高速緩存頁(yè),kmem_freepages()最終調(diào)用的是free_pages().當(dāng)然,slab層的關(guān)鍵就是避免頻繁分配和釋放頁(yè)。由此可知,slab頁(yè)只有當(dāng)給定的高速緩存中既沒(méi)有部分滿也沒(méi)有空的slab時(shí)候才會(huì)調(diào)用頁(yè)分配函數(shù)。而只有在下列情況下才會(huì)調(diào)用釋放函數(shù):當(dāng)可用內(nèi)存變得緊缺時(shí),系統(tǒng)試圖釋放出更多內(nèi)存以供使用,或者當(dāng)高速緩存顯式地被銷毀時(shí)。slab層的管理是在每個(gè)高速緩存的基礎(chǔ)上,通過(guò)提供個(gè)整個(gè)內(nèi)核一個(gè)簡(jiǎn)單的接口來(lái)完成的。通過(guò)接口就可以創(chuàng)建和銷毀新的高速緩存,并在高速緩存內(nèi)分配和釋放對(duì)象。高速緩存及slab的復(fù)雜管理完全通過(guò)slab層的內(nèi)部機(jī)制來(lái)處理。當(dāng)創(chuàng)建一個(gè)高速緩存后,slab層所起的作用就像一個(gè)專用的分配器,可以為具體的對(duì)象類型進(jìn)行分配。一個(gè)新的高速緩存是通過(guò)一下接口進(jìn)行創(chuàng)建的:
1
kmem_cache_t * kmem_cache_create(const char *name, size_t size,size_t align, unsigned long flags,
1
2
void (*ctor)(void*, kmem_cache_t *, unsigned long),
void (*dtor)(void*, kmem_cache_t *, unsigned long));
1
<font face="微軟雅黑"> 有關(guān)這個(gè)函數(shù)的說(shuō)明,我就省略了,需要的網(wǎng)上一大堆。這個(gè)函數(shù)成功時(shí)會(huì)返回一個(gè)執(zhí)行所創(chuàng)建高速緩存的指針,否則,返回空。這個(gè)函數(shù)由于會(huì)睡眠,因此不能在中斷上下文中使用。要銷毀一個(gè)高速緩存,調(diào)用:int kmem_cache_destroy(kmem_cache_t *cachep),同樣,也是不能在中斷上下文中使用。調(diào)用該函數(shù)之前必須確保存在以下兩個(gè)條件:</font>
1.高速緩存中的所有slab都必須為空。
2.在調(diào)用kmem_cache_destory()期間不再訪問(wèn)這個(gè)高速緩存,調(diào)用者必須確保這種同步。
創(chuàng)建了高速緩存以后,就可以通過(guò)下列函數(shù)從中獲取對(duì)象:void * kmem_cache_alloc(kmem_cache_t *cachep, int flags)。該函數(shù)從高速緩存cachep中返回一個(gè)指向?qū)ο蟮闹羔?。如果高速緩存的所有slab中都沒(méi)有空閑的對(duì)象,那么slab層必須通過(guò)kmem_getpages()獲取新的頁(yè),flags的值傳遞給__get_free_pages().最后,釋放一個(gè)對(duì)象,并把它返回給原來(lái)的slab,可以使用下面的函數(shù):
1
void kmem_cache_free(kmem_cache_t *cachep,void *objp)
這樣就能把cachep中的對(duì)象objp標(biāo)記為空閑了,關(guān)于slab分配器的使用實(shí)例,參考資料上有,我就不說(shuō)了。相比較以前的用戶空間棧而言,內(nèi)核棧是非常小的。每個(gè)進(jìn)程都有自己的內(nèi)核棧進(jìn)程在內(nèi)核執(zhí)行期間的整個(gè)調(diào)用鏈必須放在自己的內(nèi)核棧上。中斷處理程序也使用被它們打斷的進(jìn)程的堆棧。這就意味著,在最惡劣的情況下,8kB的內(nèi)核棧可能會(huì)由多個(gè)函數(shù)的嵌套調(diào)用鏈和幾個(gè)中斷處理程序來(lái)共享。顯然,深度的嵌套會(huì)導(dǎo)致溢出。
根據(jù)定義,在高端內(nèi)存中的頁(yè)不能永久地映射到內(nèi)核地址空間上。因此,通過(guò)alloc_pages()函數(shù)以__GFP_HIGHMEM標(biāo)志獲得的頁(yè)不可能有邏輯地址。一旦這些頁(yè)被分配,就必須映射到內(nèi)核的邏輯地址空間上。要映射一個(gè)給定的page結(jié)構(gòu)到內(nèi)核地址空間,可以使用void *kmap(struct page *page) 這個(gè)函數(shù)在高端內(nèi)存或低端內(nèi)存上都能用。如果page結(jié)構(gòu)對(duì)應(yīng)的是低端內(nèi)存中的一頁(yè),函數(shù)只會(huì)單純地返回該頁(yè)的虛擬地址,如果頁(yè)位于高端內(nèi)存,則會(huì)建立一個(gè)永久映射,在返回地址。這個(gè)函數(shù)可以睡眠,所以kmap()只能用在進(jìn)程上下文中。當(dāng)不再需要內(nèi)存映射的時(shí)候,就用下列函數(shù)進(jìn)行解除映射:
1
void kunmem(struct page* page)
當(dāng)必須創(chuàng)建一個(gè)映射而當(dāng)前的上下文又不能睡眠時(shí),內(nèi)核提供了臨時(shí)睡眠(也就是原子睡眠)。只要有一組保留的永久映射,它們就可以臨時(shí)持有新創(chuàng)建的一個(gè)映射。內(nèi)核可以原子地把高端內(nèi)存中的一個(gè)頁(yè)映射到某個(gè)保留的映射中。因此,臨時(shí)映射可以用在不能睡眠的地方。建立臨時(shí)映射:void *kmap_atomic(struct page *page,enum km_type type).參數(shù)type是下列枚舉類型之一,描述了臨時(shí)映射的目的,如下:
這個(gè)函數(shù)不會(huì)阻塞,它也禁止內(nèi)核搶占,通過(guò)函數(shù)void *kunmap_atomic(void *kvaddr,enum km_type type).這個(gè)函數(shù)還是不會(huì)映射。
最后,我們總結(jié)一下,說(shuō)說(shuō)分配函數(shù)的選擇吧,總結(jié)如下:
1.如果需要連續(xù)的物理頁(yè),就可以使用某個(gè)低級(jí)頁(yè)分配器或kmalloc().
2.如果想從高端內(nèi)存進(jìn)行分配,使用alloc_pages().
3.如果不需要物理上連續(xù)的頁(yè),而僅僅是虛擬地址上連續(xù)的頁(yè),那么就是用vmalloc
4.如果要?jiǎng)?chuàng)建和銷毀很多大的數(shù)據(jù)結(jié)構(gòu),那么考慮建立slab高速緩存。
好了,有關(guān)內(nèi)存管理的也說(shuō)完了,其實(shí)也不算我說(shuō),有很多都是參考書(shū)上資料的。