Linux内存管理12(基于6.1内核)

Linux内存管理12(基于6.1内核)---伙伴系统概述

一、伙伴系统


内核初始化完成之后, 内存管理的责任就由伙伴系统来承担. 伙伴系统基于一种相对简单然而令人吃惊的强大算法。

Linux内核使用二进制伙伴算法来管理和分配物理内存页面, 该算法由Knowlton设计, 后来Knuth又进行了更深刻的描述。

伙伴系统是一个结合了2的方幂个分配器和空闲缓冲区合并计技术的内存分配方案, 其基本思想很简单. 内存被分成含有很多页面的大块, 每一块都是2个页面大小的方幂. 如果找不到想要的块, 一个大块会被分成两部分, 这两部分彼此就成为伙伴. 其中一半被用来分配, 而另一半则空闲. 这些块在以后分配的过程中会继续被二分直至产生一个所需大小的块. 当一个块被最终释放时, 其伙伴将被检测出来, 如果伙伴也空闲则合并两者。

  • 内核如何记住哪些内存块是空闲的。

  • 分配空闲页面的方法。

  • 影响分配器行为的众多标识位。

  • 内存碎片的问题和分配器如何处理碎片。

二、伙伴系统的结构


2.1 伙伴系统数据结构


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

include/linux/mmzone.h

struct zone
{
...

	/* free areas of different sizes */
	struct free_area	free_area[NR_PAGE_ORDERS];

...
};

struct free_area是一个伙伴系统的辅助数据结构,include/linux/mmzone.h

struct free_area {
	struct list_head	free_list[MIGRATE_TYPES];
	unsigned long		nr_free;
};
字段描述
free_list是用于连接空闲页的链表. 页链表包含大小相同的连续内存区。
nr_free指定了当前内存区中空闲页块的数目(对0阶内存区逐页计算,对1阶内存区计算页对的数目,对2阶内存区计算4页集合的数目,依次类推。

伙伴系统的分配器维护空闲页面所组成的块,这里每一块都是2的方幂个页面, 方幂的指数称为

阶是伙伴系统中一个非常重要的术语. 它描述了内存分配的数量单位. 内存块的长度是2order2order, 其中order的范围从0到MAX_ORDER

zone->free_area[MAX_ORDER]数组中阶作为各个元素的索引, 用于指定对应链表中的连续内存区包含多少个页帧。

  • 数组中第0个元素的阶为0, 它的free_list链表域指向具有包含区为单页(20=120=1)的内存页面链表。

  • 数组中第1个元素的free_list域管理的内存区为两页(21=221=2)。

  • 第3个管理的内存区为4页, 依次类推。

  • 直到2MAXORDER−12MAXORDER−1个页面大小的块。

2.2 最大阶MAX_ORDER与FORCE_MAX_ZONEORDER配置选项


一般来说MAX_ORDER默认定义为10, 这意味着一次分配可以请求的页数最大是2^10=1024

 include/linux/mmzone.h

/* Free memory management - zoned buddy allocator.  */
#ifndef CONFIG_ARCH_FORCE_MAX_ORDER
#define MAX_ORDER 10
#else
#define MAX_ORDER CONFIG_ARCH_FORCE_MAX_ORDER
#endif
#define MAX_ORDER_NR_PAGES (1 << MAX_ORDER)

#define IS_MAX_ORDER_ALIGNED(pfn) IS_ALIGNED(pfn, MAX_ORDER_NR_PAGES)

#define NR_PAGE_ORDERS (MAX_ORDER + 1)

2.3 内存区是如何连接的


内存区中第1页内的链表元素, 可用于将内存区维持在链表中。因此,也不必引入新的数据结构来管理物理上连续的页,否则这些页不可能在同一内存区中。如下图所示:

伙伴不必是彼此连接的。如果一个内存区在分配其间分解为两半, 内核会自动将未用的一半加入到对应的链表中。

如果在未来的某个时刻, 由于内存释放的缘故, 两个内存区都处于空闲状态, 可通过其地址判断其是否为伙伴. 管理工作较少, 是伙伴系统的一个主要优点。

基于伙伴系统的内存管理专注于某个结点的某个内存域, 例如, DMA或高端内存域. 但所有内存域和结点的伙伴系统都通过备用分配列表连接起来。

下图说明了这种关系:

最后要注意, 有关伙伴系统和当前状态的信息可以在/proc/buddyinfo中获取:

上述输出给出了各个内存域中每个分配阶中空闲项的数目, 从左至右, 阶依次升高。

三、传统伙伴系统算法


在内核分配内存时, 必须记录页帧的已分配或空闲状态, 以免两个进程使用同样的内存区域. 由于内存分配和释放非常频繁, 内核还必须保证相关操作尽快完成. 内核可以只分配完整的页帧. 将内存划分为更小的部分的工作, 则委托给用户空间中的标准库. 标准库将来源于内核的页帧拆分为小的区域, 并为进程分配内存。

内核中很多时候要求分配连续页. 为快速检测内存中的连续区域, 内核采用了一种古老而历经检验的技术: 伙伴系统

系统中的空闲内存块总是两两分组, 每组中的两个内存块称作伙伴. 伙伴的分配可以是彼此独立的. 但如果两个伙伴都是空闲的, 内核会将其合并为一个更大的内存块, 作为下一层次上某个内存块的伙伴。

下图示范了该系统, 图中给出了一对伙伴,初始大小均为8页。 即系统中所有的页面都是8页的。

内核对所有大小相同的伙伴(1、2、4、8、16或其他数目的页),都放置到同一个列表中管理. 各有8页的一对伙伴也在相应的列表中。

如果系统现在需要8个页帧, 则将16个页帧组成的块拆分为两个伙伴。 其中一块用于满足应用程序的请求, 而剩余的8个页帧则放置到对应8页大小内存块的列表中。

如果下一个请求只需要2个连续页帧, 则由8页组成的块会分裂成2个伙伴, 每个包含4个页帧. 其中一块放置回伙伴列表中,而另一个再次分裂成2个伙伴, 每个包含2页。其中一个回到伙伴系统,另一个则传递给应用程序。

在应用程序释放内存时, 内核可以直接检查地址, 来判断是否能够创建一组伙伴, 并合并为一个更大的内存块放回到伙伴列表中, 这刚好是内存块分裂的逆过程。这提高了较大内存块可用的可能性。

在系统长期运行时,服务器运行几个星期乃至几个月是很正常的,许多桌面系统也趋向于长期开机运行,那么会发生称为碎片的内存管理问题。频繁的分配和释放页帧可能导致一种情况:系统中有若干页帧是空闲的,但却散布在物理地址空间的各处。换句话说,系统中缺乏连续页帧组成的较大的内存块,而从性能上考虑,却又很需要使用较大的连续内存块。通过伙伴系统可以在某种程度上减少这种效应,但无法完全消除。如果在大块的连续内存中间刚好有一个页帧分配出去,很显然这两块空闲的内存是无法合并的。

四、避免碎片


4.1 内存碎片


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

假定内存由60页组成,这显然不是超级计算机,但用于示例却足够。左侧的地址空间中散布着空闲页。尽管大约25%的物理内存仍然未分配,但最大的连续空闲区只有一页。这对用户空间应用程序没有问题:其内存是通过页表映射的,无论空闲页在物理内存中的分布如何,应用程序看到的内存
似乎总是连续的。右图给出的情形中,空闲页和使用页的数目与左图相同,但所有空闲页都位于一个连续区中。

但对内核来说,碎片是一个问题。由于(大多数)物理内存一致映射到地址空间的内核部分, 那么在左图的场景中, 无法映射比一页更大的内存区. 尽管许多时候内核都分配的是比较小的内存, 但也有时候需要分配多于一页的内存。显而易见, 在分配较大内存的情况下, 右图中所有已分配页和空闲页都处于连续内存区的情形,是更为可取的。

很有趣的一点是, 在大部分内存仍然未分配时, 就也可能发生碎片问题. 下图所示:

只分配了4页,但可分配的最大连续区只有8页,因为伙伴系统所能工作的分配范围只能是2的幂次。

内存碎片只涉及内核,这只是部分正确的。大多数现代CPU都提供了使用巨型页的可能性,比普通页大得多。这对内存使用密集的应用程序有好处。在使用更大的页时,地址转换后备缓冲器只需处理较少的项,降低了TLB缓存失效的可能性。但分配巨型页需要连续的空闲物理内存!

很长时间以来,物理内存的碎片确实是Linux的弱点之一。尽管已经提出了许多方法,但没有哪个方法能够既满足Linux需要处理的各种类型工作负荷提出的苛刻需求,同时又对其他事务影响不大。

4.2 依据可移动性组织页


文件系统也有碎片,该领域的碎片问题主要通过碎片合并工具解决。它们分析文件系统,重新排序已分配存储块,从而建立较大的连续存储区。理论上,该方法对物理内存也是可能的,但由于许多物理内存页不能移动到任意位置,阻碍了该方法的实施。因此,内核的方法是反碎片(anti-fragmentation), 即试图从最初开始尽可能防止碎片
反碎片的工作原理如何?

为理解该方法,我们必须知道内核将已分配页划分为下面3种不同类型:

页面类型描述举例
不可移动页在内存中有固定位置, 不能移动到其他地方.核心内核分配的大多数内存属于该类别
可移动页可以随意地移动.属于用户空间应用程序的页属于该类别. 它们是通过页表映射的
如果它们复制到新位置,页表项可以相应地更新,应用程序不会注意到任何事
可回收页不能直接移动, 但可以删除, 其内容可以从某些源重新生成.例如,映射自文件的数据属于该类别
kswapd守护进程会根据可回收页访问的频繁程度,周期性释放此类内存. , 页面回收本身就是一个复杂的过程. 内核会在可回收页占据了太多内存时进行回收, 在内存短缺(即分配失败)时也可以发起页面回收.

页的可移动性,依赖该页属于3种类别的哪一种. 内核使用的反碎片技术, 即基于将具有相同可移动性的页分组的思想。
为什么这种方法有助于减少碎片?

由于页无法移动, 导致在原本几乎全空的内存区中无法进行连续分配. 根据页的可移动性, 将其分配到不同的列表中, 即可防止这种情形. 例如, 不可移动的页不能位于可移动内存区的中间, 否则就无法从该内存区分配较大的连续内存块。

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

但要注意, 从最初开始, 内存并未划分为可移动性不同的区。这些是在运行时形成的。内核的另一种方法确实将内存分区, 分别用于可移动页和不可移动页的分配, 我会下文讨论其工作原理。但这种划分对这里描述的方法是不必要的。

4.3 避免碎片数据结构


1 迁移类型


尽管内核使用的反碎片技术卓有成效,它对伙伴分配器的代码和数据结构几乎没有影响。内核定义了一些枚举常量(早期用宏来实现)来表示不同的迁移类型, 参见:include/linux/mmzone.h

enum {
        MIGRATE_UNMOVABLE,
        MIGRATE_MOVABLE,
        MIGRATE_RECLAIMABLE,
        MIGRATE_PCPTYPES,       /* the number of types on the pcp lists */
        MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES,
#ifdef CONFIG_CMA
        /*
         * MIGRATE_CMA migration type is designed to mimic the way
         * ZONE_MOVABLE works.  Only movable pages can be allocated
         * from MIGRATE_CMA pageblocks and page allocator never
         * implicitly change migration type of MIGRATE_CMA pageblock.
         *
         * The way to use it is to change migratetype of a range of
         * pageblocks to MIGRATE_CMA which can be done by
         * __free_pageblock_cma() function.  What is important though
         * is that a range of pageblocks must be aligned to
         * MAX_ORDER_NR_PAGES should biggest page be bigger then
         * a single pageblock.
         */
        MIGRATE_CMA,
#endif
#ifdef CONFIG_MEMORY_ISOLATION
        MIGRATE_ISOLATE,        /* can't allocate from here */
#endif
        MIGRATE_TYPES
};

类型
MIGRATE_UNMOVABLE不可移动页
MIGRATE_MOVABLE可移动页
MIGRATE_RECLAIMABLE可回收页
MIGRATE_PCPTYPES是per_cpu_pageset, 即用来表示每CPU页框高速缓存的数据结构中的链表的迁移类型数目
MIGRATE_HIGHATOMIC= MIGRATE_PCPTYPES, 在罕见的情况下,内核需要分配一个高阶的页面块而不能休眠.如果向具有特定可移动性的列表请求分配内存失败,这种紧急情况下可从MIGRATE_HIGHATOMIC中分配内存
MIGRATE_CMALinux内核最新的连续内存分配器(CMA), 用于避免预留大块内存
MIGRATE_ISOLATE是一个特殊的虚拟区域, 用于跨越NUMA结点移动物理内存页. 在大型系统上, 它有益于将物理内存页移动到接近于使用该页最频繁的CPU.
MIGRATE_TYPES只是表示迁移类型的数目, 也不代表具体的区域


对于MIGRATE_CMA类型, 其中在使用ARM等嵌入式Linux系统的时候, 一个头疼的问题是GPU, Camera, HDMI等都需要预留大量连续内存,这部分内存平时不用,但是一般的做法又必须先预留着. 目前, Marek Szyprowski和Michal Nazarewicz实现了一套全新的Contiguous Memory Allocator. 通过这套机制, 我们可以做到不预留内存,这些内存平时是可用的,只有当需要的时候才被分配给Camera,HDMI等设备. 参照:连续内存分配器CMA。内核为此提供了函数is_migrate_cma来检测当前类型是否为MIGRATE_CMA, 该函数定义:include/linux/mmzone.h

/* In mm/page_alloc.c; keep in sync also with show_migration_types() there */
extern char * const migratetype_names[MIGRATE_TYPES];

#ifdef CONFIG_CMA
#  define is_migrate_cma(migratetype) unlikely((migratetype) == MIGRATE_CMA)
#  define is_migrate_cma_page(_page) (get_pageblock_migratetype(_page) == MIGRATE_CMA)
#else
#  define is_migrate_cma(migratetype) false
#  define is_migrate_cma_page(_page) false
#endif

对伙伴系统数据结构的主要调整, 是将空闲列表分解为MIGRATE_TYPE个列表, 可以参见free_area的定义: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)可用于迭代指定迁移类型的所有分配阶。include/linux/mmzone.h

#define for_each_migratetype_order(order, type) \
        for (order = 0; order < MAX_ORDER; order++) \
                for (type = 0; type < MIGRATE_TYPES; type++)

2 迁移备用列表fallbacks

如果内核无法满足针对某一给定迁移类型的分配请求, 会怎么样?

此前已经出现过一个类似的问题, 即特定的NUMA内存域无法满足分配请求时. 我们需要从其他内存域中选择一个代价最低的内存域完成内存的分配, 因此内核在内存的结点pg_data_t中提供了一个备用内存域列表zonelists.

内核在内存迁移的过程中处理这种情况下的做法是类似的. 提供了一个备用列表fallbacks, 规定了在指定列表中无法满足分配请求时. 接下来应使用哪一种迁移类型。mm/page_alloc.c

/*
 * This array describes the order lists are fallen back to when
 * the free lists for the desirable migrate type are depleted
 *
 * The other migratetypes do not have fallbacks.
 */
static int fallbacks[MIGRATE_TYPES][MIGRATE_PCPTYPES - 1] = {
	[MIGRATE_UNMOVABLE]   = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE   },
	[MIGRATE_MOVABLE]     = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE },
	[MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE,   MIGRATE_MOVABLE   },
};

 该数据结构大体上是自明的:每一行对应一个类型的备用搜索域的顺序, 在内核想要分配不可移动页MIGRATE_UNMOVABLE时, 如果对应链表为空, 则遍历fallbacks[MIGRATE_UNMOVABLE], 首先后退到可回收页链表MIGRATE_RECLAIMABLE, 接下来到可移动页链表MIGRATE_MOVABLE, 最后到紧急分配链表MIGRATE_TYPES

3 pageblock_order变量


全局变量和辅助函数尽管页可移动性分组特性总是编译到内核中,但只有在系统中有足够内存可以分配到多个迁移类型对应的链表时,才是有意义的。由于每个迁移链表都应该有适当数量的内存,内核需要定义”适当”的概念. 这是通过两个全局变量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 unsigned int pageblock_order;

#else /* CONFIG_HUGETLB_PAGE_SIZE_VARIABLE */

/*
 * Huge pages are a constant size, but don't exceed the maximum allocation
 * granularity.
 */
#define pageblock_order		min_t(unsigned int, HUGETLB_PAGE_ORDER, MAX_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

#endif /* CONFIG_HUGETLB_PAGE */

#define pageblock_nr_pages	(1UL << pageblock_order)

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

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

/* If huge pages are not used, group by MAX_ORDER_NR_PAGES */
#define pageblock_order         (MAX_ORDER-1)

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

内核如何知道给定的分配内存属于何种迁移类型?

内核提供了两个标志,分别用于表示分配的内存是可移动的(__GFP_MOVABLE)或可回收的(__GFP_RECLAIMABLE)。

4 gfpflags_to_migratetype函数


如果这些标志都没有设置, 则分配的内存假定为不可移动的. 辅助函数gfp_migratetype可用于转换分配标志及对应的迁移类型, 该函数定义include/linux/gfp.h

static inline int gfp_migratetype(const gfp_t gfp_flags)
{
	VM_WARN_ON((gfp_flags & GFP_MOVABLE_MASK) == GFP_MOVABLE_MASK);
	BUILD_BUG_ON((1UL << GFP_MOVABLE_SHIFT) != ___GFP_MOVABLE);
	BUILD_BUG_ON((___GFP_MOVABLE >> GFP_MOVABLE_SHIFT) != MIGRATE_MOVABLE);
	BUILD_BUG_ON((___GFP_RECLAIMABLE >> GFP_MOVABLE_SHIFT) != MIGRATE_RECLAIMABLE);
	BUILD_BUG_ON(((___GFP_MOVABLE | ___GFP_RECLAIMABLE) >>
		      GFP_MOVABLE_SHIFT) != MIGRATE_HIGHATOMIC);

	if (unlikely(page_group_by_mobility_disabled))
		return MIGRATE_UNMOVABLE;

	/* Group based on mobility */
	return (__force unsigned long)(gfp_flags & GFP_MOVABLE_MASK) >> GFP_MOVABLE_SHIFT;
}

5 pageblock_flags变量与其函数接口


最后要注意, 每个内存域都提供了一个特殊的字段, 可以跟踪包含pageblock_nr_pages个页的内存区的属性. 即zone->pageblock_flags字段, 当前只有与页可移动性相关的代码使用, include/linux/mmzone.h

struct zone
{
#ifndef CONFIG_SPARSEMEM
    /*
     * 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;
#endif /* CONFIG_SPARSEMEM */
};

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

/* Bit indices that affect a whole block of pages */
enum pageblock_bits {
    PB_migrate,
    PB_migrate_end = PB_migrate + 3 - 1,
            /* 3 bits required for migrate types */
    PB_migrate_skip,/* If set the block is skipped by compaction */

    /*
     * Assume the bits will always align on a word. If this assumption
     * changes then get/set pageblock needs updating.
     */
    NR_PAGEBLOCK_BITS
};

内核提供set_pageblock_migratetype负责设置以page为首的一个内存区的迁移类型, 该函数定义mm/page_alloc.c

void set_pageblock_migratetype(struct page *page, int migratetype)
{
	if (unlikely(page_group_by_mobility_disabled &&
		     migratetype < MIGRATE_PCPTYPES))
		migratetype = MIGRATE_UNMOVABLE;

	set_pfnblock_flags_mask(page, (unsigned long)migratetype,
				page_to_pfn(page), MIGRATETYPE_MASK);
}

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

#define get_pageblock_migratetype(page)					\
	get_pfnblock_flags_mask(page, page_to_pfn(page), MIGRATETYPE_MASK)

最后请注意, 在各个迁移链表之间, 当前的页面分配状态可以从/proc/pagetypeinfo获得.

五、初始化基于可移动性的分组


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

/*
 * Initially all pages are reserved - free ones are freed
 * up by memblock_free_all() once the early boot process is
 * done. Non-atomic initialization, single-pass.
 *
 * All aligned pageblocks are initialized to the specified migratetype
 * (usually MIGRATE_MOVABLE). Besides setting the migratetype, no related
 * zone stats (e.g., nr_isolate_pageblock) are touched.
 */
void __meminit memmap_init_range(unsigned long size, int nid, unsigned long zone,
		unsigned long start_pfn, unsigned long zone_end_pfn,
		enum meminit_context context,
		struct vmem_altmap *altmap, int migratetype)
{
	unsigned long pfn, end_pfn = start_pfn + size;
	struct page *page;

	if (highest_memmap_pfn < end_pfn - 1)
		highest_memmap_pfn = end_pfn - 1;

#ifdef CONFIG_ZONE_DEVICE
	/*
	 * Honor reservation requested by the driver for this ZONE_DEVICE
	 * memory. We limit the total number of pages to initialize to just
	 * those that might contain the memory mapping. We will defer the
	 * ZONE_DEVICE page initialization until after we have released
	 * the hotplug lock.
	 */
	if (zone == ZONE_DEVICE) {
		if (!altmap)
			return;

		if (start_pfn == altmap->base_pfn)
			start_pfn += altmap->reserve;
		end_pfn = altmap->base_pfn + vmem_altmap_offset(altmap);
	}
#endif

	for (pfn = start_pfn; pfn < end_pfn; ) {
		/*
		 * There can be holes in boot-time mem_map[]s handed to this
		 * function.  They do not exist on hotplugged memory.
		 */
		if (context == MEMINIT_EARLY) {
			if (overlap_memmap_init(zone, &pfn))
				continue;
			if (defer_init(nid, pfn, zone_end_pfn)) {
				deferred_struct_pages = true;
				break;
			}
		}

		page = pfn_to_page(pfn);
		__init_single_page(page, pfn, zone, nid);
		if (context == MEMINIT_HOTPLUG)
			__SetPageReserved(page);

		/*
		 * Usually, we want to mark the pageblock MIGRATE_MOVABLE,
		 * such that unmovable allocations won't be scattered all
		 * over the place during system boot.
		 */
		if (pageblock_aligned(pfn)) {
			set_pageblock_migratetype(page, migratetype);
			cond_resched();
		}
		pfn++;
	}
}

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

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

总而言之, 这种做法避免了启动期间内核分配的内存(经常在系统的整个运行时间都不释放)散布到物理内存各处, 从而使其他类型的内存分配免受碎片的干扰,这也是页可移动性分组框架的最重要的目标之一。

六、分配器API


6.1 分配内存的接口


就伙伴系统的接口而言, NUMA或UMA体系结构是没有差别的, 二者的调用语法都是相同的.

所有函数的一个共同点是 : 只能分配2的整数幂个页.

因此,接口中不像C标准库的malloc函数或bootmem和memblock分配器那样指定了所需内存大小作为参数. 相反, 必须指定的是分配阶, 伙伴系统将在内存中分配2order2order页. 内核中细粒度的分配只能借助于slab分配器(或者slub、slob分配器), 后者基于伙伴系统。

内存分配函数

功能

alloc_pages(mask, order)

分配2order2order页并返回一个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的页.


在空闲内存无法满足请求以至于分配失败的情况下,所有上述函数都返回空指针(比如alloc_pages和alloc_page)或者0(比如get_zeroed_page、__get_free_pages和__get_free_page).

因此内核在各次分配之后都必须检查返回的结果. 这种惯例与设计得很好的用户层应用程序没什么不同, 但在内核中忽略检查会导致严重得多的故障

内核除了伙伴系统函数之外, 还提供了其他内存管理函数. 它们以伙伴系统为基础, 但并不属于伙伴分配器自身. 这些函数包括vmalloc和vmalloc_32, 使用页表将不连续的内存映射到内核地址空间中, 使之看上去是连续的.

还有一组kmalloc类型的函数, 用于分配小于一整页的内存区. 其实现.

6.2 释放函数


有4个函数用于释放不再使用的页,与所述函数稍有不同。

内存释放函数

描述

free_page(struct page )
free_pages(struct page , order)

用于将一个或2order页返回给内存管理子系统。内存区的起始地址由指向该内存区的第一个page实例的指针表示

__free_page(addr)
__free_pages(addr, order)

类似于前两个函数,但在表示需要释放的内存区时,使用了虚拟内存地址而不是page实例

6.3 分配掩码(gfp_mask标志)


1 分配掩码


前述所有函数中强制使用的mask参数,到底是什么语义?

我们知道Linux将内存划分为内存域. 内核提供了所谓的内存域修饰符(zone modifier)(在掩码的最低4个比特位定义), 来指定从哪个内存域分配所需的页。

内核使用宏的方式定义了这些掩码, 一个掩码的定义被划分为3个部分进行定义, include/linux/gfp_types.h

/* Room for N __GFP_FOO bits */
#define __GFP_BITS_SHIFT (26 + IS_ENABLED(CONFIG_LOCKDEP))
#define __GFP_BITS_MASK ((__force gfp_t)((1 << __GFP_BITS_SHIFT) - 1))

2 掩码分类


Linux中这些掩码标志gfp_mask分为3种类型 :

类型描述
区描述都符内核把物理内存分为多个区, 每个区用于不同的目的, 区描述符指明到底从这些区中的哪一区进行分配
行为修饰符表示内核应该如何分配所需的内存. 在某些特定情况下, 只能使用某些特定的方法分配内存
类型标志组合了行为修饰符和区描述符, 将这些可能用到的组合归纳为不同类型

3 内核中掩码的定义


内核中的定义方式:include/linux/gfp_types.h


/* Plain integer GFP bitmasks. Do not use this directly. */
#define ___GFP_DMA		0x01u
#define ___GFP_HIGHMEM		0x02u
#define ___GFP_DMA32		0x04u
#define ___GFP_MOVABLE		0x08u
#define ___GFP_RECLAIMABLE	0x10u
#define ___GFP_HIGH		0x20u
#define ___GFP_IO		0x40u
#define ___GFP_FS		0x80u
#define ___GFP_ZERO		0x100u
/* 0x200u unused */
#define ___GFP_DIRECT_RECLAIM	0x400u
#define ___GFP_KSWAPD_RECLAIM	0x800u
#define ___GFP_WRITE		0x1000u
#define ___GFP_NOWARN		0x2000u
#define ___GFP_RETRY_MAYFAIL	0x4000u
#define ___GFP_NOFAIL		0x8000u
#define ___GFP_NORETRY		0x10000u
#define ___GFP_MEMALLOC		0x20000u
#define ___GFP_COMP		0x40000u
#define ___GFP_NOMEMALLOC	0x80000u
#define ___GFP_HARDWALL		0x100000u
#define ___GFP_THISNODE		0x200000u
#define ___GFP_ACCOUNT		0x400000u
#define ___GFP_ZEROTAGS		0x800000u
#ifdef CONFIG_KASAN_HW_TAGS
#define ___GFP_SKIP_ZERO	0x1000000u
#define ___GFP_SKIP_KASAN	0x2000000u
#else
#define ___GFP_SKIP_ZERO	0
#define ___GFP_SKIP_KASAN	0
#endif
#ifdef CONFIG_LOCKDEP
#define ___GFP_NOLOCKDEP	0x4000000u
#else
#define ___GFP_NOLOCKDEP	0
#endif
/* If the above are modified, __GFP_BITS_SHIFT may need updating */

/*
 * Physical address zone modifiers (see linux/mmzone.h - low four bits)
 *
 * Do not put any conditional on these. If necessary modify the definitions
 * without the underscores and use them consistently. The definitions here may
 * be used in bit comparisons.
 */
#define __GFP_DMA	((__force gfp_t)___GFP_DMA)
#define __GFP_HIGHMEM	((__force gfp_t)___GFP_HIGHMEM)
#define __GFP_DMA32	((__force gfp_t)___GFP_DMA32)
#define __GFP_MOVABLE	((__force gfp_t)___GFP_MOVABLE)  /* ZONE_MOVABLE allowed */
#define GFP_ZONEMASK	(__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE)

#define __GFP_RECLAIMABLE ((__force gfp_t)___GFP_RECLAIMABLE)
#define __GFP_WRITE	((__force gfp_t)___GFP_WRITE)
#define __GFP_HARDWALL   ((__force gfp_t)___GFP_HARDWALL)
#define __GFP_THISNODE	((__force gfp_t)___GFP_THISNODE)
#define __GFP_ACCOUNT	((__force gfp_t)___GFP_ACCOUNT)


#define __GFP_HIGH	((__force gfp_t)___GFP_HIGH)
#define __GFP_MEMALLOC	((__force gfp_t)___GFP_MEMALLOC)
#define __GFP_NOMEMALLOC ((__force gfp_t)___GFP_NOMEMALLOC)

#define __GFP_IO	((__force gfp_t)___GFP_IO)
#define __GFP_FS	((__force gfp_t)___GFP_FS)
#define __GFP_DIRECT_RECLAIM	((__force gfp_t)___GFP_DIRECT_RECLAIM) /* Caller can reclaim */
#define __GFP_KSWAPD_RECLAIM	((__force gfp_t)___GFP_KSWAPD_RECLAIM) /* kswapd can wake */
#define __GFP_RECLAIM ((__force gfp_t)(___GFP_DIRECT_RECLAIM|___GFP_KSWAPD_RECLAIM))
#define __GFP_RETRY_MAYFAIL	((__force gfp_t)___GFP_RETRY_MAYFAIL)
#define __GFP_NOFAIL	((__force gfp_t)___GFP_NOFAIL)
#define __GFP_NORETRY	((__force gfp_t)___GFP_NORETRY)


#define GFP_ATOMIC	(__GFP_HIGH|__GFP_KSWAPD_RECLAIM)
#define GFP_KERNEL	(__GFP_RECLAIM | __GFP_IO | __GFP_FS)
#define GFP_KERNEL_ACCOUNT (GFP_KERNEL | __GFP_ACCOUNT)
#define GFP_NOWAIT	(__GFP_KSWAPD_RECLAIM)
#define GFP_NOIO	(__GFP_RECLAIM)
#define GFP_NOFS	(__GFP_RECLAIM | __GFP_IO)
#define GFP_USER	(__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
#define GFP_DMA		__GFP_DMA
#define GFP_DMA32	__GFP_DMA32
#define GFP_HIGHUSER	(GFP_USER | __GFP_HIGHMEM)
#define GFP_HIGHUSER_MOVABLE	(GFP_HIGHUSER | __GFP_MOVABLE | __GFP_SKIP_KASAN)
#define GFP_TRANSHUGE_LIGHT	((GFP_HIGHUSER_MOVABLE | __GFP_COMP | \
			 __GFP_NOMEMALLOC | __GFP_NOWARN) & ~__GFP_RECLAIM)
#define GFP_TRANSHUGE	(GFP_TRANSHUGE_LIGHT | __GFP_DIRECT_RECLAIM)


其中在开始的位置定义了对应的区修饰符。

区修饰符标志描述
__GFP_DMA从ZONE_DMA中分配内存
__GFP_HIGHMEM从ZONE_HIGHMEM活ZONE_NORMAL中分配内存
__GFP_DMA32从ZONE_DMA32中分配内存
__GFP_MOVABLE从__GFP_MOVABLE中分配内存

其次还定义了我们程序和函数中所需要的掩码MASK的信息, 由于其中__GFP_DMA, __GFP_DMA32, __GFP_HIGHMEM, __GFP_MOVABLE是在内存中分别有对应的内存域信息, 因此我们定义了内存域的掩码GFP_ZONEMASK。

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

接着内核定义了行为修饰符

/* __GFP_WAIT表示分配内存的请求可以中断。也就是说,调度器在该请求期间可随意选择另一个过程执行,或者该请求可以被另一个更重要的事件中断. 分配器还可以在返回内存之前, 在队列上等待一个事件(相关进程会进入睡眠状态)。

行为修饰符描述
__GFP_RECLAIMABLE
__GFP_MOVABLE
是页迁移机制所需的标志. 顾名思义,它们分别将分配的内存标记为可回收的或可移动的。这影响从空闲列表的哪个子表获取内存
__GFP_WRITE
__GFP_HARDWALL只在NUMA系统上有意义. 它限制只在分配到当前进程的各个CPU所关联的结点分配内存。如果进程允许在所有CPU上运行(默认情况),该标志是无意义的。只有进程可以运行的CPU受限时,该标志才有效果
__GFP_THISNODE也只在NUMA系统上有意义。如果设置该比特位,则内存分配失败的情况下不允许使用其他结点作为备用,需要保证在当前结点或者明确指定的结点上成功分配内存
__GFP_ACCOUNT
__GFP_ATOMIC
__GFP_HIGH如果请求非常重要, 则设置__GFP_HIGH,即内核急切地需要内存时。在分配内存失败可能给内核带来严重后果时(比如威胁到系统稳定性或系统崩溃), 总是会使用该标志
__GFP_MEMALLOC
__GFP_NOMEMALLOC
__GFP_IO说明在查找空闲内存期间内核可以进行I/O操作. 实际上, 这意味着如果内核在内存分配期间换出页, 那么仅当设置该标志时, 才能将选择的页写入硬盘
__GFP_FS允许内核执行VFS操作. 在与VFS层有联系的内核子系统中必须禁用, 因为这可能引起循环递归调用.
__GFP_DIRECT_RECLAIM
__GFP_KSWAPD_RECLAIM
__GFP_RECLAIM
__GFP_REPEAT在分配失败后自动重试,但在尝试若干次之后会停止
__GFP_NOFAIL在分配失败后一直重试,直至成功
__GFP_NORETRY在分配失败后不重试,因此可能分配失败
__GFP_COLD如果需要分配不在CPU高速缓存中的“冷”页时,则设置__GFP_COLD
__GFP_NOWARN在分配失败时禁止内核故障警告。在极少数场合该标志有用
__GFP_COMP添加混合页元素, 在hugetlb的代码内部使用
__GFP_ZERO在分配成功时,将返回填充字节0的页
__GFP_NOTRACK
__GFP_NOTRACK_FALSE_POSITIVE
__GFP_NOTRACK
__GFP_OTHER_NODE

那自然还有__GFP_BITS_SHIFT来表示我们所有的掩码位, 由于我们共计26个掩码位

/* Room for N __GFP_FOO bits */
#define __GFP_BITS_SHIFT 26
#define __GFP_BITS_MASK ((__force gfp_t)((1 << __GFP_BITS_SHIFT) - 1))

可以同时指定这些分配标志, 例如:

ptr = kmalloc(size, __GFP_IO | __GFP_FS);

说明页分配器(最终会调用alloc_page)在分配时可以执行I/O, 在必要时还可以执行文件系统操作. 这就让内核有很大的自由度, 以便它尽可能找到空闲的内存来满足分配请求. 大多数分配器都会执行这些修饰符, 但一般不是这样直接指定, 而是将这些行为描述符标志进行分组, 类型标志

4 掩码分组

最后来看第三部分, 由于这些标志几乎总是组合使用,内核作了一些分组,包含了用于各种标准情形的适当的标志. 称之为类型标志, 定义include/linux/gfp_types.h

类型标志指定所需的行为和区描述符以安城特殊类型的处理, 正因为这一点, 内核总是趋于使用正确的类型标志, 而不是一味地指定它可能用到的多种描述符. 这么做既简单又不容易出错误.

如果有可能的话, 在内存管理子系统之外, 总是把下列分组之一用于内存分配. 在内核源代码中, 双下划线通常用于内部数据和定义. 而这些预定义的分组名没有双下划线前缀, 点从侧面验证了上述说法.

#define GFP_ATOMIC      (__GFP_HIGH|__GFP_ATOMIC|__GFP_KSWAPD_RECLAIM)
#define GFP_KERNEL      (__GFP_RECLAIM | __GFP_IO | __GFP_FS)
#define GFP_KERNEL_ACCOUNT (GFP_KERNEL | __GFP_ACCOUNT)
#define GFP_NOWAIT      (__GFP_KSWAPD_RECLAIM)
#define GFP_NOIO        (__GFP_RECLAIM)
#define GFP_NOFS        (__GFP_RECLAIM | __GFP_IO)
#define GFP_TEMPORARY   (__GFP_RECLAIM | __GFP_IO | __GFP_FS | \
                         __GFP_RECLAIMABLE)
#define GFP_USER        (__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
#define GFP_DMA         __GFP_DMA
#define GFP_DMA32       __GFP_DMA32
#define GFP_HIGHUSER    (GFP_USER | __GFP_HIGHMEM)
#define GFP_HIGHUSER_MOVABLE    (GFP_HIGHUSER | __GFP_MOVABLE)
#define GFP_TRANSHUGE   ((GFP_HIGHUSER_MOVABLE | __GFP_COMP | \
                         __GFP_NOMEMALLOC | __GFP_NORETRY | __GFP_NOWARN) & \
                         ~__GFP_RECLAIM)

/* Convert GFP flags to their corresponding migrate type */
#define GFP_MOVABLE_MASK (__GFP_RECLAIMABLE|__GFP_MOVABLE)
#define GFP_MOVABLE_SHIFT 3
掩码组描述
GFP_ATOMIC用于原子分配,在任何情况下都不能中断, 可能使用紧急分配链表中的内存, 这个标志用在中断处理程序, 下半部, 持有自旋锁以及其他不能睡眠的地方
GFP_KERNEL这是一种常规的分配方式, 可能会阻塞. 这个标志在睡眠安全时用在进程的长下文代码中. 为了获取调用者所需的内存, 内核会尽力而为. 这个标志应该是首选标志
GFP_KERNEL_ACCOUNT
GFP_NOWAIT与GFP_ATOMIC类似, 不同之处在于, 调用不会退给紧急内存池, 这就增加了内存分配失败的可能性
GFP_NOIO这种分配可以阻塞, 但不会启动磁盘I/O, 这个标志在不能引发更多的磁盘I/O时阻塞I/O代码, 这可能导致令人不愉快的递归
GFP_NOFS这种分配在必要时可以阻塞, 但是也可能启动磁盘, 但是不会启动文件系统操作, 这个标志在你不鞥在启动另一个文件系统操作时, 用在文件系统部分的代码中
GFP_TEMPORARY
GFP_USER这是一种常规的分配方式, 可能会阻塞. 这个标志用于为用户空间进程分配内存时使用
GFP_DMA
GFP_DMA32
用于分配适用于DMA的内存, 当前是__GFP_DMA的同义词, GFP_DMA32也是__GFP_GMA32的同义词
GFP_HIGHUSER是GFP_USER的一个扩展, 也用于用户空间. 它允许分配无法直接映射的高端内存. 使用高端内存页是没有坏处的,因为用户过程的地址空间总是通过非线性页表组织的
GFP_HIGHUSER_MOVABLE用途类似于GFP_HIGHUSER,但分配将从虚拟内存域ZONE_MOVABLE进行
GFP_TRANSHUGE


* 其中GFP_NOIO和GFP_NOFS, 分别明确禁止I/O操作和访问VFS层, 但同时设置了__GFP_RECLAIM,因此可以被回收。

  • 而GFP_KERNEL和GFP_USER. 分别是内核和用户分配的默认设置。二者的失败不会立即威胁系统稳定性, GFP_KERNEL绝对是内核源代码中最常使用的标志 |

最后内核设置了碎片管理的可移动依据组织页的MASK信息GFP_MOVABLE_MASK, include/linux/gfp_types.h。

/* Convert GFP flags to their corresponding migrate type */
#define GFP_MOVABLE_MASK (__GFP_RECLAIMABLE|__GFP_MOVABLE)
#define GFP_MOVABLE_SHIFT 3
  • 写的绝大多数代码中, 用么用到的是GFP_KERNEL, 要么是GFP_ATOMIC, 当然各个类型标志也均有其应用场景。
情形相应标志
进程上下文, 可以睡眠使用GFP_KERNEL
进程上下文, 不可以睡眠使用GFP_KERNEL, 在你睡眠之前或之后以GFP_KERNEL执行内存分配
中断处理程序使用GFP_ATMOIC
软中断使用GFP_ATMOIC
tasklet使用GFP_ATMOIC
需要用于DMA的内存, 可以睡眠使用(GFP_DMA GFP_KERNEL)
需要用于DMA的内存, 不可以睡眠使用(GFP_DMA GFP_ATOMIC), 或在你睡眠之前执行内存分配

5 总结


从注释中找到这样的信息, 可以作为参考

bit       result
=================
0x0    => NORMAL
0x1    => DMA or NORMAL
0x2    => HIGHMEM or NORMAL
0x3    => BAD (DMA+HIGHMEM)
0x4    => DMA32 or DMA or NORMAL
0x5    => BAD (DMA+DMA32)
0x6    => BAD (HIGHMEM+DMA32)
0x7    => BAD (HIGHMEM+DMA32+DMA)
0x8    => NORMAL (MOVABLE+0)
0x9    => DMA or NORMAL (MOVABLE+DMA)
0xa    => MOVABLE (Movable is valid only if HIGHMEM is set too)
0xb    => BAD (MOVABLE+HIGHMEM+DMA)
0xc    => DMA32 (MOVABLE+DMA32)
0xd    => BAD (MOVABLE+DMA32+DMA)
0xe    => BAD (MOVABLE+DMA32+HIGHMEM)
0xf    => BAD (MOVABLE+DMA32+HIGHMEM+DMA)

GFP_ZONES_SHIFT must be <= 2 on 32 bit platforms.

很有趣的一点是,没有__GFP_NORMAL常数,而内存分配的主要负担却落到ZONE_NORMAL内存域。

内核考虑到这一点, 提供了一个函数gfp_zone来计算与给定分配标志兼容的最高内存域. 那么内存分配可以从该内存域或更低的内存域进行, 该函数定义include/linux/gfp.h

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

其中GFP_ZONES_SHIFT的定义如下, include/linux/gfp_types.h

#if defined(CONFIG_ZONE_DEVICE) && (MAX_NR_ZONES-1) <= 4
/* ZONE_DEVICE is not a valid GFP zone specifier */
#define GFP_ZONES_SHIFT 2
#else
#define GFP_ZONES_SHIFT ZONES_SHIFT
#endif

#if 16 * GFP_ZONES_SHIFT > BITS_PER_LONG
#error GFP_ZONES_SHIFT too large to create GFP_ZONE_TABLE integer
#endif

由于内存域修饰符的解释方式不是那么直观, 表3-7给出了该函数结果的一个例子, 其中DMA和DMA32内存域相同. 假定在下文中没有设置__GFP_MOVABLE修饰符.

修饰符扫描的内存域
ZONE_NORMAL、ZONE_DMA
__GFP_DMAZONE_DMA
__GFP_DMA & __GFP_HIGHMEMZONE_DMA
__GFP_HIGHMEMZONE_HIGHMEM、ZONE_NORMAL、ZONE_DMA

* 如果__GFP_DMA和__GFP_HIGHMEM都没有设置, 则首先扫描ZONE_NORMAL, 后面是ZONE_DMA。

  • 如果设置了__GFP_HIGHMEM没有设置__GFP_DMA,则结果是从ZONE_HIGHMEM开始扫描所有3个内存域。

  • 如果设置了__GFP_DMA,那么__GFP_HIGHMEM设置与否没有关系. 只有ZONE_DMA用于3种情形. 这是合理的, 因为同时使用__GFP_HIGHMEM和__GFP_DMA没有意义. 高端内存从来都不适用于DMA。

设置__GFP_MOVABLE不会影响内核的决策,除非它与__GFP_HIGHMEM同时指定. 在这种情况下, 会使用特殊的虚拟内存域ZONE_MOVABLE满足内存分配请求. 对前文描述的内核的反碎片策略而言, 这种行为是必要的。

除了内存域修饰符之外, 掩码中还可以设置一些标志。

下图中给出了掩码的布局,以及与各个比特位置关联的常数. __GFP_DMA32出现了几次,因为它可能位于不同的地方。

与内存域修饰符相反, 这些额外的标志并不限制从哪个物理内存段分配内存, 但确实可以改变分配器的行为. 例如, 它们可以修改查找空闲内存时的积极程度。

6.4 分配页


1 内存分配统一到alloc_pages接口


通过使用标志、内存域修饰符和各个分配函数,内核提供了一种非常灵活的内存分配体系.尽管如此, 所有接口函数都可以追溯到一个简单的基本函数(alloc_pages_node)。

分配单页的函数include/linux/gfp.h 


#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)

#define __get_free_page(gfp_mask) \
    __get_free_pages((gfp_mask), 0)`

#define __get_dma_pages(gfp_mask, order) \
    __get_free_pages((gfp_mask) | GFP_DMA, (order))

 get_zeroed_page实现也没什么困难, 对__get_free_pages使用__GFP_ZERO标志,即可分配填充字节0的页. 再返回与页关联的内存区地址即可.mm/page_alloc.c


unsigned long get_zeroed_page(gfp_t gfp_mask)
{
        return __get_free_pages(gfp_mask | __GFP_ZERO, 0);
}
EXPORT_SYMBOL(get_zeroed_page);

__get_free_pages调用alloc_pages完成内存分配, 而alloc_pages又借助于alloc_pages_node

__get_free_pages函数的定义.mm/page_alloc.c

/*
 * Common helper functions. Never use with __GFP_HIGHMEM because the returned
 * address cannot represent highmem pages. Use alloc_pages and then kmap if
 * you need to access high mem.
 */
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
	struct page *page;

	page = alloc_pages(gfp_mask & ~__GFP_HIGHMEM, order);
	if (!page)
		return 0;
	return (unsigned long) page_address(page);
}
EXPORT_SYMBOL(__get_free_pages);

在这种情况下, 使用了一个普通函数而不是宏, 因为alloc_pages返回的page实例需要使用辅助。函数page_address转换为内存地址. 在这里,只要知道该函数可根据page实例计算相关页的线性内存地址即可. 对高端内存页这是有问题的。
这样, 就完成了所有分配内存的API函数到公共的基础函数alloc_pages的统一。

所有体系结构都必须实现的标准函数clear_page, 可帮助alloc_pages对页填充字节0。

2 alloc_pages函数分配页


既然所有的内存分配API函数都可以追溯掉alloc_page函数, 从某种意义上说,该函数是伙伴系统主要实现的”发射台”.

alloc_pages函数的定义是依赖于NUMA或者UMA架构的, 定义如下mm/page_alloc.c

#ifdef CONFIG_NUMA
...
static inline struct page *
alloc_pages(gfp_t gfp_mask, unsigned int order)
{
        return alloc_pages_current(gfp_mask, order);
}

#else
...
#define alloc_pages(gfp_mask, order) \
                alloc_pages_node(numa_node_id(), gfp_mask, order)
#endif

UMA结构下的alloc_pages是通过alloc_pages_node函数实现的, 下面我们看看alloc_pages_node函数的定义include/linux/gfp.h

/*
 * Allocate pages, preferring the node given as nid. When nid == NUMA_NO_NODE,
 * prefer the current CPU's closest node. Otherwise node must be valid and
 * online.
 */
static inline struct page *alloc_pages_node(int nid, gfp_t gfp_mask,
						unsigned int order)
{
	if (nid == NUMA_NO_NODE)
		nid = numa_mem_id();

	return __alloc_pages_node(nid, gfp_mask, order);
}

它只是执行了一个简单的检查, 如果指定负的结点ID(不存在, NUMA_NO_NODE = -1), 内核自动地使用当前执行CPU对应的结点nid =numa_mem_id()然后调用__alloc_pages_node函数进行了内存分配

__alloc_pages_node函数定义include/linux/gfp.h

/*
 * Allocate pages, preferring the node given as nid. The node must be valid and
 * online. For more general interface, see alloc_pages_node().
 */
static inline struct page *
__alloc_pages_node(int nid, gfp_t gfp_mask, unsigned int order)
{
	VM_BUG_ON(nid < 0 || nid >= MAX_NUMNODES);
	warn_if_node_offline(nid, gfp_mask);

	return __alloc_pages(gfp_mask, order, nid, NULL);
}

内核假定传递给改alloc_pages_node函数的结点nid是被激活, 即online的.但是为了安全它还是检查并警告内存结点不存在的情况. 接下来的工作委托给__alloc_pages, 只需传递一组适当的参数, 其中包括节点nid的备用内存域列表zonelist.。include/linux/gfp.h

struct page *__alloc_pages(gfp_t gfp, unsigned int order, int preferred_nid,
		nodemask_t *nodemask);

3 伙伴系统的心脏__alloc_pages_nodemask


内核源代码将__alloc_pages称之为”伙伴系统的心脏”(`the ‘heart’ of the zoned buddy allocator“), 因为它处理的是实质性的内存分配.

由于”心脏”的重要性, 我将在下文详细介绍该函数.

__alloc_pages函数定义mm/page_alloc.c

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

	/*
	 * There are several places where we assume that the order value is sane
	 * so bail out early if the request is out of bound.
	 */
	if (WARN_ON_ONCE_GFP(order > MAX_ORDER, gfp))
		return NULL;

	gfp &= gfp_allowed_mask;
	/*
	 * Apply scoped allocation constraints. This is mainly about GFP_NOFS
	 * resp. GFP_NOIO which has to be inherited for all allocation requests
	 * from a particular context which has been marked by
	 * memalloc_no{fs,io}_{save,restore}. And PF_MEMALLOC_PIN which ensures
	 * movable zones are not used during allocation.
	 */
	gfp = current_gfp_context(gfp);
	alloc_gfp = gfp;
	if (!prepare_alloc_pages(gfp, order, preferred_nid, nodemask, &ac,
			&alloc_gfp, &alloc_flags))
		return NULL;

	/*
	 * Forbid the first pass from falling back to types that fragment
	 * memory until all local zones are considered.
	 */
	alloc_flags |= alloc_flags_nofragment(ac.preferred_zoneref->zone, gfp);

	/* First allocation attempt */
	page = get_page_from_freelist(alloc_gfp, order, alloc_flags, &ac);
	if (likely(page))
		goto out;

	alloc_gfp = gfp;
	ac.spread_dirty_pages = false;

	/*
	 * Restore the original nodemask if it was potentially replaced with
	 * &cpuset_current_mems_allowed to optimize the fast-path attempt.
	 */
	ac.nodemask = nodemask;

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

out:
	if (memcg_kmem_online() && (gfp & __GFP_ACCOUNT) && page &&
	    unlikely(__memcg_kmem_charge_page(page, gfp, order) != 0)) {
		__free_pages(page, order);
		page = NULL;
	}

	trace_mm_page_alloc(page, order, alloc_gfp, ac.migratetype);
	kmsan_alloc_page(page, order, alloc_gfp);

	return page;
}
EXPORT_SYMBOL(__alloc_pages);

6.5 __free_pages


类似地,内存释放函数也可以归约到一个主要的函数(__free_pages), 只是用不同的参数调用而已

内核释放的两个主要函数有__free_page和free_page, 它们的定义在include/linux/gfp.h

#define __free_page(page) __free_pages((page), 0)
#define free_page(addr) free_pages((addr), 0)

而free_pages是通过__free_pages来完成内存释放的mm/page_alloc.c

void __free_pages(struct page *page, unsigned int order)
{
	/* get PageHead before we drop reference */
	int head = PageHead(page);

	if (put_page_testzero(page))
		free_the_page(page, order);
	else if (!head)
		while (order-- > 0)
			free_the_page(page + (1 << order), order);
}
EXPORT_SYMBOL(__free_pages);

void free_pages(unsigned long addr, unsigned int order)
{
	if (addr != 0) {
		VM_BUG_ON(!virt_addr_valid((void *)addr));
		__free_pages(virt_to_page((void *)addr), order);
	}
}

EXPORT_SYMBOL(free_pages);

free_pages__free_pages之间的关系通过函数而不是宏建立, 因为首先必须将虚拟地址转换为指向struct page的指针

virt_to_page将虚拟内存地址转换为指向page实例的指针. 基本上, 这是讲解内存分配函数时介绍的page_address辅助函数的逆过程.

下图以图形化方式综述了各个内存释放函数之间的关系:

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值