本文目的在于分析Linux内存管理机制中的伙伴系统。内核版本为2.6.31。
在系统运行过程中,经常需要分配一组连续的页,而频繁的申请和释放内存页会导致内存中散布着许多不连续的页,这样,当某一时刻要申请一块较大的连续内存时,虽然系统内存余量足够,即很多页是空闲的,但找不到一大块连续的内存供使用。
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。
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。
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标记就会被设置,否则被清除。
释放页的接口为__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; }