- 了解memblock机制。
1.概述
在引导内核的过程中,需要使用内存, 而这个时候内核的内存管理并没有被创建, 因此也就需要一种精简的内存管理系统先接受这个工作, 而在初始化完成后, 再将旧的接口废弃, 转而使用强大的buddy系统来进行内存管理。
早期的Linux内核在引导阶段都是通过bootmem来完成初期的内存管理的,但是后来的版本开始把bootmem弃用了,使用memblock机制。[refer to: Use memblock interface instead of bootmem]
拓展:bootmem
在启动过程期间,尽管内存管理尚未初始化,但内核仍然需要分配内存以创建各种数据结构。bootmem分配器用于在启动阶段早期分配内存。
显然,对该分配器的需求集中于简单性方面,而不是性能和通用性。因此内核开发者决定实现一个最先适配(first-fit)分配器用于在启动阶段管理内存。这是可能想到的最简单的方式。
该分配器使用一个位图来管理页,位图比特位的数目与系统中物理内存页的数目相同。比特位位1,表示页以用;比特位位0,表示页空闲。
在需要分配内存时,分配器逐位扫描位图,知道找到一个能提供足够连续页的位置,即所谓的最先最佳(first-best)或最先适配位置。
该过程不是很高效,因为每次分配都必须从头扫描比特链。因此在内核完全初始化之后,不能将该适配器用于内存管理。伙伴系统(连同slab、slub、或slob分配器)是一个好得多的备选方案。
1.1.memblock 主要功能
mmemblock 内存页帧分配器是 Linux 启动早期内存管理器,在伙伴系统(Buddy System)接管内存管理之前为系统提供内存分配、预留等功能。
memblock 将系统启动时获取的可用内存范围(如从设备树中获取的内存范围)纳入管理,为内核启动阶段内存分配需求提供服务,直到 memblock 分配器将内存管理权移交给伙伴系统。同时 memblock 分配器也维护预留内存(reserved memory),使其不会被分配器直接用于分配,保证其不被非预留者使用。
默认情况,内核会为所有的内存建立线性地址空间,并将可用内存释放到伙伴系统中。但是像内核镜像、dtb以及initrd这些系统本身使用的内存,并不能再次被分配做它用,因此也不能被释放到伙伴系统中。更有甚者,有些驱动或者异构核可能需要保留一些专用内存,这些内存当然也不能释放给伙伴系统。因此,memblock又抽象出了保留内存类型,它同样通过一组region进行管理,以标识这些内容已经被使用,或者是专用的。
首先在内核启动后,对于内存分成好几块:
- 内存中的某些部分使永久分配给内核的,例如代码段和数据段、ramdisk和dtb占用的空间、临时页表和设备数中的保留区域等,是系统内存的一部分,不能被侵占,也不参与内存的分配,称之为静态内存;
- GPU/camera/多核共享的内存都需要预留大量连续内存,这部分内存平时不使用,但是必须为各个应用场景预留,这样的内存称之为预留内存;
- 内存其余的部分,是需要内核管理的内存,称之为动态内存;
那么memblock就是将以上内存按功能划分为若干内存区,使用不同的类型存放在memory和reserved的两个集合中,memory即为动态内存,而resvered包括静态内存等。
在mm_init中会建立内核的内存分配器,停用memblock,释放内存给伙伴系统和slab分配器。memblock是bootmem的升级版本,在config中配置:CONFIG_NO_BOOTMEM=Y。
2.memblock 数据结构
内核中定义了一个 memblock 实体,作为 memblock 分配器管理载体,其类型为 struct memblock 。
memblock 分配器管理结构共有三层,从顶向下分别为 struct memblock , struct memblock_type , struct memblock_region ,三层结构关系如下图所示,可结合代码理解。下面将详细分析。
2.1.struct memblock
memblock结构描述了系统中所有物理内存的布局信息,它通过扫描dtb的memory节点,获取所有内存的起始地址和长度信息,并将其依次存放到memblock的memory region中。将需要保留的内存信息存放到memblock的reserved region中。
include/linux/memblock.h
struct memblock {
bool bottom_up; /* is bottom up direction?
如果true, 则允许由下而上地分配内存*/
phys_addr_t current_limit; /*指出了内存块的大小限制*/
/* 接下来的三个域描述了内存块的类型,即预留型,内存型和物理内存*/
struct memblock_type memory;
struct memblock_type reserved;
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
struct memblock_type physmem;
#endif
};
2.2.struct memblock_type
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;
};
2.3.struct memblock_region
struct memblock_region
{
phys_addr_t base;
phys_addr_t size;
unsigned long flags;
#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP
int nid;
#endif
};
内存区块被维护在不同的 struct memblock_type 的 regions 链表上,这是一个由数组构成的链表,链表通过每个区块 的基地址的大小,从小到大的排列。每个内存区块代表的内存区不能与本链表中的其他内存区块相 互重叠,可以相连。内核初始定义了两个内存区块数组,如下:
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_memblok;
以上介绍了 memblock 的三层结构,其中前两层在源码中已经定义。第三层 region 内存区块则是在内核启动过程中,内核调用相关接口函数动态添加。
2.4.memblock_region flags字段
/* Definition of memblock flags. */
enum {
MEMBLOCK_NONE = 0x0, /* No special request */
MEMBLOCK_HOTPLUG = 0x1, /* hotpluggable region */
MEMBLOCK_MIRROR = 0x2, /* mirrored region */
MEMBLOCK_NOMAP = 0x4, /* don't add to kernel direct mapping */
};
MEMBLOCK 内存分配器基础框架如下:
MEMBLOCK 分配器使用一个 struct memblock 结构维护着两种内存, 其中成员 memory 维护着可用物理内存区域;成员 reserved 维护着操作系统预留的内存区域。 每个区域使用数据结构 struct memblock_type 进行管理,其成员 regions 负责维护该类型内存的所有内存区,每个内存区使用数据结构 struct memblock_region 进行维护。
2.5.struct memblock 实例化
内核实例化struct memblock 结构,以供 MEMBLOCK 进行内存的管理,其定义如下:
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,
.memory.name = "memory",
.reserved.regions = memblock_reserved_init_regions,
.reserved.cnt = 1, /* empty dummy entry */
.reserved.max = INIT_MEMBLOCK_RESERVED_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,
};
2.5.1.__initdata_memblock 宏指定存储位置
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
include/linux/init.h
#define __meminitdata __section(.meminit.data)
编译链接后,从内核镜像中的__initdata_memblock段中划分了内存空间,内核启动后的拷贝动作,便从SRAM中得到了对应的物理内存空间,memblock也就初始化完成,可以工作了。
memblock只用4个数据结构就描述了所有的内存信息。
使用struct memblock描述系统所有的物理内存信息,物理内存将按照在启动阶段是否作为特殊用途挂靠到struct memblock的memory以及reserved两个成员中。
memory以及reserved是struct memblock_type类型的数据结构,表征内存是否作为特殊用途的类型,在启动早期,所有的内存都先挂靠到memory中,在启动过程中,将系统自身使用的、编程预留以及其他特殊功能预留的内存,也会将这些内存的地址挂靠到reserved中。这些预留的内存,将同时被memory以及reserved一起管理。
memblock使用struct memblock_region来描述内存的细节信息,在memblock生命周期中,所有管理的内存块都用一个memblock_region来描述,简单来说,就算我们只是给页表分配了PAGE_SIZE的内存,这PAGE_SIZE大小的内存块也是一个memblock_region,但是系统对memblock_region的成员数量也有限制,避免memblock本身使用过多的内存。所以memblock使用了memblock标志字段来描述每一个memblock_region,从而控制memblock_region的合并算法。
在memblock的后期,memblock将会memory中没有与reserved共享的内存填充到buddy内存管理器,完成自己的使命。
2.5.2.memblock_type初始化
对于可用物理内存,其名字设定为 “memory”,初始状态系统下,可用物理物理的所有内存区块都维护在 memblock_memory_init_regions 上,当前情况下,可用物理内存区只包含一个内存区块,然而可用物理内存可管理 INIT_MEMBLOCK_REGIONS 个内存区块;
对于预留内存,其名字设定为 “reserved”,初始状态下,预留物理内存的所有区块都维护在 memblock_reserved_init_regions 上,当前情况下,预留物理内存区只包含一个内存区块,然而预留内存区可以维护管理 INIT_MEMBLOCK_RESERVED_REGIONS 个内存区块。
3.memblock 主要接口函数
memblock 系统提供相关接口供内核使用,包括内存区块的添加、预留、内存申请等功能。本文将对以下五个关键接口函数进行分析,其余函数可举一反三:
-
memblock_add 将内存区块添加到可用内存集合。通过此函数可展示 memblock 添加区块的思路。
-
memblock_reserve 将内存区块添加到预留内存集合。
-
for_each_reserved_mem_range 遍历预留内存区块。通过此函数可展示 memblock 遍历区块的逻辑和思路。
-
memblock_phys_alloc 用于申请 memblock 中的物理内存。
-
memblock_alloc 用于申请 memblock 的内存并返回虚拟地址。可供内核申请内存是 memblock 价值实现的关键。
4.memblock做的事情
1)setup_machine_fdt,解析设备树文件,保存系统所有的内存信息。
解释fdt前,保存fdt本身占据的内存到memblock.reserved类型中
解析fdt的memory节点,保存所有物理内存范围到memblokc.memory类型中
2)arm64_memblock_init,初始化内核启动过程中的内存配置信息,对内存做细分。主要涉及到后续buddy内存管理器所需要的一些内存信息,比如物理内存的起始地址、结束地址、将内核镜像本身占据的内存、fdt reserved类型的内存以及PERCPU、CMA的内存预留出来。总的来说,就是将内存再按照功能需求再做细分,将特殊用途的内存都划到reserved中。
- 解析设备树中的chose节点,读取"linux,usable-memory-range",设置可用内存范围;
- 保存物理内存的起始地址到全局变量memstart_addr;
- 将内核本身使用的内存保存到预留区,这样整个内核运作过程中所使用的内存独立出来,也就得到保护。
- 将DMA zone的内存划分给CMA,保存到reserved区,这里请注意,因为memblock后续将可用内存交给buddy,这个可用内存指的是memory类型且没有保存到reserved中的内存。熟悉buddy都知道,DMA32肯定属于buddy管,所以,后续这个CMA区域的内存将会做特殊处理,也会交给buddy管理。
- 还有其他一些在内核启动以及后续内核一直占用的内存,都会划到reserved区域。
3)paging_init,创建映射,调用memblock分配器做内存分配,比如页表的创建等
4)mem_init,调用memblock_free_all将可用内存转到buddy,memblock的使命也就结束,并且memblock本身所占据的内存也会释放。
4.1.memblock 可用内存初始化
内核启动后,执行 start_kernel 函数,该函数中 setup_arch 函数对特定架构进行初始化。arm64 调用 setup_machine_fdt 解析设备树,setup_machine_fdt 函数与 memblock 相关的分支如下:
- setup_machine_fdt
- early_init_dt_scan
- early_init_dt_scan_nodes
- early_init_dt_scan_memory
- early_init_dt_add_memory_arch
- memblock_add
可见最终会调用上节所述 memblock_add 函数将可用内存写入 memblock 全局变量中,使可用内存区域受 memblock 分配器管理。
4.2.arm64_memblock_init
void __init arm64_memblock_init(void)
{
/*
将memblock.reserve保留区域大小强制限定,因为总内存大小有限
并将最后超标的内存用memblock_remove_range进行截断
*/
memblock_enforce_memory_limit(memory_limit);
/*
* Register the kernel text, kernel data, initrd, and initial
* pagetables with memblock.
*/
memblock_reserve(__pa(_text), _end - _text);
#ifdef CONFIG_BLK_DEV_INITRD
if (initrd_start)
memblock_reserve(__virt_to_phys(initrd_start), initrd_end - initrd_start);
#endif
/*
1. 将dtb区域加入memblock.reserve, initial_boot_params=fdt
2. 通过fdt_header.off_mem_rsvmap指针,寻找/memreserve/ fields,并加入memblock.reserve
3. 使用回调__fdt_scan_reserved_mem, 找出所有"reserved-memory",分析后加入memblock.reserve,并分配内存空间
*/
early_init_fdt_scan_reserved_mem();
/* 4GB maximum for 32-bit only capable devices */
if (IS_ENABLED(CONFIG_ZONE_DMA))
arm64_dma_phys_limit = max_zone_dma_phys();
else
arm64_dma_phys_limit = PHYS_MASK + 1;
/* CMA区域或CMA上下文, 保留连续的内存空间给Global CMA area使用,稍后返回给伙伴系统从而可以被用作正常申请, 关于CMA具体可参考:https://www.cnblogs.com/newjiang/p/9592797.html */
dma_contiguous_reserve(arm64_dma_phys_limit);
/* 设置memblock_can_resize标志,目前暂未确定用途?*/
memblock_allow_resize();
/* 如果打开了memblock_debug开关,则会打印memblock结构体中目前保存的memory、reserve/*
memblock_dump_all();
}
在arm64架构下, 内核在start_kernel()->setup_arch()中通过arm64_memblock_init( )完成了memblock的初始化,主要是通过memblock_remove将某些memblock_region区域从memblock.memory中移除,这些区域包含了DDR物理地址所不包含的区域,以及内核线性映射区所不能涵盖的区域;同时将某些物理区间添加到memblock.reserved中,这额区间包含dts中预留区域,命令行中通过参数预留的CMA区域,内核的代码段、initrd、页表、数据段等所在区域,crash kernel保留区域以及elf相关区域。这个过程中也会初始化一些全局变量,如物理内存起始地址memstart_addr。
4.3 memblock 预留内存初始化
将需要保留的内存添加进预留内存类型集合(memblock.reserved),使得后续使用 memblock 分配内存时,避开预留内存。例如在分页系统初始化过程中会调用 memblock_reserve 函数将内核程序在内存中的范围保留,保证其不会被覆盖,调用关系如下:
- paging_init
- setup_bootmem()
- memblock_reserve(vmlinux_start, vmlinux_end - vmlinux_start)
类似的,其他预留内存的地方(如设备树中设置的预留内存)也均是通过调用 memblock_reserve 接口函数实现。 关于预留内存的使用,将会在后续分析驱动相关的 ioremap 文章中举例说明。
5.memblock调试
在kernel command中加入“memblock=debug”。
在内核启动后,通过/proc/kmsg查看调试信息。
查看内存地址范围和reserved区域可以通过:
/sys/kernel/debug/memblock/memory
/sys/kernel/debug/memblock/reserved
refer to
- Documentation/core-api/boot-time-mm.rst
- https://www.lagou.com/lgeduarticle/23340.html
- https://github.com/BiscuitOS/HardStack/tree/master/Memory-Allocator/Memblock-allocator/API
- https://tinylab.org/riscv-memblock/
- https://www.cnblogs.com/LoyenWang/p/11440957.html
- https://0xax.gitbooks.io/linux-insides/content/MM/linux-mm-1.html