memblock管理器
"node" {
"label": "memblock",
"categories": ["mem"],
"info": "memblock manage",
"depends": [
"obsolate param",
"fixmap"
]
}
介绍
在之前的内存映射过程中,我们看到我们都是使用的静态内存,在系统boot启动阶段,由于此时内核内存子系统还没有初始化,无法使用动态内存申请
但是内存子系统的初始化实际上也要依赖内存的动态分配,因此linux 提供了一个更加早期的内存管理系统memblock,memblock是使用静态内存管理内存的,因此不依赖其他任何内存管理模块
设计实现
memblock_region
最核心的结构体,定义描述一段被管理的内存区域block
struct memblock_region {
phys_addr_t base; // 内存地址
phys_addr_t size; // 内存大小
enum memblock_flags flags; //标识内存区域特殊标志
#ifdef CONFIG_NUMA
int nid; // 标识内存所属NODE
#endif
};
memblock_flags用于标识内存属性
enum memblock_flags {
MEMBLOCK_NONE = 0x0, // 默认的内存类型
// 标识此段内存是支持热插拔 内存初始化时需要特殊处理
MEMBLOCK_HOTPLUG = 0x1,
// X86 特有 镜像内存 内存分配会优先选择这段内存
MEMBLOCK_MIRROR = 0x2,
// 此段内存标识内存不会被加入线性映射
MEMBLOCK_NOMAP = 0x4, /* don't add to kernel direct mapping */
MEMBLOCK_DRIVER_MANAGED = 0x8, /* always detected via a driver */
MEMBLOCK_RSRV_NOINIT = 0x10, /* don't initialize struct pages */
};
memblock type
管理相同类型的 memblock_region ,其实现在只有两类:
-
reserved: 当前物理内存已经被使用的内存区域 -
memory: 内核可以看到的内存区域
struct memblock_type {
//已经使用的数组
unsigned long cnt;
//regions 数组长度
unsigned long max;
// 覆盖的内存大小
phys_addr_t total_size;
struct memblock_region *regions;
char *name;
};
memblock
struct memblock {
bool bottom_up; /* is bottom up direction? */
phys_addr_t current_limit;
struct memblock_type memory;
struct memblock_type reserved;
};
在上面已经解释过了,持有 2个memblock_type
bottom_up: 决定内存申请方向
core data
内核静态初始化定义了一个memblock,内部包含了两个大小固定的静态数组存储memblock-region
struct memblock memblock __initdata_memblock = {
.memory.regions = memblock_memory_init_regions,
.memory.max = INIT_MEMBLOCK_MEMORY_REGIONS,
.memory.name = "memory",
.reserved.regions = memblock_reserved_init_regions,
.reserved.max = INIT_MEMBLOCK_RESERVED_REGIONS,
.reserved.name = "reserved",
.bottom_up = false,
.current_limit = MEMBLOCK_ALLOC_ANYWHERE,
};

Note
reserved: 内存表示已经申请或者被使用的内存, memory:不是用来表示可以使用的内存,而是表示内核可以看到的内存
核心API
memblock_add
int memblock_add(phys_addr_t base, phys_addr_t size)
功能 : 添加一段新的物理内存到memblock->memory ,并会尝试自动合并
区间插入逻辑核心实现:
for_each_region:
case1: 插入区间位于当前区间左边
找到了可以插入的位置,插入
case2: 插入区间位于当前区间右边
继续到下一个区间
case3: 插入区间和当前区间左侧有交集
检查区间属性是否一致 并尝试合并
case4: 插入区间和当前区间右侧有交集
检查区间属性是否一致 并尝试合并
memblock_remove
int memblock_remove(phys_addr_t base, phys_addr_t size)核心API:
从在meblock->memmoy内核可见内存区域 移动走一段内存
memblock_reserve
memblock_reserve(phys_addr_t base, phys_addr_t size)
在reserve 区域增加一段内存,标记该内存已经被使用
memblock_free
memblock_free(void *ptr, size_t size)
从reserver区域移除一段内存,表示该内存不再被使用
memblock_phys_alloc
phys_addr_t memblock_phys_alloc(phys_addr_t size, phys_addr_t align)
从可用区域申请一段对齐的物理内存,申请成功后 会被添加到reserved 区域
注意: 此接口在slab内存分配器初始化之前使用,初始化之后不应该在使用此接口,内存申请会被转到kzalloc_node 接口
memblock_phys_free
释放内存,实际上只是从reserved区域移除
memblock_phys_free
此接口和上面接口稍有不同,如果在buddy allocator初始化之前,你应该使用memblock_phys_free 释放内存,不应该使用此接口;
但是如果 buddy allocator初始化之后, 如果依然调用memblock_phys_free 接口,那么这段内存就不会被buddy alloctor看到,内存泄漏,因此如果是这种情况,需要掉用此接口,此接口会释放内存还给buddy
memblock_mark_(hotplug/mirror/nomap(base, size);
设置内存flag
memblock_start(end)_of_DRAM
获得当前系统物理内存区域的 起始/结束 位置.实际实现非常简单,读取memblock->memory.regions的头尾
memblock_alloc*
这个接口只有在内存完成线性映射之后才可以使用,返回的内存地址是虚拟内存地址
相关配置
bootconfig
通过bootconfig 设置memblock=debug 可以开启日志打印,查看内存申请和释放过程
Kconfig
CONFIG_ARCH_KEEP_MEMBLOCK
启动后是否保留memblock,一般关闭,可以在调试的时候开启,开启后可以在 /debugfs/memblock 查看 memory/reserved 字段
关于region扩容
memblock 初始region 大小现在是128,一旦内存区域节点数量不够使用,支持重新申请更大的数组扩容(resize) resize提供两种方法:
-
slab内存分配器初始化之后,使用slab内存分配 -
使用
memblock内存分配
memblock内存分配得到的是物理地址,必须要保证该内存是已经映射过的,关于内存的映射,我们在下一节会讲到
关于数据竞争
通过观察memblock的代码,看到 数据结构管理是没有任何锁保护的,因此要注意使用时机
内存初始化流程
内存基本初始化包括两个部分
-
根据硬件实际的物理内存初始化内核内存可见区域
-
标记内核已经使用的内存 (设备树、内核镜像等)
可见物理内存初始化
以arm64架构为例
start_kernel()
-> setup_arch()
-> setup_machine_fdt()
-> fixmap_remap_fdt() //fdt 内存映射
-> memblock_reserve(dt_phys, size); // 标记设备树内存已经被占用
->early_init_dt_scan()
-> early_init_dt_add_memory_arch()
-> memblock_add(base, size); // 添加内核可见内存到memblock
->arm64_memblock_init()
内核扫描设备树硬件信息,通过memblock_add 把扫描出来的物理内存添加到memory区域
另外,还会再根据体系架构,对memory区域完成裁剪
arm64_memblock_init 实现会完成体系架构有关的内存,大概如下:
-
裁剪超过架构可以访问的最大范围(
PA_BITS) -
memstart_addr: 初始化记录当前设备可访问物理内存的起始地址,再下一小节关于线性地址映射 会用到 -
裁剪超过
线性映射内存范围的物理内存 -
标记
initrd内存为reserved -
标记 内核镜像地址为
reserved -
标记设备树中主动被标记为
reserved的内存
reserved 内存初始化
除了上面提到的已经reserved内存,也会扫描设备树中标记内存
start_kernel()
-> setup_arch()
-> setup_machine_fdt()
-> fixmap_remap_fdt() //fdt 内存映射
-> memblock_reserve(dt_phys, size); // 标记设备树内存已经被占用
->early_init_dt_scan()
->arm64_memblock_init()
-> early_init_fdt_scan_reserved_mem()
有必要分析一下 early_init_fdt_scan_reserved_mem
数据结构
内核使用struct reserved_mem 描述硬件保留内存
struct reserved_mem {
const char *name;
unsigned long fdt_node;
const struct reserved_mem_ops *ops;
phys_addr_t base;
phys_addr_t size;
void *priv;
};
内核使用静态数组,记录系统reserved_mem
#define MAX_RESERVED_REGIONS 64
static struct reserved_mem reserved_mem[MAX_RESERVED_REGIONS];
数组初始化在early_init_fdt_scan_reserved_mem 扫描设备树时完成
然后系统会尝试从当前内存中分配指定大小的内存 ,并根据设备数种对内存的约束,设置nomap等标志
结果
一旦memblock 初始内存都初始化之后,此时其他模块可以通过memblock分配物理内存了
最终初始化完成之后,memblock 可以得到一个类似这样的结构,记录当前系统的内存和已经使用的内存

debug
如果CONFIG_ARCH_KEEP_MEMBLOCK 以及CONFIG_DEBUG_FS 开启时,
可以通过/sys/kernel/debug/memblock 查看系统启动时的内存布局
可以通过开启CONFIG_DEBUG_KMEMLEAK,可以检测memblock是否存在内存泄漏问题