:memblock 早期内存分配器实现剖析)
【免费下载链接】linux-insides-zhLinux 内核揭秘项目地址https://gitcode.com/hust-open-atom-club/linux-insides-zh点击查看免费下载本文基于仓库 MM/linux-mm-1.md 展开系统讲解 Linux 内核在引导初期、通用内核内存分配器slab/slub尚未就绪时如何通过memblock完成物理内存区域的登记、预留与分配。你将掌握memblock、memblock_type、memblock_region三大核心数据结构的关系与初始化细节理解memblock_add/memblock_reserve底层的区域插入、扩容与合并算法并学会使用memblockdebug启动参数与 debugfs 对早期内存管理进行调试排障。本文所有代码引用均对应 Linux 内核源码中的 include/linux/memblock.h 与 mm/memblock.c仓库未收录内核源码此处仅为路径标识内核源码位于 https://github.com/torvalds/linux。为什么需要 memblock内存管理是操作系统内核中最复杂的部分之一或许没有之一。在 Initialization/linux-initialization-3.md 讲解内核进入点之前的准备工作时讲解停在调用start_kernel函数之前。start_kernel在内核启动第一个init进程之前初始化了所有内核特性包括那些依赖于架构的特性。你可能还记得在引导时已经建立了初期页表、识别页表和固定映射页表但复杂的内存管理部分此时还没有开始工作。当start_kernel被调用时我们会看到从初期内存管理到更复杂的内存管理数据结构和技术的转变。memblock早期名为逻辑内存块内核接纳 Yinghai Lu 提供的补丁后改名正是承担这一过渡任务的框架在引导初期、泛用内核内存分配器还没有开始工作时负责对内存区域进行管理。x86_64架构上的内核使用该方法在 Initialization/linux-initialization-3.md 中我们已经见过它的身影——reserve_ebda_region通过memblock_reserve为扩展 BIOS 数据区域EBDA预留内存。本文则深入到它的内部实现。memblock 的三大核心数据结构memblock的所有数据结构都定义在 include/linux/memblock.h 头文件中按总控结构 → 类型结构 → 区域结构三层组织。顶层结构体 memblockstruct memblock { bool bottom_up; phys_addr_t current_limit; struct memblock_type memory; -- array of memblock_region struct memblock_type reserved; -- array of memblock_region #ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP struct memblock_type physmem; #endif };这个结构体包含五个域域类型含义bottom_upbool置为true时允许内存以自底向上低地址优先模式进行分配current_limitphys_addr_t描述当前内存块分配的尺寸限制分配上限地址memorystruct memblock_type可用free内存区域集合reservedstruct memblock_type被保留reserved内存区域集合physmemstruct memblock_type物理内存区域集合仅在CONFIG_HAVE_MEMBLOCK_PHYS_MAP开启时存在phys_addr_t类型的宽度取决于CONFIG_PHYS_ADDR_T_64BIT开启时为u64否则为u32见 Initialization/linux-initialization-3.md 中对phys_to_virt的分析。类型结构体 memblock_typestruct memblock_type { unsigned long cnt; unsigned long max; phys_addr_t total_size; struct memblock_region *regions; };该结构体提供某类内存的整体信息cnt当前内存块中内存区域region的数量maxregions已分配数组的容量上限total_size所有内存区域的总大小regions指向memblock_region数组的指针即区域列表的入口。区域结构体 memblock_regionstruct memblock_region { phys_addr_t base; phys_addr_t size; unsigned long flags; #ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP int nid; #endif };memblock_region描述一个具体的内存区域base是基址size是大小flags是区域属性标志。flags的取值包括#define MEMBLOCK_ALLOC_ANYWHERE (~(phys_addr_t)0) #define MEMBLOCK_ALLOC_ACCESSIBLE 0 #define MEMBLOCK_HOTPLUG 0x1MEMBLOCK_ALLOC_ANYWHERE不限制分配位置全1即地址上限为0xffffffffffffffffMEMBLOCK_ALLOC_ACCESSIBLE仅可在current_limit限定的可访问范围内分配MEMBLOCK_HOTPLUG标记该区域支持热插拔。若CONFIG_HAVE_MEMBLOCK_NODE_MAP编译选项开启memblock_region还包含节点 IDnid作为 NUMA 节点选择器用于在非一致内存访问NUMA系统上区分内存归属节点。三者关系的示意图--------------------------- --------------------------- | memblock | | | | _______________________ | | | | | memory | | | Array of the | | | memblock_type |-|--| membock_region | | |_______________________| | | | | | --------------------------- | _______________________ | --------------------------- | | reserved | | | | | | memblock_type |-|--| Array of the | | |_______________________| | | memblock_region | | | | | --------------------------- ---------------------------memblock通过三个memblock_type成员memory/reserved/physmem分别管理三类内存每个memblock_type内部又通过指针持有memblock_region的动态数组。这套三层嵌套 动态数组的结构是理解全部memblockAPI 的基础。memblock 的初始化全局变量与 __initdata_memblock 宏所有memblockAPI 的声明位于 include/linux/memblock.h实现集中在 mm/memblock.c。全局变量memblock的初始化如下struct memblock memblock __initdata_memblock { .memory.regions memblock_memory_init_regions, .memory.cnt 1, .memory.max INIT_MEMBLOCK_REGIONS, .reserved.regions memblock_reserved_init_regions, .reserved.cnt 1, .reserved.max INIT_MEMBLOCK_REGIONS, #ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP .physmem.regions memblock_physmem_init_regions, .physmem.cnt 1, .physmem.max INIT_PHYSMEM_REGIONS, #endif .bottom_up false, .current_limit MEMBLOCK_ALLOC_ANYWHERE, };几个关键点__initdata_memblock宏其定义依赖CONFIG_ARCH_DISCARD_MEMBLOCK#ifdef CONFIG_ARCH_DISCARD_MEMBLOCK #define __init_memblock __meminit #define __initdata_memblock __meminitdata #else #define __init_memblock #define __initdata_memblock #endif若该选项开启memblock代码与数据会被放置在.init/.meminit.data段中在内核引导完毕后整段释放从而不占用运行期的宝贵内存。这正是早期引导分配器用完即弃设计思想的体现。区域数组的预分配每个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_REGIONS] __initdata_memblock; #ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP static struct memblock_region memblock_physmem_init_regions[INIT_PHYSMEM_REGIONS] __initdata_memblock; #endif其中#define INIT_MEMBLOCK_REGIONS 128每个数组初始包含 128 个memblock_region槽位且数组本身同样用__initdata_memblock标记与memblock变量一起在引导后释放。cnt初始为 1、max初始为INIT_MEMBLOCK_REGIONS表示初始状态下每个类型已有 1 个占位区域、最多容纳 128 个区域。当区域数量超过max时memblock会触发数组扩容详见后文memblock_double_array。bottom_up false默认关闭自底向上分配current_limit MEMBLOCK_ALLOC_ANYWHERE默认分配上限为(~(phys_addr_t)0)即0xffffffffffffffff64 位平台上的物理地址最大值等价于不限地址。到此memblock结构体的初始化即告完成接下来可以进入 API 层面。memblock 核心 API从 memblock_add 看区域插入算法内核中有大量地方使用memblock。例如 arch/x86/kernel/e820.c 中的memblock_x86_fill函数位于#L1061附近它使用 e820 提供的内存映射并调用memblock_add把内核保留的内存区域登记进memblock。我们就从memblock_add开始剖析。memblock_add → memblock_add_rangememblock_add获取物理基址base和大小size把它们加入memblock。它本身不做特殊处理只是调用memblock_add_range(memblock.memory, base, size, MAX_NUMNODES, 0);参数分别是内存块类型memory、基址、大小、MAX_NUMNODES与标志。MAX_NUMNODES的取值规则为若CONFIG_NODES_SHIFT未设置则为1否则为1 CONFIG_NODES_SHIFT参见 Initialization/linux-initialization-3.md 中对MAX_NUMNODES/NODES_SHIFT的分析。memblock_add_range负责把新区城加入memblock_type。流程如下空区域检查先检查传入大小若为 0 直接返回。空类型直接填充若memblock_type中尚无区域regions[0].size 0则直接用给定值填充第一个memblock_region并返回。我们在 Initialization/linux-initialization-3.md 对reserve_ebda_region的分析中已看到这一实现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; }非空类型先计算区域结束地址并防止溢出phys_addr_t end base memblock_cap_size(base, size);memblock_cap_size把size调整为base size不会溢出的最大值static inline phys_addr_t memblock_cap_size(phys_addr_t base, phys_addr_t *size) { return *size min(*size, (phys_addr_t)ULLONG_MAX - base); }两步式插入拆分不重叠部分 合并相邻区域memblock_add_range采用两步走策略把新区域插入memblock第一步把新区域的不重叠部分作为单独区域加入第二步合并所有可相接的区域。函数先迭代所有已存储区域检查是否与新区域重叠for (i 0; i type-cnt; i) { struct memblock_region *rgn type-regions[i]; phys_addr_t rbase rgn-base; phys_addr_t rend rbase rgn-size; if (rbase end) break; if (rend base) continue; ... }若新区域完全在已存区域之后rbase end说明不存在重叠直接退出循环准备插入若已存区域完全在新区域之前rend base则跳过继续比较下一个。若发现需要插入的新区域数量会导致数组溢出则调用memblock_double_array扩容while (type-cnt nr_new type-max) if (memblock_double_array(type, obase, size) 0) return -ENOMEM; insert true; goto repeat;memblock_double_array会把区域数组长度加倍这也是为什么初始化时max INIT_MEMBLOCK_REGIONS 128只是一个起点后续可动态增长。随后置insert true并跳回repeat标签重跑循环第二次循环中用memblock_insert_region真正插入if (base end) { nr_new; if (insert) memblock_insert_region(type, i, base, end - base, nid, flags); }memblock_insert_region的实现与向空memblock_type插入几乎相同取目标下标处的区域指针用memmove把后续区域整体后移一个槽位再填充新区域并增加cntstruct memblock_region *rgn type-regions[idx]; ... memmove(rgn 1, rgn, (type-cnt - idx) * sizeof(*rgn));函数末尾memblock_add_range调用memblock_merge_regions执行第二步——合并相邻可合并区域。完全重叠场景与合并算法还有第二种情况新区域与已存储区域完全重叠。例如memblock中已有region10x0 ~ 0x10000 0x1000 ----------------------- | | | region1 | -----------------------现在添加region2基址0x100结束于0x20000x100 0x2000 ----------------------- | region2 | -----------------------此时新区域的基址被截断到已存区域末尾base min(rend, end);本例中base被设置为0x1000。第二步插入时if (base end) { nr_new; if (insert) memblock_insert_region(type, i, base, end - base, nid, flags); }只插入不重叠的高地址部分低地址部分已包含在重叠区域里最后用memblock_merge_regions合并剩余区域。合并函数遍历memblock_type的所有区域检查相邻的两个区域this与next是否满足标志相同、节点相同、第一个区域的末尾地址恰好等于第二个区域的基址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; }条件全部满足时把第二个区域的长度并入第一个区域并将后续区域整体前移一个下标同时递减cntthis-size next-size; ... memmove(next, next 1, (type-cnt - (i 2)) * sizeof(*next)); ... type-cnt--;合并完成后两个区域合二为一0 0x2000 ------------------------------------------------ | region1 | ------------------------------------------------这就是memblock_add_range的工作机制重叠截断 → 插入不重叠部分 → 合并相邻区域全程保证区域数组始终有序、不重叠、可合并。memblock_reserve与 memblock_add 的孪生函数memblock_reserve与memblock_add几乎完成同样的工作唯一区别是它操作memblock_type.reserved而非memblock_type.memory即把区域标记为被保留而非可用memblock_reserve(base, size) 相当于把 (base, size) 加入 memblock.reserved这正是 Initialization/linux-initialization-3.md 中reserve_ebda_region为 EBDA 预留内存、以及 Initialization/linux-initialization-4.md 中memblock_reserve(__pa_symbol(_text), ...)为内核_text到__bss_stop段预留内存时调用的函数。memblock 的其他常用 API除了memblock_add/memblock_reservememblock还提供以下 APIAPI作用memblock_remove从内存块中移除内存区域memblock_find_in_range在给定范围内寻找未使用可分配的区域memblock_free释放内存块中的区域重新标记为可用for_each_mem_range迭代遍历内存块区域memblock_mem_size统计所有mem区域不含保留区的总大小memblock_virt_alloc分配一段虚拟地址连续、物理地址也连续的引导期内存这些 API 在真实的初始化流程中频繁出现。例如 Initialization/linux-initialization-7.md 记录了reserve_initrd对memblock的完整使用链用memblock_mem_size(max_pfn_mapped)计算直接映射区域的总大小检查 initrd 是否越界用memblock_find_in_range(0, PFN_PHYS(max_pfn_mapped), area_size, PAGE_SIZE)在0到最大已映射物理地址之间寻找大小对齐的可用区域用于重定位 initrd重定位完成后用memblock_free(ramdisk_image, ramdisk_end - ramdisk_image)释放原 RAM 磁盘占用的区域。再如 Initialization/linux-initialization-7.md 提到的memblock_virt_alloc它内部调用memblock_virt_alloc_try_nid在 slab 不可用时走memblock_reserve分配引导内存块否则回退到kzalloc_node它使用BOOTMEM_LOW_LIMITPAGE_OFFSET 0x1000000的物理地址与BOOTMEM_ALLOC_ACCESSIBLE等于当前memblock.current_limit作为最小、最大分配地址。内核命令行拷贝strlen(boot_command_line) 1字节正是通过它完成的。获取已分配区域的信息memblock还提供两个查询 API用于获取已分配区域的物理地址与尺寸get_allocated_memblock_memory_regions_info获取有关可用内存区域的信息get_allocated_memblock_reserved_regions_info获取有关保留区域的信息。两者实现几乎相同以get_allocated_memblock_reserved_regions_info为例phys_addr_t __init_memblock get_allocated_memblock_reserved_regions_info( phys_addr_t *addr) { if (memblock.reserved.regions memblock_reserved_init_regions) return 0; *addr __pa(memblock.reserved.regions); return PAGE_ALIGN(sizeof(struct memblock_region) * memblock.reserved.max); }逻辑解析首先检查memblock.reserved.regions是否仍指向初始静态数组memblock_reserved_init_regions。若仍是说明从未触发过扩容返回 0否则把保留区域数组的物理地址__pa转换写入addr返回数组占用的页对齐后尺寸PAGE_ALIGN(sizeof(struct memblock_region) * memblock.reserved.max)。其中PAGE_ALIGN宏的定义为#define PAGE_ALIGN(addr) ALIGN(addr, PAGE_SIZE)即按页大小PAGE_SIZEx86_64 上为 4096 字节向上对齐。get_allocated_memblock_memory_regions_info唯一的不同是把memblock.reserved换成memblock.memory。这两个函数是外部模块把memblock数组交接给后续内存管理框架如 page allocator时的重要接口。memblock 调试memblockdebug 与 debugfsmemblock实现中遍布memblock_dbg调用用于在内核命令行传入memblockdebug时输出详细日志。memblock_dbg本质是printk的包装宏#define memblock_dbg(fmt, ...) \ if (memblock_debug) printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__)例如memblock_reserve中的调试输出memblock_dbg(memblock_reserve: [%#016llx-%#016llx] flags %#02lx %pF\n, (unsigned long long)base, (unsigned long long)base size - 1, flags, (void *)_RET_IP_);在带memblockdebug启动的内核上日志会形如下方截图——记录每次memblock_reserve的地址区间、标志与调用来源如alloc_large_system_hash、swiotlb_init以及memblock_virt_alloc_try_nid[_nopanic]类分配函数的参数细节大小、对齐、节点 ID、地址范围等实际使用中可在 QEMU 或真机内核命令行中追加memblockdebug loglevel7日志级别提高到能显示KERN_INFO即可在 dmesg 中看到完整的早期内存登记、预留与分配轨迹是排查引导期内存冲突如 EBDA、initrd、ramdisk 与内核镜像重叠的利器。此外memblock支持通过 debugfs 导出内容。在非 X86 架构下运行时可以访问以下节点获取memblock内容的核转储/sys/kernel/debug/memblock/memory/sys/kernel/debug/memblock/reserved/sys/kernel/debug/memblock/physmem分别对应memblock.memory、memblock.reserved与开启CONFIG_HAVE_MEMBLOCK_PHYS_MAP时的memblock.physmem。小结memblock是 Linux 内核在引导初期管理物理内存的基础框架它以memblock总控→memblock_type分类→memblock_region区域数组的三层结构登记可用内存与保留内存通过memblock_add_range的两步式截断插入 合并算法维护区域的有序性通过memblock_double_array支持数组动态扩容并通过memblockdebug与 debugfs 提供完整可观测性。理解memblock是后续理解固定映射地址与早期ioremap见 MM/linux-mm-2.md、kmemcheck见 MM/linux-mm-3.md乃至整个内核内存管理章节总览见 MM/README.md的基石。相关文档导航对内核内存管理框架的初览reserve_ebda_region与memblock_reserve的首次相遇Initialization/linux-initialization-4.mdmemblock_reserve为内核代码/数据/BSS 段预留内存Initialization/linux-initialization-7.mdinitrd 重定位过程中memblock_find_in_range/memblock_free/memblock_mem_size/memblock_virt_alloc的实战用例MM/linux-mm-2.md固定映射地址与早期ioremapMM/linux-mm-3.mdkmemcheck工具。赞分享【免费下载链接】linux-insides-zhLinux 内核揭秘项目地址https://gitcode.com/hust-open-atom-club/linux-insides-zh点击查看免费下载相关推荐Linux内核内存管理机制探析一Memblock内存块子系统Linux内核内存管理机制探析一Memblock内存块子系统 前言 内存管理作为操作系统内核中最核心的子系统之一其复杂度和重要性不言而喻。在Linux内文档教程操作系统Linux 内核启动期内存管理memblock深度解析结构、分配 API 与生命周期Linux 内核启动期内存管理memblock深度解析结构、分配 API 与生命周期 导读 在内核启动的极早期常规的内存分配器buddy 页分配器、s桌面应用系统编程Bluetooth-jammer-esp32硬件选择NRF24L01模块选购与兼容性指南Bluetooth jammer esp32硬件选择NRF24L01模块选购与兼容性指南 想要构建一个高效的Bluetooth jammer esp32设备吗上一篇open-multi-agent路线图与社区支持从MIT开源到多智能体编排的未来方向下一篇10秒素材变会说话的数字人Duix-Avatar AI数字人本地部署速通创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考