Linux Kernel:启动时内存管理(MemBlock 分配器)

本文采用 Linux 内核 v3.10 版本 x86_64 架构

一、Bootmem 与 Memblock

系统初始化早期,由于"正常"的内存管理还未完成设置,所以无法使用。 此时,仍然需要为各种数据结构分配内存。 为了解决这个问题,引入了一种称为 Boot Memory Allocatorbootmem 的专用分配器。 几年后,PowerPC 开发人员添加了 Logical Memory Blocks 分配器,后来被其他架构采用并重命名为 memblock。 另外,还有一个名为 nobootmem 的兼容层,它将 bootmem 内存分配接口转换为对 memblock 的函数调用。

早期分配器的选择是通过 CONFIG_NO_BOOTMEMCONFIG_HAVE_MEMBLOCK 内核配置选项完成的。 这些选项在 Kconfig 文件中静态启用或禁用。

  • 仅依赖 bootmem 的架构,选择 CONFIG_NO_BOOTMEM=n && CONFIG_HAVE_MEMBLOCK=n
  • 具有 nobootmem 兼容层的 memblock 用户,设置 CONFIG_NO_BOOTMEM=y && CONFIG_HAVE_MEMBLOCK=y
  • 对于同时使用 memblockbootmem 的用户,设置 CONFIG_NO_BOOTMEM=n && CONFIG_HAVE_MEMBLOCK=y

无论使用哪个分配器,架构特定的初始化都需要在 setup_arch() 函数中进行设置并在 mem_init() 函数中将其移除。

一旦早期的内存管理可用,它就会提供各种用于内存分配的函数和宏。 内存分配请求可以指定第一个(也可能是唯一的)节点或 NUMA 系统中的特定节点。

二、Memblock 概述

Memblock 将系统内存视为连续区域的集合。 这些集合有以下几种类型:

  • memory - 描述内核可用的物理内存区域;
  • reserved - 描述已分配的内存区域。

每个区域由 struct memblock_region 表示,它定义了区域范围以及 NUMA 节点 ID。 每种内存类型都由 struct memblock_type 描述,其中包含内存区域数组以及元数据。 内存类型被包装在 struct memblock 结构体中,该结构在构建时被静态初始化。 memoryreserved类型的内存区域数组初始大小为 INIT_MEMBLOCK_REGIONSmemblock_allow_resize() 允许在添加新区域时自动调整区域数组的大小。

在早期进行架构相关的初始化时,应该使用 memblock_add()memblock_add_node() 函数指定 memblock 中的物理内存布局。 memblock_add()函数未指定内存区域的 NUMA 节点,它适用于 UMA 系统。 当在 NUMA 系统上使用时,可在稍后使用 memblock_set_node() 函数设置内存区域的 NUMA 节点。 而memblock_add_node() 函数直接一步到位,会将内存区域添加到指定节点。

随着系统引导的进行,特定于体系结构的 mem_init() 函数会将内存释放给伙伴(buddy)页面分配器。

三、数据结构

如上文所述,memblock 由 3 种数据结构组成:struct memblock_regionstruct memblock_typestruct memblock

3.1 memblock_region

memblock_region 表示一个内存区域 或者说内存块,包含 3 个字段:

  • base -- 内存区域的物理基地址;
  • size -- 内存区域的大小;
  • nid -- 内存区域所属的 NUMA 节点 ID。
c 复制代码
// file: include/linux/memblock.h
struct memblock_region {
	phys_addr_t base;
	phys_addr_t size;
#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP
	int nid;
#endif
};

3.2 memblock_type

memblock_type 表示一定类型的内存块的集合,包含 4 个字段:

  • cnt -- 集合中内存块的数量;
  • max -- 已分配的数组大小;
  • total_size -- 内存块总容量,即各内存块的容量之和;
  • regions -- 内存块数组。
c 复制代码
// file: include/linux/memblock.h
struct memblock_type {
	unsigned long cnt;	/* number of regions */
	unsigned long max;	/* size of the allocated array */
	phys_addr_t total_size;	/* size of all regions */
	struct memblock_region *regions;
};

3.3 memblock

memblock 描述 Memblock 内存分配器元数据,包含 3 个字段:

  • current_limit -- 分配器物理地址上限;
  • memory -- 可用的内存块集合;
  • reserved -- 保留的内存块集合。
c 复制代码
// file: include/linux/memblock.h
struct memblock {
	phys_addr_t current_limit;
	struct memblock_type memory;
	struct memblock_type reserved;
};

各数据结构的关系如下图所示:

四、memblock 初始化

memblockstruct memblock 的同名变量,该变量存储着 Memblock 分配器的相关数据。

c 复制代码
// file: mm/memblock.c
struct memblock memblock __initdata_memblock = {
	.memory.regions		= memblock_memory_init_regions,
	.memory.cnt		= 1,	/* empty dummy entry */
	.memory.max		= INIT_MEMBLOCK_REGIONS,

	.reserved.regions	= memblock_reserved_init_regions,
	.reserved.cnt		= 1,	/* empty dummy entry */
	.reserved.max		= INIT_MEMBLOCK_REGIONS,

	.current_limit		= MEMBLOCK_ALLOC_ANYWHERE,
};

其中,memory.regionsreserved.regions 字段,被初始化为包含 128 个元素的数组 memblock_memory_init_regions 以及 memblock_reserved_init_regions

c 复制代码
// file: mm/memblock.c
static struct memblock_region memblock_memory_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock;
static struct memblock_region memblock_reserved_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock;

INIT_MEMBLOCK_REGIONS 扩展为 128:

c 复制代码
// file: include/linux/memblock.h
#define INIT_MEMBLOCK_REGIONS	128

分配器的物理地址上限 current_limit,被初始化为 MEMBLOCK_ALLOC_ANYWHERE

c 复制代码
// file: include/linux/memblock.h 
#define MEMBLOCK_ALLOC_ANYWHERE	(~(phys_addr_t)0)

该宏会扩展为最大地址 0xffffffffffffffff

另外,变量 memblock 使用宏 __initdata_memblock 来修饰:

c 复制代码
// file: include/linux/memblock.h
#ifdef CONFIG_ARCH_DISCARD_MEMBLOCK
#define __init_memblock __meminit
#define __initdata_memblock __meminitdata
#else
#define __init_memblock
#define __initdata_memblock
#endif

该宏的扩展依赖于内核配置选项 CONFIG_ARCH_DISCARD_MEMBLOCK,当指定了该选项时,扩展为 __meminitdata

c 复制代码
// file: include/linux/init.h
#define __meminitdata    __section(.meminit.data)
c 复制代码
// file: include/linux/compiler.h
/* Simple shorthand for a section definition */
#ifndef __section
# define __section(S) __attribute__ ((__section__(#S)))
#endif

__meminitdata 修饰的数据,会被放置到 .meminit.data 节( section) 中。

至于 .meminit.data 节的数据,最终会被放到什么位置,依赖于内核配置选项 CONFIG_MEMORY_HOTPLUG

c 复制代码
// file: include/asm-generic/vmlinux.lds.h
#if defined(CONFIG_MEMORY_HOTPLUG)
#define MEM_KEEP(sec)    *(.mem##sec)
#define MEM_DISCARD(sec)
#else
#define MEM_KEEP(sec)
#define MEM_DISCARD(sec) *(.mem##sec)
#endif

当配置了内存热插拔时, .meminit.data 节的数据会被放置到宏 MEM_KEEP中;否则,会被放置到宏 MEM_DISCARD 中。从名称也能够看出,宏 MEM_KEEP 中的数据将会被保留;而宏 MEM_DISCARD 中的数据,在内核启动完成后会被释放。

MEM_KEEP 中的数据,会被放置到 .data 节中:

c 复制代码
// file: include/asm-generic/vmlinux.lds.h
/* .data section */
#define DATA_DATA							\
	*(.data)							\
	......						\
	MEM_KEEP(init.data)						\
	......

MEM_DISCARD 中的数据,会被放置到 .init.data 节中:

c 复制代码
// file: include/asm-generic/vmlinux.lds.h
/* init and exit section handling */
#define INIT_DATA							\
	*(.init.data)							\
	......						\
	MEM_DISCARD(init.data)						\
	......							\
	IRQCHIP_OF_MATCH_TABLE()

也就是说当内核配置选项 CONFIG_ARCH_DISCARD_MEMBLOCK=y && CONFIG_MEMORY_HOTPLUG=n 时,memblock 相关的数据将会在系统启动后丢弃。

五、接口函数

5.1 新增内存区域

内核提供了 3 个接口用来增加 memblock_region,分别是 memblock_add()memblock_add_node()memblock_reserve()

5.1.1 memblock_add()

c 复制代码
// file: mm/memblock.c
int __init_memblock memblock_add(phys_addr_t base, phys_addr_t size)
{
	return memblock_add_region(&memblock.memory, base, size, MAX_NUMNODES);
}

该接口会新增一个 memory 类型的内存块,由于接口未指定 NUMA 节点,会将新增内存块的节点 ID 设置为 MAX_NUMNODES。其内部函数 memblock_add_region() 的实现详见 5.1.4 节

MAX_NUMNODES 表示系统支持的最大 NUMA 节点数量,其值依赖于宏 NODES_SHIFT

c 复制代码
// file: include/linux/numa.h
#define MAX_NUMNODES    (1 << NODES_SHIFT)

NODES_SHIFT 的扩展值依赖于内核配置选项 CONFIG_NODES_SHIFT

c 复制代码
// file: include/linux/numa.h
#ifdef CONFIG_NODES_SHIFT
#define NODES_SHIFT     CONFIG_NODES_SHIFT
#else
#define NODES_SHIFT     0
#endif

在我们的配置中,宏 CONFIG_NODES_SHIFT 扩展为 10:

c 复制代码
// file: include/generated/autoconf.h
#define CONFIG_NODES_SHIFT 10

所以,MAX_NUMNODES 扩展为 1 << 10,即 1024。

5.1.2 memblock_add_node()

c 复制代码
// file: mm/memblock.c
int __init_memblock memblock_add_node(phys_addr_t base, phys_addr_t size,
				       int nid)
{
	return memblock_add_region(&memblock.memory, base, size, nid);
}

该接口会新增一个 memory 类型的内存块,并设置该内存块的节点 ID 为 nid

memblock_add_region() 函数的实现详见 5.1.4 节

5.1.3 memblock_reserve()

c 复制代码
// file: mm/memblock.c
int __init_memblock memblock_reserve(phys_addr_t base, phys_addr_t size)
{
	struct memblock_type *_rgn = &memblock.reserved;

	memblock_dbg("memblock_reserve: [%#016llx-%#016llx] %pF\n",
		     (unsigned long long)base,
		     (unsigned long long)base + size,
		     (void *)_RET_IP_);

	return memblock_add_region(_rgn, base, size, MAX_NUMNODES);
}

该接口会新增一个 reserved 类型的内存块,由于接口未指定 NUMA 节点,会将新增内存块的节点 ID 设置为 MAX_NUMNODES

如果添加成功,返回 0;否则返回负的错误码。

以上 3 个接口,都调用了memblock_add_region()函数,我们来看下该函数的实现。

5.1.4 memblock_add_region()

该函数会新增一个指定类型的内存块,并设置其节点 ID。如果成功,返回 0;否则返回负的错误码。

c 复制代码
// file: mm/memblock.c
/**
 * memblock_add_region - add new memblock region
 * @type: memblock type to add new region into
 * @base: base address of the new region
 * @size: size of the new region
 * @nid: nid of the new region
 *
 * Add new memblock region [@base,@base+@size) into @type.  The new region
 * is allowed to overlap with existing ones - overlaps don't affect already
 * existing regions.  @type is guaranteed to be minimal (all neighbouring
 * compatible regions are merged) after the addition.
 *
 * RETURNS:
 * 0 on success, -errno on failure.
 */
static int __init_memblock memblock_add_region(struct memblock_type *type,
				phys_addr_t base, phys_addr_t size, int nid)
{
	bool insert = false;
	phys_addr_t obase = base;
	phys_addr_t end = base + memblock_cap_size(base, &size);
	int i, nr_new;

	if (!size)
		return 0;

	/* special case for empty array */
	if (type->regions[0].size == 0) {
		WARN_ON(type->cnt != 1 || type->total_size);
		type->regions[0].base = base;
		type->regions[0].size = size;
		memblock_set_region_node(&type->regions[0], nid);
		type->total_size = size;
		return 0;
	}
repeat:
	/*
	 * The following is executed twice.  Once with %false @insert and
	 * then with %true.  The first counts the number of regions needed
	 * to accomodate the new area.  The second actually inserts them.
	 */
	base = obase;
	nr_new = 0;

	for (i = 0; i < type->cnt; i++) {
		struct memblock_region *rgn = &type->regions[i];
		phys_addr_t rbase = rgn->base;
		phys_addr_t rend = rbase + rgn->size;

		if (rbase >= end)
			break;
		if (rend <= base)
			continue;
		/*
		 * @rgn overlaps.  If it separates the lower part of new
		 * area, insert that portion.
		 */
		if (rbase > base) {
			nr_new++;
			if (insert)
				memblock_insert_region(type, i++, base,
						       rbase - base, nid);
		}
		/* area below @rend is dealt with, forget about it */
		base = min(rend, end);
	}

	/* insert the remaining portion */
	if (base < end) {
		nr_new++;
		if (insert)
			memblock_insert_region(type, i, base, end - base, nid);
	}

	/*
	 * If this was the first round, resize array and repeat for actual
	 * insertions; otherwise, merge and return.
	 */
	if (!insert) {
		while (type->cnt + nr_new > type->max)
			if (memblock_double_array(type, obase, size) < 0)
				return -ENOMEM;
		insert = true;
		goto repeat;
	} else {
		memblock_merge_regions(type);
		return 0;
	}
}

函数接收 4 个参数:

  • @type: 新增内存块的类型;
  • @base: 新增内存块的基地址;
  • @size: 新增内存块的区间大小;
  • @nid: 新增内存块的 NUMA 节点 ID。

如果指定的 memblock_type 类型集合是个空数组,则直接添加并返回。否则,repeat 逻辑要执行 2 遍。第一遍要计算出本次要新增的内存块数量 nr_new;第 2 遍才会调用 memblock_insert_region() 函数执行真正的插入流程。如果在插入过程中,发现数组空间不足,还会调用 memblock_double_array() 函数将数组空间翻倍。插入完成后,还要调用 memblock_merge_regions() 函数将相邻的区域合并。

详细执行流程如下。

c 复制代码
	bool insert = false;
	phys_addr_t obase = base;
	phys_addr_t end = base + memblock_cap_size(base, &size);
	int i, nr_new;

	if (!size)
		return 0;

insertfalse,指示不执行真正的插入流程;base 是内存块的基地址,end 是内存块的结束地址,其中 memblock_cap_size() 函数(见 5.1.5 节 )用来防止内存块区间过大时发生地址回绕。当 size 为 0 时,内存块无效,不需要插入,直接返回 0。

c 复制代码
	/* special case for empty array */
	if (type->regions[0].size == 0) {
		WARN_ON(type->cnt != 1 || type->total_size);
		type->regions[0].base = base;
		type->regions[0].size = size;
		memblock_set_region_node(&type->regions[0], nid);
		type->total_size = size;
		return 0;
	}

当索引为 0 的内存块为空时,说明是首次插入,此时不需要考虑区域合并之类的复杂逻辑,直接更新 regions[0] 的各字段即可。另外,由于是首个内存块,集合中的总容量就等于该内存块的容量。memblock_set_region_node() 函数(见 5.1.7 节)为内存块设置节点 ID。

在插入新的内存块时,主要包括 2 个步骤:

  • 将新的内存块中非重叠部分作为一个独立的块添加到数组中,可能会导致插入多个内存块
  • 将同类型的邻居块进行合并

总体流程示意图:

c 复制代码
	for (i = 0; i < type->cnt; i++) {
		struct memblock_region *rgn = &type->regions[i];
		phys_addr_t rbase = rgn->base;
		phys_addr_t rend = rbase + rgn->size;

		if (rbase >= end)
			break;
		if (rend <= base)
			continue;
		/*
		 * @rgn overlaps.  If it separates the lower part of new
		 * area, insert that portion.
		 */
		if (rbase > base) {
			nr_new++;
			if (insert)
				memblock_insert_region(type, i++, base,
						       rbase - base, nid);
		}
		/* area below @rend is dealt with, forget about it */
		base = min(rend, end);
	}

在上面的 for 循环中,将会遍历数组 regions 中的元素,检查每个元素与新区域的是否有重叠。如果没有重叠(rbase >= end),说明可以直接插入。此时会跳出循环执行下面的代码:

c 复制代码
	/* insert the remaining portion */
	if (base < end) {
		nr_new++;
		if (insert)
			memblock_insert_region(type, i, base, end - base, nid);
	}

在第一次遍历中,由于变量 insertfalse ,所以不会直接插入, nr_new 会自增一。

c 复制代码
	/*
	 * If this was the first round, resize array and repeat for actual
	 * insertions; otherwise, merge and return.
	 */
	if (!insert) {
		while (type->cnt + nr_new > type->max)
			if (memblock_double_array(type, obase, size) < 0)
				return -ENOMEM;
		insert = true;
		goto repeat;
	} else {
		memblock_merge_regions(type);
		return 0;
	}

然后检查数组容量,如果容量不足,则需要调用 memblock_double_array() 函数将容量翻倍。然后将变量 insert 更新为 true,跳转到标签 repeat 中继续执行。第二次执行时,inserttrue,所以会执行真正的插入操作。

在第二次 repeat 逻辑执行的最后,会调用 memblock_merge_regions() 函数合并邻居块。

上述这种是最简单的情况,其执行流程如下所示:

如果 rend <= base,那么可能有 5 种情况,如下图所示:

我们以情况 2 为例,来分析其执行过程,其它几种情况与其类似。

c 复制代码
		if (rend <= base)
			continue;

rend <= base 时,会进入下一轮循环与下一个内存块进行比较。

c 复制代码
		if (rbase > base) {
			nr_new++;
			if (insert)
				memblock_insert_region(type, i++, base,
						       rbase - base, nid);
		}

此时,满足条件 rbase > base,如果是第一次遍历,则 nr_new 会自增一;否则,会执行插入操作。

c 复制代码
base = min(rend, end);

接下来,计算新的 basebase = min(rend, end),小于该值的部分都是重叠区域,不用理会。

此时,进入下一轮循环。

c 复制代码
		if (rbase >= end)
			break;

在本次循环中,由于满足 rbase >= end的条件,所以会跳出循环。

c 复制代码
	/* insert the remaining portion */
	if (base < end) {
		nr_new++;
		if (insert)
			memblock_insert_region(type, i, base, end - base, nid);
	}

跳出循环后,由于不满足 base < end的条件(此时 base == end),不会执行该条件下的代码。

c 复制代码
	if (!insert) {
		while (type->cnt + nr_new > type->max)
			if (memblock_double_array(type, obase, size) < 0)
				return -ENOMEM;
		insert = true;
		goto repeat;
	} else {
		memblock_merge_regions(type);
		return 0;
	}

接下来进入通用流程了

  • 根据计算出的 nr_new,检查数组容量是否充足,不足的话需要调用 memblock_double_array() 进行扩容,每次扩容后数组容量翻倍。
  • insert 值修改为 true,下次 repeat 操作时就会执行插入操作
  • 跳转到 repeat 标签处进行第二遍执行,将新的内存块插入到数组中
  • 调用 memblock_merge_regions() 函数合并相邻区域。

情况 2 完整流程如下所示:

情况 3 完整流程如下所示:

5.1.5 memblock_cap_size()

c 复制代码
// file: mm/memblock.c
/* adjust *@size so that (@base + *@size) doesn't overflow, return new size */
static inline phys_addr_t memblock_cap_size(phys_addr_t base, phys_addr_t *size)
{
	return *size = min(*size, (phys_addr_t)ULLONG_MAX - base);
}

memblock_cap_size() 函数调整内存块的大小,防止因容量过大导致地址回绕。该函数接收 2 个参数:

  • @base:内存块基地址
  • @size:内存块大小

其中宏 ULLONG_MAX 表示 unsigned long long 类型数据的最大值,也就是内存地址的最大值:

c 复制代码
// file: include/linux/kernel.h
#define ULLONG_MAX	(~0ULL)

该宏扩展为 0xFFFFFFFFFFFFFFFF

5.1.6 memblock_insert_region()

c 复制代码
/**
 * memblock_insert_region - insert new memblock region
 * @type:	memblock type to insert into
 * @idx:	index for the insertion point
 * @base:	base address of the new region
 * @size:	size of the new region
 * @nid:	node id of the new region
 *
 * Insert new memblock region [@base,@base+@size) into @type at @idx.
 * @type must already have extra room to accomodate the new region.
 */
static void __init_memblock memblock_insert_region(struct memblock_type *type,
						   int idx, phys_addr_t base,
						   phys_addr_t size, int nid)
{
	struct memblock_region *rgn = &type->regions[idx];

	BUG_ON(type->cnt >= type->max);
	memmove(rgn + 1, rgn, (type->cnt - idx) * sizeof(*rgn));
	rgn->base = base;
	rgn->size = size;
	memblock_set_region_node(rgn, nid);
	type->cnt++;
	type->total_size += size;
}

memblock_insert_region() 函数接收 5 个参数:

  • @type:要插入的内存块类型;
  • @idx:插入的位置,即数组索引;
  • @base:新内存块的基地址;
  • @size:新内存块的大小;
  • @nid:新内存块的节点 ID。

该函数在 @type 类型的内存块数组的 @idx 索引处,插入一个新的内存块,该内存块的范围为 [base, base+size)

由于要将新的内存块插入到数组中,所以要将插入位置之后(包括插入位置)的元素全部向后移动一个位置。元素迁移的工作,是通过 memmove() 函数来完成的。

元素迁移之后,将新内存块的数据写入数组中,然后调用 memblock_set_region_node() 函数为新内存块设置节点 ID;最后更新集合中内存块的总数量和总容量。

5.1.7 memblock_set_region_node()

c 复制代码
// file: include/linux/memblock.h
static inline void memblock_set_region_node(struct memblock_region *r, int nid)
{
	r->nid = nid;
}

memblock_set_region_node() 函数为内存块设置节点 ID。

5.1.8 memblock_get_region_node()

c 复制代码
// file: include/linux/memblock.h
static inline int memblock_get_region_node(const struct memblock_region *r)
{
	return r->nid;
}

memblock_get_region_node() 函数获取内存块的节点 ID。

5.1.9 memblock_merge_regions()

c 复制代码
// file: mm/memblock.c
/**
 * memblock_merge_regions - merge neighboring compatible regions
 * @type: memblock type to scan
 *
 * Scan @type and merge neighboring compatible regions.
 */
static void __init_memblock memblock_merge_regions(struct memblock_type *type)
{
	int i = 0;

	/* cnt never goes below 1 */
	while (i < type->cnt - 1) {
		struct memblock_region *this = &type->regions[i];
		struct memblock_region *next = &type->regions[i + 1];

		if (this->base + this->size != next->base ||
		    memblock_get_region_node(this) !=
		    memblock_get_region_node(next)) {
			BUG_ON(this->base + this->size > next->base);
			i++;
			continue;
		}

		this->size += next->size;
		/* move forward from next + 1, index of which is i + 2 */
		memmove(next, next + 1, (type->cnt - (i + 2)) * sizeof(*next));
		type->cnt--;
	}
}

memblock_merge_regions() 函数会扫描内存块数组,将相邻的内存块进行合并。

能够合并的前提有 2 个,这 2 个条件必须全部满足才能够合并:

  • 内存块的节点 ID 必须相同
  • 合并后的内存块,地址范围必须是连续的;也就是说前一个内存块的结束地址必须等于后一个内存块的起始地址。

所以函数内部,首先判断相邻的内存块是否能够合并。如果能够合并,将后一个内存块的数据合并到前一个内存块中,然后调用 memmove() 函数将第 2 个内存块之后的元素向前迁移一个位置。最后,将数据块的总数减一。

5.1.10 memblock_set_node()

c 复制代码
// file: mm/memblock.c
/**
 * memblock_set_node - set node ID on memblock regions
 * @base: base of area to set node ID for
 * @size: size of area to set node ID for
 * @nid: node ID to set
 *
 * Set the nid of memblock memory regions in [@base,@base+@size) to @nid.
 * Regions which cross the area boundaries are split as necessary.
 *
 * RETURNS:
 * 0 on success, -errno on failure.
 */
int __init_memblock memblock_set_node(phys_addr_t base, phys_addr_t size,
				      int nid)
{
	struct memblock_type *type = &memblock.memory;
	int start_rgn, end_rgn;
	int i, ret;

	ret = memblock_isolate_range(type, base, size, &start_rgn, &end_rgn);
	if (ret)
		return ret;

	for (i = start_rgn; i < end_rgn; i++)
		memblock_set_region_node(&type->regions[i], nid);

	memblock_merge_regions(type);
	return 0;
}

memblock_set_node() 函数接收 3 个参数:

  • @base:要设置节点 ID 的内存区域的基地址;
  • @size:要设置节点 ID 的内存区域的大小;
  • @nid: 要设置的 node ID 。

该函数将内存区域 [base, base+size) 的节点 ID 设置为 nid。当指定内存区域的起始和结束地址与现存区域相交时,需要对现存区域进行分割。分割后,最多会增加 2 个区域。

其执行流程也很简单,首先调用 memblock_isolate_range() 函数对现存区域进行分割。其中,参数 start_rgn 以及 end_rgn 会保存分割后待设置区域的起始和结束索引。

然后调用 memblock_set_region_node() 函数,将索引区间内的所有内存块的节点 ID设置为 nid

最后,合并内存块。

5.1.11 memblock_isolate_range()

c 复制代码
// file: mm/memblock.c
/**
 * memblock_isolate_range - isolate given range into disjoint memblocks
 * @type: memblock type to isolate range for
 * @base: base of range to isolate
 * @size: size of range to isolate
 * @start_rgn: out parameter for the start of isolated region
 * @end_rgn: out parameter for the end of isolated region
 *
 * Walk @type and ensure that regions don't cross the boundaries defined by
 * [@base,@base+@size).  Crossing regions are split at the boundaries,
 * which may create at most two more regions.  The index of the first
 * region inside the range is returned in *@start_rgn and end in *@end_rgn.
 *
 * RETURNS:
 * 0 on success, -errno on failure.
 */
static int __init_memblock memblock_isolate_range(struct memblock_type *type,
					phys_addr_t base, phys_addr_t size,
					int *start_rgn, int *end_rgn)
{
	phys_addr_t end = base + memblock_cap_size(base, &size);
	int i;

	*start_rgn = *end_rgn = 0;

	if (!size)
		return 0;

	/* we'll create at most two more regions */
	while (type->cnt + 2 > type->max)
		if (memblock_double_array(type, base, size) < 0)
			return -ENOMEM;

	for (i = 0; i < type->cnt; i++) {
		struct memblock_region *rgn = &type->regions[i];
		phys_addr_t rbase = rgn->base;
		phys_addr_t rend = rbase + rgn->size;

		if (rbase >= end)
			break;
		if (rend <= base)
			continue;

		if (rbase < base) {
			/*
			 * @rgn intersects from below.  Split and continue
			 * to process the next region - the new top half.
			 */
			rgn->base = base;
			rgn->size -= base - rbase;
			type->total_size -= base - rbase;
			memblock_insert_region(type, i, rbase, base - rbase,
					       memblock_get_region_node(rgn));
		} else if (rend > end) {
			/*
			 * @rgn intersects from above.  Split and redo the
			 * current region - the new bottom half.
			 */
			rgn->base = end;
			rgn->size -= end - rbase;
			type->total_size -= end - rbase;
			memblock_insert_region(type, i--, rbase, end - rbase,
					       memblock_get_region_node(rgn));
		} else {
			/* @rgn is fully contained, record it */
			if (!*end_rgn)
				*start_rgn = i;
			*end_rgn = i + 1;
		}
	}

	return 0;
}

memblock_isolate_range() 函数将选定的区域从现存区域中隔离出来。如果选定区域的起始或截止边界与现存内存块有交集,那么需要对现存内存块进行分割,分割后最多会新增 2 个内存块。

该函数接收 5 个参数:

  • @type: 要隔离的内存区域类型
  • @base: 要隔离区间的基地址
  • @size: 要隔离区间的大小
  • @start_rgn: 输出参数,保存隔离后区间的起始索引
  • @end_rgn: 输出参数,保存隔离后区间的结束索引
c 复制代码
	phys_addr_t end = base + memblock_cap_size(base, &size);
	int i;

	*start_rgn = *end_rgn = 0;

	if (!size)
		return 0;

首先,计算隔离区域的结束地址并将 start_rgnend_rgn 指向的值初始化为 0。

如果待隔离区间的 size 为 0,不需任何操作,直接返回。

c 复制代码
	/* we'll create at most two more regions */
	while (type->cnt + 2 > type->max)
		if (memblock_double_array(type, base, size) < 0)
			return -ENOMEM;

由于最多会增加 2 个内存块,所以不管是否真的增加,直接按增加 2 个来检查是否需要扩容。如果需要扩容,调用 memblock_double_array() 函数将数组容量翻倍。

c 复制代码
	for (i = 0; i < type->cnt; i++) {
		struct memblock_region *rgn = &type->regions[i];
		phys_addr_t rbase = rgn->base;
		phys_addr_t rend = rbase + rgn->size;

		if (rbase >= end)
			break;
		if (rend <= base)
			continue;

		......
        ......
	}

接下来,遍历数组,查找与隔离区间有交集的内存块。

如果 rbase >= end,说明当前区域与要隔离区域没有交集,所以直接跳出循环。

如果 rend <= base,说明当前内存块与要隔离区域没有交集,此时有 5 种可能。

由于没有交集,所以跳转到下一个内存块进行检查。

c 复制代码
	for (i = 0; i < type->cnt; i++) {
		......

		if (rbase < base) {
			/*
			 * @rgn intersects from below.  Split and continue
			 * to process the next region - the new top half.
			 */
			rgn->base = base;
			rgn->size -= base - rbase;
			type->total_size -= base - rbase;
			memblock_insert_region(type, i, rbase, base - rbase,
					       memblock_get_region_node(rgn));
		} else if (rend > end) {
			......
		} else {
			......
		}
	}

如果 rbase < base,此时需要处理第4、5 种情况。

由于要隔离区域与当前内存块有交集,所以将当前内存块分割成 2 块,即上、下半部,并进入下一轮循环:

进入下一轮循环后,rbaserend 的值分别指向分割后的上半部的起始及结束地址。

c 复制代码
		if (rbase < base) {
			......
		} else if (rend > end) {
			/*
			 * @rgn intersects from above.  Split and redo the
			 * current region - the new bottom half.
			 */
			rgn->base = end;
			rgn->size -= end - rbase;
			type->total_size -= end - rbase;
			memblock_insert_region(type, i--, rbase, end - rbase,
					       memblock_get_region_node(rgn));
		} else {
			......
		}

如果 rend > end,此时是第 4 种情况,会将上半部继续分割成 2 个内存块,并进入下一轮循环:

由于在上一轮执行中,调用 memblock_insert_region() 函数进行插入时,索引参数为 i-- ,在进行一次循环后,执行了 i++,所以 i 保持不变,指向分割后的下半部内存块,此时 rbase == base && rend == end,所以会进入 else 分支:

c 复制代码
		if (rbase < base) {
			......
		} else if (rend > end) {
			......
		} else {
			/* @rgn is fully contained, record it */
			if (!*end_rgn)
				*start_rgn = i;
			*end_rgn = i + 1;
		}

else 分支里,会更新 *start_rgn*end_rgn 的值。在当前情况下,只包含一个内存块,所以 *end_rgn 表示的结束索引,是不包含的,最终返回的索引范围应该是前开后闭的区间 [*start_rgn, *end_rgn)

至此,函数执行完成,原区域被分割成了 3 部分。

其它各种情况,与第 4 种情况相似,就不再一一分析了。

5.2 删除内存区域

5.2.1 memblock_remove_region()

c 复制代码
// file: mm/memblock.c
static void __init_memblock memblock_remove_region(struct memblock_type *type, unsigned long r)
{
	type->total_size -= type->regions[r].size;
	memmove(&type->regions[r], &type->regions[r + 1],
		(type->cnt - (r + 1)) * sizeof(type->regions[r]));
	type->cnt--;

	/* Special case for empty arrays */
	if (type->cnt == 0) {
		WARN_ON(type->total_size != 0);
		type->cnt = 1;
		type->regions[0].base = 0;
		type->regions[0].size = 0;
		memblock_set_region_node(&type->regions[0], MAX_NUMNODES);
	}
}

memblock_remove_region() 函数删除 memblock_type 中指定索引处的内存区域,并更新内存块总数量及总容量。说是删除,实际就是调用 memmove() 函数将大于指定索引 r 的所有元素向前移动一个位置,将 索引 r 处的内容覆盖掉。

如果移除后数组为空,将 memblock_type 恢复到初始值。

5.2.2 memblock_remove()

c 复制代码
// file: mm/memblock.c
int __init_memblock memblock_remove(phys_addr_t base, phys_addr_t size)
{
	return __memblock_remove(&memblock.memory, base, size);
}

memblock_remove() 函数删除 memory 类型中从基地址 base 开始,总共 size 大小的内存区域。其内部调用了 __memblock_remove() 函数完成实际移除工作。

5.2.3 __memblock_remove()

c 复制代码
// file: mm/memblock.c
static int __init_memblock __memblock_remove(struct memblock_type *type,
					     phys_addr_t base, phys_addr_t size)
{
	int start_rgn, end_rgn;
	int i, ret;

	ret = memblock_isolate_range(type, base, size, &start_rgn, &end_rgn);
	if (ret)
		return ret;

	for (i = end_rgn - 1; i >= start_rgn; i--)
		memblock_remove_region(type, i);
	return 0;
}

__memblock_remove() 函数执行内存区域的实际删除工作。该函数接收 3 个参数:

  • @type:待删除内存区域类型
  • @base:待删除内存区域的基地址
  • @size:待删除内存区域的大小

该函数流程也非常简单。首先,调用 memblock_isolate_range() 函数将指定的内存区域从现存区域中隔离出来,与现存内存块有交集的,需要对其进行分割。隔离后,待删除内存区域在数组中的索引在 [start_rgn, end_rgn) 区间。这是一个前闭后开的区间,所以索引为 end_rgn 的内存块并不在删除范围内。

隔离完成后,使用 for 循环,从后向前调用 memblock_remove_region() 函数依次删除索引区间内的内存块。由于 end_rgn 不在删除范围内,所以是从 end_rgn - 1 开始删除的。

5.3 查找空闲区域

5.3.1 memblock_find_in_range()

c 复制代码
// file: mm/memblock.c
/**
 * memblock_find_in_range - find free area in given range
 * @start: start of candidate range
 * @end: end of candidate range, can be %MEMBLOCK_ALLOC_{ANYWHERE|ACCESSIBLE}
 * @size: size of free area to find
 * @align: alignment of free area to find
 *
 * Find @size free area aligned to @align in the specified range.
 *
 * RETURNS:
 * Found address on success, %0 on failure.
 */
phys_addr_t __init_memblock memblock_find_in_range(phys_addr_t start,
					phys_addr_t end, phys_addr_t size,
					phys_addr_t align)
{
	return memblock_find_in_range_node(start, end, size, align,
					   MAX_NUMNODES);
}

memblock_find_in_range() 函数接收 4 个参数:

  • @start: 候选区域的起始地址
  • @end: 候选区域的结束地址
  • @size:需要查找的空闲区域大小
  • @align:空闲区域的对齐字节

该函数从可用内存( memory 类型)中,在 startend 范围内,查找大小为 size 的空闲区域,空闲区域要对齐到 align 字节。函数内部,直接调用 memblock_find_in_range_node() 完成查找功能。由于未指定查找节点,在 memblock_find_in_range_node() 函数中使用了最大节点 MAX_NUMNODES ,此时,允许从任意节点进行查找。

查找成功,返回找到的地址;失败,返回 0。

5.3.2 memblock_find_in_range_node()

c 复制代码
// file: mm/memblock.c
/**
 * memblock_find_in_range_node - find free area in given range and node
 * @start: start of candidate range
 * @end: end of candidate range, can be %MEMBLOCK_ALLOC_{ANYWHERE|ACCESSIBLE}
 * @size: size of free area to find
 * @align: alignment of free area to find
 * @nid: nid of the free area to find, %MAX_NUMNODES for any node
 *
 * Find @size free area aligned to @align in the specified range and node.
 *
 * RETURNS:
 * Found address on success, %0 on failure.
 */
phys_addr_t __init_memblock memblock_find_in_range_node(phys_addr_t start,
					phys_addr_t end, phys_addr_t size,
					phys_addr_t align, int nid)
{
	phys_addr_t this_start, this_end, cand;
	u64 i;

	/* pump up @end */
	if (end == MEMBLOCK_ALLOC_ACCESSIBLE)
		end = memblock.current_limit;

	/* avoid allocating the first page */
	start = max_t(phys_addr_t, start, PAGE_SIZE);
	end = max(start, end);

	for_each_free_mem_range_reverse(i, nid, &this_start, &this_end, NULL) {
		this_start = clamp(this_start, start, end);
		this_end = clamp(this_end, start, end);

		if (this_end < size)
			continue;

		cand = round_down(this_end - size, align);
		if (cand >= this_start)
			return cand;
	}
	return 0;
}

memblock_find_in_range_node() 函数比 memblock_find_in_range() 函数多了个参数 nid,允许在指定节点查找空闲区域。当 nidMAX_NUMNODES 时,查找范围扩大到任意节点。

首先,对候选区域的结束地址 end 进行修正。当 endMEMBLOCK_ALLOC_ACCESSIBLE 时,将其修正为内存块的最大地址 memblock.current_limit

MEMBLOCK_ALLOC_ACCESSIBLE 扩展为 0:

c 复制代码
// file: include/linux/memblock.h
#define MEMBLOCK_ALLOC_ACCESSIBLE	0

然后,为了避免将 zeropage 分配出去,将查找起始地址修正到 PAGE_SIZE 以上。zeropage 中保存着启动参数,不能被破坏。

接下来,使用 for_each_free_mem_range_reverse 宏(见 5.3.3 节),遍历可用(memory 类型)及保留(reserved 类型)的内存块,查找空闲区域。成功找到后,this_start 中保存着空闲区域的起始地址,this_end 中保存着结束地址。

接下来就要检查找到的空闲区域,其空间大小是否满足需求。

使用 clamp() 函数将 this_startthis_end 限制在 startend 之间。

clamp() 函数首先会检查入参类型是否一致,然后将 val 限制在 minmax 之间。即如果 val < min,就返回 min;如果 val > max,就返回 max;否则,返回 val 本身。

c 复制代码
// file: include/linux/kernel.h
/**
 * clamp - return a value clamped to a given range with strict typechecking
 * @val: current value
 * @min: minimum allowable value
 * @max: maximum allowable value
 *
 * This macro does strict typechecking of min/max to make sure they are of the
 * same type as val.  See the unnecessary pointer comparisons.
 */
#define clamp(val, min, max) ({			\
	typeof(val) __val = (val);		\
	typeof(min) __min = (min);		\
	typeof(max) __max = (max);		\
	(void) (&__val == &__min);		\
	(void) (&__val == &__max);		\
	__val = __val < __min ? __min: __val;	\
	__val > __max ? __max: __val; })

如果 this_end < size ,说明空闲区域的空间肯定不够,则进行下一轮查找。

否则,需要进一步判断空闲区域的容量是否满足需求。由于有对齐要求,调用 round_down() 函数将起始地址线下圆整对齐到 align 字节。如果对齐后的起始地址 cand 比空闲区域的起始地址 this_start 大,说明该区域足够大,满足要求,返回对齐后的地址 cand;否则返回 0。

5.3.3 for_each_free_mem_range_reverse

完成查找操作的核心代码在 for_each_free_mem_range_reverse 宏中,我们来看下该宏的实现:

c 复制代码
// file: include/linux/memblock.h
/**
 * for_each_free_mem_range_reverse - rev-iterate through free memblock areas
 * @i: u64 used as loop variable
 * @nid: node selector, %MAX_NUMNODES for all nodes
 * @p_start: ptr to phys_addr_t for start address of the range, can be %NULL
 * @p_end: ptr to phys_addr_t for end address of the range, can be %NULL
 * @p_nid: ptr to int for nid of the range, can be %NULL
 *
 * Walks over free (memory && !reserved) areas of memblock in reverse
 * order.  Available as soon as memblock is initialized.
 */
#define for_each_free_mem_range_reverse(i, nid, p_start, p_end, p_nid)	\
	for (i = (u64)ULLONG_MAX,					\
	     __next_free_mem_range_rev(&i, nid, p_start, p_end, p_nid);	\
	     i != (u64)ULLONG_MAX;					\
	     __next_free_mem_range_rev(&i, nid, p_start, p_end, p_nid))

该宏接收 5 个参数:

  • @i:u64 类型的循环变量;
  • @nid :指定搜索区域的节点 ID。如果是 MAX_NUMNODES,则对节点不做限制,任何节点都可以;
  • @p_start:用来保存匹配到的区域的起始地址;
  • @p_end:用来保存匹配到的区域的结束地址;
  • @p_nid:用来保存匹配到的区域的节点 ID。

该宏内部调用了 __next_free_mem_range_rev() 函数。在介绍函数的实现之前,先说一下该函数的实现逻辑。

memblock 中,保存有两种类型的内存块:memoryreserve。其中,memory 类型中保存的是可用的内存块;reserve 类型中保存的是保留的、已用的内存块,这些内存块不能被分配。我们在查找空闲区域时,找的是存在于 memory 中但不在 reserved 中的区域,这些才是可分配内存。换句话说,要找reserved中的间隙区域,如果间隙区域与 memory 中的内存块有交集,说明有可分配空间。

所以,问题转换为查找reserved 中的间隙块与 memory 中的内存块的重叠区域。

关于保留的内存块与间隙块之间的关系,__next_free_mem_range() 函数的注释中给出了很好的示例:

c 复制代码
/**The lower 32bit of
 * *@idx contains index into memory region and the upper 32bit indexes the
 * areas before each reserved region.  For example, if reserved regions
 * look like the following,
 *
 *	0:[0-16), 1:[32-48), 2:[128-130)
 *
 * The upper 32bit indexes the following regions.
 *
 *	0:[0-0), 1:[16-32), 2:[48-128), 3:[130-MAX)
   ......                                 
*/

也就是说,假如保留的内存块有 3个,分别为:

c 复制代码
0:[0-16), 1:[32-48), 2:[128-130)

那么间隙块就有 4 个,分别为:

c 复制代码
0:[0-0), 1:[16-32), 2:[48-128), 3:[130-MAX)

冒号前是索引,冒号后是内存地址区间。

5.3.4 __next_free_mem_range_rev()

c 复制代码
// file: mm/memblock.c
/**
 * __next_free_mem_range_rev - next function for for_each_free_mem_range_reverse()
 * @idx: pointer to u64 loop variable
 * @nid: nid: node selector, %MAX_NUMNODES for all nodes
 * @out_start: ptr to phys_addr_t for start address of the range, can be %NULL
 * @out_end: ptr to phys_addr_t for end address of the range, can be %NULL
 * @out_nid: ptr to int for nid of the range, can be %NULL
 *
 * Reverse of __next_free_mem_range().
 */
void __init_memblock 	__next_free_mem_range_rev(u64 *idx, int nid,
					   phys_addr_t *out_start,
					   phys_addr_t *out_end, int *out_nid)
{
	struct memblock_type *mem = &memblock.memory;
	struct memblock_type *rsv = &memblock.reserved;
	int mi = *idx & 0xffffffff;
	int ri = *idx >> 32;

	if (*idx == (u64)ULLONG_MAX) {
		mi = mem->cnt - 1;
		ri = rsv->cnt;
	}

	for ( ; mi >= 0; mi--) {
		struct memblock_region *m = &mem->regions[mi];
		phys_addr_t m_start = m->base;
		phys_addr_t m_end = m->base + m->size;

		/* only memory regions are associated with nodes, check it */
		if (nid != MAX_NUMNODES && nid != memblock_get_region_node(m))
			continue;

		/* scan areas before each reservation for intersection */
		for ( ; ri >= 0; ri--) {
			struct memblock_region *r = &rsv->regions[ri];
			phys_addr_t r_start = ri ? r[-1].base + r[-1].size : 0;
			phys_addr_t r_end = ri < rsv->cnt ? r->base : ULLONG_MAX;

			/* if ri advanced past mi, break out to advance mi */
			if (r_end <= m_start)
				break;
			/* if the two regions intersect, we're done */
			if (m_end > r_start) {
				if (out_start)
					*out_start = max(m_start, r_start);
				if (out_end)
					*out_end = min(m_end, r_end);
				if (out_nid)
					*out_nid = memblock_get_region_node(m);

				if (m_start >= r_start)
					mi--;
				else
					ri--;
				*idx = (u32)mi | (u64)ri << 32;
				return;
			}
		}
	}

	*idx = ULLONG_MAX;
}

接下来分析 __next_free_mem_range_rev() 函数的执行过程。

c 复制代码
	struct memblock_type *mem = &memblock.memory;
	struct memblock_type *rsv = &memblock.reserved;
	int mi = *idx & 0xffffffff;
	int ri = *idx >> 32;

首先,获取到 memory 类型及 reserved 类型内存集合的地址,保存到指针 memrsv 中。

其次,idx 是一个 u64 类型变量,它的低 32 位保存可用的内存块索引,高 32 位保存间隙块 的索引。将这 2 个索引提取出来,分别保存在变量 mi(memroy index) 及 ri (reserved index)中。

c 复制代码
	if (*idx == (u64)ULLONG_MAX) {
		mi = mem->cnt - 1;
		ri = rsv->cnt;
	}

循环开始前,将 miri 初始化。注意,mi 被初始化为可用内存块 的最大索引,而 ri 被初始化为间隙块最大索引。间隙块的数量比保留内存块多一个(见上文)。

c 复制代码
	for ( ; mi >= 0; mi--) {
		struct memblock_region *m = &mem->regions[mi];
		phys_addr_t m_start = m->base;
		phys_addr_t m_end = m->base + m->size;

		/* only memory regions are associated with nodes, check it */
		if (nid != MAX_NUMNODES && nid != memblock_get_region_node(m))
			continue;

		/* scan areas before each reservation for intersection */
		for ( ; ri >= 0; ri--) {
			......
            ......
            ......
		}
	}

接下来进行外层循环,遍历可用内存块,m_startm_end 分别表示该内存块的起始和结束地址。如果该内存块的 nid 与指定的节点 ID 不匹配,而又不允许使用任意节点,则进行下一轮匹配。

c 复制代码
	for ( ; mi >= 0; mi--) {
		......
        ......
        ......

		/* scan areas before each reservation for intersection */
		for ( ; ri >= 0; ri--) {
			struct memblock_region *r = &rsv->regions[ri];
			phys_addr_t r_start = ri ? r[-1].base + r[-1].size : 0;
			phys_addr_t r_end = ri < rsv->cnt ? r->base : ULLONG_MAX;

			/* if ri advanced past mi, break out to advance mi */
			if (r_end <= m_start)
				break;
			/* if the two regions intersect, we're done */
			if (m_end > r_start) {
				if (out_start)
					*out_start = max(m_start, r_start);
				if (out_end)
					*out_end = min(m_end, r_end);
				if (out_nid)
					*out_nid = memblock_get_region_node(m);

				if (m_start >= r_start)
					mi--;
				else
					ri--;
				*idx = (u32)mi | (u64)ri << 32;
				return;
			}
		}
	}

接下来遍历间隙块,ri 是间隙块的索引,r_startr_end 分别是间隙块的起始和结束地址。

如果 r_end <= m_start,说明可用内存块与间隙块没有交集,跳出内层循环,将外层循环的内存块向前推进一个。

如果 m_end > r_start ,说明内存块和间隙块有交集,重叠部分就是可分配内存。此时就需要找出重叠部分的起始及结束地址,并保存到 out_startout_end 指向的变量中,同时将可用内存的节点 ID 保存到 out_nid 指向的变量中。接下来执行以下流程:

  • 如果 m_start >= r_start,说明该内存块 已经完全使用了,则使 mi 自减一,再次匹配时从下一个内存块开始。

  • 否则,如果m_start < r_start,说明该间隙块 已经完全使用了,则使 ri 自减一,再次匹配时从下一个间隙块开始。

  • 接下来,将当前的 rimi 索引保存到 idx 中,供下次查找使用。

  • 最后,return 返回。

如果整个循环完成后,没有找到可分配的内存区间,则将 idx 指向的变量设置为 ULLONG_MAX。在 for_each_free_mem_range_reverse 循环中,如果发现 *idx 的值为 ULLONG_MAX,循环就会停止。

5.4 分配内存

分配内存主要涉及以下几个函数:

c 复制代码
// file: include/linux/memblock.h
phys_addr_t memblock_alloc_nid(phys_addr_t size, phys_addr_t align, int nid);
phys_addr_t memblock_alloc_try_nid(phys_addr_t size, phys_addr_t align, int nid);

phys_addr_t memblock_alloc(phys_addr_t size, phys_addr_t align);

phys_addr_t memblock_alloc_base(phys_addr_t size, phys_addr_t align,
				phys_addr_t max_addr);
phys_addr_t __memblock_alloc_base(phys_addr_t size, phys_addr_t align,
				  phys_addr_t max_addr);

5.4.1 memblock_alloc()

c 复制代码
// file: mm/memblock.c
phys_addr_t __init memblock_alloc(phys_addr_t size, phys_addr_t align)
{
	return memblock_alloc_base(size, align, MEMBLOCK_ALLOC_ACCESSIBLE);
}

memblock_alloc() 函数接收 2 个参数:

  • @size:内存大小
  • @align:对齐字节

该函数用来分配 size 大小的内存,并对齐到 align 字节。

函数内部直接调用 memblock_alloc_base() 来执行实际分配工作,该函数的第 3 个参数表示可分配的最大地址。宏 MEMBLOCK_ALLOC_ACCESSIBLE 扩展为 0,表示可用的最大地址。

c 复制代码
// file: include/linux/memblock.h
/* Flags for memblock_alloc_base() amd __memblock_alloc_base() */
#define MEMBLOCK_ALLOC_ACCESSIBLE	0

5.4.2 memblock_alloc_base()

c 复制代码
// file: mm/memblock.c
phys_addr_t __init memblock_alloc_base(phys_addr_t size, phys_addr_t align, phys_addr_t max_addr)
{
	phys_addr_t alloc;

	alloc = __memblock_alloc_base(size, align, max_addr);

	if (alloc == 0)
		panic("ERROR: Failed to allocate 0x%llx bytes below 0x%llx.\n",
		      (unsigned long long) size, (unsigned long long) max_addr);

	return alloc;
}

memblock_alloc_base() 内部调用 __memblock_alloc_base() 函数完成内存分配。

5.4.3 __memblock_alloc_base()

c 复制代码
phys_addr_t __init __memblock_alloc_base(phys_addr_t size, phys_addr_t align, phys_addr_t max_addr)
{
	return memblock_alloc_base_nid(size, align, max_addr, MAX_NUMNODES);
}

__memblock_alloc_base 内部调用 memblock_alloc_base_nid() 函数完成内存分配。由于未指定节点 ID,默认使用 MAX_NUMNODES ,表示可在任意节点进行分配。

5.4.4 memblock_alloc_base_nid()

c 复制代码
static phys_addr_t __init memblock_alloc_base_nid(phys_addr_t size,
					phys_addr_t align, phys_addr_t max_addr,
					int nid)
{
	phys_addr_t found;

	if (WARN_ON(!align))
		align = __alignof__(long long);

	/* align @size to avoid excessive fragmentation on reserved array */
	size = round_up(size, align);

	found = memblock_find_in_range_node(0, max_addr, size, align, nid);
	if (found && !memblock_reserve(found, size))
		return found;

	return 0;
}

memblock_alloc_base_nid() 函数在指定节点分配内存,该函数接收 4 个参数:

  • @size:要分配的内存大小;
  • @align:对齐字节;
  • @max_addr:查找的最大地址,即分配的内存区域不能超过该地址;
  • @nid:要分配内存的节点 ID,即在哪个节点上分配内存。

当对齐字节 align 为 0 时,会打印警告信息,并将 align 修改为对齐到 long long 类型。__alignof__是 gcc 内建关键字,用来获取指定类型的对齐字节,可参考 gcc 文档 Alignment

然后将 size 向上圆整,对齐到 align 字节。

随后,调用 memblock_find_in_range_node() 函数在指定的区域及节点查找可分配内存。如果成功找到,返回内存的起始地址;否则,返回 0。

接下来,如果找到可分配内存并且成功将该段内存添加到保留区里,则分配成功,返回内存的起始地址;否则,返回 0。

注:memblock_reserve() 函数执行成功,返回 0;否则返回负的错误码。

5.4.5 memblock_alloc_nid()

c 复制代码
phys_addr_t __init memblock_alloc_nid(phys_addr_t size, phys_addr_t align, int nid)
{
	return memblock_alloc_base_nid(size, align, MEMBLOCK_ALLOC_ACCESSIBLE, nid);
}

memblock_alloc_nid() 函数在指定节点分配内存,内部直接调用了 memblock_alloc_base_nid() 函数。

5.4.6 memblock_alloc_try_nid()

c 复制代码
phys_addr_t __init memblock_alloc_try_nid(phys_addr_t size, phys_addr_t align, int nid)
{
	phys_addr_t res = memblock_alloc_nid(size, align, nid);

	if (res)
		return res;
	return memblock_alloc_base(size, align, MEMBLOCK_ALLOC_ACCESSIBLE);
}

memblock_alloc_try_nid() 函数首先调用 memblock_alloc_nid() 函数在指定节点分配内存。如果分配成功,直接返回内存地址。否则,调用 memblock_alloc_base() 函数在任意节点分配内存。

5.5 释放内存

5.5.1 memblock_free()

c 复制代码
int __init_memblock memblock_free(phys_addr_t base, phys_addr_t size)
{
	memblock_dbg("   memblock_free: [%#016llx-%#016llx] %pF\n",
		     (unsigned long long)base,
		     (unsigned long long)base + size,
		     (void *)_RET_IP_);

	return __memblock_remove(&memblock.reserved, base, size);
}

memblock_free() 函数内部直接调用 __memblock_remove() 函数删除保留区内从 base 开始,大小为 size 的区域。

六、memblock 填充 -- memblock_x86_fill()

c 复制代码
// file: arch/x86/kernel/setup.c
void __init setup_arch(char **cmdline_p)
{
    ......
        
	memblock_x86_fill();
    
    ......
}

setup_arch() 函数中,调用 memblock_x86_fill()memblock 进行填充。

c 复制代码
// file: arch/x86/kernel/e820.c
void __init memblock_x86_fill(void)
{
	int i;
	u64 end;

	/*
	 * EFI may have more than 128 entries
	 * We are safe to enable resizing, beause memblock_x86_fill()
	 * is rather later for x86
	 */
	memblock_allow_resize();

	for (i = 0; i < e820.nr_map; i++) {
		struct e820entry *ei = &e820.map[i];

		end = ei->addr + ei->size;
		if (end != (resource_size_t)end)
			continue;

		if (ei->type != E820_RAM && ei->type != E820_RESERVED_KERN)
			continue;

		memblock_add(ei->addr, ei->size);
	}

	/* throw away partial pages */
	memblock_trim_memory(PAGE_SIZE);

	memblock_dump_all();
}

函数内部,首先调用 memblock_allow_resize() 函数,将 memblock_can_resize 标志设置为 1,允许数组容量不足时进行自动扩容。

c 复制代码
void __init memblock_allow_resize(void)
{
	memblock_can_resize = 1;
}

接下来,遍历 e820 中的内存项,将可用内存(E820_RAME820_RESERVED_KERN 类型)添加到内存块中。这里调用的是 memblock_add() 函数,所以会添加到 memory 类型的内存块中。

我们在 Linux Kernel:物理内存布局探测 中介绍了物理内存的探测,探测出的内存在 setup_memory_map() 函数中经过净化后保存到 e820 变量中。

c 复制代码
// file: arch/x86/kernel/e820.c
struct e820map e820;

e820struct e820map 结构体类型的变量,结构体定义如下:

c 复制代码
// file: arch/x86/include/uapi/asm/e820.h
struct e820entry {
	__u64 addr;	/* start of memory segment */
	__u64 size;	/* size of memory segment */
	__u32 type;	/* type of memory segment */
} __attribute__((packed));

struct e820map {
	__u32 nr_map;
	struct e820entry map[E820_X_MAX];
};

添加完成后,调用 memblock_trim_memory() 函数对内存块进行裁剪。该函数接收 1 个参数,即对齐字节。

c 复制代码
void __init_memblock memblock_trim_memory(phys_addr_t align)
{
	int i;
	phys_addr_t start, end, orig_start, orig_end;
	struct memblock_type *mem = &memblock.memory;

	for (i = 0; i < mem->cnt; i++) {
		orig_start = mem->regions[i].base;
		orig_end = mem->regions[i].base + mem->regions[i].size;
		start = round_up(orig_start, align);
		end = round_down(orig_end, align);

		if (start == orig_start && end == orig_end)
			continue;

		if (start < end) {
			mem->regions[i].base = start;
			mem->regions[i].size = end - start;
		} else {
			memblock_remove_region(mem, i);
			i--;
		}
	}
}

该函数会遍历所有可用内存块,将起始地址向上圆整 对齐到指定字节;将结束地址向下圆整对齐到指定字节。

如果对齐后,起始地址和结束地址没什么变化,说明该内存块本身已经对齐了,不需要任何处理。

如果对齐后,满足 start < end ,说明有可用空间,更新内存块的数据;否则,说明对齐后内存块大小为负数,该内存块无效,调用memblock_remove_region() 函数将该内存块删除。

memblock_x86_fill() 函数中,会将内存块对齐到 PAGE_SIZE 大小,即页大小。

裁剪完之后,调用 memblock_dump_all() 函数打印出 memblock 信息:

c 复制代码
// file: include/linux/memblock.h
static inline void memblock_dump_all(void)
{
	if (memblock_debug)
		__memblock_dump_all();
}

如果配置了 memblock_debug 项,则调用 __memblock_dump_all() 函数打印信息。

c 复制代码
// file: mm/memblock.c
int memblock_debug __initdata_memblock;

memblock_debug 是一个 int 类型的全局变量。

c 复制代码
// file: mm/memblock.c
static int __init early_memblock(char *p)
{
	if (p && strstr(p, "debug"))
		memblock_debug = 1;
	return 0;
}
early_param("memblock", early_memblock);

如果指定了命令行选项 memblock=debug,则该变量被设置为 1,允许进行 memblock 相关的调试。

c 复制代码
// file: mm/memblock.c
void __init_memblock __memblock_dump_all(void)
{
	pr_info("MEMBLOCK configuration:\n");
	pr_info(" memory size = %#llx reserved size = %#llx\n",
		(unsigned long long)memblock.memory.total_size,
		(unsigned long long)memblock.reserved.total_size);

	memblock_dump(&memblock.memory, "memory");
	memblock_dump(&memblock.reserved, "reserved");
}

__memblock_dump_all() 函数会打印出相关信息。

七、参考资料

1、Boot time memory management

2、Memblock

相关推荐
原机小子2 小时前
探索Vim的窗口艺术:窗口拆分功能全解析
linux·编辑器·vim
一颗星的征途7 小时前
宝塔-Linux模板常用命令-centos7
linux·运维·服务器
打打打劫7 小时前
Linux字符设备驱动
linux
cssl-虞老师7 小时前
Ubuntu安装Docker
linux·ubuntu·docker
Xu-小安安8 小时前
linux less命令详解
linux·less
huhy~8 小时前
PDI-kettle工具连接本地虚拟机Ubuntu上的数据库
linux·数据库·ubuntu
w_outlier8 小时前
gcc/g++的四步编译
linux·c++·gcc·g++
AllenLeungX8 小时前
Linux 安装 sftp
linux·运维·服务器
lendq9 小时前
k8s-第九节-命名空间
linux·容器·kubernetes
陪我养猪吧9 小时前
Linux 服务器环境搭建
linux·服务器·redis·mysql·nginx·jdk·maven