Linux内核保留内存机制:从CMA到自定义驱动的深度解析与实践
1. 项目概述为什么我们需要“保留”内存在Linux系统启动的早期阶段屏幕上滚动的日志里你可能会注意到一行类似Reserving 64MB of memory at 0x10000000 for crash kernel的信息。对于大多数用户而言这行信息一闪而过无足轻重。但对于嵌入式开发者、内核黑客或者正在构建高可用性服务器集群的运维工程师来说这行日志背后隐藏的“保留内存”机制却是系统稳定性和功能实现的一块基石。简单来说保留内存就是一块在系统启动初期由引导加载程序如U-Boot或内核自身从物理内存中“圈”出来的一块特殊区域。这块区域对Linux内核的通用内存管理器即伙伴系统是不可见的。内核在初始化时会跳过这块区域不会将其纳入常规的页框分配池。那么为什么要这么做这听起来像是在浪费宝贵的物理内存。其核心价值在于隔离与专用。想象一下你有一套精密的实验设备其中有一个核心部件对振动极其敏感。你会把它和产生巨大震动的重型设备放在同一张桌子上吗显然不会。你会为它准备一个独立的、带有减震基座的平台。保留内存扮演的就是这个“独立平台”的角色。它确保了某些对内存访问时序、连续性或安全性有严苛要求的硬件或软件模块能够拥有一块“纯净”且“专属”的工作空间不受通用内存分配器繁忙的分配/释放操作、页面迁移或内存压缩等活动的干扰。常见的应用场景包括连续内存分配器CMA为需要大量物理连续内存的硬件如GPU、视频编解码器、高速网卡DMA缓冲区预留大块连续空间。内核崩溃转储Kdump预留一块内存给第二个“捕获内核”用于在主内核崩溃时安全地保存崩溃现场信息。安全协处理器或可信执行环境TEE为安全世界如ARM TrustZone预留受保护的内存区域。特定硬件寄存器映射某些外设的寄存器窗口需要映射到固定的物理地址。实时应用或自定义内存管理为实时任务或用户态驱动提供确定性的、低延迟的内存访问。理解保留内存的初始化原理意味着你掌握了在复杂硬件平台上进行深度定制和性能优化的钥匙。它不仅是内核启动流程中的一个步骤更是连接硬件特性、内核子系统和上层应用需求的关键桥梁。接下来我们将深入内核源码拆解这块内存是如何被“保留”下来的并实战如何为自己的设备配置它。2. 保留内存的初始化原理深度拆解保留内存的初始化是一个从硬件描述到内核接纳的连贯过程贯穿于设备树DTB、早期启动到内存管理子系统初始化的多个阶段。其核心逻辑是先通过设备树声明再由内核解析并注册到全局保留内存链表最后在伙伴系统初始化时将其排除在外。2.1 设备树DTB中的声明一切的源头在现代ARM等体系结构中硬件配置信息通过设备树二进制文件DTB传递给内核。保留内存的区域正是在这里定义的。它通常位于设备树的/reserved-memory节点下。// 示例一个典型的保留内存设备树节点 / { reserved-memory { #address-cells 2; #size-cells 2; ranges; // 示例1为CMA保留256MB内存 linux,cma { compatible shared-dma-pool; reusable; // 关键属性内核空闲时可被CMA机制用于可移动页分配 size 0x0 0x10000000; // 256MB alignment 0x0 0x200000; // 2MB对齐 linux,cma-default; }; // 示例2为Crash Kernel保留64MB内存 crashkernel_reserved: region10000000 { reg 0x0 0x10000000 0x0 0x04000000; // 起始0x10000000大小64MB no-map; // 关键属性内核不会为此区域创建页表映射严格保留 }; // 示例3为自定义驱动保留一块内存 mydriver_reserved: region20000000 { compatible vendor,mydriver-memory; reg 0x0 0x20000000 0x0 0x00100000; // 1MB no-map; }; }; };关键属性解析reg: 定义了区域的物理起始地址和大小。格式为起始地址高位 起始地址低位 大小高位 大小低位具体位数由父节点的#address-cells和#size-cells决定。compatible: 驱动匹配的关键。“shared-dma-pool”用于CMA“vendor,mydriver-memory”这样的自定义字符串需要对应的驱动来识别。reusable: 这是CMA区域特有的标志。它告诉内核这块内存虽然被保留但在未被CMA使用时可以被内核的可移动页面分配器如用于文件缓存使用。这是CMA高效利用内存的精髓。no-map: 最重要的属性之一。它指示内核不要为这块保留内存建立页表映射到内核线性地址空间。这意味着内核代码无法直接通过虚拟地址访问这块内存。访问它需要驱动通过ioremap或memremap等API显式建立映射。这增强了安全性避免了意外访问。alignment: 指定区域的对齐要求对于DMA操作尤为重要。注意no-map和reusable通常是互斥的。no-map意味着严格保留、内核不动常用于 Crash Kernel 或安全区域reusable意味着弹性共享是CMA工作的基础。错误配置可能导致内存浪费或功能失效。2.2 内核初始化流程从解析到注册内核启动过程中与保留内存相关的初始化主要发生在start_kernel函数调用链的早期。早期固定映射建立 (setup_arch-early_fixmap_init)在内核页表完全建立之前会先建立一个早期固定映射页表用于访问设备树等关键数据。此时保留内存的信息还未被处理。设备树扫描与保留内存解析 (arm64_memblock_init或setup_machine_fdt)内核调用early_init_fdt_scan_reserved_mem函数。该函数遍历设备树中的/reserved-memory节点对其每一个子节点读取reg属性获取物理地址和大小。读取no-map、reusable等属性。调用__reserved_mem_reserve_reg函数最终将这片区域的信息提交给memblock 分配器。memblock 分配器启动期的内存管家在伙伴系统Buddy System完全初始化之前内核使用一个简单的memblock分配器来管理物理内存。它维护两个全局数组memory(可用内存) 和reserved(已保留内存)。当上述解析函数执行时它会调用memblock_reserve或memblock_mark_nomap。memblock_reserve将区域加入reserved数组。对于reusable的区域如CMA通常先调用这个。memblock_mark_nomap不仅保留还标记为no-map。这会导致后续memblock_free_all时这些页面不会被释放到伙伴系统并且内核线性映射也会跳过它们。这是no-map属性在启动期的体现。保留内存区域注册 (fdt_init_reserved_mem)对于设备树中定义了compatible属性的保留内存区域内核会调用reserved_mem_init_node尝试初始化它。这个过程会根据compatible字符串在__reservedmem_of_table段中查找匹配的reserved_mem操作结构体。如果找到例如“shared-dma-pool”对应rmem_cma_setup则调用其初始化函数initfn。对于CMArmem_cma_setup会调用cma_init_reserved_mem将该区域注册为CMA区域。如果未找到匹配的驱动该区域仍被保留但不会被特定子系统管理等待后续驱动通过of_reserved_mem_device_init来认领。伙伴系统初始化与内存释放 (mm_init-mem_init-memblock_free_all)这是关键一步。在memblock_free_all()函数中内核遍历所有被memblock标记为memory的可用区域并将其中未被reserved的页面释放到伙伴系统成为可供分配的自由页面。那些被memblock_reserve或memblock_mark_nomap的区域在此过程中被跳过。这就是“保留”的最终实现——它们从未进入伙伴系统的自由列表。对于标记为reusable的区域如CMA其页面虽然未被释放到伙伴系统的常规可移动/不可移动列表但CMA机制会将其作为一个特殊的“CMA空闲页面池”来管理。2.3 CMA与普通保留内存的异同理解CMA和普通no-map保留内存的区别至关重要。特性连续内存分配器 (CMA)普通保留内存 (no-map)设计目的为DMA等需求提供大块物理连续内存同时提高内存利用率。严格隔离一块内存供特定硬件或安全功能专用。内核可见性通过CMA API (cma_alloc) 可见。伙伴系统知其存在但不动用其页面。对伙伴系统完全不可见如同这块内存不存在。内存利用高效。通过reusable属性空闲时CMA内存可作为可移动页被系统用于文件缓存等需要时再被CMA回收。独占。无论是否使用内核都不会触碰这块内存可能造成浪费。访问方式驱动通过cma_alloc分配得到的是内核线性地址映射的页面可直接用virt_to_phys转物理地址给设备。驱动需通过of_reserved_mem_device_init_by_idx获取区域信息并用ioremap/memremap建立映射后才能访问。典型应用GPU显存、视频编解码缓冲区、大数据块DMA。内核崩溃转储、TrustZone安全内存、特定硬件寄存器窗口。设备树属性compatible “shared-dma-pool”;且通常带reusable。自定义compatible或无名通常带no-map。核心思想CMA是一种“弹性保留”在保证连续性的前提下追求利用率而no-map保留是一种“刚性隔离”追求绝对的确定性和安全性。3. 实战为自定义驱动配置并使用保留内存理论之后我们来点实际的。假设我们正在开发一个名为mydriver的高速数据采集卡驱动该卡上的FPGA需要通过DMA将数据直接写入一块固定的物理内存区域且要求该区域不被系统其他部分干扰。3.1 步骤一修改设备树首先我们需要在硬件平台对应的设备树文件如arch/arm64/boot/dts/vendor/myboard.dts中声明这块内存。// 在根节点 / 下添加或修改 reserved-memory 节点 / { reserved-memory { #address-cells 2; #size-cells 2; ranges; // 为我们的驱动保留16MB内存起始地址根据平台内存映射图选择这里假设0x70000000空闲 mydriver_reserved: region70000000 { compatible vendor,mydriver-ram; // 自定义的兼容性字符串 reg 0x0 0x70000000 0x0 0x01000000; // 起始 0x70000000, 大小 16MB no-map; // 非常重要内核不映射完全由驱动控制 }; }; // 在对应的总线节点下如PCIe、AXI添加对mydriver的设备节点引用 mydriver0 { compatible vendor,mydriver; reg ...; // 设备寄存器地址 memory-region mydriver_reserved; // **关键**引用上面定义的保留内存区域 // ... 其他属性 }; };关键点地址选择0x70000000必须是一个物理上存在且未被其他硬件如GPU、PCIe BAR占用的地址。你需要查阅SoC的数据手册和内核启动日志中的内存映射信息来确定。memory-region属性这是将设备节点与保留内存区域绑定的标准方法。驱动通过这个属性找到对应的内存。3.2 步骤二在驱动中初始化和映射保留内存在mydriver的驱动代码例如mydriver.c中我们需要在probe函数中完成保留内存的初始化和映射。#include linux/of.h #include linux/of_reserved_mem.h #include linux/io.h struct mydriver_dev { void __iomem *reserved_mem_base; phys_addr_t reserved_mem_phys; size_t reserved_mem_size; // ... 其他设备数据 }; static int rmem_mydriver_setup(struct reserved_mem *rmem) { // 这个函数在早期由 reserved_mem_init_node 调用 // 我们可以在这里对保留内存区域进行一些早期设置比如设置名称 rmem-ops rmem_mydriver_ops; pr_info(Reserved memory for mydriver at %pa, size %zx\n, rmem-base, rmem-size); return 0; } static const struct reserved_mem_ops rmem_mydriver_ops { .device_init rmem_mydriver_device_init, // 设备特定的初始化 .device_release rmem_mydriver_device_release, }; static int mydriver_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct mydriver_dev *mdev; int ret; mdev devm_kzalloc(dev, sizeof(*mdev), GFP_KERNEL); if (!mdev) return -ENOMEM; // **核心操作**初始化保留内存区域到本设备 ret of_reserved_mem_device_init_by_idx(dev, dev-of_node, 0); if (ret) { dev_err(dev, Failed to init reserved memory region\n); return ret; } // 获取保留内存的物理地址和大小 mdev-reserved_mem_phys of_reserved_mem_device_get_phys_addr(dev); mdev-reserved_mem_size of_reserved_mem_device_get_size(dev); if (!mdev-reserved_mem_phys || !mdev-reserved_mem_size) { dev_err(dev, Invalid reserved memory region\n); ret -EINVAL; goto err_release_mem; } // **核心操作**将保留内存映射到内核虚拟地址空间 // 使用 memremap因为它可以处理多种内存类型如设备内存 mdev-reserved_mem_base memremap(mdev-reserved_mem_phys, mdev-reserved_mem_size, MEMREMAP_WB); // 使用回写模式如果设备支持缓存 if (!mdev-reserved_mem_base) { dev_err(dev, Failed to remap reserved memory\n); ret -ENOMEM; goto err_release_mem; } dev_info(dev, Reserved memory mapped: phys%pa, virt%p, size%zx\n, mdev-reserved_mem_phys, mdev-reserved_mem_base, mdev-reserved_mem_size); // 现在 mdev-reserved_mem_base 就可以像普通内核内存一样被访问了 // 并且其背后的物理地址是固定的、保留的。 // 你可以将这个物理地址配置到你的硬件DMA描述符中。 // ... 初始化硬件配置DMA等 return 0; err_release_mem: of_reserved_mem_device_release(dev); return ret; } static int mydriver_remove(struct platform_device *pdev) { struct device *dev pdev-dev; struct mydriver_dev *mdev platform_get_drvdata(pdev); if (mdev-reserved_mem_base) { memunmap(mdev-reserved_mem_base); } of_reserved_mem_device_release(dev); // ... 其他清理 return 0; } // 告诉内核这个兼容性字符串的保留内存由我们管理 RESERVEDMEM_OF_DECLARE(mydriver_ram, vendor,mydriver-ram, rmem_mydriver_setup);代码解析与注意事项of_reserved_mem_device_init_by_idx这是驱动与设备树中memory-region属性关联的标准API。它会查找设备节点下第idx个通常为0memory-region句柄并将对应的保留内存区域“分配”给这个设备。对于no-map区域这主要是在内核中建立关联记录。memremapvsioremap对于保留内存推荐使用memremap。ioremap通常用于映射设备寄存器非缓存、顺序访问而memremap更通用可以根据标志如MEMREMAP_WB决定映射属性更适合映射用作数据缓冲区的内存。如果硬件要求非缓存访问则应使用MEMREMAP_WT或MEMREMAP_WC。物理地址获取of_reserved_mem_device_get_phys_addr获取的是保留内存的起始物理地址。这个地址是固定的就是你设备树里写的reg属性值。你可以安全地将这个地址写入硬件的DMA目标地址寄存器。错误处理映射和初始化都可能失败必须做好错误处理并在remove函数中对称地释放资源memunmap和of_reserved_mem_device_release。RESERVEDMEM_OF_DECLARE这个宏将你的rmem_mydriver_setup函数注册到__reservedmem_of_table段。当内核解析到设备树中compatible “vendor,mydriver-ram”的保留内存节点时会自动调用该函数。这对于执行一些早期、全局的初始化很有用但并非绝对必需因为驱动在probe中也能通过其他API获取区域信息。3.3 步骤三编译、更新与验证编译设备树使用你的SDK或内核源码树编译出新的DTB文件。make dtbs更新设备树将新的DTB文件部署到你的开发板或模拟器的启动位置如U-Boot加载地址。启动系统并验证查看内核启动日志 (dmesg)寻找关于保留内存的信息$ dmesg | grep -i reserved [ 0.000000] Reserved memory: created CMA memory pool at 0x00000000b0000000, size 256 MiB [ 0.000000] Reserved memory: initialized node linux,cma, compatible id shared-dma-pool [ 0.000000] Reserved memory: created DMA memory pool at 0x0000000070000000, size 16 MiB [ 0.000000] Reserved memory: initialized node mydriver_reserved, compatible id vendor,mydriver-ram检查/proc/iomem文件。这里列出了系统的物理内存映射视图。你的保留内存区域应该在这里显示并且可能被标记为“Reserved”。$ cat /proc/iomem | grep -A2 -B2 70000000 70000000-70ffffff : Reserved加载你的驱动 (insmod mydriver.ko)。如果驱动probe成功你应该能在dmesg中看到你打印的映射成功的信息。使用cat /proc/vmallocinfo可以查看通过memremap/ioremap映射的区域理论上能看到你映射的地址范围。4. 高级应用与疑难排查掌握了基础配置后我们来看一些更复杂的场景和常见问题。4.1 动态配置Crash Kernel内存大小Crash Kernel是Kdump机制的核心它需要在主内核崩溃前预留好内存。其大小可以通过内核启动参数动态指定这比写死设备树更灵活。方法一通过内核命令行参数在内核启动参数如U-Boot的bootargs中添加crashkernel256M或者指定偏移地址crashkernel256M512M内核的crashk_res资源会在启动时根据这个参数自动调用memblock_reserve保留指定内存。你可以在/proc/iomem中看到名为“Crash kernel”的区域。方法二设备树与命令行结合推荐在设备树中定义一个“占位”的保留内存节点但不指定大小或指定一个范围。reserved-memory { crashkernel_reserved: crashkernel0 { compatible nomap; // 大小由命令行决定这里可以给一个很大的范围限制 reg 0x0 0x40000000 0x0 0x40000000; // 从1GB开始最大保留1GB空间 }; };然后通过命令行crashkernel256M指定具体大小。内核的崩溃转储驱动会解析命令行并在设备树定义的范围内保留内存。实操心得对于产品化部署使用方法二更优。它在设备树中固定了Crash Kernel的地址范围避免了每次启动因内存布局变化导致保留地址不同。命令行只控制大小提供了灵活性。务必确保命令行指定的大小不超过设备树中定义的范围。4.2 排查保留内存相关问题问题1驱动probe时无法获取保留内存。检查设备树确认memory-region属性拼写正确且引用的标签mydriver_reserved与定义一致。使用dtc工具反编译DTB文件验证dtc -I dtb -O dts myboard.dtb | less。检查内核配置确保CONFIG_OF_RESERVED_MEM和CONFIG_OF_RESERVED_MEM_DEVICE已启用。查看启动日志确认内核是否成功解析并初始化了你的保留内存节点。如果compatible不匹配或节点格式错误日志中可能没有对应信息。问题2驱动访问映射的内存时发生内核崩溃Oops。映射属性错误这是最常见的原因。如果你的硬件DMA引擎不支持缓存一致性即非一致性DMANon-coherent DMA那么CPU缓存中的数据可能与内存中的数据不一致。此时你必须使用非缓存映射。错误做法使用MEMREMAP_WB回写缓存映射但DMA是非一致性的。DMA写入的数据可能在CPU缓存中CPU读不到CPU写入缓存的数据可能未刷回内存DMA读不到。正确做法使用MEMREMAP_WT透写或MEMREMAP_WC合并写或者直接使用ioremap默认非缓存。并在DMA操作前后使用dma_sync_single_for_device/cpu等API进行缓存同步。如何判断查阅你的硬件数据手册。SOC内部集成的DMA如SD/MMC控制器通常是一致性的而一些独立的FPGA或PCIe设备可能不是。地址对齐问题确保保留内存的起始地址和大小符合硬件DMA的对齐要求通常是4KB、64KB或更大。在设备树中使用alignment属性并在驱动中检查of_reserved_mem_device_get_phys_addr返回的地址是否对齐。问题3系统可用内存比物理内存少了很多怀疑保留内存设置过大。检查/proc/meminfo和/proc/iomem对比两者找出被标记为“Reserved”且不在MemTotal中的区域。计算其总和。分析设备树检查所有reserved-memory子节点特别是没有reusable属性的no-map区域。这些区域是永久“丢失”的。优化策略对于CMA区域确保其大小合理。可以通过/sys/kernel/mm/cma/cma-N/free_pages等接口查看CMA实际使用情况避免过度预留。对于no-map区域精确计算所需最小尺寸。例如Crash Kernel大小取决于内核和initramfs的大小通常128M-256M足够。考虑内存复用如果某块保留内存只是阶段性使用如启动后某个服务初始化时是否可以设计成在驱动卸载时通过memblock_free需非常小心或类似的机制释放回系统这需要非常精细的设计和测试。问题4多个驱动想共享同一块保留内存。这通常不是一个好主意因为缺乏同步机制容易导致冲突。如果必须共享可以考虑以下模式一个主驱动管理提供子API由一个核心驱动如“内存池驱动”负责初始化和映射保留内存。它通过自定义的ioctl或sysfs接口将分配好的内存块偏移大小传递给其他驱动。其他驱动再根据得到的物理地址各自调用ioremap映射属于自己的那部分。使用DMA-BUF或ION框架这些是内核标准的缓冲区共享框架。你可以将整块保留内存注册为一个DMA-BUF然后多个驱动可以导入这个DMA-BUF并获取文件描述符从而实现安全的共享和同步。这比自行设计更规范、更安全。设备树中定义多个子区域在reserved-memory节点下为每个驱动定义独立的子区域。这是最清晰、最推荐的方式从硬件资源层面隔离。保留内存是Linux内核提供给开发者的一项强大而底层的功能。它打破了内核统一管理内存的抽象允许我们为了性能、安全或兼容性与硬件进行更直接的对话。理解其原理意味着你不仅能解决“内存不够用”或“DMA失败”的表面问题更能从系统架构层面设计出更高效、更稳定的嵌入式或高性能计算解决方案。每一次对保留内存的配置都是对系统资源的一次精密规划。