Linux内存管理第六章 -- Physical Page Allocation(Buddy Allocator)

Linux内存管理第六章 – Physical Page Allocation

本章将描述在Linux中物理内存如何分配和管理。其主要算法是Binary Buddy Allocator。Buddy Allocator相比较其他分配器要快很多。

这是一种将普通pow-of-two分配器与空闲buffer块相结合的分配方案。其背后的基本概念实际上相当简单。memory被分成若干个page组成的大块,其中每个大块中page的个数是2的x次幂。如果一个大块中可用的memory size小于期望分配的size,那么一个更大的块将会对半切分,切分后的两个子块相互为buddies。其中一个子块用于分配,另一个是空闲的。这些块在必要时连续减半,直到达到所需大小的块可用为止。当一个block被释放后,会检查它的buddy是否也是空闲的,如果是,那么这对buddies将会合并。

本章将从描述Linux是如何记住哪些块是free的开始。然后再来讨论分配和释放pages的方法。再后续的小节会将到影响allocator分配行为的flags,最后来讨论下碎片问题以及allocator如何处理碎片问题。

Managing Free Blocks

如上所述,buddy allocator维护了一组2的x此幂个page的free page block。其中指数x被称之为order。一个struct free_area的数组中保存了每个order所属于的free page 链表。
其示意图如下:
buddy
因此,数组中的第0个元素将指一个free page block的链表,其中每个block由20 free page组成,第1个元素中的每个block由21 free pages组成,直到2^MAX_ORDER -1^ pages.当前MAX_ORDER被定义为10. 这就消除了当小块能够满足分配需求的时候而切分大块的机会。page blocks通过page->list连成一个线性链表被维护起来。

struct zone {
		.............
	/*
	 * free areas of different sizes
	 */
	struct free_area	free_area[MAX_ORDER];
	..............
}
struct free_area {
	struct list_head	free_list;
	unsigned long		*map;
};
  • free_list: free page block链表的表头
  • map:代表一对buddies的状态的bitmap

Linux为了节省memory仅仅使用一个bit而不是两个bit来表示每队buddies的状态。每当一个buddy被分配或者被释放,代表该对buddies的bit位将被反转,因此一个bit为0时,表示两个buddy都空闲或者都在使用中,bit为1时,表示两个buddy中只有一个在使用中。为了能正确反转bit,定义了宏函数MARK_USED(),其定义如下:

#define MARK_USED(index, order, area) \
	__change_bit((index) >> (1+(order)), (area)->map)

其中index表示一个page在全局数组mem_map中的索引。将index右移1+order位后,所得的值就是map中代表该page所属的buddies的bit。

Buddy Alocator初始化源码分析

从前面一片博客boot memory中可以知道,当boot memory retire的时候,会把pages释放给buddy allocator。其实这个释放的过程就是buddy allocator的free_area数组中各个order中的free_area[order]->free_list的添加page的过程。
下面来分析下核心算法代码:free_all_bootmem_core() --> __free_pages() or __free_page() --> … -->__free_pages_bulk()
buddy
结合上图,假设调用__free_pages_bulk 来释放page2,则此时的order =0

static inline void __free_pages_bulk (struct page *page, struct page *base,
		struct zone *zone, struct free_area *area, unsigned int order)
{
	unsigned long page_idx, index, mask;

	if (order)
		destroy_compound_page(page, order);
	mask = (~0UL) << order;//如果order为0则mask =1111 1111,order = 1 则mask = 1111 1110 ......
	page_idx = page - base;//传入page在zone->zone_mem_map中的偏移位置,假设为2
	if (page_idx & ~mask) //检查传入的page 是否与传入的order匹配
		BUG();            //如果page_idx = 2,order = 0,则mask = 1111 1111,~mask = 0,page_idx & ~mask = 0,不会报错
	index = page_idx >> (1 + order);//index = 2 >> (1 + 0) = 1  0010 >> 1 = 0001

	zone->free_pages += 1 << order;//free_pages 加上1,因为只释放page2
	while (order < MAX_ORDER-1) {
		struct page *buddy1, *buddy2;

		BUG_ON(area >= zone->free_area + MAX_ORDER);
		if (!__test_and_change_bit(index, area->map)) //此时bit 1应该为1,翻转后则为0,所以不会break
			/* the buddy page is still allocated.*/   //*** 当再次循环时,order =1, index = 0,
			                                          //*** 此时因为buddyA:page0 ~ page1在使用,所以会跳出循环
			break; 
		/* Move the buddy up one level. */
		buddy1 = base + (page_idx ^ (1 << order));//2 ^ (1 << 0) = 0010 ^ 0001 = 0011.buddy1 = page3   
		buddy2 = base + page_idx;                 //buddy2 = page2
		BUG_ON(bad_range(zone, buddy1));
		BUG_ON(bad_range(zone, buddy2));
		list_del(&buddy1->lru);//因为page2和page3要被合并到order1中,所以要将page3从free_area[0]->free_list中删除
		mask <<= 1;// 1111 1111 << 1 = 1111 1110
		order++;// 0 + 1 = 1
		area++;//area = free_area[1]
		index >>= 1;//index = 1 >> 1 = 0,
		            //也就是order1中buddyA:page0 ~ page1,buddyB:page2 ~ page3 在free_area[1]->map中的bit index是0
		page_idx &= mask;// 0000 0010 & 1111 1110 = 2
	}
	list_add(&(base + page_idx)->lru, &area->free_list);//跳出循环后,将page2加入到free_area[1]->free_list中
}

Allocating Pages

Linux提供了相当多的API来进行分配物理页框。这些API都带一个gfp_mask的参数,这些参数将决定了buddy allocator的行为。这些gfp_mask的具体含义在后续章节中会详细描述。
所有的这些分配API最终都会调用到核心函数__alloc_pages().但这些API存在的意义是可以提前选择正确的node和正确的zone。不同的使用者要求从不同的zone中获取memory,比如相当多的device driver要从ZONE_DMA中获取memory,而disk buffers需要从ZONE_NORMAL中获取memory,但调用者不必关心当前正在使用的是哪一个node。下面来看看这些API的说明:

struct page * alloc_page(unsigned int gfp_mask)
分配单个page,并返回struct page的地址
struct page * alloc_pages(unsigned int gfp_mask, unsigned int order)
分配2order个page,并返回大头struct page的地址
unsigned long get_free_page(unsigned int gfp_mask)
分配单个page,并初始化为0,然后再返回对应的虚拟地址
unsigned long __get_free_page(unsigned int gfp_mask)
分配单个page并返回虚拟地址
unsigned long __get_free_pages(unsigned int gfp_mask, unsigned int order)
分配2order个page并返回虚拟地址
struct page * __get_dma_pages(unsigned int gfp_mask, unsigned int order)
从DMA zone中分配2order个page并返回struct page的地址

在分配时总是需要指定order,如果是0则代表申请一个page。如果当指定的order中找不到一个空闲的block,那么更高一级的order中的block将被切分为两个buddies。其中一个被拥挤分配,而另外一个被放置到low order的free list中。下图中展示了当一个24 block被切分后的buddies如何被加入到低级别order的free list中,直到拿到与申请size相比比较合适的block为止。
buddies
当一个block被释放后,它的buddy将会被检查。如果两个buddy都是free的,则它们要被合并到高阶的block中,并放置在高阶的free list中。如果buddy不是free的,被释放的block将被加入到当前order的free list中。在操作这些链表的过程中,中断时被禁止的从而来阻止这种case:中断处理函数正在处理list,而某个进程此时拿到的状态还没有同步过来。这个是通过使用中断安全的自选锁实现的。
第二个需要做的决定是选择哪一个node。Linux使用使用node-local分配策略,即当前CPU中正在运行的进程需要分配page,当前进程在的代码在哪个node中,动态获取page就在哪个node中分配。因此_alloc_pages()是很重要的一个函数因为该函数会根据内核被编译成UMA还是NUMA的不同而不同。
无论是使用上述哪一个API来进行分配,__alloc_pages()是buddy allocator的核心。该函数从来不会被直接调用,该函数会检查被选中的zone并检查zone中是否又合适的空闲内存块使用。如果被指定的Zone不合适,分配器会转移到其他zone中尝试分配。分配器转移zone的顺序是在内核启动阶段build_zonelist()函数决定的,但通常的一个转移顺序是ZONE_HIGHMEM->ZONE_NORMAL->ZONE_DMA。如果zone中空闲page的数量达到page_low水位线,系统将唤醒kswapd守护进程开始在zones中释放一些pages,如果内存很紧张的情况下,调用者会直接来释放pages就像kswapd做的事情一样。
一旦被分配的zone被确定后,函数rmqueue()将会被调用,用来分配pages或者如果没有合适的size,就将高阶的块进行切分。
2222

Buddy Allocator分配过程源码分析

bd
依据上图状态1,在通过调用alloc_pages(),其中order = 2

static struct page *__rmqueue(struct zone *zone, unsigned int order)
{
	struct free_area * area;
	unsigned int current_order;
	struct page *page;
	unsigned int index;

	for (current_order = order; current_order < MAX_ORDER; ++current_order) {
		area = zone->free_area + current_order;
		if (list_empty(&area->free_list)) //由于在状态1时,order2中无空闲的block,此时会跳到order3中去找空闲的blocks
			continue;

		page = list_entry(area->free_list.next, struct page, lru);//从zone->free_area[3]->free_list中取出page8
		list_del(&page->lru);//从zone->free_area[3]->free_list删除page8
		index = page - zone->zone_mem_map;//index = 8
		if (current_order != MAX_ORDER-1)
			MARK_USED(index, current_order, area);//将从zone->free_area[3]->map中bit 0翻转,此时应该为0,表示
			                                      // buddy1:page0 ~page7,buddy2:page8 ~ page15,两个buddy都在使用
		zone->free_pages -= 1UL << order;//将free_pages 减去8
		return expand(zone, page, index, order, current_order, area);//传入的page = page8,index = 8,order=2, current_order = 3,
		                                                             //area = zone->free_area[3]
	}

	return NULL;
}

下面再来看expand()函数:

static inline struct page *
expand(struct zone *zone, struct page *page,
	 unsigned long index, int low, int high, struct free_area *area)
{
	unsigned long size = 1 << high; // 1 << 3 = 8
    // low = 2, high = 3
	while (high > low) { //条件成立
		area--;//进入到order2
		high--;//high 变为2
		size >>= 1;// 8 >> 1 = 4
		BUG_ON(bad_range(zone, &page[size]));
		list_add(&page[size].lru, &area->free_list);//page = page8, 所以page[4]就是page12,
		                                            //将page12加入到zone->free_area[2]-》free_list
		MARK_USED(index + size, high, area);//将buddy:page12 ~ page15在zone->free_area[2]->map中的bit 1翻转
	}
	return page;//返回page8
}

Free Pages

释放pages的API要简单很多,但其必须要记住要释放的block所在的order。这也是buddy allocator的一个缺点:调用者必须记住原始分配时的page的个数。其具体的API如下:

void __free_pages(struct page *page, unsigned int order)
从page开始释放2order个page
void __free_page(struct page *page)
释放order 0中的单个page
void free_page(void *addr)
释放一个虚拟地址对应的page

Buddy Alocator释放page源码分析

上述这些函数最终都会调用到__free_pages_bulk(),其原理参见本章小节:Buddy Alocator初始化源码分析

Get Free Page (GFP) Flags

一个贯穿整个VM的永久的概念是GFP标志。这些标志决定了allocator和kswapd的分配和释放page的行为。举个例子:一个中断处理函数可能不允许被中断因此中断处理函数中分配内存就不需要__GFP_WAIT标志集,这些标志表示当前调用者可以进入睡眠。有三组GFP标志,全部定义在<linux/mm.h>中。

  • 第一组标志是zone modifiers,这些标志表示调用者必须尽量从指定的zone分配memory.此时有人会问怎么没有ZONE_NORMAL的zone modifier?因为这些标志是一个数组中的偏移量。0默认就代表从ZONE_NORMAL分配内存。
Flag Description
__GFP_DMA 如果可能,尽量从ZONE_DMA分配内存
__GFP_HIGHMEM 如果可能,尽量从ZONE_HIGHMEM分配内存
GFP_DMA __GFP_DMA的别名
  • 第二组标志是action modifier,这些标志可以改变VM和调用进程的行为。这些标志位属于low level标志位,由于太过原始从而不容易使用。
Flag Description
__GFP_WAIT 表示调用者不是高优先级所以可以进入睡眠或者重新调度
__GFP_HIGH 被高优先级进程使用或者内核进程使用
__GFP_IO 表示调用者可以操作low level IO
__GFP_HIGHIO 决定了IO可以在被映射到高端内存的page上
__GFP_FS 表示调用者可以调用文件系统层,当调用者与文件系统相关则可以使用,举例来说buffer cache,可以用这个标记来避免自己调用自己
__GFP_NOFAIL 如果使用该flag表示此次分配不允许分配器分配失败,分配器应该无限地尝试继续分配
__GFP_REPEAT 当分配失败后应该重复进行分配。它和__GFP_NOFAIL的区别是执行再次分配的决定比__GFP_NOFAIL稍微迟一些
__GFP_NORETRY 作用和__GFP_NOFAIL完全相反,如果失败了就立即返回
  • 第三组high level flag。由于low level flag不好用,很难知道在某个场景使用哪些正确的low level flag组合。因此定义了一些由low level flag的组合而成的high level flag。
Flag Low Level Flag Combination
GFP_ATOMIC __GFP_HIGH
GFP_NOIO __GFP_HIGH | __GFP_WAIT
GFP_NOHIGHIO __GFP_HIGH | __GFP_WAIT | _GFP IO
GFP_NOFS __GFP_HIGH | __GFP_WAIT | __GFP_IO | __GFP_HIGHIO
GFP_KERNEL __GFP_HIGH | __GFP_WAIT | __GFP_IO | __GFP_HIGHIO | __GFP_FS
GFP_NFS __GFP_HIGH | __GFP_WAIT | __GFP_IO | __GFP_HIGHIO | __GFP_FS
GFP_USER __GFP_WAIT | __GFP_IO |_GFP HIGHIO | __GFP_FS
GFP_HIGHUSER __GFP_WAIT | __GFP_IO | __GFP_HIGHIO | __GFP_FS | __GFP_HIGHMEM
GFP_KSWAPD __GFP_WAIT| __GFP_IO |_GFP HIGHIO |__GFP_FS
Flag Description
GFP_AUOMIC 调用者无论何时都不能进入睡眠并且必须尽所有可能为其服务。任何中断处理函数中申请memory必须使用这个flag来避免睡眠或者操作IO。许多子系统在初始化的时候会用到这个flag如:buffer_init(),inode_init()
GFP_NOIO 当调用者已经在一个正在进行的IO相关操作中时,再分配memory就需要使用该flag。举例:当一个loop back device正在尝试从一个buffer头部获取一个page,它使用这个flag将会保证不会执行一些导致更多IO的操作。事实上,该flag尽用于loop back device中避免死锁
GFP_NOHIGHIO 当在high memory中为IO创建一个bounce buffer,该flag仅仅被用在alloc_bounce_page()中
GFP_NOFS 该flag仅仅被buffer cache和文件系统使用来保证他们自己不会递归调用他们自己
GFP_KERNEL 这是一个最常用的flag。它表示调用者可以自由的干它想干的事情。严格来讲该flag与GFP_USER不同的地方在于该flag可以使用紧急池中的page,但在kernel2.4中已经没有紧急池
GFP_USER 具有历史意义的flag。在kernel2.2x中,以此分配被给定为LOW,MEDIUM,HIGH三个优先级。如果内存紧张,使用GFP_USER的调用者优先级是LOW,此时内存申请会失败同时其他优先级的申请还可以继续进行。但在kernel2.4中已经不再重要了,它和GFP_KERNEL没什么区别
GFP_HIGHUSER 该标志表示分配器应该尽可能从ONE_HIGHMEM中分配内存。当代表用户空间的进程分配内存时使用这个flag
GFP_NFS 该flag已经失效
GFP_KSWAPD 该flag具有历史意义,但现在已经可GFP_KERNEL没有什么区别了
  • Process Flags:一个进程在进程描述符task_struct中设置的一些flag也可能会影响buddy allocator的行为。这些flag定义在<linux/sched.h>中。下面来介绍下回影响VM的几个flag:
Flag Description
PF_MEMALLOC 该标志表示当前进程自己就是一个memory allocator。kswapd进程就设置了这个flag即将被OOM killer杀掉的进程将会设置这个flag,它告诉buddy allocator忽略zone的水位线尽可能分配内存
PF_MEMDIE OOM killer给要杀死的进程设置该flag,其含义和PF_MEMALLOC差不多告诉buddy allocator尽量分配内存因为该进程马上要死了
PF_FREE_PAGES 当buddy allocator调用try_to_free_pages()时表示这些free pages应该预留给当前调用的进程而不是返回给free lists中

Avoiding Fragmentation

任何分配器都必须要解决的一个重要问题是内部碎片和外部碎片。

  • 外部碎片:是指内存中有很多个不连续的小块,当需要分配连续的大块的时候,没有可用的大块。一般是指整个page的不连续。
  • 内部碎片:内部碎片被定义为浪费的空间,其中必须分配一个大块来服务一个小请求。

在Linux中,外部随便不是一个很严重的问题因为大块请求连续的物理内存的概率很小,Linux中经常使用vmalloc()来服务大块请求,返回的是不连续的物理page。
而内部碎片对于binary buddy system来说是一个很严重的问题。因为buddy allocator每次分配必须是2order个page同时分配,这样就会造成一些浪费。Linux为了解决这个问题引入了slab allocator将pages切割成细小的内存块用于分配。组合使用buddy allocator和slab allocator,内核可以确保由于内部碎片而浪费的内存量保持在最低限度。

Per-CPU Page Lists

在kernel2.6中,pages通过buffered_rmqueue()函数从struct per_cpu_pageset中分配。如果low水位(per_cpu_pageset->low)没有到达,page将从此pageset中分配而无需获取spinlock。一旦low水位到达,大量的page将从buddy allocator中转移到per-cpu list中此次分配过程是需要获取spinlock的。然后再将per-cpu中的一个page返回给调用者。
尽管high order分配机率很小,但它还是要获取中断安全的spinlock因此在切分和合并page的时候无延时。而0 order的分配的切割将被延时到per-cpu list的低水位到达时才执行,而0order的合并将延时到高水位到达时才执行。
加入per-cpu list的效果是很明显的。因为获取自旋锁来保护buddy lists的次数降低了。而在Linux中大块memory申请又很少所以此次优化是针对常用的case。该改动在多核机器上效果明显而在单核机器上效果不是很明显。
per-cpu也存在一些问题但这些问题都不认为是很严重的问题:

  • 第一个问题,如果per-cpu list中的pages可以被合并长连续page大块,那么high order allocation可能会失败,因为buddy allocator中没有更多的大块内存了。
  • 第二个问题,当内存很紧张的时候且当前CPU的per-cpu list空了而其他CPU list却是满的,当前CPU中的0 order allocation也会fail,因为当前没有机制从别的CPU per-cpu list中回收内存。
  • 第三个潜在的问题,新释放page的buddy在别的CPU list中,这也会导致high order allocation失败。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章