本文采用 Linux 内核 v3.10 版本 x86_64 架构
一、Bootmem 与 Memblock
系统初始化早期,由于"正常"的内存管理还未完成设置,所以无法使用。 此时,仍然需要为各种数据结构分配内存。 为了解决这个问题,引入了一种称为 Boot Memory Allocator
或 bootmem
的专用分配器。 几年后,PowerPC 开发人员添加了 Logical Memory Blocks
分配器,后来被其他架构采用并重命名为 memblock
。 另外,还有一个名为 nobootmem
的兼容层,它将 bootmem
内存分配接口转换为对 memblock
的函数调用。
早期分配器的选择是通过 CONFIG_NO_BOOTMEM
和 CONFIG_HAVE_MEMBLOCK
内核配置选项完成的。 这些选项在 Kconfig 文件中静态启用或禁用。
- 仅依赖
bootmem
的架构,选择CONFIG_NO_BOOTMEM=n
&&CONFIG_HAVE_MEMBLOCK=n
。 - 具有
nobootmem
兼容层的memblock
用户,设置CONFIG_NO_BOOTMEM=y
&&CONFIG_HAVE_MEMBLOCK=y
。 - 对于同时使用
memblock
和bootmem
的用户,设置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
结构体中,该结构在构建时被静态初始化。 memory
和reserved
类型的内存区域数组初始大小为 INIT_MEMBLOCK_REGIONS
。 memblock_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_region
、struct memblock_type
、struct 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 初始化
memblock
是 struct 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.regions
和 reserved.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;
insert
为 false
,指示不执行真正的插入流程;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);
}
在第一次遍历中,由于变量 insert
为 false
,所以不会直接插入, 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
中继续执行。第二次执行时,insert
为 true
,所以会执行真正的插入操作。
在第二次 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);
接下来,计算新的 base
值 base = 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_rgn
及 end_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 块,即上、下半部,并进入下一轮循环:
进入下一轮循环后,rbase
及 rend
的值分别指向分割后的上半部的起始及结束地址。
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
类型)中,在 start
到 end
范围内,查找大小为 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
,允许在指定节点查找空闲区域。当 nid
为 MAX_NUMNODES
时,查找范围扩大到任意节点。
首先,对候选区域的结束地址 end
进行修正。当 end
为 MEMBLOCK_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_start
及 this_end
限制在 start
到 end
之间。
clamp()
函数首先会检查入参类型是否一致,然后将 val
限制在 min
和 max
之间。即如果 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
中,保存有两种类型的内存块:memory
和 reserve
。其中,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
类型内存集合的地址,保存到指针 mem
及 rsv
中。
其次,idx
是一个 u64
类型变量,它的低 32 位保存可用的内存块索引,高 32 位保存间隙块 的索引。将这 2 个索引提取出来,分别保存在变量 mi
(memroy index) 及 ri
(reserved index)中。
c
if (*idx == (u64)ULLONG_MAX) {
mi = mem->cnt - 1;
ri = rsv->cnt;
}
循环开始前,将 mi
及ri
初始化。注意,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_start
和 m_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_start
和 r_end
分别是间隙块的起始和结束地址。
如果 r_end <= m_start
,说明可用内存块与间隙块没有交集,跳出内层循环,将外层循环的内存块向前推进一个。
如果 m_end > r_start
,说明内存块和间隙块有交集,重叠部分就是可分配内存。此时就需要找出重叠部分的起始及结束地址,并保存到 out_start
和 out_end
指向的变量中,同时将可用内存的节点 ID 保存到 out_nid
指向的变量中。接下来执行以下流程:
-
如果
m_start >= r_start
,说明该内存块 已经完全使用了,则使mi
自减一,再次匹配时从下一个内存块开始。 -
否则,如果
m_start < r_start
,说明该间隙块 已经完全使用了,则使ri
自减一,再次匹配时从下一个间隙块开始。 -
接下来,将当前的
ri
及mi
索引保存到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_RAM
及 E820_RESERVED_KERN
类型)添加到内存块中。这里调用的是 memblock_add()
函数,所以会添加到 memory
类型的内存块中。
我们在 Linux Kernel:物理内存布局探测 中介绍了物理内存的探测,探测出的内存在 setup_memory_map()
函数中经过净化后保存到 e820
变量中。
c
// file: arch/x86/kernel/e820.c
struct e820map e820;
e820
是 struct 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()
函数会打印出相关信息。
七、参考资料
2、Memblock