0. 前言
经过[linux内存管理] 第007篇 fixmap映射详解的讲解,系统内存经过了FIXMAP映射,目前只是将fdt的地址映射并解析了设备树。
而在 Linux kernel 初始化完成之后,系统中的内存分配和回收是由 buddy 系统、 slab分配器 来管理,但是在 kernel 初始化阶段时内存的分配和释放是由memblock分配器管理,记录物理内存的使用情况,
首先我们知道在内核启动后,对于内存,分成好几块
- 内存中的某些部分使永久分配给内核的,例如代码段和数据段,ramdisk和dtb占用的空间等,是系统内存的一部分,不能被侵占,也不参与内存的分配,称之为静态内存
- GPU/camera/多核共享的内存都需要预留大量连续内存,这部分内存平时不使用,但是必须为各个应用场景预留,这样的内存称之为预留内存
- 内存其余的部分,是需要内核管理的内存,称之为动态内存
那么memblock就是将以上内存按功能划分为若干内存区,使用不同的类型存放在memory和reserved的两个集合中,memory即为动态内存,而resvered包括静态内存等。
本文主要介绍在系统启动阶段 memblock 的初始化过程。
1. memblock子系统
memblock内存管理机制用于在Linux启动后管理内存,一直到 free_initmem() 为止。
在 buddy系统初始化 之前,内存由memblock管理,需要注意的是,memblock 管理的内存为 物理地址,非虚拟地址。
1.1 memblock的数据结构
1.1.1 struct memblock
struct memblock {
///true:low to high
///false:high to low
bool bottom_up; /* is bottom up direction? */
///内存块大小限制
phys_addr_t current_limit;
///可以被memblock管理分配的内存
struct memblock_type memory;
///预留已分配的空间
struct memblock_type reserved;
};
- bottom_up: 申请内存时分配器分配方式,true 表示从低地址到高地址分配,false表示从高地址到低地址分配;
- current_limit: 内存块大小限制,一般可在 memblock_alloc 申请内存时进行检查限制;
- memory: 可以被 memblock 管理分配的内存(系统启动时,会因为内核镜像加载等原因,需要提前预留内存,这些都不再memory 之中);
- reserved: 预留已经分配的空间,主要包括 memblock 之前占用的内存以及通过memblock_alloc 从memroy 中申请的内存空间;
- physmem: 需要开启 CONFIG_HAVE_MEMBLOCK_PHYS_MAP,所有物理内存的集合;
1.1.2 struct memblock_type
struct memblock_type {
///本memblock_type包含regions个数
unsigned long cnt;
///本memblock_type包含regions最大个数
unsigned long max;
///本memblock_type若有regions总的size
phys_addr_t total_size;
///regions数组
struct memblock_region *regions;
///本memblock_type名字
char *name;
};
- cnt: 该 memblock_type 内包含多少个 **regions **;
- max: memblock_type 内包含 regions 的最大个数,默认为 **INIT_MEMBLOCK_REGIONS **(128);
- total_size: 该 memblock_type 内所有 regions 加起来的size 大小;
- regions: regions 数组,指向的是数组的首地址;
- name: memblock_type 的名称;
1.1.3 struct memblock_region
struct memblock_region {
///本region起始地址
phys_addr_t base;
///本region大小
phys_addr_t size;
///region的标记
enum memblock_flags flags;
#ifdef CONFIG_NUMA
int nid;
#endif
};
memblock_region 代表了一块物理内存区域。
- base: 该region 的物理地址;
- size: 该region 区域大小;
- flags: region 区域的flags;
- nid: 对于 CONFIG_HAVE_MEMBLOCK_NODE_MAP 使能时存放的nid;
1.1.4 enum memblock_flags
enum memblock_flags {
///没有特殊需求
MEMBLOCK_NONE = 0x0, /* No special request */
///该块支持热插拔
MEMBLOCK_HOTPLUG = 0x1, /* hotpluggable region */
///内存镜像
MEMBLOCK_MIRROR = 0x2, /* mirrored region */
///不能被kernel用于直接映射/线性映射
MEMBLOCK_NOMAP = 0x4, /* don't add to kernel direct mapping */
};
- MEMBLOCK_NONE: 表示没有特殊需求正常使用;
- MEMBLOCK_HOTPLUG: 该块内存支持热插拔,用于后续创建zone时,归ZONE_MOVABLE 管理;
- MEMBLOCK_MIRROR: 用于mirror 功能。内存镜像是内存冗余技术的一种,工作原理与硬盘的热备份类似,将内存数据做两个复制,分别放在主内存和镜像内存中。
- MEMBLOCK_NOMAP: 不能被kernel 用于直接映射(即线性映射区域);
1.2 全局变量memblock
通过上述数据结构,我们大致了解到 memblock 是通过 memblock_type ( memory 和 reserved) 的形式管理物理内存,每个memblock_type 内存有很多 memblock_region,每个 region 记录该region 的物理起始地址、region 的大小、region 的属性 (memblock_flags 中记录hotplug、mirror、nomap等属性)。
而最初始的一个结构体也就是struct memblock,这是一个全局的变量。
extern struct memblock memblock;
下面我们看一下这个memblock在哪里被初始化的:
1.2.1 memblock初始化
static struct memblock_region memblock_memory_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock;
static struct memblock_region memblock_reserved_init_regions[INIT_MEMBLOCK_RESERVED_REGIONS] __initdata_memblock;
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
static struct memblock_region memblock_physmem_init_regions[INIT_PHYSMEM_REGIONS];
#endif
struct memblock memblock __initdata_memblock = {
///内存区域数组,存放在内核镜像的.meminit.data段
.memory.regions = memblock_memory_init_regions,
.memory.cnt = 1, /* empty dummy entry */
///系统预留memory类型,内存区域最大个数128,不足可以补充
.memory.max = INIT_MEMBLOCK_REGIONS,
///memory类型内存区域管理器名字
.memory.name = "memory",
.reserved.regions = memblock_reserved_init_regions,
.reserved.cnt = 1, /* empty dummy entry */
///系统预留reserved类型,内存区域最大值128+CPU个数+1,不足可以补充
.reserved.max = INIT_MEMBLOCK_RESERVED_REGIONS,
///reserved类型内存区域管理器名字
.reserved.name = "reserved",
///内存管理器记录的内存区域的物理地址,默认从小到大排列
.bottom_up = false,
///Memblock内存管理器最大物理内存,64位系统是(uint64_t)-1
.current_limit = MEMBLOCK_ALLOC_ANYWHERE,
};
定义的memblock
为全局变量,在定义的时候就进行了初始化。初始化的时候,regions
指向的也是静态全局的数组,其中数组的大小为INIT_MEMBLOCK_REGIONS
,也就是128个,限制了这些内存块的个数了,实际在代码中可以看到,当超过这个数值时,数组会以2倍的速度动态扩大。
经过初始化后,memblock全局变量就变成了这样:
1.3 memblock_add函数
memblock
子模块,基本的逻辑都是围绕内存的添加和移除操作来展开,最终是通过调用memblock_add_range/memblock_remove_range
来实现的。
static 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;
///防止反转,end不超过最大值
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:
/*
* The following is executed twice. Once with %false @insert and
* then with %true. The first counts the number of regions needed
* to accommodate the new area. The second actually inserts them.
*/
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)
//情形1,跳出循环
break;
if (rend <= base)
//情形6,继续找下一块
continue;
/*
* @rgn overlaps. If it separates the lower part of new
* area, insert that portion.
*/
if (rbase > base) {
///情形2/3
#ifdef CONFIG_NUMA
WARN_ON(nid != memblock_get_region_node(rgn));
#endif
WARN_ON(flags != rgn->flags);
///需新增一个内存块
nr_new++;
///第一趟不执行,只计算需新增的memblock_region个数
if (insert)
memblock_insert_region(type, idx++, base,
rbase - base, nid,
flags);
}
/* area below @rend is dealt with, forget about it */
///更新base,继续匹配
base = min(rend, end);
}
/* insert the remaining portion */
///base < end 情形5,需要再新增一个内存块
///base >= end 情形4,无需新增
if (base < end) {
nr_new++;
if (insert)
memblock_insert_region(type, idx, base, end - base,
nid, flags);
}
///如果不需要新增,直接返回
if (!nr_new)
return 0;
/*
* If this was the first round, resize array and repeat for actual
* insertions; otherwise, merge and return.
*/
///第一躺处理
if (!insert) {
///确保可以新增足够的memblock_regiong个数
while (type->cnt + nr_new > type->max)
///若不够,扩展
if (memblock_double_array(type, obase, size) < 0)
return -ENOMEM;
///执行第二趟
insert = true;
goto repeat;
} else {
///新增memblock_region后,处理重叠合并
memblock_merge_regions(type);
return 0;
}
}
根据插入的region的大小以及位置有以上六种情况,核心的逻辑都是对插入的region
进行判断,如果出现了物理地址范围重叠的部分,那就进行split
操作,最终对具有相同flag
的region
进行merge
操作。
1.4 memblock_remove
实际调用的是memblock_remove_range
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;
}
假如现在需要移除掉一片区域,而该区域跨越了多个region
,则会先调用memblock_isolate_range
来对这片区域进行切分,最后再调用memblock_remove_region
对区域范围内的region
进行移除操作。
2. arm64_memblock_init
/*
* 整理内存区域,将一些特殊区域添加到memblock内存管理模块中
* 移除不归内核管理的区域(dts的no-map区域),还申请一个公共区域的CMA
*
* 最终通过memblock模块,把内存中的空闲和被占用的区域进行分开管理
*
* */
void __init arm64_memblock_init(void)
{
///计算虚拟地址可以覆盖的线性区域
//该虚拟地址bits受CONFIG_ARM64_VA_BITS影响
//如果针对VA_BITS为39,计算得虚拟地址覆盖的大小为40-0000-0000,即
//对于VA_BITS虚拟地址可以映射的内存最多可以达到256GB
s64 linear_region_size = PAGE_END - _PAGE_OFFSET(vabits_actual);
/*
* Corner case: 52-bit VA capable systems running KVM in nVHE mode may
* be limited in their ability to support a linear map that exceeds 51
* bits of VA space, depending on the placement of the ID map. Given
* that the placement of the ID map may be randomized, let's simply
* limit the kernel's linear map to 51 bits as well if we detect this
* configuration.
*/
if (IS_ENABLED(CONFIG_KVM) && vabits_actual == 52 &&
is_hyp_mode_available() && !is_kernel_in_hyp_mode()) {
pr_info("Capping linear region to 51 bits for KVM in nVHE mode on LVA capable hardware.\n");
linear_region_size = min_t(u64, linear_region_size, BIT(51));
}
///移除实际物理地址以上的内存空间区域,比如VA_BITS=48,大于2^48以上地址,memblock不必存在
/* Remove memory above our supported physical address size */
memblock_remove(1ULL << PHYS_MASK_SHIFT, ULLONG_MAX);
/*
* Select a suitable value for the base of physical memory.
*/
///物理地址起始地址,尚未memblock_add,此处为0
memstart_addr = round_down(memblock_start_of_DRAM(),
ARM64_MEMSTART_ALIGN);
if ((memblock_end_of_DRAM() - memstart_addr) > linear_region_size)
pr_warn("Memory doesn't fit in the linear mapping, VA_BITS too small\n");
/*
* Remove the memory that we will not be able to cover with the
* linear mapping. Take care not to clip the kernel which may be
* high in memory.
*/
///移除线性映射以外的内存
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 we are running with a 52-bit kernel VA config on a system that
* does not support it, we have to place the available physical
* memory in the 48-bit addressable part of the linear region, i.e.,
* we have to move it upward. Since memstart_addr represents the
* physical address of PAGE_OFFSET, we have to *subtract* from it.
*/
if (IS_ENABLED(CONFIG_ARM64_VA_BITS_52) && (vabits_actual != 52))
memstart_addr -= _PAGE_OFFSET(48) - _PAGE_OFFSET(52);
/*
* Apply the memory limit if it was set. Since the kernel may be loaded
* high up in memory, add back the kernel region that must be accessible
* via the linear mapping.
*/
///如果limit被配置,根据meory_limit重新配置,一般不会配置
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) && phys_initrd_size) {
/*
* Add back the memory we just removed if it results in the
* initrd to become inaccessible via the linear mapping.
* Otherwise, this is a no-op
*/
u64 base = phys_initrd_start & PAGE_MASK;
u64 size = PAGE_ALIGN(phys_initrd_start + phys_initrd_size) - base;
/*
* We can only add back the initrd memory if we don't end up
* with more memory than we can address via the linear mapping.
* It is up to the bootloader to position the kernel and the
* initrd reasonably close to each other (i.e., within 32 GB of
* each other) so that all granule/#levels combinations can
* always access both.
*/
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")) {
phys_initrd_size = 0;
} else {
///如果initrd地址符合要求,重新加入memblock.memory,且reserved这块内存
memblock_remove(base, size); /* clear MEMBLOCK_ flags */
memblock_add(base, size);
memblock_reserve(base, size);
}
}
if (IS_ENABLED(CONFIG_RANDOMIZE_BASE)) {
extern u16 memstart_offset_seed;
u64 mmfr0 = read_cpuid(ID_AA64MMFR0_EL1);
int parange = cpuid_feature_extract_unsigned_field(
mmfr0, ID_AA64MMFR0_PARANGE_SHIFT);
s64 range = linear_region_size -
BIT(id_aa64mmfr0_parange_to_phys_shift(parange));
/*
* If the size of the linear region exceeds, by a sufficient
* margin, the size of the region that the physical memory can
* span, randomize the linear region as well.
*/
if (memstart_offset_seed > 0 && range >= (s64)ARM64_MEMSTART_ALIGN) {
range /= ARM64_MEMSTART_ALIGN;
memstart_addr -= ARM64_MEMSTART_ALIGN *
((range * memstart_offset_seed) >> 16);
}
}
/*
* Register the kernel text, kernel data, initrd, and initial
* pagetables with memblock.
*/
///将内核设置为reserved类型
///paging_init()后会释放
memblock_reserve(__pa_symbol(_stext), _end - _stext);
if (IS_ENABLED(CONFIG_BLK_DEV_INITRD) && phys_initrd_size) {
///计算initrd对应虚拟地址
/* the generic initrd code expects virtual addresses */
initrd_start = __phys_to_virt(phys_initrd_start);
initrd_end = initrd_start + phys_initrd_size;
}
///扫描dtb中的reserved-memory节点,将每个子结点添加到memblock.reserved,设置为reserved类型
///如果节点定义了nomap,会从memblock.memory删除,就像物理上没有这片内存
///cma具有reuseable属性,则不会从memblock.memory删除
early_init_fdt_scan_reserved_mem();
///ARM64中不需要高端内存,为了向前兼容,这里将高端内存的起始地址设置为物理内存的结束地址
high_memory = __va(memblock_end_of_DRAM() - 1) + 1;
}
arm64_memblock_init() 是前期内存管理十分重要的函数。所需要做的事情很多,下面将分步逐个剖析该过程。
2.1 linear_region_size的确定
linear_region_size
系统内存计算的开始,函数最开始根据实际的虚拟地址空间计算虚拟地址可以覆盖的线性区域。
s64 linear_region_size = PAGE_END - _PAGE_OFFSET(vabits_actual);
#define VA_BITS (CONFIG_ARM64_VA_BITS)
#define _PAGE_OFFSET(va) (-(UL(1) << (va)))
#define PAGE_END (_PAGE_END(VA_BITS_MIN))
#define _PAGE_END(va) (-(UL(1) << ((va) - 1)))
#if VA_BITS > 48
#define VA_BITS_MIN (48)
#else
#define VA_BITS_MIN (VA_BITS) // 以VA_BITS=39为例
#endif
{% tip success %}
PAGE_END = (-(1 << (39-1))) = -256 GB
_PAGE_OFFSET(vabits_actual) = (-(1 << 39)) = -512GB
那么linear_region_size = -256 GB - (-512GB) = 256GB
{% endtip %}
2.2 移除物理地址以外的内存
memblock_remove(1ULL << PHYS_MASK_SHIFT, ULLONG_MAX);
通过 memblock_remove() 删除memory中指定 PA_BITS 以上的区域,例如 PA_BITS 为48,则物理地址位于 0x0000 0000 0000 0000 ~ 0x0000 FFFF FFFF FFFF,0x0000 FFFF FFFF FFFF以上的物理地址是不能访问的,memblock 不必存在。
2.3 确定memstart_addr
///物理地址起始地址,尚未memblock_add,此处为0
memstart_addr = round_down(memblock_start_of_DRAM(),
ARM64_MEMSTART_ALIGN);
这里 round_down() 在linux 中向下对齐, 第二个参数必须是 2 的次幂 。例如 round_down(14,4) 得到的是 12。
相反, round_up() 在linux 中是向上对齐,第二个参数必须是 2 的次幂。例如 round_up(14,4) 得到的是 16。
2.4 移除线性映射之外的内存
memblock_remove(max_t(u64, memstart_addr + linear_region_size,
__pa_symbol(_end)), ULLONG_MAX);
当线性映射时,虚拟地址有个最大的值,对于 VA_BITS 是通过 CONFIG 进行配置,例如对于VA_BITS 为39,那么 39 位以上的区域是不会映射的,移除这部分的区域。
2.5 移除虚拟地址无法覆盖的内存
///如果物理内存足够大,将虚拟地址无法覆盖的区域删除掉
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);
}
2.6 确认memory_limit区域
///如果limit被配置,根据meory_limit重新配置,一般不会配置
if (memory_limit != PHYS_ADDR_MAX) {
memblock_mem_limit_remove_map(memory_limit);
memblock_add(__pa_symbol(_text), (u64)(_end - _text));
}
2.7 确定 initrd 内存
if (IS_ENABLED(CONFIG_BLK_DEV_INITRD) && phys_initrd_size) {
/*
* Add back the memory we just removed if it results in the
* initrd to become inaccessible via the linear mapping.
* Otherwise, this is a no-op
*/
u64 base = phys_initrd_start & PAGE_MASK;
u64 size = PAGE_ALIGN(phys_initrd_start + phys_initrd_size) - base;
/*
* We can only add back the initrd memory if we don't end up
* with more memory than we can address via the linear mapping.
* It is up to the bootloader to position the kernel and the
* initrd reasonably close to each other (i.e., within 32 GB of
* each other) so that all granule/#levels combinations can
* always access both.
*/
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")) {
phys_initrd_size = 0;
} else {
///如果initrd地址符合要求,重新加入memblock.memory,且reserved这块内存
memblock_remove(base, size); /* clear MEMBLOCK_ flags */
memblock_add(base, size);
memblock_reserve(base, size);
}
}
这里当处理这块内存时,需要重新页对齐,如果在正常内存范围内,将会刷新到memblock.memory 并添加到 memblock.reserved 中。
2.8 将内核镜像所在内存reserved
///将内核设置为reserved类型
///paging_init()后会释放
memblock_reserve(__pa_symbol(_stext), _end - _stext);
if (IS_ENABLED(CONFIG_BLK_DEV_INITRD) && phys_initrd_size) {
///计算initrd对应虚拟地址
/* the generic initrd code expects virtual addresses */
initrd_start = __phys_to_virt(phys_initrd_start);
initrd_end = initrd_start + phys_initrd_size;
}
{% tip success %}
注意:
这里reserved 的内存是 [ _text, _end) 所指向的内核镜像所在的物理内存,其中包括了text段、rodata段、init段、data段、bss段,也包括了memblock的初始化页表所占用的内存,该内存的起始地址记录在 init_pg_dir,结尾地址记录在 init_pg_end 中。在 [linux内存管理] 第010篇 paging_init详解
一文中在map_kernel()和map_mem()映射完成之后,前期的初始化页表就不需要了,所以在 paging_init() 结束时会将这块区域从 memblock.reserved 中释放,详细可以查看 [linux内存管理] 第010篇 paging_init详解
一文第 1 节和第 10 节。
{% endtip %}
另外,在 kernel_init() 中内核初始化完成后会调用 free_initmem() 将 init 段的内存释放掉。
2.9 解析dtb中reserved-memory节点
这部分不展开详细说明过程,具体内容请查看 [linux内存管理] 第009篇 reserved-memory详解
这其中还涉及了DMA以及CMA相关的内容,具体请查看[linux内存管理] 第xxx篇 CMA分配器详解
当物理内存都添加进系统之后,arm64_memblock_init会对整个物理内存进行整理,主要的工作就是将一些特殊的区域添加进reserved内存中。函数执行完后,如下图所示
3. 总结
本文主要介绍了linux 在boot 阶段物理内存管理机制,包括数据结构和内存申请、释放等基础算法。
memblock 内存管理是将所有的物理内存放到 memblock.memory 中作为可用内存来管理,然后再将不同方式的预留内存进行不同方式处理:
no-map 的reserved memory,将从 memblock.memory 中移除;
shared-dma-pool 的reserved memory,将保留在 memblock.memory 中,会打上CMA 的标签;
其他 的reserved memory 也会保留在memblock.memory 中,同时也会放到 memblock.reserved 中,在系统初始化之后, memblock.reserved 中的内存会被打上 reserved 标签,并且不会移交到 buddy 系统管理;
{% tip success %}
内核在debug 开启的时候,提供了两个节点,用以查看memblock:
/sys/kernel/debug/memblock/reserved 可以查看 reserved 内存布局。
/sys/kernel/debug/memblock/memory 可以查看 memory 内存布局。
也可以通过节点 /proc/iomem 可以查看 reserved 和 memory 内存布局。
{% endtip %}
附上本章的流程图