linux伙伴系统(三):避免碎片(中)

3.5.2 

(1) 数据结构

尽管内核使用的烦碎片技术卓有成效,它对伙伴分配器的代码和数据结构几乎没有影响。内核定义了一些宏来表示不同的迁移类型(在3.18.3中已经是枚举值):

include/linux/mmzone.h

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
};


类型MIGRATE_UNMOVABLEMIGRATE_RECLAIMABLEMIGRATE_MOVABLE分别为前面介绍的不可移动页,可回收页和可移动页。如果向具有特定可移动性的列表请求分配内存失败,这种紧急情况下可从MIGRATE_RESERVE中分配内存。MIGRATE_ISOLATE是一个特殊的虚拟区域,用于跨越NUMA结点移动物理内存页。在大型系统上,它有益于将物理内存页移动到接近于使用该页最频繁的CPUMIGRATE_TYPES只是表示迁移页类型的数目,页不代表具体的区域。

对伙伴系统数据结构的主要调整,是将空闲列表分解为MIGRATE_TYPE该链表。

include/linux/mmzone.h

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


Nr_free统计了所有链表上空闲页的数目,而每种迁移类型都对应于一个空闲链表。宏for_each_migratetype_order(order, type)可用于迭代指定迁移类型的所有分配阶。

如果内核无法满足针对某一给定迁移类型的分配请求,会怎么样?此前已经出现过一个类似的问题,即特定的NUMA内存域无法满足分配请求时。内核在这种情况下的做法是类似的,提供了一个备用链表,规定了在指定链表中无法满足分配请求时,接下来应使用哪一种迁移类型:

/* 该数组描述了指定迁移类型的空闲列表耗尽时,其他空闲列表在备用列表中的次序 */
static int fallbacks[MIGRATE_TYPES][4] = {
  [MIGRATE_UNMOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE, MIGRATE_RESERVE },
  [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE, MIGRATE_RESERVE },
#ifdef CONFIG_CMA
  [MIGRATE_MOVABLE] = { MIGRATE_CMA, MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_RESERVE },
  [MIGRATE_CMA] = { MIGRATE_RESERVE }, /* Never used */
#else
  [MIGRATE_MOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_RESERVE },
#endif
  [MIGRATE_RESERVE] = { MIGRATE_RESERVE }, /* Never used */
#ifdef CONFIG_MEMORY_ISOLATION
  [MIGRATE_ISOLATE] = { MIGRATE_RESERVE }, /* Never used */
#endif
};


该数据结构大体上是自明的:在内核想要分配不可移动页时,如果对应链表为空,则后退到可回收页链表,接下来是可移动页链表,最后到紧急分配链表。

(2) 全局变量和辅助函数

尽管页可移动性分组特性总是编译到内核中,但只有在系统中有足够内存可以分配到多个迁移类型对应的链表时,才是有意义的。由于每个迁移链表都应该有适当数量的内存,内核需要定义“适当”的概念。这是通过两个全局变量pageblock_orderpageblock_nr_pages提供的。第一个表示内核认为是“大”的一个分配阶,pageblock_nr_pages则表示该分配阶对应的页数。如果体系结构提供了巨型页机制,则pageblock_order通常定义为巨型页对应的分配阶:

include/linux/pageblock-flags.h 

#ifdef CONFIG_HUGETLB_PAGE
 
#ifdef CONFIG_HUGETLB_PAGE_SIZE_VARIABLE
 
/* Huge page sizes are variable */
extern int pageblock_order;
 
#else /* CONFIG_HUGETLB_PAGE_SIZE_VARIABLE */
 
/* Huge pages are a constant size */
#define pageblock_order         HUGETLB_PAGE_ORDER
 
#endif /* CONFIG_HUGETLB_PAGE_SIZE_VARIABLE */
 
#else /* CONFIG_HUGETLB_PAGE */
 
/* If huge pages are not used, group by MAX_ORDER_NR_PAGES */
#define pageblock_order         (MAX_ORDER-1)
 
#endif /* CONFIG_HUGETLB_PAGE */
 
#define pageblock_nr_pages      (1UL << pageblock_order)


IA-32体系结构上,巨型页长度是4MB,因此每个巨型页由1024个普通页组成,而HUGETLB_PAGE_ORDER则定义为10。相比之下,IA-64体系结构允许设置可变的普通和巨型页长度,因此HUGETLB_PAGE_ORDER的值取决于内核配置。

如果体系结构不支持巨型页,则将其定义为第二高的分配阶:

#define pageblock_order         (MAX_ORDER-1)

如果各迁移类型的链表中没有一块较大的连续内存,那么页面迁移不会提供任何好处,因此在可用内存太少时内核会关闭该特性。这是在build_all_zonelists函数中检查的,该函数用于初始化内存域列表。如果没有足够的内存可用,则全局变量page_group_by_mobility设置为0,否则设置为1.

内核如何知道给定的分配内存属于何种迁移类型?有关各个内存分配的细节都通过分配掩码指定(__GFP_RECLAIMABLE)。如果这些标志都没有设置,则分配的内存假定为不可移动的。下列辅助函数可用于转换分配标志及对应的迁移类型:

Linux-3.18.3/include/linux/gfp.h

/* Convert GFP flags to their corresponding migrate type */
static inline int gfpflags_to_migratetype(const gfp_t gfp_flags)
{
        WARN_ON((gfp_flags & GFP_MOVABLE_MASK) == GFP_MOVABLE_MASK);
 
        if (unlikely(page_group_by_mobility_disabled))
                return MIGRATE_UNMOVABLE;
 
        /* Group based on mobility */
        return (((gfp_flags & __GFP_MOVABLE) != 0) << 1) |
                ((gfp_flags & __GFP_RECLAIMABLE) != 0);
}


2.6.25中为如下接口:

/* Convert GFP flags to their corresponding migrate type */
static inline int allocflags_to_migratetype(gfp_t gfp_flags)
{
        WARN_ON((gfp_flags & GFP_MOVABLE_MASK) == GFP_MOVABLE_MASK);
 
        if (unlikely(page_group_by_mobility_disabled))
                return MIGRATE_UNMOVABLE;
 
        /* Group based on mobility */
        return (((gfp_flags & __GFP_MOVABLE) != 0) << 1) |
                ((gfp_flags & __GFP_RECLAIMABLE) != 0);
}


如果停用了页面迁移特性,则所有的页都是不可移动的。否则,该函数的返回值可以直接用作free_area.free_list的数组索引。

最后要注意,每个内存域都提供了一个特殊的字段,可以跟踪包含pageblock_nr_pages个页的内存区的属性。由于该字段当前只有与页可移动性相关的代码使用,所以前面没有介绍该特性:

Include/linux/mmzone.h

struct zone {
......
        /*
         * Flags for a pageblock_nr_pages block. See pageblock-flags.h.
         * In SPARSEMEM, this map is stored in struct mem_section
         */
        unsigned long           *pageblock_flags;
......
}


在初始化期间,内核自动确保对内存域中的每个不同的迁移类型分组,在pageblock_flags中都分配了足够存储NR_PAGEBLOCK_BITS个比特位的空间。当前,表示一个连续内存区的迁移类型需要3个比特位:

//pageblock-flags.h

/* Macro to aid the definition of ranges of bits */
#define PB_range(name, required_bits) \
        name, name ## _end = (name + required_bits) - 1
 
/* Bit indices that affect a whole block of pages */
enum pageblock_bits {
        PB_range(PB_migrate, 3), /* 3 bits required for migrate types */
        NR_PAGEBLOCK_BITS
};
 
static void set_pageblock_migratetype(struct page *page, int migratetype)
{
        set_pageblock_flags_group(page, (unsigned long)migratetype,
                                        PB_migrate, PB_migrate_end);
}
 

set_pageblock_migratetype负责设置以page为首的一个内存区的迁移类型。

Migratetype参数可以通过上文介绍的gfpflags_to_migratetype辅助函数构建。请注意很重要的一点,页的迁移类型是预先分配好的,对应的比特位总是可用,与页是否由伙伴系统管理无关。在释放内存时,页必须返回到正确的迁移链表。这之所以可行,是因为能够从get_pageblock_migratetype获得所需的信息。

在各个迁移链表之间,当前的页面分配状态可以从/proc/pagetypeinfo获得:


(3)初始化基于可移动性的分组

在内存子系统初始化期间,memmap_init_zone负责处理内存域的page实例。该函数完成了一些不怎么有趣的标准初始化工作,但其中有一件是实质性的,即所有的页最初都标记为可移动的。

 

/*
 * Initially all pages are reserved - free ones are freed
 * up by free_all_bootmem() once the early boot process is
 * done. Non-atomic initialization, single-pass.
 */
void __meminit memmap_init_zone(unsigned long size, int nid, unsigned long zone,
                unsigned long start_pfn, enum memmap_context context)
{
        struct page *page;
        unsigned long end_pfn = start_pfn + size;
        unsigned long pfn;
        struct zone *z;
 
        z = &NODE_DATA(nid)->node_zones[zone];
        for (pfn = start_pfn; pfn < end_pfn; pfn++) {
                /*
                 * There can be holes in boot-time mem_map[]s
                 * handed to this function.  They do not
                 * exist on hotplugged memory.
                 */
                if (context == MEMMAP_EARLY) {
                        if (!early_pfn_valid(pfn))
                                continue;
                        if (!early_pfn_in_nid(pfn, nid))
                                continue;
                }
                page = pfn_to_page(pfn);
                set_page_links(page, zone, nid, pfn);
                init_page_count(page);
                reset_page_mapcount(page);
                SetPageReserved(page);
                /*
                 * Mark the block movable so that blocks are reserved for
                 * movable at startup. This will force kernel allocations
                 * to reserve their blocks rather than leaking throughout
                 * the address space during boot when many long-lived
                 * kernel allocations are made. Later some blocks near
                 * the start are marked MIGRATE_RESERVE by
                 * setup_zone_migrate_reserve()
                 *
                 * bitmap is created for zone's valid pfn range. but memmap
                 * can be created for invalid pages (for alignment)
                 * check here not to call set_pageblock_migratetype() against
                 * pfn out of zone.
                 */
                if ((z->zone_start_pfn <= pfn)
                    && (pfn < z->zone_start_pfn + z->spanned_pages)
                    && !(pfn & (pageblock_nr_pages - 1)))
                        set_pageblock_migratetype(page, MIGRATE_MOVABLE);
 
                INIT_LIST_HEAD(&page->lru);
#ifdef WANT_PAGE_VIRTUAL
                /* The shift won't overflow because ZONE_NORMAL is below 4G. */
                if (!is_highmem_idx(zone))
                        set_page_address(page, __va(pfn << PAGE_SHIFT));
#endif
        }
}

在分配内存时,如果必须“盗取”不同于预定迁移类型的内存区,内核在策略上更倾向于获取更大的内存区。由于所有页最初都是可迁移的,那么在内核分配不可迁移的内存区时,则必须“盗取”。

实际上,在启动期间分配可移动内存区的情况较少,那么分配器有很高几率分配长度最大的内存区,并将其从可移动链表换到不可移动链表。由于分配的内存区长度是最大的,因此不会向可移动内存中引入碎片。

总而言之,这种做法避免了启动期间内核分配的内存散布到物理内存各处,从而使其他类型的内存分配免受碎片的干扰,这也是可移动性分组框架的最重要的目标之一。

未完待续:

linux伙伴系统(二):避免碎片(下)

3.5.2

2. 虚拟可移动内存域

1)数据结构

2)实现


你可能感兴趣的:(linux伙伴系统(三):避免碎片(中))