本文目的在於分析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 <

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 <

}

判斷是否可以合併的函式:

/*

* 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;

}

版權宣告:本文為CSDN博主「落塵紛擾」的原創文章,遵循CC 4。0 BY-SA版權協議,轉載請附上原文出處連結及本宣告。

原文連結:Linux記憶體管理(2) - buddy系統