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_UNMOVABLE、MIGRATE_RECLAIMABLE和MIGRATE_MOVABLE分别为前面介绍的不可移动页,可回收页和可移动页。如果向具有特定可移动性的列表请求分配内存失败,这种紧急情况下可从MIGRATE_RESERVE中分配内存。MIGRATE_ISOLATE是一个特殊的虚拟区域,用于跨越NUMA结点移动物理内存页。在大型系统上,它有益于将物理内存页移动到接近于使用该页最频繁的CPU。MIGRATE_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_order和pageblock_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)实现