- 了解linux buddy system
1.描述
在内核初始化完成之后, 内存管理的责任就由伙伴系统来承担。Linux内核使用伙伴算法来管理和分配物理内存页面,该算法由Knowlton设计,后来Knuth又进行了更深刻的描述。
伙伴系统是一个结合2的方幂个分配器和空闲缓冲区合并技术的内存分配方案, 内存被分成含有很多页面的大块, 每一块都是2个页面大小的方幂. 如果找不到想要的块, 一个大块会被分成两部分, 这两部分彼此就成为伙伴. 其中一半被用来分配, 而另一半则空闲. 这些块在以后分配的过程中会继续被二分直至产生一个所需大小的块. 当一个块被最终释放时, 其伙伴将被检测出来, 如果伙伴也空闲则合并两者。
2.常用内存分配函数
2.1.分配掩码gfp_mask
分配掩码包括两部分:
- 内存域修饰符(低4位)
- 内存分配标志(从第5位开始)
2.1.1.内存域修饰符
内存域zone的几种类型:ZONE_DMA、ZONE_DMA32、ZONE_NORMAL、ZONE_HIGHMEM、ZONE_MOVABLE。与类型不同,内存域的修饰符只有___GFP_DMA、___GFP_HIGHMEM、___GFP_DMA32、___GFP_MOVABLE 4种,没有ZONE_NORMAL对应的修饰符,因为ZONE_NORMAL是默认的内存申请类型。如下所示,为内存域修饰符的定义:
19 #define ___GFP_DMA 0x01u
20 #define ___GFP_HIGHMEM 0x02u
21 #define ___GFP_DMA32 0x04u
22 #define ___GFP_MOVABLE 0x08u
56 #define __GFP_DMA ((__force gfp_t)___GFP_DMA)
57 #define __GFP_HIGHMEM ((__force gfp_t)___GFP_HIGHMEM)
58 #define __GFP_DMA32 ((__force gfp_t)___GFP_DMA32)
59 #define __GFP_MOVABLE ((__force gfp_t)___GFP_MOVABLE) /* ZONE_MOVABLE allowed */
60 #define GFP_ZONEMASK (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE)
2.1.2.内存分配flag
除了内存域修饰符之外,分配掩码中还包含了大量的分配标志,如下所示:
2.1.3.掩码分组
由于这些标志几乎总是组合使用,内核作了一些分组,包含了用于各种标准情形的适当的标志, 称之为类型标志。
#define GFP_ATOMIC (__GFP_HIGH|__GFP_ATOMIC|__GFP_KSWAPD_RECLAIM)
#define GFP_KERNEL (__GFP_RECLAIM | __GFP_IO | __GFP_FS)
#define GFP_KERNEL_ACCOUNT (GFP_KERNEL | __GFP_ACCOUNT)
#define GFP_NOWAIT (__GFP_KSWAPD_RECLAIM)
#define GFP_NOIO (__GFP_RECLAIM)
#define GFP_NOFS (__GFP_RECLAIM | __GFP_IO)
#define GFP_TEMPORARY (__GFP_RECLAIM | __GFP_IO | __GFP_FS | \
__GFP_RECLAIMABLE)
#define GFP_USER (__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
#define GFP_DMA __GFP_DMA
#define GFP_DMA32 __GFP_DMA32
#define GFP_HIGHUSER (GFP_USER | __GFP_HIGHMEM)
#define GFP_HIGHUSER_MOVABLE (GFP_HIGHUSER | __GFP_MOVABLE)
#define GFP_TRANSHUGE ((GFP_HIGHUSER_MOVABLE | __GFP_COMP | \
__GFP_NOMEMALLOC | __GFP_NORETRY | __GFP_NOWARN) & \
~__GFP_RECLAIM)
/* Convert GFP flags to their corresponding migrate type */
#define GFP_MOVABLE_MASK (__GFP_RECLAIMABLE|__GFP_MOVABLE)
#define GFP_MOVABLE_SHIFT 3
在编写的绝大多数代码中, 用么用到的是GFP_KERNEL, 要么是GFP_ATOMIC,当然各个类型标志也均有其应用场景。
掩码布局如下所示:
3.内存分配API统一到alloc_pages接口
3.1.alloc_pages
491 #ifdef CONFIG_NUMA
492 extern struct page *alloc_pages_current(gfp_t gfp_mask, unsigned order);
493
494 static inline struct page *
495 alloc_pages(gfp_t gfp_mask, unsigned int order)
496 {
497 return alloc_pages_current(gfp_mask, order);
498 }
499 extern struct page *alloc_pages_vma(gfp_t gfp_mask, int order,
500 struct vm_area_struct *vma, unsigned long addr,
501 int node, bool hugepage);
502 #define alloc_hugepage_vma(gfp_mask, vma, addr, order) \
503 alloc_pages_vma(gfp_mask, order, vma, addr, numa_node_id(), true)
504 #else
505 #define alloc_pages(gfp_mask, order) \
506 alloc_pages_node(numa_node_id(), gfp_mask, order)
507 #define alloc_pages_vma(gfp_mask, order, vma, addr, node, false)\
508 alloc_pages(gfp_mask, order)
509 #define alloc_hugepage_vma(gfp_mask, vma, addr, order) \
510 alloc_pages(gfp_mask, order)
511 #endif
- 如果定义CONFIG_NUMA,则调用alloc_pages_current分配内存;否则调用 alloc_pages_node;
alloc_pages_node
->__alloc_pages_node
->__alloc_pages
->__alloc_pages_nodemask
__alloc_pages_nodemask()的实现:
【file:/mm/page_alloc.c】
/*
* This is the 'heart' of the zoned buddy allocator.
*/
struct page *
__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order,
struct zonelist *zonelist, nodemask_t *nodemask)
{
enum zone_type high_zoneidx = gfp_zone(gfp_mask);
struct zone *preferred_zone;
struct page *page = NULL;
int migratetype = allocflags_to_migratetype(gfp_mask);
unsigned int cpuset_mems_cookie;
int alloc_flags = ALLOC_WMARK_LOW|ALLOC_CPUSET|ALLOC_FAIR;
struct mem_cgroup *memcg = NULL;
gfp_mask &= gfp_allowed_mask;
lockdep_trace_alloc(gfp_mask);
might_sleep_if(gfp_mask & __GFP_WAIT);
if (should_fail_alloc_page(gfp_mask, order))
return NULL;
/*
* Check the zones suitable for the gfp_mask contain at least one
* valid zone. It's possible to have an empty zonelist as a result
* of GFP_THISNODE and a memoryless node
*/
if (unlikely(!zonelist->_zonerefs->zone))
return NULL;
/*
* Will only have any effect when __GFP_KMEMCG is set. This is
* verified in the (always inline) callee
*/
if (!memcg_kmem_newpage_charge(gfp_mask, &memcg, order))
return NULL;
retry_cpuset:
cpuset_mems_cookie = get_mems_allowed();
/* The preferred zone is used for statistics later */
first_zones_zonelist(zonelist, high_zoneidx,
nodemask ? : &cpuset_current_mems_allowed,
&preferred_zone);
if (!preferred_zone)
goto out;
#ifdef CONFIG_CMA
if (allocflags_to_migratetype(gfp_mask) == MIGRATE_MOVABLE)
alloc_flags |= ALLOC_CMA;
#endif
retry:
/* First allocation attempt */
page = get_page_from_freelist(gfp_mask|__GFP_HARDWALL, nodemask, order,
zonelist, high_zoneidx, alloc_flags,
preferred_zone, migratetype);
if (unlikely(!page)) {
if (alloc_flags & ALLOC_FAIR) {
reset_alloc_batches(zonelist, high_zoneidx,
preferred_zone);
alloc_flags &= ~ALLOC_FAIR;
goto retry;
}
/*
* Runtime PM, block IO and its error handling path
* can deadlock because I/O on the device might not
* complete.
*/
gfp_mask = memalloc_noio_flags(gfp_mask);
page = __alloc_pages_slowpath(gfp_mask, order,
zonelist, high_zoneidx, nodemask,
preferred_zone, migratetype);
}
trace_mm_page_alloc(page, order, gfp_mask, migratetype);
out:
/*
* When updating a task's mems_allowed, it is possible to race with
* parallel threads in such a way that an allocation can fail while
* the mask is being updated. If a page allocation is about to fail,
* check if the cpuset changed during allocation and if so, retry.
*/
if (unlikely(!put_mems_allowed(cpuset_mems_cookie) && !page))
goto retry_cpuset;
memcg_kmem_commit_charge(page, memcg, order);
return page;
}
这是伙伴管理算法的核心,__alloc_pages_nodemask()分配内存页面的关键函数是:get_page_from_freelist()和__alloc_pages_slowpath()。
- get_page_from_freelist():最先用于尝试页面分配,如果分配失败的情况下,则会进一步调用__alloc_pages_slowpath();
- __alloc_pages_slowpath()是用于慢速页面分配,允许等待和内存回收。
3.2.get_page_from_freelist
该函数主要是遍历各个内存管理区列表zonelist以尝试页面申请。其中for_each_zone_zonelist_nodemask()则是用于遍历zonelist的,每个内存管理区尝试申请前,都将检查内存管理区是否有可分配的内存空间、根据alloc_flags判断当前CPU是否允许在该内存管理区zone中申请以及做watermark水印检查以判断zone中的内存是否足够等。该函数调用 rmqueue函数,然后调用 __rmqueue_smallest。
3615 try_this_zone:
3616 page = rmqueue(ac->preferred_zoneref->zone, zone, order,
3617 gfp_mask, alloc_flags, ac->migratetype);
__rmqueue_smallest:
该函数实现了分配算法的核心功能,首先for()循环其由指定的伙伴管理算法链表order阶开始,如果该阶的链表不为空,则直接通过list_del()从该链表中获取空闲页面以满足申请需要;如果该阶的链表为空,则往更高一阶的链表查找,直到找到链表不为空的一阶,至于若找到了最高阶仍为空链表,则申请失败;否则将在找到链表不为空的一阶后,将空闲页面块通过list_del()从链表中摘除出来,然后通过expand()将其对等拆分开,并将拆分出来的一半空闲部分挂接至低一阶的链表中,直到拆分至恰好满足申请需要的order阶,最后将得到的满足要求的页面返回回去。至此,页面已经分配到了。
3.3.伙伴管理算法页面释放
void __free_pages(struct page *page, unsigned int order);
void free_pages(unsigned long addr, unsigned int order);
#define __free_page(page) __free_pages((page), 0)
#define free_page(addr) free_pages((addr), 0)
3.3.1. free_pages
4270 void __free_pages(struct page *page, unsigned int order)
4271 {
4272 if (put_page_testzero(page)) { /*判断页没有被使用*/
4273 if (order == 0)
4274 free_hot_cold_page(page, false); /*单页则释放到每CPU页框高速缓存中*/
4275 else
4276 __free_pages_ok(page, order); /*多页则释放到伙伴系统*/
4277 }
4278 }
4279
4280 EXPORT_SYMBOL(__free_pages);
4281
4282 void free_pages(unsigned long addr, unsigned int order)
4283 {
4284 if (addr != 0) {
4285 VM_BUG_ON(!virt_addr_valid((void *)addr));
4286 __free_pages(virt_to_page((void *)addr), order);
4287 }
4288 }
代码分析:
put_page_testzero():是对page结构的_count引用计数做原子减及测试,用于检查内存页面是否仍被使用,如果不再使用,则进行释放。其中order表示页面数量,如果释放的是单页,则会调用free_hot_cold_page()将页面释放至per-cpu page缓存中,而不是伙伴管理算法;真正的释放至伙伴管理算法的是__free_pages_ok(),同时也是用于多个页面释放的情况。
分析free_hot_cold_page():释放单个页,如果cold是1,则释放的是冷页,是0,则释放的是热页。
void free_hot_cold_page(struct page *page, int cold)
{
struct zone *zone = page_zone(page);
struct per_cpu_pages *pcp;
unsigned long flags;
int migratetype;
if (!free_pages_prepare(page, 0)) //释放页之前要检查一下,是否真的适合释放
return;
migratetype = get_pageblock_migratetype(page); //获得该页所在块的迁移类型。
set_freepage_migratetype(page, migratetype);
local_irq_save(flags);
__count_vm_event(PGFREE);
/*只有不可移动页,可回收页和可移动页才能放到每CPU页框高速缓存中,如果
迁移类型不属于这个范围,则要将该页释放回伙伴系统*/
if (migratetype >= MIGRATE_PCPTYPES) {
if (unlikely(is_migrate_isolate(migratetype))) {
free_one_page(zone, page, 0, migratetype);
goto out;
}
migratetype = MIGRATE_MOVABLE;
}
//获得per-CPU缓存链表
pcp = &this_cpu_ptr(zone->pageset)->pcp;
if (cold) /*冷页插入表尾*/
list_add_tail(&page->lru, &pcp->lists[migratetype]);
else /*热页插入表头*/
list_add(&page->lru, &pcp->lists[migratetype]);
pcp->count++;
//如果per-CPU缓存中页数目超出pcp->high,则需要进行惰性合并,将数量为pcp->batch的页退还给伙伴系统
if (pcp->count >= pcp->high) {
unsigned long batch = ACCESS_ONCE(pcp->batch);
free_pcppages_bulk(zone, batch, pcp);
pcp->count -= batch;
}
out:
local_irq_restore(flags);
}
-
free_pages_prepare
其中trace_mm_page_free()用于trace追踪机制;而kmemcheck_free_shadow()用于内存检测工具kmemcheck,如果未定义CONFIG_KMEMCHECK的情况下,它是一个空函数。接着后面的PageAnon()等都是用于检查页面状态的情况,以判断页面是否允许释放,避免错误释放页面。由此可知该函数主要作用是检查和调试。 -
get_pageblock_migratetype()和set_freepage_migratetype()分别是获取和设置页面的迁移类型,即设置到page->index;
-
判断migratetype >= MIGRATE_PCPTYPES,MIGRATE_PCPTYPES用来表示每CPU页框高速缓存的数据结构中的链表的迁移类型数目,如果某个页面类型大于MIGRATE_PCPTYPES则表示其可挂到可移动列表中;如果迁移类型是MIGRATE_ISOLATE则直接将该其释放到伙伴管理算法中。
迁移类型如下所示:
enum {
MIGRATE_UNMOVABLE,
MIGRATE_RECLAIMABLE,
MIGRATE_MOVABLE,
MIGRATE_PCPTYPES, /* the number of types on the pcp lists */
MIGRATE_RESERVE = MIGRATE_PCPTYPES,
#ifdef CONFIG_CMA
MIGRATE_CMA,
#endif
#ifdef CONFIG_MEMORY_ISOLATION
MIGRATE_ISOLATE, /* can't allocate from here */
#endif
MIGRATE_TYPES
};
迁移类型的目标就是反内存碎片,Linux3.10内核有多达7中迁移类型,比较明显的是per-CPU类型的迁移类型的增加。
- MIGRATE_UNMOVABLE标识类型的内存在内存中有固定的地址范围且不能够被移动,多数的内核核心代码从此类型内存申请空间。
- MIGRATE_RECLAIMABLE,不能够直接移动,但是内存可以被回收,数据结构可以被重建。由文件映射的数据就是这一类型。
- MIGRATE_MOVABLE:可以被移动,属于用户空间的页就是这一类型的。
- MIGRATE_PCPTYPES:管理per-CPU变量的类型。
- MIGRATE_RESERVE:其和MIGRATE_PCPTYPES是一样的类型的,数据为紧急情况预留的类型。
- MIGRATE_ISOLATE:是一个特殊的虚拟内存域,用于NUMA情况下夸节点移动物理页,不能从其申请内存。
- this_cpu_ptr:其中pcp表示内存管理区的每CPU管理结构,cold表示冷热页面,如果是冷页就将其挂接到对应迁移类型的链表尾,而若是热页则挂接到对应迁移类型的链表头。其中if (pcp->count >= pcp->high)判断值得注意,其用于如果释放的页面超过了每CPU缓存的最大页面数时,则将其批量释放至伙伴管理算法中,其中批量数为pcp->batch。
重点来了:free_pcppages_bulk
static void free_pcppages_bulk(struct zone *zone, int count,
struct per_cpu_pages *pcp)
{
int migratetype = 0;
int batch_free = 0;
int to_free = count;
spin_lock(&zone->lock);
zone->pages_scanned = 0;
while (to_free) {
struct page *page;
struct list_head *list;
/*
* Remove pages from lists in a round-robin fashion. A
* batch_free count is maintained that is incremented when an
* empty list is encountered. This is so more pages are freed
* off fuller lists instead of spinning excessively around empty
* lists
*/
do {
batch_free++;
if (++migratetype == MIGRATE_PCPTYPES)
migratetype = 0;
list = &pcp->lists[migratetype];
} while (list_empty(list));
/* This is the only non-empty list. Free them all. */
if (batch_free == MIGRATE_PCPTYPES)
batch_free = to_free;
do {
int mt; /* migratetype of the to-be-freed page */
page = list_entry(list->prev, struct page, lru);
/* must delete as __free_one_page list manipulates */
list_del(&page->lru);
mt = get_freepage_migratetype(page);
/* MIGRATE_MOVABLE list may include MIGRATE_RESERVEs */
__free_one_page(page, zone, 0, mt);
trace_mm_page_pcpu_drain(page, 0, mt);
if (likely(!is_migrate_isolate_page(page))) {
__mod_zone_page_state(zone, NR_FREE_PAGES, 1);
if (is_migrate_cma(mt))
__mod_zone_page_state(zone, NR_FREE_CMA_PAGES, 1);
}
} while (--to_free && --batch_free && !list_empty(list));
}
spin_unlock(&zone->lock);
}
__free_one_page:
while (order < MAX_ORDER-1)前面主要是对释放的页面进行检查校验操作。而while循环内,通过__find_buddy_index()获取与当前释放的页面处于同一阶的伙伴页面索引值,同时藉此索引值计算出伙伴页面地址,并做伙伴页面检查以确定其是否可以合并,若否则退出;接着if (page_is_guard(buddy))用于对页面的debug_flags成员做检查,由于未配置CONFIG_DEBUG_PAGEALLOC,page_is_guard()固定返回false;则剩下的操作主要就是将页面从分配链中摘除,同时将页面合并并将其处于的阶提升一级。
退出while循环后,通过set_page_order()设置页面最终可合并成为的管理阶。最后判断当前合并的页面是否为最大阶,否则将页面放至伙伴管理链表的末尾,避免其过早被分配,得以机会进一步与高阶页面进行合并。末了,将最后的挂入的阶的空闲计数加1。至此伙伴管理算法的页面释放完毕。