注:本文中提到的ICE為一Android工程,對(duì)應(yīng)Linux內(nèi)核版本為2.6.29。
2.6 slab分配器
從前面分析可知,內(nèi)核對(duì)內(nèi)存的管理都是以頁(yè)為最小單位的,也就是說(shuō)想從內(nèi)核申請(qǐng)內(nèi)存,必須是頁(yè)的倍數(shù)。如果只想申請(qǐng)幾十個(gè)字節(jié),獲取到的也至少是一頁(yè),而且這一頁(yè)的剩余部分別人是不能使用的,因此明顯造成浪費(fèi)。Linux使用slab分配器對(duì)從內(nèi)核獲取到的頁(yè)再次分配,以減少浪費(fèi)。
slab分配器有三個(gè)目標(biāo):
I 減少系統(tǒng)分配小塊內(nèi)存時(shí)產(chǎn)生的碎片;
II 把經(jīng)常使用的對(duì)象緩存起來(lái),減少分配、初始化及釋放對(duì)象的時(shí)間開(kāi)銷;
III 調(diào)整對(duì)象以更好的使用硬件高速緩存。
打個(gè)比方:slab就是高速列車上搞零售的列車員。從廠家整批貨物,然后搬到列車上分拆成單件由乘客挑選購(gòu)買。這里的關(guān)鍵是高速列車,對(duì)應(yīng)我們的高速緩存(cache),而批進(jìn)的貨物即是通過(guò)頁(yè)框分配器獲取的大塊內(nèi)存,單件就是被slab拆分初始化后的對(duì)象(小段內(nèi)存),而對(duì)象才是真正供給開(kāi)發(fā)者(乘客)使用的。不同的是它是免費(fèi)的,但注意這里是文明的世界,你不可以搶的。slab為了實(shí)現(xiàn)上面目標(biāo),做了如下操作:
I 獲取一塊內(nèi)存;
II 科學(xué)劃分成大小為2n或某個(gè)結(jié)構(gòu)體長(zhǎng)度的小塊(稱為對(duì)象);
III 將初始化好的slab放入高速緩存;
IV 利用slab著色及對(duì)象對(duì)齊提高訪問(wèn)速度。
slab與高速緩存密不可分,下面先來(lái)看看它們的描述符:
高速緩存描述符結(jié)構(gòu)為kmem_cache。
類型 | 名字 | 說(shuō)明 |
struct array_cache * [] | array | 每CPU指針數(shù)組,指向包含空閑對(duì)象的本地高速緩存 |
unsigned int | batchcount | 要移進(jìn)/移出本地高速緩存的大批對(duì)象的數(shù)量 |
unsigned int | limit | 本地高速緩存中空閑對(duì)象的最大數(shù)目。可調(diào) |
unsigned int | buffer_size | 單個(gè)對(duì)象的大小,即kmem_cache_alloc()每次可獲得的內(nèi)存的大小 |
unsigned int | reciprocal_buffer_size | buffer_size的倒數(shù),系統(tǒng)中沒(méi)有使用該變量 |
unsigned int | flags | 描述高速緩存永久屬性的一組標(biāo)志 |
unsigned int | num | 封裝在一個(gè)單獨(dú)slab中的對(duì)象個(gè)數(shù) |
unsigned int | gfporder | 一個(gè)單獨(dú)slab中包含的連續(xù)頁(yè)框數(shù)目的對(duì)數(shù) |
gfp_t | gfpflags | 分配頁(yè)框時(shí)傳遞給伙伴系統(tǒng)函數(shù)的一組標(biāo)志 |
size_t | colour | slab使用的顏色個(gè)數(shù) |
unsigned int | colour_off | slab中的基本對(duì)齊偏移 |
struct kmem_cache * | slabp_cache | 指向包含slab描述符的普通slab高速緩存,如果使用內(nèi)部slab描述符,該字段為NULL |
unsigned int | slab_size | 單個(gè)slab的大小 |
unsigned int | dflags | 描述高速緩存動(dòng)態(tài)屬性的一組標(biāo)志 |
void (*ctor)(void *obj) | ctor | 指向與高速緩存相關(guān)的構(gòu)造/析構(gòu)方法 |
const char * | name | 指向高速緩存的名字 |
struct list_head | next | 高速緩存描述符雙向鏈表使用的指針 |
struct kmem_list3 *[] | nodelists | 每個(gè)節(jié)點(diǎn)上屬于該高速緩存的所有slab鏈表,請(qǐng)看下表 |
表2-5:高速緩存描述符
kmem_list3結(jié)構(gòu)。
類型 | 名字 | 說(shuō)明 |
struct list_head | slabs_partial | 包含空閑和非空閑對(duì)象的slab描述符雙向循環(huán)鏈表 |
struct list_head | slabs_full | 不包含空閑對(duì)象的slab描述符雙向循環(huán)鏈表 |
struct list_head | slabs_free | 只包含空閑對(duì)象的slab描述符雙向循環(huán)鏈表 |
unsigned long | free_objects | 高速緩存中空閑對(duì)象的個(gè)數(shù) |
spinlock_t | list_lock | 本結(jié)構(gòu)使用的自旋鎖 |
struct array_cache * | shared | 指向所有CPU共享的一個(gè)本地高速緩存的指針 |
unsigned long | next_reap | 由slab分配器的頁(yè)回收算法使用 |
int | free_touched | 由slab分配器的頁(yè)回收算法使用 |
表2-6:kmem_list3結(jié)構(gòu)
slab描述結(jié)構(gòu)為struct slab。
類型 | 名字 | 說(shuō)明 |
struct list_head | list | slab描述符的三個(gè)雙向循環(huán)鏈表中的一個(gè)(在高速緩存描述符的kmem_list3結(jié)構(gòu)中的slabs_full、slabs_partial或slabs_free鏈表)使用的指針 |
struct list_head | colouroff | slab中第一個(gè)對(duì)象的偏移 |
void * | s_mem | slab中的第一個(gè)對(duì)象的地址 |
unsigned int | inuse | 當(dāng)前正在使用的slab中的對(duì)象個(gè)數(shù) |
kmem_bufctl_t | free | slab中下一個(gè)空閑對(duì)象的下標(biāo) |
unsigned short | nodeid | 本slab所屬節(jié)點(diǎn)的ID號(hào) |
表2-7:slab描述符
下圖(來(lái)自于《Linux虛擬內(nèi)存管理》)展示了高速緩存與slab關(guān)系:
圖2-4:高速緩存、slab及對(duì)象關(guān)系圖
高速緩存分通用和專用,所謂通用就是系統(tǒng)初始化時(shí)實(shí)現(xiàn)準(zhǔn)備了一組常用大小的內(nèi)存對(duì)象組供內(nèi)核開(kāi)發(fā)者使用,比如使用kmalloc()獲取。下圖為ICE平臺(tái)上使用的通用高速緩存。
圖2-5:ICE平臺(tái)的通用高速緩存
專用高速緩存是指專為一個(gè)結(jié)構(gòu)體創(chuàng)建的高速緩存。比如文件的inode結(jié)構(gòu),用的很多,但其大小在通用高速緩存中找不到,如果強(qiáng)行從通用高速緩存中申請(qǐng)空間勢(shì)必會(huì)造成內(nèi)存浪費(fèi)。因此,系統(tǒng)專門(mén)為其創(chuàng)建一個(gè)專用高速緩存,使得對(duì)應(yīng)對(duì)象的大小就是inode結(jié)構(gòu)的大?。ㄐ枰?/span>4字節(jié)對(duì)齊)。下圖為ICE平臺(tái)上使用的專用高速緩存。
圖2-6:ICE平臺(tái)的專用高速緩存
無(wú)論是通用還是專用高速緩存都是通過(guò)kmem_cache_create()函數(shù)創(chuàng)建的。它們的區(qū)別是:通用是在系統(tǒng)初始化時(shí)(start_kernel()->kmem_cache_init())系統(tǒng)創(chuàng)建的,而專用則是由內(nèi)核開(kāi)發(fā)者根據(jù)需要?jiǎng)?chuàng)建的。這個(gè)函數(shù)只是獲取一個(gè)高速緩存描述符并將其插入到高速環(huán)存鏈表中,如圖2-4。
一個(gè)新創(chuàng)建的高速緩存沒(méi)有包含任何slab,因此也沒(méi)有空閑的對(duì)象。只有當(dāng)以下兩個(gè)條件都為真時(shí),才給高速緩存分配slab:
I 已發(fā)出一個(gè)分配新對(duì)象的請(qǐng)求,即調(diào)用kmem_cache_alloc()請(qǐng)求對(duì)象;
II 高速緩存不包含任何空閑對(duì)象。
當(dāng)上述兩個(gè)條件都滿足時(shí),slab分配器調(diào)用cache_grow()函數(shù)來(lái)增加一個(gè)slab。這個(gè)函數(shù)又調(diào)用kmem_getpages()從頁(yè)框分配器獲得一組頁(yè)框(由高速緩存描述符中的gfporder確定為2 gfporder個(gè)頁(yè)框)。接著cache_grow()調(diào)用cache_init_objs(),將前一組頁(yè)框分成n個(gè)對(duì)象(大小有高速緩存描述符中的obj_size指定),再調(diào)用描述符中的構(gòu)造函數(shù)(由kmem_cache_create()指定)對(duì)各個(gè)對(duì)象進(jìn)行初始化。然后將初始化好的slab插入到空閑slab鏈表中(slabs_free鏈表)。
如果slab分配器、伙伴系統(tǒng)算法弄明白了,kmalloc()就不用講,它幾乎完全等同于kmem_cache_alloc(),不同的是kmalloc()有自己選擇通用高速緩存的機(jī)制,而后者需要申請(qǐng)者指定高速緩存。看下代碼就全明白了:
第33行__builtin_constant_p(size)是gcc的內(nèi)部函數(shù),意思是說(shuō)如果size是常量函數(shù)就返回true,否則返回false。但David Howells他們說(shuō)了,該函數(shù)在kmalloc里恒為真,可參考android/kernel目錄下的aa(內(nèi)核開(kāi)發(fā)者修改提交記錄)文件:
圖2-7:內(nèi)核開(kāi)發(fā)者提交記錄片段
通過(guò)增加打印證明kmalloc()函數(shù)中無(wú)論是變量還是常量__builtin_constant_p()返回的值均為1。
39~46行的目的即是到通用高速緩存表中查找符合size的高速緩存,來(lái)看一下kmalloc_sizes.h,全部?jī)?nèi)容如下:
如果找到則調(diào)用kmem_cache_alloc()函數(shù)到找到的那個(gè)高速緩存中的slab鏈表上找一個(gè)未用的對(duì)象(內(nèi)存塊的首地址)給申請(qǐng)者,否則換回NULL。
從上面的分析及驗(yàn)證來(lái)看,第55行應(yīng)該不會(huì)被調(diào)用到。但我們?nèi)匀粊?lái)分析一下,函數(shù)體如下:
void *__kmalloc(size_t size, gfp_t flags)
{
return __do_kmalloc(size, flags, NULL);
}
再看__do_kmalloc()函數(shù):
static __always_inline void *__do_kmalloc(size_t size, gfp_t flags,
void *caller)
{
struct kmem_cache *cachep;
/* If you want to save a few bytes .text space: replace
* __ with kmem_.
* Then kmalloc uses the uninlined functions instead of the inline
* functions.
*/
cachep = __find_general_cachep(size, flags);
if (unlikely(ZERO_OR_NULL_PTR(cachep)))
return cachep;
return __cache_alloc(cachep, flags, caller);
}
可以看出仍然是調(diào)用__find_general_cachep()從通用高速緩存總找到相應(yīng)大小的高速緩存,然后再調(diào)用__cache_alloc()(事實(shí)上kmem_cache_alloc()中只有一行,就是調(diào)用__cache_alloc())從slab中找一個(gè)空閑對(duì)象(內(nèi)存塊的首地址)返回給申請(qǐng)者。
伙伴系統(tǒng)算法一節(jié)中講到減少內(nèi)存碎片的兩種辦法:一種是用伙伴系統(tǒng)有效管理內(nèi)存;二是將不連續(xù)的內(nèi)存映射到連續(xù)的線性地址,vmalloc()就是這種方法的實(shí)現(xiàn)。
vmalloc()映射到的連續(xù)線性地址不是隨意的,內(nèi)核專門(mén)為其劃了一塊空間,位于物理內(nèi)存映射末端偏移8M的地方。請(qǐng)見(jiàn)圖1-1,范圍為0x9D800000~0xE0000000,請(qǐng)參考內(nèi)核中VMALLOC_START及VMALLOC_END定義。也就是說(shuō)映射在內(nèi)核空間,用戶態(tài)的程序是不能訪問(wèn)的,要訪問(wèn)得進(jìn)入內(nèi)核態(tài)。
來(lái)看一下該過(guò)程中的關(guān)鍵數(shù)據(jù)結(jié)構(gòu)vm_struct,下表為其參數(shù)描述:
類型 | 名字 | 說(shuō)明 |
struct vm_struct * | next | 指向下一個(gè)vm_struct結(jié)構(gòu)的指針 |
void * | addr | 內(nèi)存區(qū)內(nèi)第一個(gè)內(nèi)存單元的線性地址 |
unsigned long | size | 內(nèi)存區(qū)的大小加上4096(一頁(yè)的安全區(qū),為了防止越界訪問(wèn)) |
unsigned long | flags | 映射的內(nèi)存的類型,如:VM_ALLOC表示使用vmalloc()得到的頁(yè),VM_MAP表示使用vmap()映射的已經(jīng)被分配的頁(yè),而VM_IOREMAP表示使用ioremap()映射的硬件設(shè)備的板上內(nèi)存 |
struct page ** | pages | 該指針數(shù)組每個(gè)元素指向一個(gè)可獲得的頁(yè)描述符 |
unsigned int | nr_pages | 內(nèi)存區(qū)填充的頁(yè)的個(gè)數(shù) |
unsigned long | phys_addr | 該字段一般不用,除非內(nèi)存被創(chuàng)建來(lái)映射一個(gè)硬件設(shè)備的I/O共享內(nèi)存 |
void * | caller | 指向該內(nèi)存區(qū)的使用者 |
表2-8:不連續(xù)內(nèi)存描述符
下面是vmalloc()源碼:
void *vmalloc(unsigned long size)
{
return __vmalloc_node(size, GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL,
-1, __builtin_return_address(0));
}
再來(lái)看__vmalloc_node(),
static void *__vmalloc_node(unsigned long size, gfp_t gfp_mask, pgprot_t prot,
int node, void *caller)
{
struct vm_struct *area;
size = PAGE_ALIGN(size);
if (!size || (size >> PAGE_SHIFT) > num_physpages)
return NULL;
area = __get_vm_area_node(size, VM_ALLOC, VMALLOC_START, VMALLOC_END,
node, gfp_mask, caller);
if (!area)
return NULL;
return __vmalloc_area_node(area, gfp_mask, prot, node, caller);
}
該函數(shù)表明不可以傳入一個(gè)比物理內(nèi)存還大的size。接著調(diào)用__get_vm_area_node()獲取一段連續(xù)的線性地址,并申請(qǐng)一個(gè)不連續(xù)內(nèi)存區(qū)描述符,即vm_struct數(shù)據(jù)塊,并初始化。__vmalloc_area_node()實(shí)現(xiàn)的是申請(qǐng)不連續(xù)內(nèi)存,并將其映射到前面獲取到的連續(xù)線性地址上。請(qǐng)看源碼:
static void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask,
pgprot_t prot, int node, void *caller)
{
struct page **pages;
unsigned int nr_pages, array_size, i;
nr_pages = (area->size - PAGE_SIZE) >> PAGE_SHIFT;
array_size = (nr_pages * sizeof(struct page *));
area->nr_pages = nr_pages;
/* Please note that the recursion is strictly bounded. */
if (array_size > PAGE_SIZE) {
pages = __vmalloc_node(array_size, gfp_mask | __GFP_ZERO,
PAGE_KERNEL, node, caller);
area->flags |= VM_VPAGES;
} else {
pages = kmalloc_node(array_size,
(gfp_mask & GFP_RECLAIM_MASK) | __GFP_ZERO,
node);
}
area->pages = pages;
area->caller = caller;
if (!area->pages) {
remove_vm_area(area->addr);
kfree(area);
return NULL;
}
for (i = 0; i < area->nr_pages; i++) {
struct page *page;
if (node < 0)
page = alloc_page(gfp_mask);
else
page = alloc_pages_node(node, gfp_mask, 0);
if (unlikely(!page)) {
/* Successfully allocated i pages, free them in __vunmap() */
area->nr_pages = i;
goto fail;
}
area->pages[i] = page;
}
if (map_vm_area(area, prot, &pages))
goto fail;
return area->addr;
fail:
vfree(area->addr);
return NULL;
}
代碼有點(diǎn)長(zhǎng),但確實(shí)很簡(jiǎn)單。先是申請(qǐng)一塊空間用來(lái)保存不連續(xù)內(nèi)存區(qū)描述中的pages數(shù)組;接著一頁(yè)一頁(yè)(這是至關(guān)重要的)的申請(qǐng)內(nèi)存,并將申請(qǐng)到的內(nèi)存的頁(yè)描述符存放到pages數(shù)組中;最后調(diào)用map_vm_area()將pages中的各頁(yè)映射到連續(xù)線性地址上。最后一步是難點(diǎn),從代碼中可知,其原理就是申請(qǐng)新的目錄項(xiàng)和頁(yè)表項(xiàng),并通過(guò)線性地址段、頁(yè)描述符里的信息重新設(shè)置它們。由于還不太了解arm硬件的這一塊,此處代碼略過(guò)。
聯(lián)系客服