memblock 早期内存分配器详解
1 介绍
memblock 是内核在系统启动早期用于收集和管理物理内存的内存管理器,不同架构从不同地方收集物理内存(设备树,BIOS-e820 等)并添加到 memblock 中进行管理,在伙伴系统(Buddy System)接管内存管理之前为系统提供内存分配、预留等功能。在伙伴系统的接管内存管理时将 memblock中可用的空闲内存全部释放给伙伴系统,并丢弃 memblock 内存分配器。
注意:早期使用的引导内存分配器是bootmem,现在memblock取代了bootmem。
2 提供的接口
首先看看如何使用 memblock,memblock提供的能力可以分为四个部分:
- 内存添加,移除,预留(
memblock_add,memblock_reserve,memblock_remove等) - 内存分配释放(
memblock_alloc,memblock_phys_alloc_nid,memblock_free等) - 内存域遍历(
for_each_memblock_type,for_each_mem_pfn_range等) - 其他杂项,包括内存最大限制,对齐调整,分配方向等
上述描述的接口每个部分都有许多变体存在这里取其他核心 API 介绍,其他的都是基于核心 API 的变体。
2.1 内存添加预留接口
(1)memblock_add_range
/* Low level functions */
int memblock_add_range(struct memblock_type *type,
phys_addr_t base, phys_addr_t size,
int nid, enum memblock_flags flags);
所有物理内存添加都会经过该接口,将指定范围的内存添加到指定的 memblock_type 中。
type:这里可以向 memory,reserved 和 physmem 添加。
nid:对应 numa 节点 id,如果传入 MAX_NUMNODES 则表示没有 numa 节点。
flags:对应的该内存区域的 flags,后面介绍。
memblock_add_range 接口有如下衍生:
memblock_add_range
-> memblock_add_node
-> memblock_add
-> memblock_reserve
(2)memblock_add_node
int memblock_add_node(phys_addr_t base, phys_addr_t size, int nid);
向 memory 这个 memblock_type 添加内存域,并标记该内存域对应 numa 节点号。
memory 这个 type 记录了所有物理内存域。
(3)memblock_add
int memblock_add(phys_addr_t base, phys_addr_t size);
同 memblock_add_node 类似,不过 nid 默认为 MAX_NUMNODES,内部检测到 nid = MAX_NUMNODES,会将 nid 重新赋值为 -1(NUMA_NO_NODE),表示没有 numa 节点。
(4)memblock_reserve
int memblock_reserve(phys_addr_t base, phys_addr_t size);
向 reseved 这个 memblock_type 添加内存域。
reseved 域记录了所有预留内存域,后续内存分配和释放内存给伙伴系统均要从 memory 域避开 reserved 域。
2.2 内存分配释放接口
(1)memblock_alloc_range
phys_addr_t __init memblock_alloc_range(phys_addr_t size, phys_addr_t align,
phys_addr_t start, phys_addr_t end,
enum memblock_flags flags);
内存分配接口,返回分配到的物理地址。
size: 要分配的内存大小
align: 分配的内存对齐大小,如果为 0,会报警告,并默认把 align 设置为 SMP_CACHE_BYTES(aarch64: 64 byte)。
start-end: 从哪一个范围内开始分配,当 end 等于 MEMBLOCK_ALLOC_ACCESSIBLE(0) 或者 MEMBLOCK_ALLOC_KASAN(1)时,则会限制能够分配最大地址为 memblock.current_limit(后续介绍)。
该接口主要用于连续内存分配器(Contiguous Memory Allocator,CMA)。
(2)memblock_alloc_base_nid
phys_addr_t memblock_alloc_base_nid(phys_addr_t size,
phys_addr_t align, phys_addr_t max_addr,
int nid, enum memblock_flags flags);
类似于 memblock_alloc_range,不过这里 start 默认为 0,我们只需要传入 max_addr,以及对应numa节点 nid, nid 指明我们从哪一个 numa 节点分配内存。
nid = NUMA_NO_NODE(-1)则从任意区域分配。
memblock_alloc_base_nid 具有许多变体
memblock_alloc_base_nid
-> memblock_phys_alloc_nid
-> memblock_phys_alloc_try_nid
-> __memblock_alloc_base
-> memblock_alloc_base
-> memblock_phys_alloc
对于分配接口没有指定 flags 的默认从 MEMBLOCK_NONE 中分配,如果配置了 mirror,则会从首先尝试从 MEMBLOCK_MIRROR(后续介绍) 中分配。
* phys_addr_t memblock_phys_alloc_nid(phys_addr_t size, phys_addr_t align, int nid);
该接口主要指定 size, align,nid 分配内存,不过会有一个判断,如果第一次分配不到会检测 flags 中是否有 MEMBLOCK_MIRROR 标记,如果有则会清除该标记重新尝试分配,分配失败返回 0。
* phys_addr_t memblock_phys_alloc_try_nid(phys_addr_t size, phys_addr_t align, int nid);
该接口是上述接口的衍生,即通过 memblock_phys_alloc_nid 还是无法分配内存则会将 nid 设置为 NUMA_NO_NODE(-1)尝试从任意 numa 节点分配,只要能够分配到,分配失败返回 0。
* phys_addr_t __memblock_alloc_base(phys_addr_t size, phys_addr_t align,
phys_addr_t max_addr);
该接口从指定的 size, align,max_addr 进行任意 numa 节点内存分配,分配失败返回 0。
* phys_addr_t memblock_alloc_base(phys_addr_t size, phys_addr_t align,
phys_addr_t max_addr);
该接口是上述 __memblock_alloc_base 的变体,功能相同,只是分配失败不会返回,而是直接 panic。
* phys_addr_t memblock_phys_alloc(phys_addr_t size, phys_addr_t align);
该接口是 memblock_alloc_base 的变体,不用指定 max_addr,而是收内部 memblock.current_limit 的限制。
(3)memblock_alloc_internal 是一个内存的分配接口,但也是我们常用的分配虚拟地址内存的最底层函数,其作用是返回一个分配到的物理内存对应的虚拟地址,内部与上述直接物理内存分配接口相比有诸多逻辑。
static void * __init memblock_alloc_internal(
phys_addr_t size, phys_addr_t align,
phys_addr_t min_addr, phys_addr_t max_addr,
int nid)
从指定的 size,align,min_addr,max_addr,nid 进行内存分配,如果成功分配则会返回物理内存对应的虚拟地址,如果失败返回 NULL。
首先因为该接口是我们内核中最常用的普通内存分配接口,所以不需要指定 flags,而是默认从 MEMBLOCK_NONE 或者 MEMBLOCK_MIRROR 中选择。
其次 nid 如果指定为 MAX_NUMNODES/NUMA_NO_NODE 则从任意 numa 节点分配,如果制定了 nid,则从对应 numa 节点分配,如果分配不到会回退为 nid = NUMA_NO_NODE(-1)再次尝试分配。
max_addr 最大不能超过 memblock.current_limit 限制。
如果 slab 在这时可用了,那么会直接从 slab 中分配(不过出现这种情况已经不正确了,内核会在这里发出一次警告)。
如果通过上述逻辑还是分配不到内存还会判断 min_addr 是否为 0,不为零还会修改 min_addr = 0再次进行尝试分配。
同样的到这里内存还是没有还会继续判断 flags 是否等于 MEMBLOCK_MIRROR,是则会修改 flags = MEMBLOCK_NONE,再次去尝试分配。
memblock_alloc_internal被封装为如下接口
memblock_alloc_internal
-> memblock_alloc_try_nid_raw
// 该接口返回的分配的虚拟地址,如果失败返回 NULL,并且对应分配的内存区域数据未作任何处理
-> memblock_alloc_try_nid_nopanic
// 和上述接口一样,不过会对分配的内存进行 memset,把内存区域清为 0。
-> memblock_alloc_try_nid
// 和 memblock_alloc_try_nid_raw 类似,不过分配不到会直接 panic。
对于 memblock_alloc_try_nid_nopanic 有如下常用变体:
static inline void * __init memblock_alloc_nopanic(phys_addr_t size,
phys_addr_t align)
static inline void * __init memblock_alloc_low_nopanic(phys_addr_t size,
phys_addr_t align)
static inline void * __init memblock_alloc_from_nopanic(phys_addr_t size,
phys_addr_t align,
phys_addr_t min_addr)
static inline void * __init memblock_alloc_node_nopanic(phys_addr_t size,
int nid)
对于 memblock_alloc_try_nid有如下变体,也是我们最常见的变体:
static inline void * __init memblock_alloc(phys_addr_t size, phys_addr_t align)
static inline void * __init memblock_alloc_from(phys_addr_t size,
phys_addr_t align,
phys_addr_t min_addr)
static inline void * __init memblock_alloc_low(phys_addr_t size,
phys_addr_t align)
static inline void * __init memblock_alloc_node(phys_addr_t size,
phys_addr_t align, int nid)
到这里基本把分配接口全部介绍完了,还有少许变体没有介绍,可以自行查看代码。
对于分配接口,内核只提供了一个 memblock_free接口用于释放分配的内存。
int memblock_free(phys_addr_t base, phys_addr_t size);
2.3 内存域遍历
该部分主要为系统提供了遍历这种内存域的方法。比如遍历所有可用内存域,对每个可用内存域进行虚拟内存映射等。
其中 for_each_mem_range,for_each_mem_range_rev属于内部使用遍历方式,其作用是正向或者逆向遍历 memory 域,并把每个找到的 memory 域和每一个 reserved域比较,避开 reserved域。主要用于遍历剩余的空闲内存域,下面介绍。
(1)for_each_free_mem_range
#define for_each_free_mem_range(i, nid, flags, p_start, p_end, p_nid) \
for_each_mem_range(i, &memblock.memory, &memblock.reserved, \
nid, flags, p_start, p_end, p_nid)
指定要遍历的 nid,flags,返回每一个空闲域对应的 start,end,nid。
(2)for_each_free_mem_range_reverse
for_each_free_mem_range的反向遍历操作。
(3)for_each_memblock
#define for_each_memblock(memblock_type, region) \
for (region = memblock.memblock_type.regions; \
region < (memblock.memblock_type.regions + memblock.memblock_type.cnt); \
region++)
遍历指定的 memblock_type 域,可以是 memory,reserved,physmem。返回对应的每一个 region。
(4)for_each_memblock_type
#define for_each_memblock_type(i, memblock_type, rgn) \
for (i = 0, rgn = &memblock_type->regions[0]; \
i < memblock_type->cnt; \
i++, rgn = &memblock_type->regions[i])
和 for_each_memblock 类似,唯一不同是多了一个 i,用于描述当前 region 的 index,作用是在域的合并分离时可以修改 i,用于重新操作当前域。
(5)for_each_mem_pfn_range
#define for_each_mem_pfn_range(i, nid, p_start, p_end, p_nid) \
for (i = -1, __next_mem_pfn_range(&i, nid, p_start, p_end, p_nid); \
i >= 0; __next_mem_pfn_range(&i, nid, p_start, p_end, p_nid))
逻辑类似于 for_each_memblock(memory, rgn) 。
不过会返回每一个 region 的 start_pfn,end_pfn 和 nid。
__next_mem_pfn_range 内部会对每一个 region 的起始地址和结束地址进行 pfn 转换,方便伙伴系统内部使用(因为伙伴系统管理内存是按照 page 对齐的,所以对内存起始和结束地址都是需要 page 对齐的。)
2.4 其他杂项
这里包含大量杂项功能,包括标记内存域的 flags 属性,设置是自底向上分配还是自顶向下分配,标记是否允许扩容 region,对内存域调整对齐,限制内存域的最大分配地址等。
(1)memblock_mark_hotplug,memblock_mark_mirror,memblock_mark_nomap
标记某一段内存域为对应的 flags 属性。比如 memblock_mark_nomap 可以标记内存为 nomap,那么在遍历内存域进行内存映射时时可以跳过 nomap 标记的区域。
(2)memblock_free_all,reset_node_managed_pages,reset_all_zones_managed_pages
释放可用空闲内存到伙伴系统,以及一些助手程序。
(3)memblock_set_bottom_up
设置从底部还是顶部开始分配内存。
(4)memblock_phys_mem_size
总的物理内存大小
(5)memblock_reserved_size
总的预留物理内存大小
(5)memblock_start_of_DRAM,memblock_end_of_DRAM
物理内存域的起始与结束。
(6)memblock_enforce_memory_limit
强制限制内存的最大总量
(7)memblock_cap_memory_range
调整内存的使用范围
(8)memblock_mem_limit_remove_map
限制内存的最大总量并调整内存范围为 [0 - max_addr]
(9)memblock_set_current_limit
设置当前能分配的最大地址范围
(10)memblock_trim_memory
调整每个 region 的对齐
至此大部分接口介绍完毕,中间还存一些未介绍的,但是都比较简单一看就知道含义。
3 内部数据结构
首先来看一张网络上的结构数据图:

3.1 struct memblock
首先有一个全局的 struct memblock 结构体用于管理所有 memblock 元数据。
struct memblock {
bool bottom_up; /* is bottom up direction? */
phys_addr_t current_limit;
struct memblock_type memory;
struct memblock_type reserved;
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
struct memblock_type physmem;
#endif
};
- bottom_up
用于表示当前从内存底部开始分配还是从内存的顶部开始分配。
- current_limit
表示当前分配内存的最大物理地址限制,默认时 PHYS_MAX_ADDR,即没有限制,可以通过 memblock_set_current_limit来修改该变量。
- struct memblock_type memory
用于管理当前系统中所有内存的集合。
- struct memblock_type reserved
用于管理当前系统中所有预留的内存集合。(reserved 应该是 memory 的子集)
- struct memblock_type physmem;
该内存域由 CONFIG_HAVE_MEMBLOCK_PHYS_MAP 宏控制,目前主要用于 s390 的 crash dump 功能使用,后续不再介绍该域。
通过上述定义,从系统中收集到的所有内存都会添加到 memory 内存域中,而一些被分配出去(memblock_alloc),驱动预留(cma,dma_alloc_from_contiguous), 内核数据(.text,.data,dtb,initrd等等)的这些内存域则会被添加到 reserved 内存域中,后续从 memory 域释放给伙伴系统的可用空闲内存需要全部避开 reserved 域的内存。
3.2 strcut memblock_type
struct memblock_type管理了一种类型的内存域集合,目前定义了的内存域集合包括memory,reserved,physmem。
结构体如下:
struct memblock_type {
unsigned long cnt;
unsigned long max;

本文详细介绍了memblock早期内存分配器,它在伙伴系统接管前为系统提供内存分配等功能。文中阐述了其提供的接口,包括内存添加预留、分配释放等;介绍了内部数据结构;分析了核心算法逻辑,如合并裁剪等;还说明了其在arm64启动中的应用及内存释放过程。
最低0.47元/天 解锁文章
788

被折叠的 条评论
为什么被折叠?



