本文目的在於分析Linux內存管理機制中的伙伴系統。內核版本為2.6.31。
1. 伙伴系統的概念
在系統運行過程中,經常需要分配一組連續的頁,而頻繁的申請和釋放內存頁會導致內存中散布着許多不連續的頁,這樣,當某一時刻要申請一塊較大的連續內存時,雖然系統內存余量足夠,即很多頁是空閑的,但找不到一大塊連續的內存供使用。
Linux內核中使用伙伴系統(buddy system)算法來管理內存頁。它把所有的空閑頁放到11個鏈表中,每個鏈表分別管理大小為1,2,4,8,16,32,64,128,256,512,1024個頁的內存塊。當系統需要分配內存時,就可以從buddy系統中獲取。例如,要申請一塊包含4個頁的連續內存,就直接從buddy系統中管理4個頁連續內存的鏈表中獲取。當系統釋放內存時,則將釋放的內存放回buddy系統對應的鏈表中,如果釋放內存后發現有兩塊相鄰的內存又可以合並為一個更高階的內存塊,例如釋放4個頁,而恰好相鄰的內存也為4個頁的空閑內存,則合並這兩塊內存並放到buddy系統管理8個連續頁的鏈表中。同樣的,如果系統需要申請3個頁的連續內存,則只能在4個頁的鏈表中獲取,剩下的一個頁被放到buddy系統中管理1個頁的鏈表中。
buddy分配器分配的最小單位是一個頁。要分配小於一頁的內存需要用到slab分配器,而slab是基於buddy分配器的。
struct zone {
......
struct free_area free_area[MAX_ORDER];
......
}____cacheline_internodealigned_in_smp;
struct zone的free_area[]數組成員存放了各階的空閑內存列表,數組下標可取0~MAX_ORDER-1,(MAX_ORDER=11)。所以,每個階(order)的內存鏈表使用struct free_area結構來記錄。
struct free_area {
struct list_head free_list[MIGRATE_TYPES];
unsigned long nr_free;
};
struct free_area有兩個成員,free_list[]是不同migrate type(遷移類型)頁鏈表的數組(我們先不關注什么是遷移類型,后面會講到),每種遷移類型都是一個struct page的鏈表,由每個struct page的page->lru連起來。nr_free表示這個order空閑頁的數量,例如,階為2的連續頁塊共有3個,則nr_free=3,實際上這個階的空閑頁數為(2^2)*3=12。
2. per-cpu的冷熱頁鏈表
struct zone結構有一個pageset[]成員:
struct zone {
......
struct per_cpu_pageset pageset[NR_CPUS];
......
}____cacheline_internodealigned_in_smp;
struct per_cpu_pageset {
struct per_cpu_pages pcp;
} ____cacheline_aligned_in_smp;
struct per_cpu_pages {
int count; /* number ofpages in the list */
int high; /* highwatermark, emptying needed */
int batch; /* chunk sizefor buddy add/remove */
struct list_head list; /*the list of pages */
};
為了方便,我下文將per cpu pageset簡稱為pcp。
pageset[]數組用於存放per cpu的冷熱頁。當CPU釋放一個頁時,如果這個頁仍在高速緩存中,就認為它是熱的,之后很可能又很快被訪問,於是將它放到pageset列表中,其他的認為是冷頁。pageset中的冷熱頁鏈表元素數量是有限制的,由per_cpu_pages的high成員控制,畢竟如果熱頁太多,實際上最早加進來的頁已經不熱了。
在CPU釋放一個頁的時候,不會急着釋放到buddy系統中,而是會先試圖將頁作為熱頁或冷頁放到pcp鏈表中,直到超出數量限制。而釋放多個頁時則直接釋放到buddy系統中。
per_cpu_pages的count成員表示鏈表中頁的數量。batch表示有時需要從伙伴系統中拿一些頁放到冷熱頁鏈表中時,一次拿多少個頁。list成員是冷熱頁鏈表,越靠近表頭的越熱。
一般情況下,當內核想申請一個頁的內存時,就先從CPU的冷熱頁鏈表中申請。但是,有時直接申請冷頁會更合理一些,因為有時cache中的頁肯定是無效的,所以內核在申請內存頁時提供了一個標記GPF_COLD來指明要申請冷頁。
注意,冷熱頁分配只針對分配和回收一個頁的時候,多個頁則直接操作buddy。
3. alloc_pages_node()在buddy系統上分配頁
static inline struct page* alloc_pages_node(int nid, gfp_t gfp_mask,
unsigned int order)
{
/* Unknown node is current node */
if (nid < 0)
nid = numa_node_id();
return __alloc_pages(gfp_mask, order, node_zonelist(nid,gfp_mask));
}
這個函數的三個參數為:
nid:節點id,UMA系統為0。
gfp_mask:GFP(get free page)掩碼,在include/linux/gfp.h中定義。
order:分配階,如分配4個頁,則order=2。
node_zonelist()返回節點的zone備用列表,即NODE_DATA(nid)->node_zonelists[]。
該函數最終調用__alloc_pages_nodemask()做實際的分配工作,這個函數的注釋為“This is the 'heart' of the zoned buddy allocator”。這個函數根據gpf_flags尋找合適的zone,然后調用函數get_page_from_freelist()進行接下來的工作,這個函數簡化后的實現如下:
static struct page *
get_page_from_freelist(gfp_tgfp_mask, nodemask_t *nodemask, unsigned int order,
struct zonelist *zonelist, int high_zoneidx, int alloc_flags,
struct zone *preferred_zone, int migratetype)
{
struct zoneref *z;
struct page *page = NULL;
int classzone_idx;
struct zone *zone;
/* 只有ZONE_NORMAL,所以都返回0 */
classzone_idx = zone_idx(preferred_zone);
/*
* Scan zonelist, lookingfor a zone with enough free.
* See alsocpuset_zone_allowed() comment in kernel/cpuset.c.
*/
for_each_zone_zonelist_nodemask(zone, z, zonelist,
high_zoneidx, nodemask) {
/* 如果沒有設置NO_WATERMARKS */
if (!(alloc_flags & ALLOC_NO_WATERMARKS)) {
unsigned long mark;
int ret;
/* 獲得設置的水印位 */
mark = zone->watermark[alloc_flags &ALLOC_WMARK_MASK];
/* 判斷是否超出了水印設置的限制 */
if (zone_watermark_ok(zone,order, mark,
classzone_idx, alloc_flags))
goto try_this_zone;
}
try_this_zone:
page = buffered_rmqueue(preferred_zone,zone, order,
gfp_mask, migratetype);
if (page)
break;
}
return page;
}
其中調用的函數主要就兩個:
zone_watermark_ok()用來判斷水印限制(zone-> watermark[]),如果要分配的order超出了水印限制,說明系統中可用內存頁不夠了,不能繼續分配。
/*
* Return 1 if free pages are above 'mark'.This takes into account the order
* of the allocation.
*/
int zone_watermark_ok(struct zone *z, int order, unsigned long mark,
int classzone_idx,int alloc_flags)
{
/* free_pages my go negative - that's OK */
long min = mark;
long free_pages = zone_page_state(z, NR_FREE_PAGES) - (1 <<order) + 1; /* 為毛加1 */
int o;
if (alloc_flags & ALLOC_HIGH) /* 如果有ALLOC_HIGH,就降低限制 */
min -= min / 2;
if (alloc_flags & ALLOC_HARDER) /* 如果有ALLOC_HARDER,就再降低限制 */
min -= min / 4;
/* 如果分配完,剩余的小於限制了 */
if (free_pages <= min + z->lowmem_reserve[classzone_idx])
return 0;
/* 如果分配完,比order小的伙伴擁有的頁數占多數,也不行。 */
for (o = 0; o < order; o++) {
/* At the next order, this order's pages become unavailable */
free_pages -= z->free_area[o].nr_free << o;
/* Require fewer higher order pages to be free */
min >>= 1;
if (free_pages <= min)
return 0;
}
return 1;
}
注意,在init_per_zone_wmark_min()函數中初始化了每個zone的水印以及lowmem_reserve等限制。這個函數被module_init()了,並且內嵌編到內核,所以在系統啟動時自動執行。
buffered_rmqueue()進行實際的分配頁的工作。
static inline
struct page *buffered_rmqueue(structzone *preferred_zone,
struct zone *zone, int order, gfp_t gfp_flags,
int migratetype)
{
unsigned long flags;
struct page *page;
int cold = !!(gfp_flags & __GFP_COLD);/* 是否設置COLD位 */
int cpu;
again:
cpu = get_cpu();
if (likely(order == 0)) { /* 如果需要分配一頁 */
/* 在pcp上分配 */
} else { /* 如果需要分配多頁 */
/* 在buddy上分配 */
}
__count_zone_vm_events(PGALLOC, zone, 1 << order);
zone_statistics(preferred_zone, zone); /* 更新統計數據. */
local_irq_restore(flags);
put_cpu();
VM_BUG_ON(bad_range(zone, page));
/* 其他工作 */
if (prep_new_page(page, order, gfp_flags))
goto again;
return page;
failed:
local_irq_restore(flags);
put_cpu();
return NULL;
}
在分配頁的時候分為兩種情況,如果只需分配一頁,則直接在pcp上進行。如果需要分配多頁,則在buddy系統上分配。
申請一個頁,在pcp上分配:
1. 通過&zone_pcp(zone,cpu)->pcp獲取pcp鏈表。
2. 如果pcp->count==0,即pcp鏈表為空,則使用rmqueue_bulk()函數在buddy上獲取batch個單頁放到pcp鏈表中,並將這些頁從buddy上移除,同時更新zone的vm_stat統計數據。
3. 根據gfp_flags有沒有GPF_COLD標志,判斷如果需要分配冷頁就從pcp鏈表的末尾取一個頁,如果需要熱頁就從鏈表頭取一個頁,獲得的頁賦值給page。
4. 將page從pcp鏈表中刪除:list_del(&page->lru),同時pcp->count--。
申請多個頁,在buddy上分配:
1. 如果設置有__GFP_NOFAIL標記,並且order>1,則給出警告。
2. 使用__rmqueue()分配2^order個頁。
3. 更新zone的vm_stat統計數據。
在buddy上申請2^order個頁都是通過__rmqueue()函數完成的,它主要分三步工作:
1. 調用__rmqueue_smallest()在指定zone的free_area[order]上特定migratetype鏈表上嘗試分配2^order個頁,如果order階沒有足夠內存,就嘗試在order+1階的特定migratetype鏈表上分配2^order個頁,依次類推直到分配到想要的頁。
2. 將被申請的頁從buddy系統上清除。同時,申請之后可能buddy系統需要重新調整,例如,本來想分配2^1=2個頁,而buddy已經沒有2個頁的伙伴了,所以在2^2=4個頁的伙伴上申請,那申請完剩下的兩個頁需要從free_area[2]上刪除,並且放到free_area[1]鏈表中,這個工作是由expand()完成的。
3. 如果在當前zone的buddy上特定migratetype的鏈表中沒有分配成功,並且migratetype != MIGRATE_RESERVE,就使用__rmqueue_fallback()在備用列表中分配。
在這里我們需要說明struct page的三個成員:
lru:鏈表節點,在存放struct page的鏈表中都是以lru為節點的,如buddy和pcp中的鏈表。
private:這個成員有多重意思,我們在這里看到,如果page在buddy系統中,private就是這個頁所在free_area的階數。如果page在pcp冷熱頁鏈表中,private就是migratetype。
flags:如果頁在buddy系統中,PG_buddy標記就會被設置,否則被清除。
4. 釋放頁
釋放頁的接口為__free_pages(),它的參數為第一個頁的指針page,以及order。
void __free_pages(structpage *page, unsigned int order)
{
if (put_page_testzero(page)) {
if (order == 0)
free_hot_page(page);
else
__free_pages_ok(page,order);
}
}
如果釋放一個頁,則先嘗試添加到pcp中,超過pcp限制再往buddy系統中添加。如果釋放多個頁,則通過__free_pages_ok()釋放。
將頁回收至buddy系統中的接口為__free_one_page()。就是一個查找page idx和合並原有buddy的過程。
static inline void__free_one_page(struct page *page,
struct zone *zone, unsigned int order,
int migratetype)
{
unsigned long page_idx;
if (unlikely(PageCompound(page)))
if (unlikely(destroy_compound_page(page, order)))
return;
VM_BUG_ON(migratetype == -1);
/* 由mem_map得到頁的index */
page_idx = page_to_pfn(page) & ((1 << MAX_ORDER) - 1);
/* 這句判斷的意思是page_idx必須是order對齊的? */
VM_BUG_ON(page_idx & ((1 << order) - 1));
VM_BUG_ON(bad_range(zone, page));
/* 從order階開始嘗試合並 */
while (order < MAX_ORDER-1) {
unsigned long combined_idx;
struct page *buddy;
/* 找到和當前page同階的buddy的理論位置 */
buddy = __page_find_buddy(page, page_idx, order);
/* 看page和buddy能否合並,不能就結束了 */
if (!page_is_buddy(page, buddy, order))
break;
/* 可以合並,則把buddy釋放 */
/* Our buddy is free, merge with it and move up one order. */
list_del(&buddy->lru);
zone->free_area[order].nr_free--;
rmv_page_order(buddy);
/* page和buddy合並,起始index就是page或buddy的index */
combined_idx = __find_combined_index(page_idx, order);
page = page + (combined_idx - page_idx);
page_idx = combined_idx;
/* 繼續看能否再往高階合並 */
order++;
}
/* 合並完成,設置最終buddy的order並添加到響應order數組的鏈表中。 */
set_page_order(page, order);
list_add(&page->lru,
&zone->free_area[order].free_list[migratetype]);
zone->free_area[order].nr_free++;
}
這段代碼邏輯很清晰。注意,只有相鄰地址的buddy才能合並,所以實際上待釋放page的buddy的頁的index是可以計算出來的:
/*
* Locate the struct page for both the matchingbuddy in our
* pair (buddy1) and the combined O(n+1) pagethey form (page).
*
* 1) Any buddy B1 will have an order O twin B2which satisfies
* the following equation:
* B2= B1 ^ (1 << O)
* For example, if the starting buddy (buddy2)is #8 its order
* 1 buddy is #10:
* B2= 8 ^ (1 << 1) = 8 ^ 2 = 10
*
* 2) Any buddy B will have an order O+1 parentP which
* satisfies the following equation:
* P= B & ~(1 << O)
*
* Assumption: *_mem_map is contiguous at leastup to MAX_ORDER
*/
static inline struct page *
__page_find_buddy(structpage *page, unsigned long page_idx, unsigned int order)
{
unsigned long buddy_idx = page_idx ^ (1<< order);
return page + (buddy_idx - page_idx);
}
合並兩個buddy得到合並后buddy的起始頁index的函數:
static inline unsigned long
__find_combined_index(unsignedlong page_idx, unsigned int order)
{
return (page_idx & ~(1 <<order));
}
判斷是否可以合並的函數:
/*
* This function checks whether a page is free&& is the buddy
* we can do coalesce a page and its buddy if
* (a) the buddy is not in a hole &&
* (b) the buddy is in the buddy system&&
* (c) a page and its buddy have the same order&&
* (d) a page and its buddy are in the samezone.
*
* For recording whether a page is in the buddysystem, we use PG_buddy.
* Setting, clearing, and testing PG_buddy isserialized by zone->lock.
*
* For recording page's order, we usepage_private(page).
*/
static inline intpage_is_buddy(struct page *page, struct page *buddy,
int order)
{
/* buddy的頁的index是否合法。 */
if (!pfn_valid_within(page_to_pfn(buddy)))
return 0;
/* 是否屬於同一個zone。 */
if (page_zone_id(page) != page_zone_id(buddy))
return 0;
/* 目標buddy必須設置了PG_buddy標記。並且和page是同order的。 */
if (PageBuddy(buddy) && page_order(buddy) == order) {
VM_BUG_ON(page_count(buddy) != 0);
return 1;
}
return 0;
}
原文:https://blog.csdn.net/jasonchen_gbd/article/details/44023801
