[内核内存] 伙伴系统---相关知识点

1 如何根据gfp flag找到对应的zone

当内核分配内存时,需要指定gfp flag, 内核通过gfp标志判断从哪个zone分配内存如:alloc_pages(gfp_mask, order),而在linux中存在ZONE_DMA, ZONE_DMA32, ZONE_NORMAL, ZONE_HIGHMEM, ZONE_MOVABLE几个zone.由于gfp_t 低4位,共有2^4=16中情况,而linux规定了低3位(DMA, DMA32, HIGHMEM)只能有1位为1.

gfp_t定义:

#define __GFP_DMA  0X01u
#define __GFP_HIGHMEM  0X02u
#define __GFP_DMA32  0X04u
#define __GFP_MOVABLE  0X08u
序号__GFP_DMA__GFP_HIGHMEM__GFP_DMA32__GFP_MOVABLE组合结果
00000从ZONE_NORMAL分配
11000从ZONE_NORMAL或ZONE_DMA分配
20100从ZONE_HIGNMEM或ZONE_NORMAL分配
31100BAD
40010从ZONE_DMA32或ZONE_NORMAL分配
51010BAD
60110BAD
71110BAD
80001从ZONE_NORMAL分配
91001从ZONE_DMA或ZONE_NORMAL分配
a0101从ZONE_MOVABLE分配Movable is valid only if HIGHMEM is set too
b1101BAD
c0011从ZONE_DMA32分配
d1011BAD
e0111BAD
f1111BAD

根据上表,很容易得到GFP_ZONE_BAD,即上述BAD的bit或在一起

#define GFP_ZONE_BAD ( \
	1 << (___GFP_DMA | ___GFP_HIGHMEM)				      \
	| 1 << (___GFP_DMA | ___GFP_DMA32)				      \
	| 1 << (___GFP_DMA32 | ___GFP_HIGHMEM)				      \
	| 1 << (___GFP_DMA | ___GFP_DMA32 | ___GFP_HIGHMEM)		      \
	| 1 << (___GFP_MOVABLE | ___GFP_HIGHMEM | ___GFP_DMA)		      \
	| 1 << (___GFP_MOVABLE | ___GFP_DMA32 | ___GFP_DMA)		      \
	| 1 << (___GFP_MOVABLE | ___GFP_DMA32 | ___GFP_HIGHMEM)		      \
	| 1 << (___GFP_MOVABLE | ___GFP_DMA32 | ___GFP_DMA | ___GFP_HIGHMEM)  \
)

内核中定义ZONE_SHIFT,根据MAX_NR_ZONES,得到相应的ZONE_SHIFT

#if MAX_NR_ZONES < 2
#define ZONES_SHIFT 0
#elif MAX_NR_ZONES <= 2
#define ZONES_SHIFT 1
#elif MAX_NR_ZONES <= 4
#define ZONES_SHIFT 2
#elif MAX_NR_ZONES <= 8
#define ZONES_SHIFT 3
#else
#error ZONES_SHIFT -- too many zones configured adjust calculation
#endif

由于每种分配策略用ZONE_SHIFT位就可以表示,所以将上图中非BAD的位bitN,对应的分配策略,左移bitN * ZONE_SHIFT位,组合成GFP_ZONE_TABLE, 即:

#define GFP_ZONE_TABLE ( \
	(ZONE_NORMAL << 0 * GFP_ZONES_SHIFT)				       \
	| (OPT_ZONE_DMA << ___GFP_DMA * GFP_ZONES_SHIFT)		       \
	| (OPT_ZONE_HIGHMEM << ___GFP_HIGHMEM * GFP_ZONES_SHIFT)	       \
	| (OPT_ZONE_DMA32 << ___GFP_DMA32 * GFP_ZONES_SHIFT)		       \
	| (ZONE_NORMAL << ___GFP_MOVABLE * GFP_ZONES_SHIFT)		       \
	| (OPT_ZONE_DMA << (___GFP_MOVABLE | ___GFP_DMA) * GFP_ZONES_SHIFT)    \
	| (ZONE_MOVABLE << (___GFP_MOVABLE | ___GFP_HIGHMEM) * GFP_ZONES_SHIFT)\
	| (OPT_ZONE_DMA32 << (___GFP_MOVABLE | ___GFP_DMA32) * GFP_ZONES_SHIFT)\
)

另外还定义GFP_ZONEMASK:

#define GFP_ZONEMASK	(__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE)

通过GFP_ZONE_TABLE宏,给定gfp的低4位,就可以找到分配内存的zone,函数如下:

static inline enum zone_type gfp_zone(gfp_t flags)
{
	enum zone_type z;
	int bit = (__force int) (flags & GFP_ZONEMASK);

	z = (GFP_ZONE_TABLE >> (bit * GFP_ZONES_SHIFT)) &
					 ((1 << GFP_ZONES_SHIFT) - 1);
	VM_BUG_ON((GFP_ZONE_BAD >> bit) & 1);
	return z;
}

2 linux zone分块管理引入migrate_type缘由分析

linux内存管理会把整个内存划分为不同的zone区域。而对于不同的zone区域内核都会把它们划分为不同的内存块进行管理,这些内存块是由2^n个连续页面组成。这种管理方式对应的结构体如下所示:

struct free_area free_area[MAX_ORDER];

struct free_area {
    struct list_head    free_list[MIGRATE_TYPES];
    unsigned long       nr_free;
};

struct zone {
    ...
    struct free_area    free_area[MAX_ORDER];
    ...
}

由上面代码可知每个free_area结构体都管理着大小一样的连续内存块(同一个zone上)。同时内核还引入了migrate_type,来对这些大小相同的内存块做进一步的分类;最后内核将大小相同,migrate_type相同的内存块通过list_head链表连接在一起。

在这里插入图片描述

内核按下面的方式对大小相同的连续内存块进行分类:

enum {
    MIGRATE_UNMOVABLE,
    //可以直接回收的内存(文件缓存)
    MIGRATE_RECLAIMABLE,
    //页内容可以被迁移的内存
    MIGRATE_MOVABLE,
    MIGRATE_PCPTYPES,   /* the number of types on the pcp lists */
    MIGRATE_RESERVE = MIGRATE_PCPTYPES,
#ifdef CONFIG_CMA
    //CMA使用的内存区域(此处内存能被迁移)
    MIGRATE_CMA,
#endif
#ifdef CONFIG_MEMORY_ISOLATION
    MIGRATE_ISOLATE,    /* can't allocate from here */
#endif
    MIGRATE_TYPES
};


上面的分类方式就是按照内存页是否可以被直接回收,页内容是否可以被迁移等性质来对其进行分类。这样做的目的就是为了防止内存碎片化,避免内存分配时相互干扰,保证内存回收时能够够获得更多的连续内存块。

这里把不同特性的内存块分类放到不同的链表中管理,比如MIGRATE_RECLAIMABLE链表中存放的都是可以直接回收的内存,一般是文件缓存页,MIGRATE_MOVABLE链表中存放的都是可以迁移的内存块,MIGRATE_CMA链表中存放的是和CMA共同使用的区域,该区域也要求必须可以被迁移,这样当外设驱动申请CMA内存时(一般用于DMA传输大块物理连续内存),伙伴系统能够及时的腾出空间给DMA使用。

当系统中剩余内存很多但不连续时,如果申请一个大块连续内存依然会导致失败,如果能够把不连续的内存通过迁移的手段进行规整,把空闲内存组合成一块连续内存,那就可以在一定程度上达到内存申请的需求,如果该区域中所有页面都可以迁移,那么问题就变得简单了,所以内核就按照页面是否可以迁移做了区分,保证了在MIGRATE_MOVABLE链表中都是可以迁移的页面,这样可以尽可能的利用页面迁移规整出大块的连续内存

参考:https://blog.csdn.net/rikeyone/article/details/10586327

3 MIGRATE_HIGHATOMIC迁移类型页面

引入MIGRATE_HIGHATOMIC迁移类型页面,是为了解决过多高阶内存块分配导致的内存碎片化严重这一问题.

linux 伙伴系统在快速内存分配阶段分配内存块时,当获取到内存块后,会判断该页块是否从高阶页块上分配下来(分配的alloc_flags 中设置了ALLOC_HARDER且order不为0则判断为该页块从高阶页块分配而来),若是会通过reserve_highatomic_pageblock函数尝试将该页块中的页的迁移类型更改为MIGRATE_HIGHATOMIC类型(需要注意的是一个zone区域中MIGRATE_HIGHATOMIC迁移类型页不能超过该zone中页框数的1/100).后续该页块在使用完被释放时,页块会被释放到该zone伙伴系统MIGRATE_HIGHATOMIC类型的空闲链表中.

//mm/page_alloc.c
static struct page *get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,
						const struct alloc_context *ac)
{
    ......
try_this_zone:
        //内存分配
		page = buffered_rmqueue(ac->preferred_zoneref->zone, zone, order,
				gfp_mask, alloc_flags, ac->migratetype);
		if (page) {
            //清除相关标志或者设置联合页
			prep_new_page(page, order, gfp_mask, alloc_flags);

			/*
			 * If this is a high-order atomic allocation then check
			 * if the pageblock should be reserved for the future
			 */
			if (unlikely(order && (alloc_flags & ALLOC_HARDER)))
				reserve_highatomic_pageblock(page, zone, order);

			return page;
		}
	}

	return NULL;
}
//mm/page_alloc.c
/*
 * Reserve a pageblock for exclusive use of high-order atomic allocations if
 * there are no empty page blocks that contain a page with a suitable order
 */
static void reserve_highatomic_pageblock(struct page *page, struct zone *zone,
				unsigned int alloc_order)
{
	int mt;
	unsigned long max_managed, flags;

	/*
	 * Limit the number reserved to 1 pageblock or roughly 1% of a zone.
	 * Check is race-prone but harmless.
	 */
	max_managed = (zone->managed_pages / 100) + pageblock_nr_pages;
	if (zone->nr_reserved_highatomic >= max_managed)
		return;

	spin_lock_irqsave(&zone->lock, flags);

	/* Recheck the nr_reserved_highatomic limit under the lock */
	if (zone->nr_reserved_highatomic >= max_managed)
		goto out_unlock;

	/* Yoink! */
	mt = get_pageblock_migratetype(page);
	if (mt != MIGRATE_HIGHATOMIC &&
			!is_migrate_isolate(mt) && !is_migrate_cma(mt)) {
		zone->nr_reserved_highatomic += pageblock_nr_pages;
		set_pageblock_migratetype(page, MIGRATE_HIGHATOMIC);
		move_freepages_block(zone, page, MIGRATE_HIGHATOMIC);
	}

out_unlock:
	spin_unlock_irqrestore(&zone->lock, flags);
}

在内存紧张或者内存碎片较多时,伙伴系统若分配内存失败,则会通过unreserve_highatomic_pageblock函数来尝试将伙伴系统中的MIGRATE_HIGHATOMIC迁移类型的空闲页释放到伙伴系统特点迁移类型的空闲链表中去,释放成功后会再次尝试内存分配.

/*
 * Used when an allocation is about to fail under memory pressure. This
 * potentially hurts the reliability of high-order allocations when under
 * intense memory pressure but failed atomic allocations should be easier
 * to recover from than an OOM.
 */
static void unreserve_highatomic_pageblock(const struct alloc_context *ac)
{
	struct zonelist *zonelist = ac->zonelist;
	unsigned long flags;
	struct zoneref *z;
	struct zone *zone;
	struct page *page;
	int order;

	for_each_zone_zonelist_nodemask(zone, z, zonelist, ac->high_zoneidx,
								ac->nodemask) {
		/* Preserve at least one pageblock */
		if (zone->nr_reserved_highatomic <= pageblock_nr_pages)
			continue;

		spin_lock_irqsave(&zone->lock, flags);
		for (order = 0; order < MAX_ORDER; order++) {
			struct free_area *area = &(zone->free_area[order]);

			page = list_first_entry_or_null(
					&area->free_list[MIGRATE_HIGHATOMIC],
					struct page, lru);
			if (!page)
				continue;

			/*
			 * In page freeing path, migratetype change is racy so
			 * we can counter several free pages in a pageblock
			 * in this loop althoug we changed the pageblock type
			 * from highatomic to ac->migratetype. So we should
			 * adjust the count once.
			 */
			if (get_pageblock_migratetype(page) ==
							MIGRATE_HIGHATOMIC) {
				zone->nr_reserved_highatomic -= min(
						pageblock_nr_pages,
						zone->nr_reserved_highatomic);
			}
			set_pageblock_migratetype(page, ac->migratetype);
			move_freepages_block(zone, page, ac->migratetype);
			spin_unlock_irqrestore(&zone->lock, flags);
			return;
		}
		spin_unlock_irqrestore(&zone->lock, flags);
	}
}

通过上面代码可知unreserve_highatomic_pageblock释放伙伴系统中的MIGRATE_HIGHATOMIC类型空闲页面,就是将对应zone中部分空闲的MIGRATE_HIGHATOMIC迁移类型页移动到内存分配者需要分配页面的迁移类型空闲链表上去。

MIGRATE_HIGHATOMIC类型空闲页的释放时机:当系统当前内存比较紧张,伙伴系统慢速内存分配经过直接内存回收后仍然难以获取到需要的内存块,则会通过unreserve_highatomic_pageblock函数对MIGRATE_HIGHATOMIC类型页面进行释放,释放后再次参数内存分配.

//__alloc_pages_slowpath()--->__alloc_pages_direct_reclaim()
/* The really slow allocator path where we enter direct reclaim */
static inline struct page *
__alloc_pages_direct_reclaim(gfp_t gfp_mask, unsigned int order,
		unsigned int alloc_flags, const struct alloc_context *ac,
		unsigned long *did_some_progress)
{
	struct page *page = NULL;
	bool drained = false;

	*did_some_progress = __perform_reclaim(gfp_mask, order, ac);
	if (unlikely(!(*did_some_progress)))
		return NULL;

retry:
	page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);

	/*
	 * If an allocation failed after direct reclaim, it could be because
	 * pages are pinned on the per-cpu lists or in high alloc reserves.
	 * Shrink them them and try again
	 */
	if (!page && !drained) {
		unreserve_highatomic_pageblock(ac);
		drain_all_pages(NULL);
		drained = true;
		goto retry;
	}

	return page;
}

需要进一步理解MIGRATE_HIGHATOMIC迁移类型引入原因参考下面博文:

  1. https://blog.csdn.net/frank_zyp/article/details/89249469?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-2&spm=1001.2101.3001.4242
  2. https://www.cnblogs.com/haoxing990/p/14495719.html

4.PF_MEMALLOC标志位

这是一个进程标记位,除了在内存管理子系统中使用外,还在其他的内核子系统中使用。之所以在伙伴系统分配器中讨论,是因为这个标记和内存管理密不可分。

当一个进程被设置PF_MEMALLOC后,那么对进程会有如下影响:

1. 当进程进行页面分配时,可以忽略内存管理的水线进行分配,这是告诉内存管理系统,给我一点紧急内存使用,我将会释放更多的内存给你。

2. 如果忽略水线分配仍然失败,那么直接返回ENOMEM,而不是等待kswapd回收或者缩减内存

3. 如果忽略水线分配仍然失败,那么直接返回ENOMEM,而不会调用OOM killer去杀死进程,释放内存

4. 2和3 说的很清楚了,就是在page_allocs中失败并不会重试。


PF_MEMALLOC含义:
当前进程有很多可以释放的内存,如果能分配一点紧急内存给当前进程,那么当前进程可以返回更多的内存给系统。非内存管理子系统不应该使用这个标记,除非这次分配保证会释放更大的内存给系统。如果每个子系统都滥用这个标记,可能会耗尽内存管理子系统的保留内存。


虽然这个标志的引入,是为了内存管理系统在紧急情况下使用,但是却被其他的内核子系统滥用,一个日本人为此曾提了7个patch来修复这些滥用的代码。

http://lkml.indiana.edu/hypermail/linux/kernel/0911.2/00576.html


不过,在我使用的内核版本2.6.34,仍然可以看到其他子系统使用PF_MEMALLOC,具体case要具体分析了。

5 ZONE_MOVABLE引入分析

linux内存管理系统将内存划分为不同的zone,通常划分情况如下图所示:

在这里插入图片描述

为了减少内存碎片会,linux内核引入了ZONE_MOVABLE,网上通常把它称为虚拟内存区(pseudo zone)。实际上内核是从从最高的内存区中化分了一部分来作为为ZONE_MOVABLE区。设置代码如下:

 static void __init find_usable_zone_for_movable(void)
 {
     int zone_index;
     for (zone_index = MAX_NR_ZONES - 1; zone_index >= 0; zone_index--) {
         if (zone_index == ZONE_MOVABLE)
             continue;

         if (arch_zone_highest_possible_pfn[zone_index] >
                 arch_zone_lowest_possible_pfn[zone_index])
             break;
     }

     VM_BUG_ON(zone_index == -1);
     movable_zone = zone_index;
 }

由于内存碎片化,虽然内存剩余还有很多,但是内核不能分配出大块的连续内存。这个时候内核可以通过内存迁移来获取连续的大块内存,但是这种迁移也不一定能保证成功获得大块连续内存,因为内存中间还夹杂着许多不可迁移的内存块。

引入ZONE_MOVABLE,其意图就是把可以移动的和不可移动的内存分区管理。引入ZONE_MOVABLE后,只有可迁移的页面才能够在ZONE_MOVABLE区域中进行申请,后面连续内存不足时,我们可以针对ZONE_MOVABLE区域对页内容进行迁移,这样就能够提高内核获得足够大的连续内存的概率,缓解内核内的存碎片化。

另外引入ZONE_MOVABLE区域还应用带内存热插拔(memory hotplug)场景中,当内核要移除一块内存区域时,必须要能够保证该内存区域的内容能够被迁移出去,所以热插拔内存区必须位于ZONE_MOVABLE区域。

zone区域划分内核代码:

//include/linux/mmzone.h
enum zone_type {
#ifdef CONFIG_ZONE_DMA
	/*
	 * ZONE_DMA is used when there are devices that are not able
	 * to do DMA to all of addressable memory (ZONE_NORMAL). Then we
	 * carve out the portion of memory that is needed for these devices.
	 * The range is arch specific.
	 *
	 * Some examples
	 *
	 * Architecture		Limit
	 * ---------------------------
	 * parisc, ia64, sparc	<4G
	 * s390			<2G
	 * arm			Various
	 * alpha		Unlimited or 0-16MB.
	 *
	 * i386, x86_64 and multiple other arches
	 * 			<16M.
	 */
	ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
	/*
	 * x86_64 needs two ZONE_DMAs because it supports devices that are
	 * only able to do DMA to the lower 16M but also 32 bit devices that
	 * can only do DMA areas below 4G.
	 */
	ZONE_DMA32,
#endif
	/*
	 * Normal addressable memory is in ZONE_NORMAL. DMA operations can be
	 * performed on pages in ZONE_NORMAL if the DMA devices support
	 * transfers to all addressable memory.
	 */
	ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
	/*
	 * A memory area that is only addressable by the kernel through
	 * mapping portions into its own address space. This is for example
	 * used by i386 to allow the kernel to address the memory beyond
	 * 900MB. The kernel will set up special mappings (page
	 * table entries on i386) for each page that the kernel needs to
	 * access.
	 */
	ZONE_HIGHMEM,
#endif
	ZONE_MOVABLE,
#ifdef CONFIG_ZONE_DEVICE
	ZONE_DEVICE,
#endif
	__MAX_NR_ZONES

};

linux在进行内存非配时会传入一个参数__GFP_MOVABLE,这是内存的一个分配标志,一定不要和ZONE_MOVABLE的概念弄混淆:

  • __GFP_MOVABLE是一种页面分配标志,表示内核要分配的页面是可以迁移的页面,这些页面既可是ZONE_MOVABLE管理的内存区域,也可能是其它ZONE区域中的可迁移页面
  • ZONE_MOVABLE表示的是内存的管理区域,可迁移的页面不一定都在ZONE_MOVABLE区域中,但ZONE_MOVABLE区域中的页面必须都是可迁移的页面

内核使能ZONE_MOVABLE方式可以通过传入不同的cmdline来设置(kernelcore=YYYY和movablecore=ZZZZ),内核文档中的介绍如下所示:

1) When kernelcore=YYYY boot option is used,
   Size of memory not for movable pages (not for offline) is YYYY.
   Size of memory for movable pages (for offline) is TOTAL-YYYY.

2) When movablecore=ZZZZ boot option is used,
   Size of memory not for movable pages (not for offline) is TOTAL - ZZZZ.
   Size of memory for movable pages (for offline) is ZZZZ.

6.判断页是否在伙伴系

#define page_order_unsafe(page)		READ_ONCE(page_private(page))
page = pfn_to_page(low_pfn)
//判读页是否在伙伴系统中,page->_mapcount = PAGE_BUDDY_MAPCOUNT_VALUE
if (PageBuddy(page)) {
	//若在伙伴系统获取页所在页块的阶
	unsigned long freepage_order = page_order_unsafe(page);
	......
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值