news 2026/10/30 7:45:27

Linux CMA连续内存分配器:原理、配置与迁移失败排查实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux CMA连续内存分配器:原理、配置与迁移失败排查实战

简介:这份资源聚焦光纤通信中的偏振模色散补偿(CMA)算法,面向通信工程、数字信号处理方向的学习者与研究人员,用于理解并复现PMD补偿的核心流程。压缩包内共1个文件,为MATLAB脚本(.m),整体约1KB,体量轻便,便于直接阅读与运行验证。资源围绕CMA算法的关键环节展开,涵盖数据预处理、PMD参数估计、补偿矩阵计算、信号恢复、迭代优化与性能评估等步骤,并涉及与FEC、自适应均衡器结合的应用思路,可帮助读者梳理从理论推导到代码实现的完整链路。目前已有396人学习下载,说明其在相关方向具有一定参考价值。对于希望深入理解PMD效应、掌握CMA补偿矩阵构建与迭代收敛细节的读者,这份代码可作为算法验证与二次开发的起点,也适合作为课程设计或科研入门的辅助材料。

1. CMA_cma_:一个被低估的底层内存分配器,为什么值得你花时间

第一次在性能剖析报告里看到CMA_cma_这个符号,是在一台跑视频解码的嵌入式设备上。perf top 显示它占用了大量 CPU 时间,但翻遍应用层代码也找不到调用点。后来才意识到,这是 Linux 内核里Contiguous Memory Allocator(连续内存分配器)的内部函数符号,CMA_cma_是它对外暴露的分配入口之一。CMA 解决的是一个很硬核的问题:当设备驱动需要一大块物理连续内存(比如摄像头、GPU、DMA 缓冲区),而系统运行久了内存碎片化严重、kmalloc已经拿不到连续页时,怎么还能稳定拿到这块内存。它的思路是预留一块区域,平时给可移动页(movable pages)用,需要时再把这些页迁移走,腾出连续空间。这套机制在 Android、车载、IPC、机顶盒里几乎是标配。如果你在做驱动、BSP、系统性能优化,或者被alloc_contig_range失败、cma_alloc超时折磨过,这篇就是写给你的。

2. CMA 的分配原理与内核配置:从 reserved memory 到 cma_alloc

2.1 CMA 区域是怎么被内核识别出来的

CMA 区域不是凭空出现的,它必须先在设备树(Device Tree)或内核命令行里声明。最常见的做法是在 DTS 里用reserved-memory节点加linux,cma属性。内核启动时,drivers/of/of_reserved_mem.c会解析这些节点,调用rmem_cma_setup把区域注册进 CMA 框架。注册完成后,这块内存会出现在/proc/meminfo的CmaTotal字段里,已用部分对应CmaUsed。

一个典型的 DTS 片段长这样:

reserved-memory { #address-cells = <2>; #size-cells = <2>; ranges; linux,cma { compatible = "shared-dma-pool"; reusable; size = <0x0 0x10000000>; /* 256MB */ alignment = <0x0 0x200000>; /* 2MB 对齐 */ linux,cma-default; }; };

reusable是关键,它告诉内核这块区域平时可以被 buddy 分配器拿去放可移动页;linux,cma-default表示这是默认 CMA 池,驱动调用dma_alloc_coherent时会优先从这里拿。alignment决定最小对齐粒度,设太小会导致迁移效率下降,设太大又浪费内存,一般跟硬件 DMA 要求对齐即可。

2.2 cma_alloc 的调用链与迁移机制

驱动侧通常不直接调cma_alloc,而是通过 DMA API 间接使用。但理解直接调用链对排查问题很有帮助。核心路径是:

cma_alloc() -> alloc_contig_range() -> start_isolate_page_range() // 隔离目标范围内的页 -> migrate_pages() // 把可移动页迁走 -> test_pages_isolated() // 确认隔离成功

alloc_contig_range是真正的苦力。它先把目标物理范围里的页标记为隔离,然后尝试迁移。如果范围内有不可移动页(比如内核 slab、页表、pin 住的页),迁移就会失败,cma_alloc返回 NULL。这就是为什么 CMA 区域大小要留够余量,也是为什么长时间运行后 CMA 分配会变慢甚至失败。

下面这段代码演示了内核模块里直接调用 CMA 分配的最小写法:

#include <linux/cma.h> #include <linux/dma-contiguous.h> static struct cma *my_cma; static int __init cma_demo_init(void) { struct page *page; void *vaddr; unsigned long pfn; /* 通过名字获取已注册的 CMA 区域,名字对应 DTS 里的 label */ my_cma = cma_get_by_name("linux,cma"); if (!my_cma) { pr_err("cma region not found\n"); return -ENODEV; } /* 申请 4MB,对齐 2MB,返回 struct page */ page = cma_alloc(my_cma, 1024, 9, GFP_KERNEL); /* 1024 pages = 4MB, order 9 */ if (!page) { pr_err("cma_alloc failed\n"); return -ENOMEM; } pfn = page_to_pfn(page); vaddr = page_address(page); pr_info("cma alloc ok, pfn=%lu vaddr=%p\n", pfn, vaddr); /* 用完必须释放,否则 CmaUsed 只增不减 */ cma_release(my_cma, page, 1024); return 0; }

cma_alloc第二个参数是页数,第三个参数是对齐的 order(这里 9 表示 2^9 页 = 2MB 对齐)。注意cma_get_by_name依赖 DTS 里的 label,如果名字对不上会直接返回 NULL。释放时页数必须和申请时一致,否则 CMA 的位图会错乱,后续分配行为变得不可预测。

2.3 内核配置项怎么选

CMA 相关配置集中在mm/Kconfig和drivers/base/Kconfig:

配置项作用建议
CONFIG_CMA总开关必开
CONFIG_CMA_SIZE_MBYTES默认 CMA 大小无 DTS 时生效,一般 64~256
CONFIG_CMA_ALIGNMENT默认对齐 order跟硬件走,常见 8 或 9
CONFIG_DMA_CMA让 DMA API 使用 CMA驱动走 DMA 时必开
CONFIG_CMA_DEBUG打印分配细节调试期开,量产关

如果 DTS 里已经声明了linux,cma,CONFIG_CMA_SIZE_MBYTES就不起作用了,以 DTS 为准。这一点经常让人困惑:改了 menuconfig 发现 CmaTotal 没变,其实是 DTS 覆盖了。

3. 在驱动里正确使用 CMA:dma_alloc_coherent 与迁移失败排查

3.1 优先用 DMA API 而不是直接调 cma_alloc

绝大多数驱动不应该直接调cma_alloc,而应该用dma_alloc_coherent或dma_alloc_attrs。原因很简单:DMA API 会处理缓存一致性、IOMMU 映射、设备地址转换,而cma_alloc只给你物理页,剩下的都得自己来。只有在写内核模块做实验、或者框架层需要预分配大块内存时,才直接碰cma_alloc。

一个标准的 DMA 分配写法:

struct device *dev = &pdev->dev; dma_addr_t dma_handle; size_t size = 4 * 1024 * 1024; void *cpu_addr; cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL); if (!cpu_addr) { dev_err(dev, "dma_alloc_coherent failed, size=%zu\n", size); return -ENOMEM; } /* dma_handle 是设备可见地址,cpu_addr 是 CPU 虚拟地址 */

dma_alloc_coherent在CONFIG_DMA_CMA开启且设备没有专属 DMA 池时,会走 CMA。分配失败时不要只打印一句 failed,把size、dev_name、当前CmaFree都打出来,否则线上根本没法定位。

3.2 迁移失败的典型现象与定位手段

CMA 分配失败最典型的报错是:

cma: cma_alloc: alloc_contig_range: [9f000, a0000) pfn 0x9f2a1 busy

busy表示这个 pfn 上的页无法迁移。常见原因有三类:一是被get_user_pages长期 pin 住的页,二是内核 slab 或页表页,三是驱动自己忘了释放。定位手段是打开CONFIG_CMA_DEBUG,然后看 dmesg 里具体哪个 pfn 卡住,再用/proc/kpageflags查这个页的属性。

# 查看 CMA 总体使用情况 grep Cma /proc/meminfo # 查看某个 pfn 的页标志(需要 root,且内核开了 CONFIG_PROC_PAGE_MONITOR) # 假设 pfn 是 0x9f2a1 python3 -c " pfn = 0x9f2a1 with open('/proc/kpageflags', 'rb') as f: f.seek(pfn * 8) flags = int.from_bytes(f.read(8), 'little') print(hex(flags)) "

/proc/kpageflags的位定义在内核文档Documentation/admin-guide/mm/pagemap.rst里。如果看到PG_pinned或PG_slab置位,基本就能确认是哪种页在捣乱。这一步是排查 CMA 问题的后悔药,没有它只能靠猜。

3.3 一个可复现的迁移失败实验

想真正理解 CMA 的边界,最好自己制造一次迁移失败。思路是在 CMA 区域里放一个不可移动的页,然后尝试分配整块区域。可以用内核模块alloc_pages拿一个页,然后get_page增加引用计数模拟 pin 住,再调cma_alloc。

/* 故意制造不可迁移页,观察 cma_alloc 失败 */ struct page *pinned = alloc_pages(GFP_KERNEL, 0); get_page(pinned); /* 增加引用,迁移时会跳过 */ pr_info("pinned page pfn=%lu\n", page_to_pfn(pinned)); /* 尝试分配整个 CMA 区域,预期失败 */ struct page *big = cma_alloc(my_cma, cma_get_size(my_cma) >> PAGE_SHIFT, 0, GFP_KERNEL); if (!big) pr_info("expected failure: cma_alloc returned NULL\n"); put_page(pinned); /* 释放引用后才能迁移 */ __free_pages(pinned, 0);

这个实验能让你直观看到alloc_contig_range的busy报错,也能验证释放引用后分配是否恢复。做一次比读十遍代码管用。

4. CMA 避坑与常见问题:那些让分配失败的隐藏原因

4.1 CmaTotal 正常但 cma_alloc 一直返回 NULL

现象:/proc/meminfo里CmaFree还有几百 MB,但驱动申请 4MB 就是失败。

原因:CmaFree统计的是空闲页数量,不代表存在连续的空闲块。CMA 区域里可能散布着不可移动页,把连续空间切碎了。CmaFree大但最大连续块不够,分配照样失败。

解决:用cma_alloc的调试输出确认卡在哪个 pfn,或者写个脚本遍历/proc/kpageflags统计 CMA 范围内的不可移动页分布。根治办法是减少 CMA 区域里的不可移动分配,比如把页表、slab 尽量引导到其他 zone。

4.2 驱动卸载后 CmaUsed 不下降

现象:模块rmmod后CmaUsed还是很高,重新insmod分配变慢。

原因:dma_alloc_coherent分配的内存在驱动卸载时如果没有对应的dma_free_coherent,CMA 位图不会释放。有些驱动在 probe 失败路径里漏了释放,或者把释放写在了一个永远不会执行的分支里。

解决:用devm_dma_alloc_coherent让设备资源管理自动释放,或者在 remove 里严格配对。检查方法是insmod/rmmod循环几次,看CmaUsed是否回到基线。

4.3 DTS 改了 size 但 CmaTotal 没变

现象:把 DTS 里 CMA size 从 128MB 改成 256MB,重新编译 dtb 启动后CmaTotal还是 128MB。

原因:大概率是 dtb 没真正更新,或者 bootloader 加载了旧的 dtb。另一个可能是内核命令行里有cma=参数覆盖了 DTS。

解决:确认/sys/firmware/devicetree/base/reserved-memory/下的节点 size 属性,和编译出的 dtb 对比。检查cat /proc/cmdline有没有 cma 相关参数。

4.4 CMA 区域和 mem= 参数冲突导致启动失败

现象:内核启动到 CMA 初始化时 panic,报cma: Failed to reserve ...。

原因:mem=命令行参数限制了内核可见内存范围,而 CMA 区域被声明在了这个范围之外。内核想预留却找不到对应物理页。

解决:确保 CMA 的地址范围落在mem=限定的区间内,或者干脆去掉mem=改用memmap=精确预留。嵌入式项目里这两个参数经常打架,改一个要检查另一个。

4.5 高负载下 CMA 分配延迟飙升

现象:平时cma_alloc几毫秒,摄像头一开、GPU 一跑,分配延迟到几百毫秒甚至超时。

原因:迁移本身要扫描页、分配临时页、复制内容,负载高时内存压力大,迁移成功率下降,alloc_contig_range会重试多次。如果 CMA 区域里可移动页比例低,重试次数会更多。

解决:给 CMA 区域留足余量,别把 size 卡得刚刚好;把大块分配放在系统启动早期完成;对延迟敏感的路径用预分配池,别在运行时频繁cma_alloc/cma_release。

5. 用 ftrace 和 kpageflags 把 CMA 分配过程看透

调试 CMA 最有效的手段不是加 printk,而是用 ftrace 跟踪cma_alloc和alloc_contig_range的内部调用。内核里这两个函数都有 tracepoint 或者可以用 function_graph 跟。

# 跟踪 cma_alloc 及其子调用,看时间花在哪 cd /sys/kernel/debug/tracing echo function_graph > current_tracer echo cma_alloc > set_graph_function echo 1 > tracing_on # 触发一次分配,比如 insmod 驱动 cat trace | head -100 echo 0 > tracing_on

输出里会看到alloc_contig_range下面migrate_pages花了多少时间、test_pages_isolated重试了几次。如果migrate_pages返回的失败页数很多,说明区域里不可移动页占比高,得从内存布局上想办法。

另一个技巧是结合/proc/kpageflags做 CMA 区域的热力图。写个脚本把 CMA 范围内的 pfn 逐个读出来,统计PG_movable、PG_slab、PG_pinned的比例:

import struct CMA_START = 0x9f000 # 从 dmesg 或 /proc/iomem 获取 CMA_END = 0xa0000 FLAG_MOVABLE = 1 << 18 FLAG_SLAB = 1 << 7 FLAG_PINNED = 1 << 18 # 注意:实际位定义以内核文档为准 stats = {"movable": 0, "slab": 0, "other": 0} with open("/proc/kpageflags", "rb") as f: for pfn in range(CMA_START, CMA_END): f.seek(pfn * 8) flags = struct.unpack("<Q", f.read(8))[0] if flags & FLAG_MOVABLE: stats["movable"] += 1 elif flags & FLAG_SLAB: stats["slab"] += 1 else: stats["other"] += 1 total = sum(stats.values()) for k, v in stats.items(): print(f"{k}: {v} ({v*100//total}%)")

这个脚本能直接告诉你 CMA 区域里有多少页是可迁移的。如果 movable 比例低于 80%,分配失败就是迟早的事。我一般会在项目早期跑一次这个统计,把 CMA 区域的健康度作为基线,后面每次改内存布局都对比一下。

最后说个习惯:每次调 CMA 参数,我都会在insmod前后各抓一次/proc/meminfo的 Cma 字段和/proc/kpageflags的统计,存成日志。这样出问题时能快速判断是配置变了还是内存布局变了。CMA 这东西不像普通内存分配,它的行为跟系统运行时长、负载、其他驱动的分配习惯强相关,没有基线数据就只能靠玄学。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/30 7:43:33

原生 JavaScript + Canvas 截图下载全链路与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/30 7:42:50

ESP32射频前端隐藏通路:从校准环回到射频健康监测

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/30 7:42:12

ESP32-P4 Windows开发环境八大避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/30 7:42:10

防爆机器人硬件合格却频繁误报?根源是EMC电磁兼容问题

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/30 7:41:16

基于知识图谱的心血管疾病问答系统实现:从本体设计到Neo4j落地

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/30 7:40:43

低功耗蓝牙芯片与IoT开发实战:协议栈、功耗优化与调试指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华