尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Linux内存预留技术详解:从memmap到CMA与ION堆的实战指南

Linux内存预留技术详解:从memmap到CMA与ION堆的实战指南 1. 为什么需要预留内存从一次线上故障说起去年我们团队负责的一个核心服务在业务高峰期突然出现了性能抖动监控显示系统负载飙升但CPU和内存使用率看起来都还有余量。经过一番紧急排查最终定位到问题出在一块特定的硬件上——我们使用了某款高性能网卡来做数据包加速DPDK技术栈。这块网卡在启动时需要向操作系统申请一大块连续的物理内存作为其专属的“数据缓冲区”。问题在于当服务运行一段时间后随着系统内其他进程的创建、文件缓存的增长物理内存变得碎片化。当网卡驱动尝试分配大块连续内存时系统无法满足导致网卡队列异常进而引发整个服务的雪崩。这次事故的根源就是没有在系统启动之初为这类有特殊需求的硬件或驱动“预留”好内存。所谓预留内存就是指在Linux内核启动的早期从物理内存中划出一部分区域标记为“保留”状态。这部分内存不会被内核的内存管理子系统如Buddy System接管也就不会分配给普通的用户进程或内核模块使用。它就像一片被圈起来的“自留地”专供特定的“住户”如硬件设备、安全模块、实时任务使用。除了为硬件预留预留内存的典型场景还有很多。比如在嵌入式领域为图形显示帧缓冲区Framebuffer预留内存确保屏幕显示稳定在实时计算场景为关键任务预留内存避免因内存分配延迟或失败导致任务超时在安全领域为可信执行环境TEE或加密模块预留独立的内存空间。理解并掌握Linux下预留内存的几种方法是系统工程师和驱动开发者必备的技能。这不仅能规避潜在风险更是进行深度系统定制和性能优化的基础。2. 内核启动参数法最直接、最古老的预留方式当Linux内核通过Bootloader如GRUB启动时我们可以通过内核命令行参数来告诉内核“嘿从物理内存的这块到那块你别动给我留着。” 这是最经典、最底层的方法其影响力从内核启动初期一直持续到系统运行的生命周期。2.1memmap参数指定物理地址范围memmap参数是达成此目的最直接的工具。它的语法格式是memmapsize[$start]%type或者更常见的是指定精确范围memmapsizestartsize: 要保留的内存大小支持K、M、G后缀如512M、1G。start: 保留内存区域的起始物理地址十六进制需加0x前缀。type: 保留类型例如reserved表示保留kernel表示给内核用等。通常我们使用reserved。实战示例与解析假设我们的服务器有4GB物理内存0x0 - 0x100000000我们想为一块PCIe设备预留位于物理地址0x20000000开始的512MB内存。在GRUB配置文件中我们会在linux行添加如下参数linux /vmlinuz ... memmap512M$0x20000000或者使用语法两者等价linux /vmlinuz ... memmap512M0x20000000系统启动后通过dmesg命令查看内核日志你会看到类似这样的信息[ 0.000000] user: [mem 0x20000000-0x3fffffff] reserved这行日志明确告诉我们从0x20000000到0x3fffffff的这块物理内存区域已经被标记为“reserved”。在/proc/iomem这个虚拟文件中我们也能清晰地看到这块“保留地”20000000-3fffffff : reserved为什么需要知道物理地址这是此方法最大的门槛和限制。你必须精确知道你要预留的内存区域的物理起始地址和大小。这个信息通常来自硬件手册设备的数据手册会明确说明其DMA缓冲区需要映射的物理地址范围。BIOS/UEFI设置有些服务器或嵌入式板卡的固件允许你配置一段内存区域给特定设备。反复测试与调试在没有明确文档时可能需要结合dmesg、/proc/iomem以及设备的调试信息来推断。注意事项与坑点地址冲突是灾难性的如果你指定的memmap区域与内核自身代码、initrd镜像或其他关键数据结构的加载地址重叠会导致内核无法启动或系统极不稳定。务必通过dmesg中的 “BIOS-provided physical RAM map” 或 “e820” 信息了解物理内存的原始布局。大小需对齐虽然内核可能处理非对齐的请求但为了最佳兼容性和性能建议size和start都按内存页大小通常是4KB对齐。无法动态管理一旦通过启动参数预留这块内存在本次系统运行周期内就被永久占用了即使预留它的设备没有使用也无法回收。这是一种“静态”的、粗粒度的预留。2.2mem参数另一种视角的预留mem参数通常用于限制内核可用的内存总量。例如在8GB的机器上设置mem6G内核就只会管理前6GB内存剩下的2GB物理内存对内核而言“不可见”。这间接达到了为其他用途如第二操作系统、特殊硬件预留内存的效果。这种方法更适用于虚拟化或双系统等场景为另一个系统实体预留内存。对于单个Linux系统内为特定设备预留memmap是更精确的选择。3. 设备树法嵌入式与ARM体系的现代标准对于x86服务器我们习惯用BIOS和内核参数。但在嵌入式世界特别是ARM体系架构中设备树Device Tree是描述硬件资源的权威标准。通过设备树预留内存是更规范、更可移植的方式。3.1 理解设备树中的reserved-memory节点设备树源文件.dts中有一个专门的节点/reserved-memory用来声明所有需要预留的内存区域。每个子节点代表一块预留区域。一个典型的预留内存节点如下所示/ { reserved-memory { #address-cells 2; #size-cells 2; ranges; my_reserved_region: region80000000 { compatible shared-dma-pool; // 或 no-map, linux,cma reg 0x0 0x80000000 0x0 0x10000000; // 起始地址0x80000000大小0x10000000 (256MB) no-map; }; }; };关键属性拆解compatible: 定义了这块预留内存的“兼容性”或类型这决定了内核和驱动如何对待它。shared-dma-pool: 表示这是一个共享的DMA内存池。驱动可以从这个池子里分配内存用于DMA操作。这是最常见的一种支持动态分配和释放。no-map: 表示这块内存不仅预留而且内核不会为其建立页表映射。通常用于需要绝对物理地址连续性、且由非CPU设备如GPU、DSP独占访问的内存。CPU无法直接读写这块内存。linux,cma: 表示这是一个连续内存分配器CMA区域。CMA是一种更智能的机制在设备不使用时这块内存可以被内核挪作他用如页面缓存当设备需要时再“挤”出来。这是平衡灵活性与保障性的好方法。reg: 定义区域的物理地址和大小。0x0 0x80000000 0x0 0x10000000表示起始地址是64位值0x0000000080000000大小是0x0000000010000000。no-map;: 一个空属性如果存在就表示“不映射”。通常与compatible no-map一起使用。3.2 驱动如何“认领”预留内存仅仅在设备树中声明了预留内存还不够需要让具体的设备驱动知道这块内存是给它用的。这通过设备树中的“内存区域引用”来实现。假设我们有一个名为my_device的设备它要使用上面定义的my_reserved_regionmy_device { memory-region my_reserved_region; status okay; };在设备驱动的探测probe函数中开发者会使用of_reserved_mem_device_init()或类似的API通过memory-region属性找到对应的预留内存区域并将其初始化为设备可用的DMA缓冲区。设备树法的优势硬件描述与代码分离硬件信息写在设备树里驱动代码无需硬编码地址提高了可移植性。系统化管理所有预留内存在一个地方/reserved-memory统一声明一目了然。支持动态类型通过compatible属性支持CMA等高级内存管理机制。实操心得在嵌入式项目中最常踩的坑就是地址对齐和大小设置错误。务必确认你预留的地址范围在物理内存的有效范围内并且没有与其他关键区域如内核镜像、设备寄存器冲突。使用dtc工具编译设备树源文件时仔细检查是否有警告信息。系统启动后可以通过/sys/kernel/debug/reserved_memory目录如果配置了CONFIG_DEBUG_FS来查看所有通过设备树预留的内存区域的状态。4. CMA连续内存分配器智能的“弹性预留”连续内存分配器CMAContiguous Memory Allocator是Linux内核提供的一种更优雅的解决方案。它试图解决“静态预留导致内存浪费”与“动态分配无法保证连续性”之间的矛盾。4.1 CMA 是如何工作的你可以把CMA区域理解为一个“有特权”的内存池。初始化在系统启动早期划出一块较大的连续物理内存作为CMA区域。“可借用”状态当没有设备申请CMA内存时这块内存并不会闲着。内核的内存管理系统可以像使用普通内存一样使用它比如分配给用户进程、作为文件页缓存等。这是与memmap或no-map预留最大的不同。“物归原主”当某个设备驱动通常是DMA驱动需要分配大块连续物理内存时它会向CMA申请。CMA收到请求后会尝试“迁移”当前占用该CMA区域的普通页面到其他地方把这块连续的物理内存腾出来交给设备使用。这个过程对用户进程是透明的可能会引起轻微的延迟。释放与回收设备用完后释放内存CMA区域又回到“可借用”状态。4.2 如何配置CMA区域配置CMA主要有两种方式它们常常结合使用方式一通过内核启动参数最常用使用cma参数语法简单cma256M这会在系统可用的物理内存中自动分配一块256MB的CMA区域。内核会自己选择一个合适的地址。你也可以指定位置cma256M0x80000000这表示在物理地址0x80000000处分配256MB的CMA区域。方式二通过设备树更精确在/reserved-memory节点下创建一个compatible linux,cma的节点。reserved-memory { linux,cma { compatible shared-dma-pool, linux,cma; reusable; size 0x0 0x10000000; // 256MB alignment 0x0 0x200000; // 2MB对齐 linux,cma-default; }; };reusable;属性是关键它表明这块内存是可重用的。linux,cma-default;属性表示这是默认的CMA区域。驱动在未指定特定区域时会从这里分配。4.3 驱动如何使用CMA对于驱动开发者来说使用CMA非常简单。在分配DMA内存时不再使用传统的dma_alloc_coherent()它可能从不同的池子分配而是使用dma_alloc_from_contiguous()或更通用的在设备树中通过memory-region关联了CMA区域后使用of_reserved_mem_device_init()初始化之后的DMA分配API会自动从CMA区域分配。CMA的优缺点与适用场景优点极大提高了内存利用率避免了专用预留内存的浪费。对于间歇性需要大块连续内存的设备如相机ISP、视频编解码器非常友好。缺点存在“内存迁移”的开销。在CMA区域被大量用作页面缓存时突然需要为设备腾出空间可能会引起短暂的性能抖动。不适合对分配延迟有极端要求的硬实时场景。适用场景移动设备手机、平板、多媒体处理设备、以及任何希望平衡内存利用率和DMA需求的中低延迟场景。注意CMA的效能高度依赖于内核的“页面迁移”机制和系统当前的内存压力。在内存紧张的系统上从CMA区域分配失败的概率会增高。5. ION/DMA-BUF 堆为多媒体与图形而生的专用分配器如果说CMA是内核通用的智能预留方案那么ION及其继任者DMA-BUF Heaps就是为Android和多媒体子系统量身定制的“高级VIP包厢”。它最初由Google为Android开发用于解决图形、相机、视频编解码等组件间高效、零拷贝地共享大量缓冲区的问题。5.1 核心概念Heap堆ION/DMA-BUF Heaps 将物理内存划分为不同类型的“堆”每个堆有不同的特性system堆从内核的普通内存非预留中分配可能不连续。system-contig堆从内核的普通内存中分配但保证物理连续内部可能调用CMA。cma堆从指定的CMA区域分配。这是最常用的一种它结合了CMA的智能管理和ION的共享框架。carveout堆从一块通过memmap或设备树预留的、静态的、物理连续的内存中分配。这是最传统的“预留内存”在ION框架下的表现形式。5.2 如何配置一个 Carveout Heap静态预留配置一个静态的carveout堆通常通过设备树完成。这实际上是将“设备树预留内存法”和“ION框架”结合了起来。在设备树中预留内存/ { reserved-memory { my_carveout: region90000000 { reg 0x0 0x90000000 0x0 0x4000000; // 64MB 0x90000000 no-map; // 静态预留不映射 }; }; };在设备树中定义ION Heap节点/ { ion { compatible myvendor,my-ion; #address-cells 1; #size-cells 0; heap1 { reg 1; heap-name my_carveout_heap; memory-region my_carveout; myvendor,heap-type carveout; // 指定堆类型 }; }; };这里的关键是memory-region my_carveout它将ION堆与之前预留的物理内存区域绑定。5.3 用户空间与内核空间如何使用ION/DMA-BUF Heaps 的强大之处在于提供了统一的内核和用户空间接口。用户空间进程可以通过/dev/dma_heap/heap_name设备节点来分配缓冲区。例如分配一块来自my_carveout_heap的1MB内存int fd open(/dev/dma_heap/my_carveout_heap, O_RDWR); struct dma_heap_allocation_data data { .len 1024 * 1024, .fd_flags O_RDWR | O_CLOEXEC, }; ioctl(fd, DMA_HEAP_IOCTL_ALLOC, data); // data.fd 就是分配出来的缓冲区文件描述符这个文件描述符可以被传递到其他进程或者通过libdrm、Wayland等图形协议传递给显示合成器、GPU驱动等实现零拷贝共享。内核空间驱动可以通过dma_bufAPI 获取用户空间传递来的文件描述符并将其映射到内核地址空间用于DMA操作。ION/DMA-BUF Heaps 的定位它不是一个替代CMA或静态预留的方案而是一个建立在它们之上的抽象层和管理框架。它定义了缓冲区分配、共享和传递的标准方式而底层的物理内存来源可以是CMA区域也可以是静态预留的carveout区域。对于复杂的多媒体应用和图形显示管道使用ION/DMA-BUF Heaps是比直接操作CMA或静态内存更现代、更高效的选择。6. 方法对比与选型指南我该用哪一种面对这么多种方法在实际项目中如何选择这张对比表可以帮你快速决策特性/方法内核启动参数 (memmap)设备树预留 (reserved-memory)CMA (连续内存分配器)ION/DMA-BUF 堆 (Carveout类型)核心原理启动时静态保留内核完全不管理。启动时静态保留通过设备树描述。启动时保留但平时可被系统借用需时再回收。基于静态保留或CMA提供缓冲区共享框架。配置方式内核命令行参数。设备树源文件 (.dts)。内核参数cma或设备树节点。设备树定义堆并关联到预留内存区域。管理粒度粗粒度整块预留。粗粒度整块预留。细粒度页面级管理可动态分配释放。缓冲区对象级管理支持共享。内存利用率低预留即锁定。低预留即锁定。高闲置时可被系统使用。取决于底层堆类型Carveout则低CMA则高。分配保证绝对保证连续性和可用性。绝对保证连续性和可用性。尽力保证可能因内存压力失败或延迟。取决于底层堆类型。延迟确定性高无分配延迟。高无分配延迟。较低分配时可能触发页面迁移。取决于底层堆类型和分配路径。适用架构x86, ARM, 通用。ARM/嵌入式主流x86也可用。通用ARM上更常见。ARM/Android/多媒体生态。典型场景传统PCI设备DMA、旧式帧缓冲、绝对物理地址要求的硬件。嵌入式外设DMA、安全隔离区、协处理器内存。移动设备相机/视频处理、间歇性需要大块DMA的通用设备。Android图形显示、相机流水线、跨进程/跨驱动零拷贝缓冲区共享。选型决策流你的硬件/驱动有明确的、固定的物理地址要求吗比如硬件寄存器指定了DMA地址是- 选择内核启动参数 (memmap)或设备树预留 (no-map)。这是唯一能精确控制物理地址的方法。否- 进入下一步。你的系统是嵌入式/ARM平台吗设备支持设备树吗是-优先使用设备树。这是现代嵌入式Linux的标准做法信息集中管理清晰。否(主要是x86 Legacy系统) - 使用内核启动参数 (memmap)。你需要预留的内存是给谁用给一个特定的、简单的内核驱动用且该驱动使用标准DMA API- 使用设备树预留shared-dma-pool或CMA。给复杂的多媒体/图形子系统用缓冲区需要在多个用户空间进程和内核驱动间共享- 使用ION/DMA-BUF 堆并根据需求选择底层是CMA还是Carveout。给一个对分配延迟极其敏感、绝对不能失败的硬实时任务用- 使用静态预留设备树no-map或memmap。你关心内存利用率吗设备是持续使用还是间歇性使用预留内存间歇性使用且允许微秒级的分配延迟 -优先考虑CMA。持续占用或对延迟要求苛刻 - 使用静态预留。个人经验之谈在现在的项目中除非遇到非常“老派”或要求极其苛刻的硬件否则我会尽量避免使用原始的memmap参数。设备树是嵌入式开发的未来它能将硬件配置从代码中解耦让系统更清晰。对于大多数需要连续内存的现代外设如网卡、显卡、视频加速器首先尝试让驱动支持CMA。如果驱动不支持再考虑为其在设备树中配置一个专属的shared-dma-pool。只有当你确确实实需要那块内存的物理地址从开机到关机都纹丝不动且CPU最好不要去碰它时才动用no-map这个终极武器。7. 调试与验证你的内存真的预留成功了吗配置好了预留内存怎么确认它生效了系统运行时又如何监控它的使用状态这里有一套完整的调试验证流程。7.1 启动阶段验证查看内核日志系统启动时关于内存布局和预留的信息会打印在内核日志中。使用dmesg | grep -i -E \reserved|memory|cma|memmap\命令过滤关键信息。对于memmap你会看到明确的user: [mem ...] reserved条目。对于设备树预留会看到类似Reserved memory: created CMA memory pool at 0x..., size 256 MiB或Reserved memory: initialized node my_reserved_region, compatible id no-map的日志。对于CMA会看到Reserved memory: created CMA memory pool at 0x..., size 256 MiB以及Reserved memory: initialized node linux,cma, compatible id shared-dma-pool。7.2 运行时查看利用虚拟文件系统Linux内核通过/proc和/sys文件系统暴露了大量信息。/proc/iomem这是最权威的物理内存资源视图。所有预留的内存区域都会在这里以 “reserved” 的标签显示。你可以清晰地看到它们的起止地址和名称如果有的话比如来自设备树的节点名。$ cat /proc/iomem | grep -A2 -B2 reserved 80000000-8fffffff : /reserved-memory/region80000000 90000000-93ffffff : reserved/proc/meminfo查看CMA的整体使用情况。关注CmaTotal和CmaFree字段。$ cat /proc/meminfo | grep Cma CmaTotal: 262144 kB CmaFree: 260096 kB/sys/kernel/debug需要内核配置CONFIG_DEBUG_FSy/sys/kernel/debug/cma/如果存在下面会有以CMA区域命名的子目录如cma-0里面包含base_pfn,count,free,used等文件可以查看CMA区域的详细使用统计。/sys/kernel/debug/reserved_memory/如果存在可以查看通过设备树预留的每个区域的状态。7.3 驱动侧验证确认驱动获取到了内存最直接的验证是看你的设备驱动是否工作正常。但更底层的你可以在驱动代码中添加调试信息打印出它通过dma_alloc_coherent、of_reserved_mem_device_init或ION接口分配到的内存的物理地址。看这个地址是否落在你预留的范围内。一个简单的调试技巧如果驱动使用了CMA你可以尝试在系统运行后手动触发大量内存分配比如dd if/dev/zero of/dev/null bs1G count10让系统内存紧张然后操作你的设备。如果设备依然能成功分配DMA内存那很可能就是CMA在背后工作因为它能保护自己的区域不被普通进程占满。而对于静态预留无论系统内存多紧张这块区域都是安全的。7.4 常见问题排查预留失败内核无法启动首先检查物理地址是否冲突。使用dmesg查看最开始的 “BIOS-e820” 或 “early_mem” 信息确认你预留的地址范围在系统可用的物理地址之内且没有覆盖内核代码本身。驱动分配不到预留内存检查设备树绑定是否正确memory-region属性是否指向了正确的预留内存节点节点的compatible属性驱动是否支持例如驱动可能只识别shared-dma-pool而你用了no-map。检查驱动代码驱动是否真的调用了从预留内存分配的相关API例如对于设备树预留驱动需要在probe函数中调用of_reserved_mem_device_init()。检查CMA配置如果使用CMA/proc/meminfo中的CmaTotal是否为0可能是CMA大小配置为0或者内核未编译CMA支持 (CONFIG_CMAy)。系统内存不足如果你预留了很大一块静态内存可能会导致系统本身可用的内存减少影响其他应用。需要权衡预留大小。使用CMA可以缓解此问题。预留内存是Linux系统底层的一项关键配置它连接着硬件特性和软件需求。理解其原理掌握其方法并熟练运用调试工具进行验证是进行系统级开发和性能调优的坚实基础。从简单的memmap到灵活的CMA再到复杂的ION堆每一种方法都是为解决特定场景下的问题而生的。在实际项目中结合硬件约束、性能要求和软件生态做出最合适的选择正是工程师价值的体现。
返回列表