早期内存分配器:memblock

    内核启动初期,常用内存分配器(memory allocator)还未被初始化而不能使用,在此期间memblock是一种用于管理内存区域的方法。memblock也是一种内存分配器,在内核启动阶段它是第一个被启用的内存分配器,在其他内存分配器可用之前使用它向内核注册指定的物理内存区间。memblock主要在启动阶段被使用,但若启用了内存热插拔机制,在内核运行时也需要使用memblock功能。2010年内核v.2.6.35版本首次引入memblock补丁,在此之前使用的内存分配器是bootmem。bootmem仅能管理启动时所需的部分物理内存(lowmem),但memblock能管理全部可用物理内存。在内核的buddy allocator(也是一种page allocator)可用之前,memblock是唯一能够在早期启动阶段管理内存的内存分配器。因此,这正是早期内存分配器(early memory allocator)术语的由来。


  • Target Platform: Rock960c
  • ARCH: arm64
  • Linux Kernel: linux-4.19.27

memblock结构

memblock可分为如下三种类型:

  • memory类型:memory类型用于描述可用的物理内存区域。通过设置内核参数能够仅注册实际物理内存的有限部分。最初,该类型的regions数组最多可以描述128个内存区域,但可以调整这个数组的大小,每次扩大都是以翻倍的形式扩充数组。
  • reserved类型:reserved类型用于描述正在使用的或即将被使用的物理内存块。起初,该类型的regions数组最多可以描述128个物理内存块,但可以通过翻倍的形式扩充这个数组。
  • physmem类型:physmem类型是在2014年额外添加的一种类型,用于描述硬件探查到的真实物理内存区间,一旦注册了将不能改变这个区间的大小。与memory类型的区间不同,physmem类型的区间只能是真实大小的物理内存区域,而不能是通过内核参数指定的,目前s390架构相关代码会注册这种类型的内存区域。该类型的regions数组起初最多可以描述四个物理内存区域。

如图1所示给出了memblock的内存区间类型和memblock数据结构的关系。

memblock_struct.svg

图1 memblock结构

    如图2所示给出了一个例子,用一个memblock_region实例描述memory类型的1GiB可用物理内存区间,并用两个memblock_region实例描述reserved类型的两个子区间,指示两个子内存区间正在被使用而保留。

memblock_config_struct.svg

图2 物理内存配置与memblock数据结构的联系

通过下面三个表格分别说明memblock、memblock_type和memblock_region三个数据结构及其各个字段含义。

表1 memblock 数据结构说明
字段类型字段名含义解释
boolbottom_up在分配内存时决定是自底向上地还是自顶向下地搜寻空闲内存区间,不管哪种方式都适用于x86_64 NUMA系统。若在NUMA系统启用了hotplug功能,不管哪种方式也都能有效地分配热插拔内存。比如尝试从内存低地址处开始搜寻空闲内存时,应尽量从某一个内存节点分配空闲内存区间,这样能使从其他节点获取内存区间的频率最小化(true=自底向上)
phys_addr_tcurrent_limit用于限定分配的物理地址。比如在64位系统,由于所有物理内存能与虚拟内存直接映射,那也就不存在低端内存(lowmem),所以该字段会被设置为MEMBLOCK_ALLOC_ANYWHERE(0xffffffff_ffffffff)
struct memblock_typememory可用的物理内存区域
struct memblock_type
reserved
正被使用而保留的内存区间
struct memblock_type
physmem
系统存在的所有物理内存区域(只有部分体系架构支持
表2 memblock_type数据结构说明
字段类型字段名含义解释
unsigned longcnt当前regions数组的大小
unsigned longmaxregions数组最大容量,也就是最多能描述的物理内存区间个数
phys_addr_ttotal_size所有已注册内存区间的长度总和
struct memblock_region *regions指向描述内存区间的数组
char *name内存区间的类型名称
表3 memblock_region数据结构说明
字段类型字段名含义解释
phys_addr_t
base物理内存区间的起始地址(基地址)
phys_addr_tsize物理内存区间的长度(大小)
enum memblock_flags
flags物理内存区间的属性,flags可以单独使用如下四个宏常量,也可以通过组合使用最后三个bit flag:
1. MEMBLOCK_NONE(0x0):没有具体要求
2. MEMBLOCK_HOTPLUG(0x1):可热插拔的内存区间
3. MEMBLOCK_MIRROR(0x2):像镜子一样的内存区域
4. MEMBLOCK_NOMAP(0x4):不能加入内核直接映射(kernel direct mapping)的内存区间
intnid内存节点编号

memblock初始化

我们先看一下在编译时memblock的初始值。
mm/memblock.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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;
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
static struct memblock_region
memblock_physmem_init_regions[INIT_PHYSMEM_REGIONS] __initdata_memblock;
#endif

struct memblock memblock __initdata_memblock = {
.memory.regions = memblock_memory_init_regions,
.memory.cnt = 1, /* empty dummy entry */
.memory.max = INIT_MEMBLOCK_REGIONS,
.memory.name = "memory",

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

#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
.physmem.regions = memblock_physmem_init_regions,
.physmem.cnt = 1, /* empty dummy entry */
.physmem.max = INIT_PHYSMEM_REGIONS,
.physmem.name = "physmem",
#endif

.bottom_up = false,
.current_limit = MEMBLOCK_ALLOC_ANYWHERE,
};
  • 1~8行:在编译时memblock定义了三种描述内存区间的数组
    • memory memblock准备了能描述128个内存区域的数组
    • reserved memblock也准备了能描述128个内存区间的数组
    • physmem memblock 只准备了能描述4个内存区域的数组

memory memblock添加内存区域

    内核通过解析DTB文件的memory节点获得可用物理内存的起始地址和大小,并能通过类memblock_add的API往memory.regions数组添加一个memblock_region实例,用于管理这个物理内存区域。不过,某些特别的体系架构还支持自动识别出已安装内存的起始地址和大小等属性。若通过解析DTB文件添加物理内存区域,memory节点的reg属性给出内存的起始地址和大小,内核执行如下的函数调用路径来完成物理内存区域的注册。如图3所示显示了解析设备树和通过memblock_add API注册物理内存区间的处理流程。

memblock_add.svg

图3 memblock_add调用流程

    不管ARM还是ARM64都能够人为地限定可用物理内存的大小。在ARM64系统,若已安装DRAM的大小超过了虚拟内存的寻址界限(VA39-1,VA42-1,VA48-1等),则超过的物理内存区间目前将不能被利用。通过mem=内核参数能够限定可用物理内存的大小。

arm64_memblock_init:初始化memblock

    在内核启动初始化阶段,会使用reserved类型的memblock_region实例来描述下列物理内存区间,表示物理内存区间被预留和使用。根据不同架构和平台的配置,所预留的物理内存区间也是存在差异的。

  • 存放kernel映像的内存区间
  • 存放initrd数据的内存区间
  • 存放页表(page table)的内存区间
  • 存放DTB文件和其中reserved-mem节点指定的内存区间
  • 用于CMA-DMA的内存区间

    此外,DTB文件中chosen节点的linux,usable-memory-range属性能指定可用物理内存的范围,在此范围外的物理内存区间将从memory memblock移除。如图4所示显示了arm64_memblock_init函数会预留的物理内存区间。

arm64_memblock_init.svg

图4 arm64_memblock_init会预留的物理内存区间

在arch/arm64/mm/init.c中arm64_memblock_init函数(1/2):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
void __init arm64_memblock_init(void)
{
const s64 linear_region_size = -(s64)PAGE_OFFSET;

/* Handle linux,usable-memory-range property */
fdt_enforce_memory_region();

/* Remove memory above our supported physical address size */
memblock_remove(1ULL << PHYS_MASK_SHIFT, ULLONG_MAX);

BUILD_BUG_ON(linear_region_size != BIT(VA_BITS - 1));

memstart_addr = round_down(memblock_start_of_DRAM(),
ARM64_MEMSTART_ALIGN);

memblock_remove(max_t(u64, memstart_addr + linear_region_size,
__pa_symbol(_end)), ULLONG_MAX);
if (memstart_addr + linear_region_size < memblock_end_of_DRAM()) {
/* ensure that memstart_addr remains sufficiently aligned */
memstart_addr = round_up(memblock_end_of_DRAM() - linear_region_size,
ARM64_MEMSTART_ALIGN);
memblock_remove(0, memstart_addr);
}

if (memory_limit != PHYS_ADDR_MAX) {
memblock_mem_limit_remove_map(memory_limit);
memblock_add(__pa_symbol(_text), (u64)(_end - _text));
}

if (IS_ENABLED(CONFIG_BLK_DEV_INITRD) && initrd_start) {
u64 base = initrd_start & PAGE_MASK;
u64 size = PAGE_ALIGN(initrd_end) - base;

if (WARN(base < memblock_start_of_DRAM() ||
base + size > memblock_start_of_DRAM() +
linear_region_size,
"initrd not fully accessible via the linear mapping -- please check your bootloader ...\n")) {
initrd_start = 0;
} else {
memblock_remove(base, size); /* clear MEMBLOCK_ flags */
memblock_add(base, size);
memblock_reserve(base, size);
}
}

.......
  • 3行:在arm64内核,linear_region_size正好等于虚拟地址空间大小的二分之一,比如虚拟内存大小为512GiB(VA_BITS=39),那么linear_region_size = 256GiB
  • 13~14行:鉴于内核不同配置,通过向下舍去方式使DRAM起始地址对齐SECTION_SIZEPUD_SIZEPMD_SIZE,memstart_addr得到一个合适的物理内存基地址
  • 16~17行:内核映像有可能位于物理内存的顶端,顶部多余的物理内存区间将从memory memblock移除
  • 18~23行:若物理内存还超过linear mapping region的大小,底部超过的物理内存区间将从memory memblock移除。比如VA_BITS=39和DRAM=1TiB,linear mapping region只能有256GiB,其他768GiB内存区间不能被linear mapping容纳,需要从memory memblock中删除。
  • 25~28行:通过mem=内核参数缩减FDT指定的物理内存,多出的内存区间需从memory memblock剔除
  • 30~44行:在reserved memblock添加用于保存initrd数据的内存区间

在arch/arm64/mm/init.c中arm64_memblock_init函数(2/2):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
	......

if (IS_ENABLED(CONFIG_RANDOMIZE_BASE)) {
extern u16 memstart_offset_seed;
u64 range = linear_region_size -
(memblock_end_of_DRAM() - memblock_start_of_DRAM());

if (memstart_offset_seed > 0 && range >= ARM64_MEMSTART_ALIGN) {
range = range / ARM64_MEMSTART_ALIGN + 1;
memstart_addr -= ARM64_MEMSTART_ALIGN *
((range * memstart_offset_seed) >> 16);
}
}

memblock_reserve(__pa_symbol(_text), _end - _text);
#ifdef CONFIG_BLK_DEV_INITRD
if (initrd_start) {
memblock_reserve(initrd_start, initrd_end - initrd_start);

/* the generic initrd code expects virtual addresses */
initrd_start = __phys_to_virt(initrd_start);
initrd_end = __phys_to_virt(initrd_end);
}
#endif

early_init_fdt_scan_reserved_mem();

/* 4GB maximum for 32-bit only capable devices */
if (IS_ENABLED(CONFIG_ZONE_DMA32))
arm64_dma_phys_limit = max_zone_dma_phys();
else
arm64_dma_phys_limit = PHYS_MASK + 1;

reserve_crashkernel();

reserve_elfcorehdr();

high_memory = __va(memblock_end_of_DRAM() - 1) + 1;

dma_contiguous_reserve(arm64_dma_phys_limit);

memblock_allow_resize();
}
  • 3~14行:出于安全考虑,CONFIG_RANDOMIZE_BASE选项决定是否随机地改变物理内存起始地址memstart_addr
  • 15行:保留kernel 映像的内存区间
  • 17~23行:在(1/2)代码片段中为initrd保留了足够的内存区间,这里将物理地址initrd_start和initrd_end转换成对应的虚拟地址并保存
  • 26行:将保留与DTB有关的三种内存区间:
    • 用于存储DTB的内存区间
    • DTB头部的off_mem_rsvmap成员会指向描述预留内存区块的信息(二进制形式),解析并保留这些内存区间
    • DTB中reserved-mem节点指定的内存区间
  • 29~40行:在reserved memblock中保留设备驱动(DMA for Coherent / CMA for DMA)所需的DMA物理内存区间,并交给CMA(Contiguous Memory Allocator)进行管理。为了初始化CMA,将已保留的连续内存区间注册入cma_areas数组。
  • 42行:允许memblock regions数组扩容

memblock分配

    在内核启动的早期和内存热插拔「hotplug」过程都会使用涉及内存分配与释放的各种memblock API。如下图所示给出了在分配内时memblock API的调用流程。

memblock_alloc.svg

图5 memblock_alloc的调用关系

    接下来,让我们仔细研究一下memblcok分配过程中涉及的各种函数。

memblock_alloc:从memblock中分配内存

    size是从memblock分配时要求的内存区间长度,align是使内存区间起始地址对齐的数值,这两参数直接传递给被调用的memblock_alloc_base函数。此外,指定max_addr为MEMBLOCK_ALLOC_ACCESSIBLE来限定可分配内存的最大物理地址。
mm/memblock.c

1
2
3
4
phys_addr_t __init memblock_alloc(phys_addr_t size, phys_addr_t align)
{
return memblock_alloc_base(size, align, MEMBLOCK_ALLOC_ACCESSIBLE);
}

理解了上面的函数后,我们查看下面这个函数的实现。
mm/memblock.c

1
2
3
4
5
6
7
8
9
10
11
12
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 %pa bytes below %pa.\n",
&size, &max_addr);

return alloc;
}

下面的代码尝试在没有NUMA内存节点(nid)和flags限定的情况下分配空闲内存区间。
mm/memblock.c

1
2
3
4
5
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, NUMA_NO_NODE,
MEMBLOCK_NONE);
}

下面代码指定了memblock分配范围的起始为0。
mm/memblock.c

1
2
3
4
5
6
phys_addr_t __init memblock_alloc_base_nid(phys_addr_t size,
phys_addr_t align, phys_addr_t max_addr,
int nid, enum memblock_flags flags)
{
return memblock_alloc_range_nid(size, align, 0, max_addr, nid, flags);
}

memblock_alloc_range_nid:从特定memblock范围分配内存

    在分配范围、NUMA内存节点和flags限定的情况下寻找空闲内存区间,并将合适的内存区间插入reserved memblock,指示该区间已被使用和保留。
mm/memblock.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static phys_addr_t __init memblock_alloc_range_nid(phys_addr_t size,
phys_addr_t align, phys_addr_t start,
phys_addr_t end, int nid,
enum memblock_flags flags)
{
phys_addr_t found;

if (!align)
align = SMP_CACHE_BYTES;

found = memblock_find_in_range_node(size, align, start, end, nid,
flags);
if (found && !memblock_reserve(found, size)) {
kmemleak_alloc_phys(found, size, 0, 0);
return found;
}
return 0;
}
  • 8~9行:若align未设置,将系统的硬件高速缓存行大小赋值给align
  • 11~12行:使用要求的分配范围、内存节点和flags搜寻一个合适的空闲内存区间
  • 13~17行:如果寻找到了,将空闲内存区间插入reserved memblock,指示这段区间已被使用保留。若分配失败,返回0。

    如图6所示显示了memblock_find_in_range_node的处理流程。

memblock_find.svg

图6 搜寻特定大小内存块的处理流程

mm/memblock.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static phys_addr_t __init_memblock
__memblock_find_range_top_down(phys_addr_t start, phys_addr_t end,
phys_addr_t size, phys_addr_t align, int nid,
enum memblock_flags flags)
{
phys_addr_t this_start, this_end, cand;
u64 i;

for_each_free_mem_range_reverse(i, nid, flags, &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;
}

9~20行:自顶向下地搜寻空闲内存区间,即包含在memory membock但不包含在reserved memblock。如果寻找到的空闲内存来自指定的内存节点(@nid),位于@start~@end范围内,那么从中截取一个子区间,使其起始地址与@align对齐,区间长度大于等于@size,最后得到一个满足这些条件的子区间,并返回该子区间的首地址。

  • 循环逐个地搜寻空闲内存区间
  • 若空闲内存区间的尾地址小于size,那么舍弃这个区间,继续进行下一轮搜寻
  • 以this_end为准线向下延伸得到一个不小于@size的子区间,并且通过向下取整的方式使其起始地址对齐@align

    如图7所示,假如在执行__memblock_find_range_top_down函数时内存已有这些使用和保留,那么在循环搜寻时会得到7个空闲内存区间,不过只有4个内存区间在要求的范围内,因此会依次从这4个内存区间寻找符合要求的子区间。

memblock_example_iteration.svg

图7 memblock iteration example

    若不考虑内存节点属性,使用下面函数直接将一个基地址(base)和区间长度(size)给定的内存区间加入reserved memblock。
mm/memblock.c

1
2
3
4
5
6
7
8
9
int __init_memblock memblock_reserve(phys_addr_t base, phys_addr_t size)
{
phys_addr_t end = base + size - 1;

memblock_dbg("memblock_reserve: [%pa-%pa] %pF\n",
&base, &end, (void *)_RET_IP_);

return memblock_add_range(&memblock.reserved, base, size, MAX_NUMNODES, 0);
}

memblock增添

    memblock_add函数只能用于非NUMA系统添加可用物理内存。NUMA系统需要使用具有nid参数的memblock_add_node函数添加NUMA节点中可用物理内存。尽管在设计芯片时会考虑绝不让各个物理内存区域互相重叠,但是可能存在例外情况,并不能保证注册的内存区域一定不与已注册的内存区域出现重叠。如图8所示显示了向memory memblock添加可用物理内存区域的处理流程。

memblock_add_node.svg

图8 memblock添加的处理流程

    如图9所示,如果不考虑区间的内存节点属性,当在插入新的内存区域时有可能与已有区间重叠,这时会出现memblock的第一种合并。

memblock_merge.svg

图9 第一种memblock合并

memblock_add: 向memory memblock添加内存区域

    由于这个函数只用于注册UMA系统中可用物理内存,因此不必设置描述内存区间的所有属性。只需设置base和size来指定起始物理地址和区间长度,区间所属的内存节点nid默认地设定为MAX_NUMNODES,以及区间flags默认地设为0,指示区间无任何特殊要求。

1
2
3
4
int __init_memblock memblock_add(phys_addr_t base, phys_addr_t size)
{
return memblock_add_range(&memblock.memory, base, size, MAX_NUMNODES, 0);
}

上面函数将直接调用memblock_add_range函数。

memblock_add_range: 将特定内存区间注册入memblock

    这个函数将具体的内存区间添加到特定类型的memblock。即使新插入的内存区间与已存有的内存区间出现重叠,在插入新内存区间时并不会改变重叠的内存区间,只是将没有重叠的新内存区间插入。在插入之后,相邻且属性兼容的内存区间会合并成一个内存区间。这个函数返回值只是0。

在mm/memblock.c中的memblock_add_range函数(1/2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
int __init_memblock memblock_add_range(struct memblock_type *type,
phys_addr_t base, phys_addr_t size,
int nid, enum memblock_flags flags)
{
bool insert = false;
phys_addr_t obase = base;
phys_addr_t end = base + memblock_cap_size(base, &size);
int idx, nr_new;
struct memblock_region *rgn;

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;
type->regions[0].flags = flags;
memblock_set_region_node(&type->regions[0], nid);
type->total_size = size;
return 0;
}
repeat:
base = obase;
nr_new = 0;

for_each_memblock_type(idx, type, rgn) {
phys_addr_t rbase = rgn->base;
phys_addr_t rend = rbase + rgn->size;

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

if (rbase > base) {
#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP
WARN_ON(nid != memblock_get_region_node(rgn));
#endif
WARN_ON(flags != rgn->flags);
nr_new++;
if (insert)
memblock_insert_region(type, idx++, base,
rbase - base, nid,
flags);
}
/* area below @rend is dealt with, forget about it */
base = min(rend, end);
}

......
  • 15~22行:如果memblock还没有具体的内存区间,不需要考虑重叠问题,直接根据参数设置第一个内存区间,然后退出这个函数。
  • 24行:借助repeat标签能够再重复执行一次插入过程,第一次执行插入被成为first round,第二次执行插入被成为second round。在first round仅更新nr_new来统计需要插入的内存区间个数,在second round才真正地将这些非重叠的内存区间逐个插入特定类型的memblock。
  • 28行:通过循环遍历特定类型的memblock内存区间,判断是否与新插入的区间重叠。
  • 32行:新内存区间位于相比较的内存区间之下且不重叠,因此不必再继续循环遍历。
  • 34行:新内存区间位于相比较的内存区间之上且不重叠,因而需要继续循环遍历并与下一区间进行比较。
  • 37行:新内存区间与相比较内存区间的底部重叠,所以在此,新内存区间的非重叠部分将会被插入,并且插入的内存区间肯定与比较区间相邻。
  • 49行:若请求的新内存区间还存在需要被插入的子内存区间,提前更新base为下一个待插入子区间的起始地址。

在mm/memblock.c中的memblock_add_range函数(2/2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
	......

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

if (!nr_new)
return 0;

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行:在循环遍历结束后,若新内存区间的末端还有需要插入的子区间,这里将待插入子区间加入特定类型的memblock
  • 14行:在first round,只要已有区间个数和待插入区间个数的总和超过memblock最多能管理的区间个数,将重复调用memblock_double_array函数使memblock regions数组空间(regions数组的元素数量)翻倍。
  • 20行:在second round, 如果新插入的区间与已有区间相邻且有相同flag,将使用memblock_merge_regions函数将这些区间合并。

memblock_insert_region函数:将内存区间插入memblock

    将内存区间插入到具体memblock的给定位置,即在regions数组的指定位置插入一个memblock_region实例。memblock_type实例的cnt字段增加1,total_size字段累加size。

mm/memblock.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void __init_memblock memblock_insert_region(struct memblock_type *type,
int idx, phys_addr_t base,
phys_addr_t size,
int nid,
enum memblock_flags flags)
{
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;
rgn->flags = flags;
memblock_set_region_node(rgn, nid);
type->cnt++;
type->total_size += size;
}
  • 10行:从给定位置的区间到末尾区间都往后移动一个位置。
  • 14行:设置区间的nid(node ID)属性,更好地支持NUMA架构系统。

memblock_double_array函数:使memblock能管理的区间翻倍

    用于描述内存区间的memblock regions数组初始化时较小,因此在需要扩大时调用这个函数使数组大小翻倍。根据如下代码分析memblock_double_array函数。

在mm/memblock.c中memblock_double_array函数(1/2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
static int __init_memblock memblock_double_array(struct memblock_type *type,
phys_addr_t new_area_start,
phys_addr_t new_area_size)
{
struct memblock_region *new_array, *old_array;
phys_addr_t old_alloc_size, new_alloc_size;
phys_addr_t old_size, new_size, addr, new_end;
int use_slab = slab_is_available();
int *in_slab;

if (!memblock_can_resize)
return -1;

/* Calculate new doubled size */
old_size = type->max * sizeof(struct memblock_region);
new_size = old_size << 1;

old_alloc_size = PAGE_ALIGN(old_size);
new_alloc_size = PAGE_ALIGN(new_size);

/* Retrieve the slab flag */
if (type == &memblock.memory)
in_slab = &memblock_memory_in_slab;
else
in_slab = &memblock_reserved_in_slab;

if (use_slab) {
new_array = kmalloc(new_size, GFP_KERNEL);
addr = new_array ? __pa(new_array) : 0;
} else {
/* only exclude range when trying to double reserved.regions */
if (type != &memblock.reserved)
new_area_start = new_area_size = 0;

addr = memblock_find_in_range(new_area_start + new_area_size,
memblock.current_limit,
new_alloc_size, PAGE_SIZE);
if (!addr && new_area_size)
addr = memblock_find_in_range(0,
min(new_area_start, memblock.current_limit),
new_alloc_size, PAGE_SIZE);

new_array = addr ? __va(addr) : NULL;
}

......
  • 11~12行:memblock_allow_resize函数用于设置全局变量memblock_can_resize。如果memblock_allow_resize函数没有调用,那么memblock_can_resize未被设置为1,因此这个函数将不起作用。
  • 16行:计算准备重新分配的regions数组大小,其是已有regions数组大小的两陪。
  • 22行:为了明确指定类型(memblock_type实例)的regions数组是编译时static定义的或通过memblock操作动态分配的,还是通过slab操作动态分配的。
  • 27行:当正式的slab内存分配器起作用时,通过kmalloc函数给regions数组分配需要的内存。
  • 32行:如果slab不可用,尝试使用来自memblock_add函数的memblock_find_in_range函数去寻找一段空闲内存空间,存放用于管理区间的regions数组。如果指定memblock的类型不是reserved类型,即memory类型,那么要求的分配范围将从0开始。
    如果在可用物理内存注册入memory类型的memblock期间需要扩充regions数组,那么新分配的内存空间只可能从以前注册的物理内存中搜寻得到。无论如何,新分配的内存空间不能与已用的内存区间相互干涉。因此为了避免与指定内存范围重叠,在第一次搜寻时会从该范围之上的内存寻找空闲内存区间。
  • 38行:如果memblock的类型是reserved且在第一次搜寻没有找到合适的内存空间,为了避免与指定内存范围重叠,将从该范围之下的内存搜寻空闲内存区间。

memblock_double_array函数(2/2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
	......

if (!addr) {
pr_err("memblock: Failed to double %s array from %ld to %ld entries !\n",
type->name, type->max, type->max * 2);
return -1;
}

new_end = addr + new_size - 1;
memblock_dbg("memblock: %s is doubled to %ld at [%pa-%pa]",
type->name, type->max * 2, &addr, &new_end);

memcpy(new_array, type->regions, old_size);
memset(new_array + type->max, 0, old_size);
old_array = type->regions;
type->regions = new_array;
type->max <<= 1;

/* Free old array. We needn't free it if the array is the static one */
if (*in_slab)
kfree(old_array);
else if (old_array != memblock_memory_init_regions &&
old_array != memblock_reserved_init_regions)
memblock_free(__pa(old_array), old_alloc_size);

if (!use_slab)
BUG_ON(memblock_reserve(addr, new_alloc_size));

*in_slab = use_slab;

return 0;
}
  • 13~17行:将已有regions数组的所有内容全部复制到新分配的内存空间内,剩余未用的部分全部清零。而且还要更新memblock_type实例的max字段为原来值的两倍。
  • 21行:将老regions数组占用的内存空间释放。但由于最初的regions数组是在编译时定义的,因此不能删除和舍弃。只要regions数组不是最初的那个,会根据分配器类型对其采用不同释放方法。
    • 如果已使用了slab,用kfree函数释放占用的内存空间
    • 如果slab还不可用,用memblock_free函数释放已用的内存空间

memblock_merge_regions函数:将memblock的相邻内存区间合并

    如果相邻内存区间具有相同flags和nid,合并这两区间为一个较大区间。

mm/memblock.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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) ||
this->flags != next->flags) {
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--;
}
}
  • 6行:一旦合并,指定类型memblock中的区间个数(cnt)会递减,从而减少循环次数。
  • 10行:如果两个区间不相邻或区间的flags不同,这对区间不能合并,继续检查下一对区间。
  • 19行:若确定这对区间符合限定条件,合并它们。

如图10所示,除非具有不同的nid或flags属性,否则相邻的多个区间将被合并。

memblock_merge_II.svg

图10 第二种memblock合并

memblock删除

    当调用memblock_free和memblock_remove函数时,会执行如图11所示的处理流程。

memblock_remove.svg

图11 memblock_remove函数的执行流程

memblock_free函数:从reserved memblock中移除一个内存区间

    如果memblock_free函数被调用,则调用memblock_remove_range函数将由起始物理地址base和区间长度size限定的内存区间从reserved memblock中删除。

mm/memblock.c

1
2
3
4
5
6
7
8
9
10
int __init_memblock memblock_free(phys_addr_t base, phys_addr_t size)
{
phys_addr_t end = base + size - 1;

memblock_dbg(" memblock_free: [%pa-%pa] %pF\n",
&base, &end, (void *)_RET_IP_);

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

memblock_remove函数:从memory memblock中移除一个内存空间

    与memblock_free函数相似,这个函数调用memblock_remove_range函数将由起始物理地址base和区间长度size限定的内存区间从memory memblock中移除。

mm/memblock.c

1
2
3
4
5
6
7
8
9
int __init_memblock memblock_remove(phys_addr_t base, phys_addr_t size)
{
phys_addr_t end = base + size - 1;

memblock_dbg("memblock_remove: [%pa-%pa] %pS\n",
&base, &end, (void *)_RET_IP_);

return memblock_remove_range(&memblock.memory, base, size);
}

memblock_remove_range函数:从一个memblock中移除指定区间

    从指定类型的memblock中移除请求的区间。

mm/memblock.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int __init_memblock memblock_remove_range(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;
}
  • 7行:将待删除区间从已存在内存区间分离出来
  • 11行:从end待删除区间的索引值到start待删除区间的索引值逆序地循环调用memblock_remove_region函数将对应的区间删除。相反顺序的循环是为了减少内存复制的大小(内存移动的数据量)。

memblock_isolate_range函数: 从给定memblock中分离出请求的区间

    从与请求区间重叠的memblock区间中分离出待删除区间。起始待删除区间的索引值保存在输出参数start_rgn中,而末尾待删除区间的索引值保存在end_rgn中。如果memblock任意区间都没有与请求区间重叠,两个输出参数的值都是0.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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 idx;
struct memblock_region *rgn;

*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_each_memblock_type(idx, type, rgn) {
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, idx, rbase, base - rbase,
memblock_get_region_node(rgn),
rgn->flags);
} 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, idx--, rbase, end - rbase,
memblock_get_region_node(rgn),
rgn->flags);
} else {
/* @rgn is fully contained, record it */
if (!*end_rgn)
*start_rgn = idx;
*end_rgn = idx + 1;
}
}

return 0;
}
  • 5行:防止内存区间的结束物理地址超过体系架构支持的最大物理地址。
  • 15行:扩充管理区间的regions数组,使其容量是原来的两倍。如果扩充失败,将返回-ENOMEM指示系统内存不足。
  • 19行:会从memblock的第一区间循环遍历到最后一个区间,rbase和rend分别指的是当前遍历区间的起始物理地址和结束物理地址。
  • 23行:如果当前区间的起始地址rbase不小于请求区间的结束地址,则不必再继续遍历后面的区间(下图所示的条件A)。
  • 25行:如果当前区间的结束地址rend不大于请求区间的起始地址,则不可能重叠,因此需要继续遍历下一个区间(下图所示的条件B)。
  • 28行:如果当前区间的起始物理地址小于请求区间的起始地址,也就是说,请求区间的起始地址位于当前区间内,两者出现重叠子区间,则按如下方式处理:(如下图所示的条件C)
    首先以base为分界线从当前区间分离出包含重叠的上半部子区间,并且将上半部区间向上移动一个索引位置,其次将非重叠的下半部区间重新插入在给定memblock的当前索引位置。因此,这样会将当前区间拆分出两个子区间。
  • 39行:如果当前区间的结束物理地址大于请求区间的结束物理地址,也就是说,请求区间的结束物理地址位于当前区间内,两者出现重叠子区间,则按如下方式处理:(如下图所示的条件D):
    首先以end为分界线从当前区间分离出非重叠的上半部子区间,并且将上半部区间向上移动一个索引位置,其次将包含重叠的下半部区间重新插入在给定memblock的当前索引位置,而且索引值idx减1。因此,这样也会将当前区间拆分出两个子区间。
  • 50行:由于不满足上面任何一个条件,所以当前区间被包含于请求区间。在这种情况下,只有end_rgn还未更新,start_rgn才记录当前区间的索引值,但无论如何都会用当前索引值 + 1更新end_rgn。

memblock_isolate_range.svg

图12 memblock_isolate_range函数的执行流程

memblock_remove_region函数:从具体memblock中删除指定区间

    从给定类型的memblock中删除指定索引位置的区间。

mm/memblock.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
type->regions[0].flags = 0;
memblock_set_region_node(&type->regions[0], MAX_NUMNODES);
}
}
  • 3~6行:将带删除区间后面的所有区间都向前移动一个位置,并将cnt减1以及从total_size减去已删除区间的大小。
  • 9行:如果彻底移除了当前memblock的所有区间,则要将type->cnt更新为1而不是0。出于设计缘故,即使memblock没有内存区间,type->cnt也要设置为1,但base和size字段需要清零。