【linux 内存管理】深入理解linux内核架构 内存管理(3)伙伴系统

        在内核初始化完成后,内存管理的责任由伙伴系统承担。伙伴系统基于一种相对简单然而令人吃惊的强大算法,已经伴随我们几乎40年。它结合了优秀内存分配器的两个关键特征:速度和效率。

一、伙伴系统结构

系统内存中的每个物理内存页(页帧),都对应于一个struct page实例。每个内存域都关联了一个struct zone的实例,其中保存了用于管理伙伴数据的主要数组。
<mmzone.h>
struct zone

{
...
/*
* 不同长度的空闲区域
*/
struct free_area free_area[MAX_ORDER];
...
};
<mmzone.h>
struct free_area

{
struct list_head free_list[MIGRATE_TYPES];
unsigned long nr_free;
};
    nr_free指定了当前内存区中空闲页块的数目,free_list是用于连接空闲页的链表。
    阶是伙伴系统中一个非常重要的术语。它描述了内存分配的数量单位。内存块的长度是2order,其中order的范围从0到MAX_ORDER。MAX_ORDER该常数通常设置为11,这意味着一次分配可以请求的页数最大是2的11次方。free_area[]数组中各个元素的索引也解释为阶,用于指定对应链表中的连续内存区包含多少个页帧。

    伙伴不必是彼此连接的。如果一个内存区在分配其间分解为两半,内核会自动将未用的一半加入到对应的链表中。如果在未来的某个时刻,由于内存释放的缘故,两个内存区都处于空闲状态,可通过其地址判断其是否为伙伴。管理工作较少,是伙伴系统的一个主要优点。
    基于伙伴系统的内存管理专注于某个结点的某个内存域,例如, DMA或高端内存域。但所有内存域和结点的伙伴系统都通过备用分配列表连接起来。图3-23说明了这种关系。在首选的内存域或节点无法满足内存分配请求时,首先尝试同一结点的另一个内存域,接下来再尝试另一个结点,直至满足请求。

二、避免碎片

    在Linux内存管理方面,有一个长期存在的问题:在系统启动并长期运行后,物理内存会产生很多碎片。

注意此时解决的是物理内存的碎片,是物理内存直接映射到内核的896M虚拟内存,由于这段虚拟内存和物理内存是一一映射的,所以此处也是解决这896M虚拟内存的碎片。

    很有趣的一点是,在大部分内存仍然未分配时,就也可能发生碎片问题。考虑图3-25的情形。只分配了4页,但可分配的最大连续区只有8页,因为伙伴系统所能工作的分配范围只能是2的幂次。

注意,由以上可以看出,由于内存碎片的存在,系统在长时间使用后,可只用的内存没有变少,但是系统在分配大内存块的时候会受到影响。

    内核在处理内存碎片的问题采用的方法是反碎片,即试图从最初开始尽可能防止碎片。

    反碎片的工作原理如何?为理解该方法,我们必须知道内核将已分配页划分为下面3种不同类型。

  • 不可移动页:在内存中有固定位置,不能移动到其他地方。核心内核分配的大多数内存属于该类别。
  • 可回收页:不能直接移动,但可以删除,其内容可以从某些源重新生成。例如,映射自文件的数据属于该类别。kswapd守护进程会根据可回收页访问的频繁程度,周期性释放此类内存。这是一个复杂的过程,本身就需要详细论述:第18章详细描述了页面回收。目前,了解到内核会在可回收页占据了太多内存时进行回收,就足够了。另外,在内存短缺(即分配失败)时也可以发起页面回收。有关内核发起页面回收的时机,更具体的信息请参考下文。
  • 可移动页可以随意地移动。属于用户空间应用程序的页属于该类别。它们是通过页表映射的。如果它们复制到新位置,页表项可以相应地更新,应用程序不会注意到任何事。

    内核使用的反碎片技术,即基于将具有相同可移动性的页分组的思想。为什么这种方法有助于减少碎片?回想图3-25中,如果页无法移动,导致在原本几乎全空的内存区中无法进行连续分配。根据页的可移动性,将其分配到不同的列表中,即可防止这种情形。例如,不可移动的页不能位于可移动内存区的中间,如果不可移动页分配到了可移动页中间就无法从可移动页内存区分配较大的连续内存块。也就是在可移动页中我们可以将其中的某些页做些移动,这样就更能容易的分出较大的内存块。

    试想一下,图3-25中大多数空闲页都属于可回收的类别,而分配的页则是不可移动的。如果这些页聚集到两个不同的列表中,如图3-26所示。在不可移动页中仍然难以找到较大的连续空闲空间,但对可回收的页,就容易多了。

内核定义了一些宏来表示不同的迁移类型:

<mmzone.h>
#define MIGRATE_UNMOVABLE 0 不可移动
#define MIGRATE_RECLAIMABLE 1 可回收
#define MIGRATE_MOVABLE 2 可移动
#define MIGRATE_RESERVE 3
#define MIGRATE_ISOLATE 4 /* 不能从这里分配 */setup_zone_migrate_reserve填充
#define MIGRATE_TYPES 5

三、初始化内存域和节点数据结构

我们知道,体系结构相关代码需要在启动期间建立以下信息:
 系统中各个内存域的页帧边界,保存在max_zone_pfn数组;
 各结点页帧的分配情况,保存在全局变量early_node_map中。


free_area_init_nodes函数简介:

void __init free_area_init_nodes(unsigned long *max_zone_pfn)
{
	unsigned long start_pfn, end_pfn;
	int i, nid;

	/* Record where the zone boundaries are */
	memset(arch_zone_lowest_possible_pfn, 0,
				sizeof(arch_zone_lowest_possible_pfn));
	memset(arch_zone_highest_possible_pfn, 0,
				sizeof(arch_zone_highest_possible_pfn));

	start_pfn = find_min_pfn_with_active_regions();

	for (i = 0; i < MAX_NR_ZONES; i++) {
		if (i == ZONE_MOVABLE)
			continue;

		end_pfn = max(max_zone_pfn[i], start_pfn);
		arch_zone_lowest_possible_pfn[i] = start_pfn;
		arch_zone_highest_possible_pfn[i] = end_pfn;

		start_pfn = end_pfn;
	}
    //以上计算了各内存域的起始和结束的物理帧的编号
	arch_zone_lowest_possible_pfn[ZONE_MOVABLE] = 0;
	arch_zone_highest_possible_pfn[ZONE_MOVABLE] = 0;

	/* Find the PFNs that ZONE_MOVABLE begins at in each node */
	memset(zone_movable_pfn, 0, sizeof(zone_movable_pfn));
	find_zone_movable_pfns_for_nodes();

	/* Initialise every node */
    //以下对所有在线节点调用free_area_init_node函数
	mminit_verify_pageflags_layout();
	setup_nr_node_ids();
	for_each_online_node(nid) {
		pg_data_t *pgdat = NODE_DATA(nid);
		free_area_init_node(nid, NULL,
				find_min_pfn_for_node(nid), NULL);

		/* Any memory on that node */
		if (pgdat->node_present_pages)
			node_set_state(nid, N_MEMORY);
		check_for_memory(pgdat, nid);
	}
}

从上述代码中看到,在内存域边界已经确定之后, free_area_init_nodes分别对各个内存域调用free_area_init_node创建数据结构。

free_area_init_node()函数简介:

void __paginginit free_area_init_node(int nid, unsigned long *zones_size,
		unsigned long node_start_pfn, unsigned long *zholes_size)
{
	pg_data_t *pgdat = NODE_DATA(nid);
	unsigned long start_pfn = 0;
	unsigned long end_pfn = 0;

	pgdat->node_id = nid;
	pgdat->node_start_pfn = node_start_pfn;
	calculate_node_totalpages(pgdat, start_pfn, end_pfn,
				  zones_size, zholes_size);
	alloc_node_mem_map(pgdat);
	reset_deferred_meminit(pgdat);
	free_area_init_core(pgdat);
}

其中calculate_node_totalpages首先累计各个内存域的页数,计算结点中页的总数。对连续内存模型而言,这可以通过zones_size_init完成,但calculate_zone_totalpages还考虑了内存域中的空洞。

static void __meminit calculate_node_totalpages(struct pglist_data *pgdat,
						unsigned long node_start_pfn,
						unsigned long node_end_pfn,
						unsigned long *zones_size,
						unsigned long *zholes_size)
{
	unsigned long realtotalpages = 0, totalpages = 0;
	enum zone_type i;

	for (i = 0; i < MAX_NR_ZONES; i++) {
		struct zone *zone = pgdat->node_zones + i;
		unsigned long size, real_size;

		size = zone_spanned_pages_in_node(pgdat->node_id, i,
						  node_start_pfn,
						  node_end_pfn,
						  zones_size);
		real_size = size - zone_absent_pages_in_node(pgdat->node_id, i,
						  node_start_pfn, node_end_pfn,
						  zholes_size);
		zone->spanned_pages = size;
		zone->present_pages = real_size;//从这里能看出来zone的那3个字段是在这里初始化

		totalpages += size;
		realtotalpages += real_size;
	}

	pgdat->node_spanned_pages = totalpages;//反应了总页数,包含空洞
	pgdat->node_present_pages = realtotalpages;//反映了实际的页数,不包含空洞
}

alloc_node_mem_map负责初始化一个简单但非常重要的数据结构。如上所述,系统中的各个物理内存页,都对应着一个struct page实例。该结构的初始化由alloc_node_mem_map执行。

mm/page_alloc.c
static void __init_refok alloc_node_mem_map(struct pglist_data *pgdat)
{
/* 跳过空结点 */
if (!pgdat->node_spanned_pages)
return;
if (!pgdat->node_mem_map) {
    unsigned long size, start, end;
    struct page *map;
    start = pgdat->node_start_pfn & ~(MAX_ORDER_NR_PAGES -1);
    end = pgdat->node_start_pfn + pgdat->node_spanned_pages;
    end = ALIGN(end, MAX_ORDER_NR_PAGES);
    size = (end -start) * sizeof(struct page);//申请的物理页的大小,当前节点所对应的所有物理页
    map = alloc_remap(pgdat->node_id, size);
    if (!map)
        map = alloc_bootmem_node(pgdat, size);
    pgdat->node_mem_map = map + (pgdat->node_start_pfn -start);
/* node_mem_map 指向了物理页的开始,这里并没有对物理页的内存进行赋值 */
}
if (pgdat == NODE_DATA(0))
    mem_map = NODE_DATA(0)->node_mem_map;
}

指向该空间的指针不仅保存在pglist_data实例中,还保存在全局变量mem_map中,前提是当前考察的结点是系统的第0个结点(如果系统只有一个内存结点,则总是这样)。 mem_map是一个全局数组,在讲解内存管理时,我们会经常遇到

mm/memory.c
struct page *mem_map;
    初始化内存域数据结构涉及的繁重工作由free_area_init_core执行,它会依次遍历结点的所有内存域。
free_area_init_core()函数简介:

static void __paginginit free_area_init_core(struct pglist_data *pgdat)
{
	enum zone_type j;
	int nid = pgdat->node_id;
	unsigned long zone_start_pfn = pgdat->node_start_pfn;
	int ret;


	for (j = 0; j < MAX_NR_ZONES; j++) {
		struct zone *zone = pgdat->node_zones + j;
		unsigned long size, realsize, freesize, memmap_pages;

		size = zone->spanned_pages;
		realsize = freesize = zone->present_pages;
        //内存域的真实长度,可通过跨越的页数减去空洞覆盖的页数而得到

		/*
		 * Adjust freesize so that it accounts for how much memory
		 * is used by this zone for memmap. This affects the watermark
		 * and per-cpu initialisations
		 */
		memmap_pages = calc_memmap_size(size, realsize);//当前内存域有多少个页,有多少个4k
    

		if (!is_highmem_idx(j))
			nr_kernel_pages += freesize;
		/* Charge for highmem memmap if there are enough kernel pages */
		else if (nr_kernel_pages > memmap_pages * 2)
			nr_kernel_pages -= memmap_pages;
		nr_all_pages += freesize;

		/*
		 * Set an approximate value for lowmem here, it will be adjusted
		 * when the bootmem allocator frees pages into the buddy system.
		 * And all highmem pages will be managed by the buddy system.
		 */
		zone->managed_pages = is_highmem_idx(j) ? realsize : freesize;
		zone->name = zone_names[j];
		spin_lock_init(&zone->lock);
		spin_lock_init(&zone->lru_lock);
		zone_seqlock_init(zone);
		zone->zone_pgdat = pgdat;
		zone_pcp_init(zone);

		/* For bootup, initialized properly in watermark setup */
		mod_zone_page_state(zone, NR_ALLOC_BATCH, zone->managed_pages);

		lruvec_init(&zone->lruvec);
		if (!size)
			continue;

		set_pageblock_order();
		setup_usemap(pgdat, zone, zone_start_pfn, size);
		ret = init_currently_empty_zone(zone, zone_start_pfn, size);
		BUG_ON(ret);
		memmap_init(size, nid, j, zone_start_pfn);
		zone_start_pfn += size;
	}
}

    内核使用两个全局变量跟踪系统中的页数。 nr_kernel_pages统计所有一致映射的页,而nr_all_pages还包括高端内存页在内。free_area_init_core剩余部分的任务是初始化zone结构中的各个表头,并将各个结构成员初始化为0。 我们比较感兴趣的是调用的两个辅助函数。
zone_pcp_init初始化该内存域的per-CPU缓存,且将在下一节广泛讨论。
init_currently_empty_zone初始化free_area列表,并将属于该内存域的所有page实例都设置为初始默认值。正如前文的讨论,调用了memmap_init_zone来初始化内存域的页。我们还可以回想前文提到的,所有页属性起初都设置MIGRATE_MOVABLE。
此外,空闲列表是在zone_init_free_lists中初始化的:
mm/page_alloc.c
static void __meminit zone_init_free_lists(struct pglist_data *pgdat,
struct zone *zone, unsigned long size)
{
    int order, t;
    for_each_migratetype_order(order, t) {
        INIT_LIST_HEAD(&zone->free_area[order].free_list[t]);//双向循环队列初始化
        zone->free_area[order].nr_free = 0;
    }
}
宏for_each_migratetype_order(order, type)可用于迭代所有迁移类型的所有分配阶。

    空闲页的数目( nr_free)当前仍然规定为0,这显然没有反映真实情况。直至停用bootmem分配器、普通的伙伴分配器生效,才会设置正确的数值。

以上的处理仅仅只是对一些字段进行初始化(比如节点中每个zone中的spanned_pages和present_pages这三个字段初始化、节点和每个zone的起始和结束帧的初始化),并且对伙伴系统的free_area进行队列初始化并将nr_free赋值为0.
 

四、分配器API

    就伙伴系统的接口而言,只能分配2的整数幂个页。因此,接口中不像C标准库的malloc函数或bootmem分配器那样指定了所需内存大小作为参数。相反,必须指定的是分配阶,伙伴系统将在内存中分配2的order次方页。内核中细粒度的分配只能借助于slab分配器(或者slub、 slob分配器),后者基于伙伴 系统(更多细节在3.6节给出)。
 alloc_pages(mask, order)分配2order页并返回一个struct page的实例,表示分配的内存块的起始页。 alloc_page(mask)是前者在order = 0情况下的简化形式,只分配一页。
 get_zeroed_page(mask)分配一页并返回一个page实例,页对应的内存填充0(所有其他函数,分配之后页的内容是未定义的)。
 __get_free_pages(mask, order)和__get_free_page(mask)的工作方式与上述函数相同,但返回分配内存块的虚拟地址,而不是page实例。
 get_dma_pages(gfp_mask, order)用来获得适用于DMA的页。
    内核除了伙伴系统函数之外,还提供了其他内存管理函数。它们以伙伴系统为基础,但并不属于伙伴分配器自身。这些函数包括vmalloc和vmalloc_32,使用页表将不连续的内存映射到内核地址空间中,使之看上去是连续的。还有一组kmalloc类型的函数,用于分配小于一整页的内存区。其实现将在本章后续的几节分别讨论。

有4个函数用于释放不再使用的页,与所述函数稍有不同。
 free_page(struct page *)和free_pages(struct page *, order)用于将一个或2order页返回给内存管理子系统。内存区的起始地址由指向该内存区的第一个page实例的指针表示。

 __free_page(addr)和__free_pages(addr, order)的语义类似于前两个函数,但在表示需要释放的内存区时,使用了虚拟内存地址而不是page实例。
 

1、mask掩码

   alloc_page的参数掩码mask和内存域修饰符是共用一个字段,如:

#define __GFP_DMA
#define __GFP_HIGHMEM
((__force gfp_t)0x01u)
((__force gfp_t)0x02u)

#define __GFP_DMA32 ((__force gfp_t)0x04u)
...
#define __GFP_MOVABLE ((__force gfp_t)0x100000u) /* 页是可移动的 */

与内存域修饰符不同的是,这些额外的标志并不限制从哪个物理内存段分配内存,但确实可以改变分配器的行为。如:

#define __GFP_WAIT
#define __GFP_HIGH
#define __GFP_IO
#define __GFP_FS
#define __GFP_COLD
#define __GFP_NOWARN
#define __GFP_REPEAT
#define __GFP_NOFAIL
#define __GFP_NORETRY
#define __GFP_NO_GROW
#define __GFP_COMP
#define __GFP_ZERO
((__force gfp_t)0x10u)
((__force gfp_t)0x20u)
((__force gfp_t)0x40u)
((__force gfp_t)0x80u)
((__force gfp_t)0x100u)
((__force gfp_t)0x200u)
((__force gfp_t)0x400u)
((__force gfp_t)0x800u)
((__force gfp_t)0x1000u)
((__force gfp_t)0x2000u)
((__force gfp_t)0x4000u)
((__force gfp_t)0x8000u)
/* 可以等待和重调度? */
/* 应该访问紧急分配池? */
/* 可以启动物理IO? */
/* 可以调用底层文件系统? */
/* 需要非缓存的冷页 */
/* 禁止分配失败警告 */
/* 重试分配,可能失败 */
/* 一直重试,不会失败 */
/* 不重试,可能失败 */
/* slab内部使用 */
/* 增加复合页元数据 */
/* 成功则返回填充字节0的页 */
 

 __GFP_WAIT表示分配内存的请求可以中断。也就是说,调度器在该请求期间可随意选择另一个过程执行,或者该请求可以被另一个更重要的事件中断。分配器还可以在返回内存之前,在队列上等待一个事件(相关进程会进入睡眠状态)。
 __GFP_IO说明在查找空闲内存期间内核可以进行I/O操作。实际上,这意味着如果内核在内存分配期间换出页,那么仅当设置该标志时,才能将选择的页写入硬盘。
 __GFP_ZERO在分配成功时,将返回填充字节0的页。
 

    但是,内核对于我们常用的修饰符已经整合成若干个红,我们在大部分情况下使用这些宏即可,如:

#define GFP_ATOMIC
#define GFP_NOIO
#define GFP_NOFS
#define GFP_KERNEL
#define GFP_USER
#define GFP_HIGHUSER
(__GFP_HIGH)
(__GFP_WAIT)
(__GFP_WAIT | __GFP_IO)
(__GFP_WAIT | __GFP_IO | __GFP_FS)
(__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
(__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL | \
__GFP_HIGHMEM)
#define GFP_HIGHUSER_MOVABLE(__GFP_WAIT | __GFP_IO | __GFP_FS | \
__GFP_HARDWALL | __GFP_HIGHMEM | \
__GFP_MOVABLE)
#define GFP_DMA
#define GFP_DMA32
__GFP_DMA
__GFP_DMA32

 前3个组合的语义是清楚的。 GFP_ATOMIC用于原子分配,在任何情况下都不能中断,可能使用紧急分配链表中的内存。 GFP_NOIO和GFP_NOFS分别明确禁止I/O操作和访问VFS层,但同时设置了__GFP_WAIT,因此可以被中断。
 GFP_KERNEL和GFP_USER分别是内核和用户分配的默认设置。二者的失败不会立即威胁系统稳定性。 GFP_KERNEL绝对是内核源代码中最常使用的标志。
 GFP_HIGHUSER是GFP_USER的一个扩展,也用于用户空间。它允许分配无法直接映射的高端内
存。使用高端内存页是没有坏处的,因为用户过程的地址空间总是通过非线性页表组织的。
 GFP_HIGHUSER_MOVABLE用途类似于GFP_HIGHUSER,但分配将从虚拟内存域ZONE_MOVABLE进行。
 GFP_DMA用 于 分配 适 用于 DMA 的 内 存 ,当 前 是 __GFP_DMA的 同 义词 。 GFP_DMA32也 是__GFP_GMA32的同义词。

    伙伴系统各分配函数和回收函数关系图

 

   

五、分配页

    所有API函数都追溯到alloc_pages_node,从某种意义上说,该函数是伙伴系统主要实现的“发射台”
 

<gfp.h>
static inline struct page *alloc_pages_node(int nid, gfp_t gfp_mask,
unsigned int order)
{
    if (unlikely(order >= MAX_ORDER))
        return NULL;
    if(nid< 0)
        nid = numa_node_id();
    return __alloc_pages(gfp_mask, order,
        NODE_DATA(nid)->node_zonelists + gfp_zone(gfp_mask));
}
//gfp_zone用于选择分配内存的内存域
static inline struct page *__alloc_pages(gfp_t gfp_mask, unsigned int order, int preferred_nid)
{
	return __alloc_pages_nodemask(gfp_mask, order, preferred_nid, NULL);
}

gfp_mask 宏:
typedef enum {
	GFP_KERNEL,
	GFP_ATOMIC,
	__GFP_HIGHMEM,
	__GFP_HIGH
} gfp_t;

页分配关键函数流程图:

    内核源代码将__alloc_pages_nodemask称之为“伙伴系统的心脏”,因为它处理的是实质性的内存分配。由于“心脏”的重要性,我将在下文详细介绍该函数。

__alloc_pages_nodemask函数:

/*
 * This is the 'heart' of the zoned buddy allocator.
 */
struct page *
__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order, int preferred_nid,
							nodemask_t *nodemask)
{
	struct page *page;
	unsigned int alloc_flags = ALLOC_WMARK_LOW;
	gfp_t alloc_mask; /* The gfp_t that was actually used for allocation */
	struct alloc_context ac = { };

	//检查order
	if (unlikely(order >= MAX_ORDER)) {
		WARN_ON_ONCE(!(gfp_mask & __GFP_NOWARN));
		return NULL;
	}

	gfp_mask &= gfp_allowed_mask;
	alloc_mask = gfp_mask;
	if (!prepare_alloc_pages(gfp_mask, order, preferred_nid, nodemask, &ac, &alloc_mask, &alloc_flags))
		return NULL;

	finalise_ac(gfp_mask, &ac);

	/* 第一次试图分配内存,若内存中有较多空余内存则可以快速分配 */
	page = get_page_from_freelist(alloc_mask, order, alloc_flags, &ac);
	if (likely(page))
		goto out;

	。。。
    /* 内存中没有较多的内存分配,是否有较多的内存分配和zone_watermark_ok函数检查有关,
       内存中没有较多内存分配,触发慢速内存分配 */

	page = __alloc_pages_slowpath(alloc_mask, order, &ac);

out:
	。。。

	trace_mm_page_alloc(page, order, alloc_mask, ac.migratetype);

	return page;
}

    prepare_alloc_pages函数在做一些分配之前的内存准备工作,主要对参数ac一些字段赋值,暂时没有看到ac参数的作用。首先这个函数先尝试第一次内存分配调用get_page_from_freelist,如果没成功尝试慢速内存分配__alloc_pages_slowpath。 

    __alloc_pages_slowpath中内核再次遍历备用列表中的所有内存域,每次都调用wakeup_kswapd。顾名思义,该函数会唤醒负责换出页的kswapd守护进程。交换守护进程的任务比较复杂以后再做了解,在这里需要注意的是,空闲内存可以通过缩减内核缓存和页面回收获得,即写回或换出很少使用的页。这两种措施都是由该守护进程发起的。在交换守护进程唤醒后,内核开始新的尝试,在内存域之一查找适当的内存块。这一次进行的搜索更为积极,对分配标志进行了调整,修改为一些在当前特定情况下更有可能分配成功的标志。同时,将 水 印 降 低 到 最 小 值 。 对 实 时 进 程 和 指 定 了__GFP_WAIT标 志 因 而 不 能 睡 眠 的 调 用 , 会 设 置ALLOC_HARDER。然后用修改的标志集,再一次调用get_page_from_freelist,试图获得所需的页。
   总之,内核会尽量的给调用者分配出内存,如果内存不够,就触发kswapd守护进程,回收一部分页,然后再继续调用get_page_from_freelist尝试分配,如果还不行,那么还有更强有力的措施,内核继续想其他办法挤出内存,当然这里就需要一些标记宏来控制是否强力挤出内存。

    get_page_from_freelist函数:

static struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,
						const struct alloc_context *ac)
{
	struct zoneref *z = ac->preferred_zoneref;
	struct zone *zone;
	struct pglist_data *last_pgdat_dirty_limit = NULL;

	/*
	 * Scan zonelist, looking for a zone with enough free.
	 * See also __cpuset_node_allowed() comment in kernel/cpuset.c.
	 */
    /*在预期内存域没有空闲空间的情况下,该列表确定了扫描系统其他内存域(和结点)的顺序。*/
	for_next_zone_zonelist_nodemask(zone, z, ac->zonelist, ac->high_zoneidx,
								ac->nodemask) {
		struct page *page;
		unsigned long mark;
        /* 检查当前cpu是否能分配内存 */
		if (cpusets_enabled() &&
			(alloc_flags & ALLOC_CPUSET) &&
			!__cpuset_zone_allowed(zone, gfp_mask))
				continue;
		。。。
        /* 通过入参alloc_flags来检查当前内存域是否可以分配内存, 
            zone_watermark_fast会调用__zone_watermark_ok, zone_watermark_ok接下来检查
            所遍历到的内存域是否有足够的空闲页,并试图分配一个连续内存块*/

		mark = zone->watermark[alloc_flags & ALLOC_WMARK_MASK];
		if (!zone_watermark_fast(zone, order, mark,
				       ac_classzone_idx(ac), alloc_flags)) {
        。。。


				continue;
			}
		}

try_this_zone:
        /* 最关键的伙伴系统移出free page 所以是rm */
		page = rmqueue(ac->preferred_zoneref->zone, zone, order,
				gfp_mask, alloc_flags, ac->migratetype);
		if (page) {
			prep_new_page(page, order, gfp_mask, alloc_flags);

            。。。
			return page;
		} else {
            。。。
		}
	}

	return NULL;
}

 

 

    zone_watermark_ok函数中检查,该函数根据设置的标志判断是否能从给定的内存域分配内存。zone_watermark_ok检查所遍历到的内存域是否有足够的空闲页,并试图分配一个连续内存块。

注:watermark
high
当剩余内存在high以上时,系统认为当前内存使用压力不大。
low
当剩余内存降低到low时,系统就认为内存已经不足了,会触发kswapd内核线程进行内存回收处理
min
当剩余内存在min以下时,则系统内存压力非常大。一般情况下min以下的内存是不会被分配的,min以下的内存默
认是保留给特殊用途使用,属于保留的页框,用于原子的内存请求操作。
比如:当我们在中断上下文申请或者在不允许睡眠的地方申请内存时,可以采用标志GFP_ATOMIC来分配内存,
此时才会允许我们使用保留在min水位以下的内存

 

#define ALLOC_NO_WATERMARKS

#define ALLOC_WMARK_MIN
#define ALLOC_WMARK_LOW
#define ALLOC_WMARK_HIGH
#define ALLOC_HARDER
#define ALLOC_HIGH
#define ALLOC_CPUSET

0x01 /* 完全不检查水印 */

0x02 /* 使用pages_min水印 */
0x04 /* 使用pages_low水印 */
0x08 /* 使用pages_high水印 */
0x10 /* 试图更努力地分配,即放宽限制 */
0x20 /* 设置了__GFP_HIGH */
0x40 /* 检查内存结点是否对应着指定的CPU集合 */

    前几个标志表示在判断页是否可分配时,需要考虑哪些水印。默认情况下(即没有因其他因素带来的压力而需要更多的内存),只有内存域包含页的数目至少为zone->pages_high时,才能分配页。这对应于ALLOC_WMARK_HIGH标志。如果要使用较低( zone->pages_low)或最低( zone->pages_min)设置,则必须相应地设置ALLOC_WMARK_MIN或ALLOC_WMARK_LOW。 ALLOC_HARDER通知伙伴系统在急需内存时放宽分配规则。在分配高端内存域的内存时, ALLOC_HIGH进一步放宽限制。最后,ALLOC_CPUSET告知内核,内存只能从当前进程允许运行的CPU相关联的内存结点分配,当然该选项只对NUMA系统有意义。

 

伙伴系统关键的分配函数rmqueue():
 

/*
 * Allocate a page from the given zone. Use pcplists for order-0 allocations.
 */
static inline
struct page *rmqueue(struct zone *preferred_zone,
			struct zone *zone, unsigned int order,
			gfp_t gfp_flags, unsigned int alloc_flags,
			int migratetype)
{
	unsigned long flags;
	struct page *page;

    /* order=0,从每cpu高速缓存中分配内存 */
	if (likely(order == 0)) {
		page = rmqueue_pcplist(preferred_zone, zone, order,
				gfp_flags, migratetype);
		goto out;
	}

    。。。

	do {
		page = NULL;
		if (alloc_flags & ALLOC_HARDER) {
			page = __rmqueue_smallest(zone, order, MIGRATE_HIGHATOMIC);
			if (page)
				trace_mm_page_alloc_zone_locked(page, order, migratetype);
		}
		if (!page)
			page = __rmqueue(zone, order, migratetype);
	} while (page && check_new_pages(page, order));
	。。。

out:
	VM_BUG_ON_PAGE(page && bad_range(zone, page), page);
	return page;


}

    rmqueue函数,如果是0阶分配,那么则直接从cpu高速缓存中来处理,这个以后再讨论。其他阶直接从__rmqueue_smallest中开始分配。

  __rmqueue_smallest()函数:

    __rmqueue_smallest的实现不是很长。本质上,它由一个循环组成,按递增顺序遍历内存域的各个特定迁移类型的空闲页列表,直至找到合适的一项。
 

/*
 * Go through the free lists for the given migratetype and remove
 * the smallest available page from the freelists
 */
static __always_inline
struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
						int migratetype)
{
	unsigned int current_order;
	struct free_area *area;
	struct page *page;

	/* 从当前需要的order阶中开始向上遍历高阶free area(++current_order),来找到可用的内存块 */
	for (current_order = order; current_order < MAX_ORDER; ++current_order) {
		area = &(zone->free_area[current_order]);
		page = list_first_entry_or_null(&area->free_list[migratetype],
							struct page, lru);
		if (!page)
			continue;
        /* 在当前阶中找到了一个内存块,则将此page从free area中删除 */
		list_del(&page->lru);
        /* rmv_page_order调用
        __ClearPageBuddy(struct page *page){atomic_set(&page->mapcount, -1)}和
        set_page_private(page, 0){page->private = 0}
        __ClearPageBuddy表示该页已经不在伙伴系统管理,set_page_private将struct page的private成员设置为0*/
		rmv_page_order(page);
		area->nr_free--;
		expand(zone, page, order, current_order, area, migratetype);
        /*set_pcppage_migratetype(page, migratetype){page->index = migratetype;},给page的index赋值上迁移类型*/
		set_pcppage_migratetype(page, migratetype);
		return page;
	}

	return NULL;
}

    如果需要分配的内存块长度小于所选择的连续页范围,即如果因为没有更小的适当内存块可用,而从较高的分配阶分配了一块内存,那么该内存块必须按照伙伴系统的原理分裂成小的块。这是通过expand函数完成的。
 

伙伴系统分裂函数expand():

static inline void expand(struct zone *zone, struct page *page,
	int low, int high, struct free_area *area,
	int migratetype)
{
	unsigned long size = 1 << high;

	while (high > low) {
		area--;
		high--;
		size >>= 1;
        。。。
		list_add(&page[size].lru, &area->free_list[migratetype]);
		area->nr_free++;
		set_page_order(&page[size], high);
	}
}

static inline void set_page_order(struct page *page, unsigned int order)
{
	set_page_private(page, order);
	__SetPageBuddy(page);
}
#define set_page_private(page, v)	((page)->private = (v))
void __SetPageBuddy(struct page *page)
{
    atomic_set(&page->_mapcount, -1);
}

  我们现在举个例子:我先现在要分配一个3阶的内存,但是__rmqueue_smallest在遍历到5阶时才发现5阶里有空闲的内存块。

  expand(page,index=0,low=3,high=5,area)

    这里做一些解释,这个page具体指的是什么?它指向每个内存块的首页地址,比如5阶的free area,这个队列里每一个内存块内部地址是连续的,但是内存块之间的地址是不连续的,当我们从对队列里获得一个内存块,则page返回这个内存块的首页。

    继续上面的expand,5阶分裂3阶过程:

1、size = 32, 5>3,进入循环,第一步先将5阶分配为4阶。此时将area--指向了4阶队列的头,注意此时size=16.

2、将page[16]开始的页放入了4阶队列里,从这里我们可以知道什么,第一,整个函数的page始终指向的是从5阶队里取出的内存块的首页;第二,这里把后16页放入了4阶队列里;第三,伙伴系统管理的内存块只需要指向内存块的首页地址即可,这里可根据“阶”自动推导出内存块,并且这个内存块里的页一定是连续的。这个page[size].lru的lru是各个数据结构用来管理该page的一个字段。

3、将4阶的nr_free++,并且调用set_page_order,设置page的属性。

    循环中各个步骤都调用了set_page_order辅助函数,对于回收到伙伴系统的内存区,该函数将第一个struct page实例的private标志设置为当前分配阶,并设置页的PG_buddy标志位。该标志表示内存块由伙伴系统管理。
 

    如果在特定的迁移类型列表上没有连续内存区可用,则__rmqueue_smallest返回NULL指针。内核接下来根据备用次序,尝试使用其他迁移类型的列表满足分配请求。该任务委托给__rmqueue_fallback。这里不做深入研究了。

    在分配单页帧内存时是从per-CPU队列中分配的并不是从伙伴系统中,http://www.voidcn.com/article/p-cchcjxeq-bcp.html这个博客写的非常好。

-------------------------每CPU告诉缓存-----------------------------

     这里再重新详细的说下每cpu高速缓存吧。

     为了提升系统性能,内核在申请和释放单个页框时,每个内存区域定义了一个“每CPU”页框高速缓存,这些每CPU高速缓存包含了一些预先分配的页框,它们被用于满足本地CPU发出的单一内存请求。注意这里是每个内存区域都对每个CPU都分别有一个高速缓存。

    每CPU高速缓存包含一个热高速缓存和一个冷高速缓存。如果内核或者用户态进程刚好分配到页框就立即向页框写,那么从热高速缓存获得页框这样跟有利,因为刚分配的页框会驻留在硬件告诉缓存中(注意meiCPU告诉缓存是软件上用链表实现的记录页框的一个队列结构,而硬件告诉缓存是实实在在的与CPU通信的物理结构)。反过来如果页框将要被DMA操作填充,那么从冷高速缓存中会的页框是方便的,因为这种情况下不会涉及到CPU,不会通过硬件高速缓存,而是直接向页框内写入数据,所以从冷告诉缓存中获取对系统更有利。这里热和冷在每CPU高速缓存队列中仅仅是顺序的关系,热的在队列头、冷的在队列尾哈哈。

    1、每CPU高速缓存涉及到的数据结构

zone结构体中pageset成员指向内存域per-CPU管理结构,NR_CPUS定义系统cpu个数

struct zone {
    ...
	struct per_cpu_pageset __percpu *pageset;
    ...
}

struct per_cpu_pageset {
	struct per_cpu_pages pcp;
#ifdef CONFIG_NUMA
	s8 expire;
	u16 vm_numa_stat_diff[NR_VM_NUMA_STAT_ITEMS];
#endif
#ifdef CONFIG_SMP
	s8 stat_threshold;
	s8 vm_stat_diff[NR_VM_ZONE_STAT_ITEMS];
#endif
};

struct per_cpu_pages {
	int count;		/* number of pages in the list */
	int high;		/* high watermark, emptying needed */
	int batch;		/* chunk size for buddy add/remove */

	/* Lists of pages, one per migrate type stored on the pcp-lists */
	struct list_head lists[MIGRATE_PCPTYPES];
};
count:每CPU高速缓冲中页框的数目
high:per_cpu缓存中页帧的上限,如果超过这个值就将释放 batch个页帧到伙伴系统中去
batch:如果per_cpu中没有可分配的页帧就从伙伴系统中分配batch个页帧到缓存中来

2、per-CPU初始化

初始化看这个文章https://blog.csdn.net/oqqYuJi12345678/article/details/100526720

    初始化仅仅是对每CPU高速缓存队列初始化为空,并对count、high、batch字段进行赋值,并没有给其预先分配页帧,我个人认为预分配的页帧是在伙伴系统第一次调用__rmqueue_pcplist时直接从伙伴系统中拿出batch个单页帧放入了每CPU高速缓存队里当中。

3、伙伴系统中的单页帧分配

struct page *rmqueue(struct zone *preferred_zone,
			struct zone *zone, unsigned int order,
			gfp_t gfp_flags, unsigned int alloc_flags,
			int migratetype)
{
	...

	if (likely(order == 0)) {
		page = rmqueue_pcplist(preferred_zone, zone, order,
				gfp_flags, migratetype);
		goto out;
	}

    ...
}
static struct page *rmqueue_pcplist(struct zone *preferred_zone,
			struct zone *zone, unsigned int order,
			gfp_t gfp_flags, int migratetype)
{
	struct per_cpu_pages *pcp;
	struct list_head *list;
	struct page *page;
	unsigned long flags;

	local_irq_save(flags);
	pcp = &this_cpu_ptr(zone->pageset)->pcp;//当前cpu的告诉缓存队列
	list = &pcp->lists[migratetype];//取出对应迁移类型的缓冲队列
	page = __rmqueue_pcplist(zone,  migratetype, pcp, list);
	if (page) {
		__count_zid_vm_events(PGALLOC, page_zonenum(page), 1 << order);
		zone_statistics(preferred_zone, zone);
	}
	local_irq_restore(flags);
	return page;
}
static struct page *__rmqueue_pcplist(struct zone *zone, int migratetype,
			struct per_cpu_pages *pcp,
			struct list_head *list)
{
	struct page *page;

	do {
		if (list_empty(list)) {//如果队列是空,则从伙伴系统中取页帧
			pcp->count += rmqueue_bulk(zone, 0,
					pcp->batch, list,
					migratetype);
			if (unlikely(list_empty(list)))
				return NULL;
		}

		page = list_first_entry(list, struct page, lru);//将队列的第一个元素取出来,返回这个page,单页帧分配完毕
		list_del(&page->lru);
		pcp->count--;
	} while (check_new_pcp(page));

	return page;
}
static int rmqueue_bulk(struct zone *zone, unsigned int order,
			unsigned long count, struct list_head *list,
			int migratetype)
{
	int i, alloced = 0;

	spin_lock(&zone->lock);
	for (i = 0; i < count; ++i) {
		struct page *page = __rmqueue(zone, order, migratetype);
		if (unlikely(page == NULL))
			break;

		if (unlikely(check_pcp_refill(page)))
			continue;

		/*
		 * Split buddy pages returned by expand() are received here in
		 * physical page order. The page is added to the tail of
		 * caller's list. From the callers perspective, the linked list
		 * is ordered by page number under some conditions. This is
		 * useful for IO devices that can forward direction from the
		 * head, thus also in the physical page order. This is useful
		 * for IO devices that can merge IO requests if the physical
		 * pages are ordered properly.
		 */
		list_add_tail(&page->lru, list);
		alloced++;
		if (is_migrate_cma(get_pcppage_migratetype(page)))
			__mod_zone_page_state(zone, NR_FREE_CMA_PAGES,
					      -(1 << order));
	}

	/*
	 * i pages were removed from the buddy list even if some leak due
	 * to check_pcp_refill failing so adjust NR_FREE_PAGES based
	 * on i. Do not confuse with 'alloced' which is the number of
	 * pages added to the pcp list.
	 */
	__mod_zone_page_state(zone, NR_FREE_PAGES, -(i << order));
	spin_unlock(&zone->lock);
	return alloced;
}

 

六、释放页

    释放相对比较简单,总的来说如果order=0直接放在的per-CPU中,其他的返回到伙伴系统里,这里介绍放回到伙伴系统

void __free_pages(struct page *page, unsigned int order)
{
	if (put_page_testzero(page)) {
		if (order == 0)
			free_unref_page(page);//放到per-CPU
		else
			__free_pages_ok(page, order);//放到伙伴系统
	}
}

__free_pages_ok最终调用__free_one_page函数
page:释放内存块的第一个page地址
pfn:内存块第一个page所对应页帧号
zone:该内存块所在的内存域
order:阶
migratetype:伙伴系统的migrate类型

static inline void __free_one_page(struct page *page,
		unsigned long pfn,
		struct zone *zone, unsigned int order,
		int migratetype)
{
	...
	while (order < max_order - 1) {
		buddy_pfn = __find_buddy_pfn(pfn, order);//找出伙伴的帧号
		buddy = page + (buddy_pfn - pfn);//找出伙伴的page地址

		if (!page_is_buddy(page, buddy, order))
			goto done_merging;
		/*
		 * Our buddy is free or it is CONFIG_DEBUG_PAGEALLOC guard page,
		 * merge with it and move up one order.
		 */
        /* 将本内存块从当前阶的伙伴队列里删除 */
		list_del(&buddy->lru);
	    zone->free_area[order].nr_free--;
		rmv_page_order(buddy);
		/* 找到若伙伴两者合并后的首帧号 */
		combined_pfn = buddy_pfn & pfn;
		page = page + (combined_pfn - pfn);
		pfn = combined_pfn;
        /* 继续向上一阶循环查找是否能继续合并 */
		order++;
	}
	...
}

/*
 * Locate the struct page for both the matching buddy in our
 * pair (buddy1) and the combined O(n+1) page they form (page).
 *
 * 1) Any buddy B1 will have an order O twin B2 which 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 parent P which
 * satisfies the following equation:
 *     P = B & ~(1 << O)
 *
 * Assumption: *_mem_map is contiguous at least up to MAX_ORDER
 */
static inline unsigned long
__find_buddy_pfn(unsigned long page_pfn, unsigned int order)
{
	return page_pfn ^ (1 << order);
}

 

    内存块回收到伙伴系统是从当前阶里查找该块内存的伙伴buddy是否在伙伴系统中,如果存在则将伙伴从当前阶删除,将二者合并到上一阶,接下来从上一阶再向上循环查找处理。一定要知道的是根据伙伴系统的分裂expand函数,两个伙伴的page地址(上文介绍过page是在alloc_node_mem_map里申请的)和帧号一定是连续的。

    __find_buddy_pfn函数找当前内存块的伙伴,返回伙伴的帧号。函数注释里已经说明的算法:

    伙伴buddy_pfn = buddy ^(1<<order)

    合并后的父伙伴的首ptn = buddy & (1<<order)

    父伙伴的首帧就是两个伙伴都在伙伴系统中,二者会合并成一个内存块,并上移到上一阶伙伴系统中,合并之后的首帧号。以上这两种算法是巧妙的算法,肯定是经过推导之后的最终结果。例如:

    释放的内存首帧=8 order=3,则:

        它的伙伴的首帧=8 ^(1<<3)=0,这里看出[0,7]和[8-15]是伙伴,这里要明确,伙伴之间的地址或者帧号一定是连续的。(这里说明一下,在伙伴系统分裂expand时,一定是将右侧的高帧号的内存块放入到低阶伙伴系统中,但是内存块回收时可不一定,也许高帧号的内存先回收进来,而此时低帧号的伙伴仍在使用中)

        假如伙伴合并之后的首帧pfn = 8 &~(1<<3)或者pfn=B1^B2=8^0,这两个结果是有一样的,数学水平有限没有推导出过程.

 

伙伴系统需要注意的一点:伙伴系统旨在提供一块大的物理地址连续的内存块,但是伙伴系统所管理的各个大的内存块之间是不一定连续的,比如,3阶内存块A和B,A和B的内部物理地址是连续的,但是A和B之间不一定连续。

        

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值