ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

嵌入式Linux内存管理实战:从物理内存分配到DMA与缓存一致性

嵌入式Linux内存管理实战:从物理内存分配到DMA与缓存一致性 1. 这堂课从一次线上事故说起去年做一款工业采集设备ARM Cortex-A8 平台跑嵌入式 Linux产品交付后不到两周客户现场反馈设备会随机死机。日志里看不到 kernel panic最后是通过反复抓 /proc/meminfo 才定位到问题物理内存分配在某个驱动路径上反复失败内存碎片化严重系统在低内存压力下直接触发了 OOM killer把关键进程杀了。排查过程花了三个晚上最后原因的代码只有一行——一个 DMA 缓冲区申请时没指定 GFP_DMA 标志导致分配到了不连续的高端内存硬件读起来全是乱码。从那之后我就养成了一个习惯嵌入式项目里凡是涉及内存我一定先画一张“内存地图”再动手写驱动和应用。这张地图决定了我后面所有调试的方向。所以这堂课我想把嵌入式内存这件事拆开揉碎讲一遍。不绕弯子全部按实际项目里踩过的坑来包括物理内存分配路径、MMU 和地址映射、缓存一致性、内存泄漏定位还有面试和入门阶段最该关注的那些点。适合正在做嵌入式 Linux 驱动、应用开发的工程师也适合还在学习路线早期、想建立内存全局观的在校生。要理解嵌入式内存先要建立一个观念嵌入式系统的内存问题不是“内存不够大”的问题而是“内存怎么组织、怎么分配、怎么保持一致”的问题。桌面 PC 上内存阿尔卑斯山一样堆着浪费几百 KB 没人care嵌入式里 64MB DDR 都可能被嫌弃多每一块内存归属谁、从哪来到哪去都必须交代清楚。这就是嵌入式内存课和普通计算机内存课最大的区别。2. 嵌入式内存的全景地图从裸机地址到 MMU 虚拟化2.1 先分清物理地址、总线地址和虚拟地址很多刚开始做嵌入式的同学对“地址”的理解是混沌的。寄存器地址 0x10000000、内存地址 0x80000000、指针打印出来 0xb6f5d000这几个地址到底是什么关系我用一个通俗类比解释。物理地址芯片出厂时定义好的、实实在在的存储单元的编号。就好比一栋楼的门牌号从 0 号开始往后排DDR、SRAM、外设寄存器各自占一段区间互不重叠。虚拟地址CPU 执行指令时用的地址。它像一个“投影”CPU 发出的地址先经过 MMU 翻译才变成真正的物理地址。这就好比你拿着一张“楼层导览图”找房间导览图上写的“B座 305”经过物业MMU核验后才告诉你实际去 3 栋 305 室。总线地址外设视角的地址通常是物理地址经过某种变换后给 DMA 控制器用的地址。在大多数 ARM 嵌入式系统里物理地址和总线地址是一致的但在带 IOMMU 的复杂 SoC 里这二者是有偏移的。嵌入式裸机开发阶段没有 MMUCPU 直接发出物理地址所以写寄存器就是*(volatile unsigned int *)0x40005000 value;简单粗暴。但一旦跑 Linux用户程序里的指针全是虚拟地址你在应用层看到的malloc()返回的地址和硬件上真实的内存单元并没有直接对应关系。这个认知不建立起来后面看/proc/iomem、/dev/mem、dma_alloc_coherent 就全是浆糊。2.2 怎么看芯片手册里的内存映射表每个 SoC 的数据手册里都必有一张“Memory Map”表格这是嵌入式工程师首先要读的图之一。以常见的 i.MX6ULL 为例它的地址空间大概长这样地址区间用途0x0000_0000 - 0x0FFF_FFFF内部存储器ROM、SRAM、Boot 区域0x1000_0000 - 0x1FFF_FFFF外设GPIO、UART、I2C 等0x2000_0000 - 0x2FFF_FFFF其它外设LCDIF、CSI 等0x4000_0000 - 0x4FFF_FFFFAXI 总线上的外设DDR 控制器、DMA 等0x8000_0000 - 0x9FFF_FFFFDDR 内存映射到外部 DRAM这里有几个容易忽略的坑。第一外设寄存器不是“内存”但你必须用访问内存的方式去访问它。所以叫 memory-mapped I/O内存映射 IO。每一个外设寄存器都占用了物理地址空间的一段区间读也好写也好都是从地址总线发请求。这也意味着物理地址空间是会被外设“吃掉”的真正能放数据的 DDR 区域并没有 4GB 那么多。第二DDR 控制器会把你插的 DDR 颗粒映射到某个地址区间。同样是 512MB 的 DDR可能被映射到 0x80000000 开始的区间也可能映射到 0x40000000 起始的区间由硬件设计决定。写驱动时DMA 目标地址必须是这段区间内的物理地址否则数据根本没进到内存里。第三部分芯片支持 remap也就是引脚或总线复用可以让同一段地址空间一会儿映射到 SRAM、一会儿映射到外部存储。BootROM 阶段常玩这种把戏。这阶段搞错了映射地址你的启动代码就会跳进一个空地址死得毫无征兆。对嵌入式 Linux 来说开机后你可以直接看/proc/iomem这个文件把内核已登记的物理地址区间都列出来了。我习惯在写外设驱动前先cat /proc/iomem确认我要访问的寄存器地址有没有被别的驱动占用避免两个驱动同时 ioremap 同一段物理地址导致访问打架。2.3 为什么嵌入式内存问题比 PC 尖锐同样是跑 LinuxPC 上 32GB 内存随便造嵌入式就几件事要格外小心。资源受限DDR 容量小往往没有 swap 空间。桌面 Linux 内存不够就疯狂换页嵌入式上直接 OOM。而且很多嵌入式系统关闭了 swap内存分配失败就是硬失败。实时性要求采集中断、DMA 搬运、音频缓冲这些场景对内存延迟和分配确定性有要求。你不可能在中断上下文里调用malloc()那就是自寻死路因为 malloc 可能睡眠。缓存一致性CPU 有 Cache外设 DMA 直接读写内存两者之间如果不同步就会出现“CPU 写的数据 DMA 读不到”“DMA 写的数据 CPU 看到的是旧值”这种灵异事件。PC 上有 PCIe 总线的严格一致性协议兜底嵌入式 SoC 里很多场景就靠你手动维护 cache 一致性。这些问题不是调完一次就完了它们是嵌入式开发每个阶段都会反复遇到的。所以这堂课的重点放在物理内存分配路径、DMA 方向的内存处理、缓存一致性维护以及内存泄漏排查这四大块上。3. 嵌入式 Linux 物理内存分配的四条主要路径3.1 用户态 malloc 最终怎么落到物理内存写嵌入式应用的人最容易犯的错误是以为malloc()从系统拿了一整块内存就能立刻用。其实用户态的 malloc 只是从进程的虚拟地址空间里划了一块区域给你它可能根本没对应到物理页。Linux 的内存分配机制是“按需分配”你申请 1MB 内存内核可能只是修改了你的页表标记这段地址可写但物理页根本没分配。直到你真正往这个地址写入数据触发缺页异常内核才会从伙伴系统里取一个物理页页框填进页表。这个机制叫 demand paging按需分页。这就带来一个嵌入式领域老生常谈的问题——malloc成功不等于内存真的够用。系统物理内存已很紧张时malloc 仍可能返回一个有效指针等你 memset 或写数据时 OOM killer 才动手。所以做长时间运行的嵌入式服务时我习惯在初始化阶段就把关键内存一次性分配好并且在写压力测试时用memset把内存实际touch一遍才能暴露真实可用性。malloc 底层走的是 glibc 的堆管理堆不够时通过brk()扩展堆段超大块内存则直接用mmap()匿名映射。这两种路径最终都会进入内核的物理内存分配但这个“最终”是有延迟的不是 malloc 返回的那一刻。3.2 内核态 kmalloc 和 vmalloc 的物理连续之争驱动开发中很多初学者搞不清kmalloc()和vmalloc()的区别面试也老考。一句话讲明白kmalloc()分配物理连续的内存虚拟地址也连续。底层从伙伴系统直接拿连续页框因此分配速度快适用于 DMA、中断上下文因为硬件 DMA 需要物理连续地址。vmalloc()分配虚拟地址连续但物理页可能不连续的内存。底层先找一段连续的虚拟地址空间再把分散的物理页映射进去分配时需要建立页表可能睡眠不能用于中断上下文。kmalloc 的实现细节值得一提。它有 slab 缓存机制内核预先把常用大小的内存对象分组缓存分配小对象时直接从 slab 里取速度快、碎片少。这也是为什么你在驱动里kmalloc(sizeof(struct xxx), GFP_KERNEL)会得到一个和上次释放的地址很可能相同的内存块。kmalloc 分配有一个上限问题。它从高端内存和低端内存的映射区间里分配通常最大是 128KB 到 4MB 不等受限于内核虚拟地址空间里直接映射区的大小。你要是想 kmalloc 一个 8MB 的连续缓冲区往往会失败。这种大块物理连续内存就要走 CMA 机制。3.3 CMA嵌入式大块连续内存的救星CMAContiguous Memory Allocator连续内存分配器。它的设计思路很有意思平时把一大块物理内存“借”给伙伴系统作为普通可移动页使用当某个驱动需要大块连续内存时通过内存迁移把这些可移动页搬走腾出一整块连续区域。这就好比停车场平时允许临时车辆随便停但有大巴团预约时管理员把散停的车引导到别处清出一大块空场地给大巴。巧的是Linux 里这叫 memory compaction内存压缩。使用 CMA 的典型路径是设备树reserved-memory { #address-cells 1; #size-cells 1; ranges; fb_reserved: framebuffer40000000 { compatible shared-dma-pool; reusable; reg 0x40000000 0x1000000; }; };这段配置在物理地址 0x40000000 处预留了 16MB 的连续内存给 Framebuffer。配合dma_alloc_coherent()或驱动里的cma_alloc()使用你就可以拿到物理连续的 DMA 缓冲区。我用 CMA 时踩过一个坑CMA 区域如果配置过大比如从 64MB 的 DDR 里割了 32MB 作为 CMA那么平时系统可用内存就显得特别少。因为 CMA 区域即使“借”给伙伴系统也算被限制了。正确做法是评估清楚最大连续需求然后按需设置别为了省心一上来就配 50% 内存。另一个坑和 CMA 的迁移时机有关。系统内存碎片严重时CMA 分配可能触发大量页面迁移导致分配耗时几十毫秒甚至几百毫秒。如果你的驱动在实时路径里调用 CMA 分配会引发调度抖动。很多项目组最后都改成memalloc或 ion/dma-buf 的预先分配池方案把缓冲区在系统初始化时一次性切好运行期只复用不新分配。3.4 DMA 方向与 dma-buf、ION 的共享哲学嵌入式内存分配的另一大主题是跨设备共享。摄像头采集的数据要给 DSP 或 GPU 处理如果是各管各的内存就要搬两次数据既浪费带宽又增加延迟。所以现代 Linux 用 dma-buf 框架让多个设备共享一块内存大家各自映射到自己的地址视图里。dma-buf 的核心 API 是dma_buf_map_attachment()和dma_buf_unmap_attachment()。驱动里分配共享缓冲区时通常用dma_alloc_coherent(dev, size, dma_handle, GFP_KERNEL)这个函数返回两个地址CPU 访问的虚拟地址virt_addr和 DMA 总线地址dma_handle。重点来了这个函数同时做了 cache 一致性处理分配出来的内存区域要么是 uncached要么是 write-through不会出现 cache 与内存不一致的问题。以前 Android 平台用的是 ION heap 机制现在已经慢慢转向 dma-buf heaps。嵌入式项目如果要在 user space 和 kernel space 之间共享内存我推荐直接使用/dev/dma_heap/system或者udmabuf机制比手动 mmap/dev/mem安全得多。手动 mmap /dev/mem 虽然也能拿到物理内存但绕过了内核的分配器容易搞出内存重叠和权限问题只适合调试阶段用。4. 案例复盘OMAP-L137 的内存映射与 C674x 缓存架构优化4.1 项目背景音频算法卡在缓存未命中几年前一个音频处理项目用 TI 的 OMAP-L137这是一个双核芯片一个 ARM926EJ-S 核跑 Linux一个 C674x DSP 核跑裸机算法。DSP 脱机仿真时算法效率没问题一搬到板子上就慢了 4 倍。当时我们用 DSP 做 1024 点 FFT 和自适应滤波DSP 处理一帧 10ms 音频居然要 35ms完全赶不上实时。一开始怀疑是 DSP 频率不够后来把内存访问模式改了之后处理时间降到 8ms。问题根源就是 C674x 的 L2 缓存命中率太低DSP 大量访问外部 DDR而 DDR 访问延迟和缓存命中时的延迟差了近一个数量级。4.2 C674x 三级缓存结构与内存映射C674x 的缓存架构分三层L1P程序缓存直接映射方式大小通常 32KB。L1D数据缓存2 路组相联大小通常 32KB。L2统一的指令/数据缓存大小可配置为 32KB 到 256KB部分 L2 还可以配成 SRAM。它能映射的存储空间分两个大区内部存储L2 SRAM、L1D SRAM和外部存储DDR、异步存储器。DSP 的地址空间里片内 L2 SRAM 和外部 DDR 各自映射到固定地址段。这些配置寄存在 C674x 的 L2CFG 寄存器里可以把它配置成纯 cache、纯 SRAM或者两者的混合。我们当时的失误是把 DSP 的 FFT 旋转因子表放在了外部 DDR而每个 FFT 帧都要查表。DDR 访问的随机性让 L2 cache 几乎起不到缓存效果。后来把旋转因子表搬到 L2 SRAM 里访问延迟立刻降下来。4.3 缓存一致性问题的三个处理手法DSP 和 ARM 之间通过共享内存传音频数据时还遇到了第二个大坑缓存一致性问题。ARM 侧往共享内存里写完一帧数据DSP 读到的却可能是旧数据。因为 ARM926EJ-S 的写缓冲和 L2 cache 还没把数据刷回 DDRDSP 就直接去 DDR 读了。解决缓存一致性问题嵌入式领域常用的手法就三种。第一种把共享内存的 cache 属性关掉也就是标记为 non-cacheable。在 ARM Linux 里设备树中可以给 reserved-memory 加no-map或shared-dma-pool属性配合dma_alloc_coherent()分配这块区域自动就是不缓存的。代价是 CPU 访问慢但对于低频率的音频控制帧来说完全可以忽略。第二种手动 cache 维护。ARM 提供flush_cache_all()、dma_map_single()这套 API原则是CPU 写完共享内存后、DMA 读之前先 flush 写缓冲确保数据落 DDRDMA 写完共享内存后、CPU 读之前先 invalidate把缓存里可能残留的旧数据丢掉。TI 的 DSP 侧也有对应的 L1D flush/invalidate 指令。第三种用硬件缓存一致性协议。比如带 CCI/CMN 总线互联的多核 ARM 处理器通过 ACE 总线自动维护各核和 IO 设备之间的缓存一致性。但 OMAP-L137 这种老芯片没这么先进只能靠软件手段。搞嵌入式任何时候碰到“数据明明写在内存里另一个核就是读不对”的问题第一反应必须是缓存一致性而不是怀疑总线坏了或者指针错了。4.4 优化效果与复盘心得优化完之后DSP 处理一帧时间从 35ms 降到 8ms主要收益来自三块把 FFT 旋转因子表放到 L2 SRAM查表延迟从 DDR 级别降到 L2 级别这项贡献了大概 70% 的优化收益。因为 FFT 每级蝶形运算都要查表随机地址访问对 cache 极不友好。把音频数据帧缓冲改成环形缓冲区并让 DSP 以 DMA 方式从 DDR 搬到 L2 SRAM改掉了原来 DSP 逐点指针访问 DDR 的低效方式。DMA 搬运是突发模式对 DDR 带宽利用效率高得多。ARM 和 DSP 之间的共享状态标志用 non-cacheable 内存并用 volatile 修饰杜绝了缓存一致性带来的握手失败。我复盘时写笔记总结出三条嵌入式性能优化的原则热数据优先放片内 SRAM外设数据搬运优先交给 DMA 而不是 CPU 逐点拷贝跨核共享区域必须提前规划缓存策略不能指望硬件自动兜底。5. 内存泄漏排查与调试实战5.1 用 /proc/meminfo 和监控脚本锁定泄漏趋势内存泄漏是嵌入式项目的常客。表现形式很多种可能是 new 了对象没 deletemalloc 了没 free内核驱动申请了页没释放甚至可能是文件描述符泄漏引起的 slab 内存占用上涨。一个最朴素但有效的思路是先把“内存到底降没降”搞清楚。写一个监控脚本周期性抓/proc/meminfo里的 MemFree、MemAvailable 和 Cachedwhile true; do grep -E MemFree|MemAvailable|Cached /proc/meminfo | tr \n echo $(date %T) sleep 5 done注意 MemAvailable 是估算值比 MemFree 更能反映真实可用情况因为它考虑了 page cache 的可回收性。泄漏如果是持续性的图表曲线会一路下滑如果是碎片化问题一段时间后会平台状波动。5.2 valgrind用户态泄漏的屠龙刀用户态程序的泄漏valgrind 基本一抓一个准。嵌入式 Linux 设备上直接跑 valgrind 往往太重我的做法是在主机上用同样的交叉编译器编译一个带调试符号的版本然后在板子跑valgrind --leak-checkfull --show-leak-kindsall ./your_app。例如一个网络服务程序崩溃后 valgrind 崩溃但泄漏报告里你能看到1234 256 bytes in 4 blocks are definitely lost in loss record 2 of 5 1234 at 0x4005E2D: malloc (vg_replace_malloc.c:299) 1234 by 0x4012344: create_packet (packet.c:88) 1234 by 0x4012ABC: process_msg (server.c:210)这直接指出是哪个文件哪一行创建的分配没有释放。valgrind 也不是万能的它的执行速度会慢 20 倍以上实时交互型程序如果在板子上跑不动需要做“分块测试”把程序模块拆开逐个跑。5.3 内核侧泄漏kmemleak 和 slab 监控内核驱动泄漏一般用两招。第一招是 kmemleak内核配置打开CONFIG_DEBUG_KMEMLEAK然后通过 debugfs 接口扫描未引用内存。触发扫描的命令是echo scan /sys/kernel/debug/kmemleak cat /sys/kernel/debug/kmemleakkmemleak 的原理是定期扫描内存中的指针看哪些分配出来的内存块没有任何指针引用就认为是泄漏。不是你马上能看到结果有时候要等几个小时扫描几次才有报告。第二招是监控 slab 信息cat /proc/slabinfo | sort -k 2 -n | tail -20看哪个 slab 对象数量持续增长就说明哪个内核模块在不断分配对象但没释放。比如kmalloc-128数量持续上涨往往说明是驱动里某个kmalloc(128)循环调用处漏了释放。我以前在一个 USB 驱动里排查过泄漏现象是设备枚举次数多了之后内存一直涨。用/proc/slabinfo发现usb_device对象数量只增不减最后定位到 disconnect 回调里少调了一次usb_put_dev()引用计数没归零导致 device 结构体永远释放不了。这类问题光靠看代码很难发现必须借助工具看对象计数趋势。5.4 编码习惯上防泄漏的四条铁律靠工具修 bug 是被动的真正高效的项目组会在编码规范上堵住泄漏入口。我总结了四条每次 code review 必查。一律使用 RAII 或清理函数模式在函数入口定义资源在函数出口统一 cleanup不要散落 free。C 语言里用 goto 集中错误处理C 里用智能指针或 scope guard。所有 malloc/ free 成对出现在同一个函数栈内如果跨函数传递内存所有权必须在函数注释里写明谁分配谁释放。嵌入式团队最怕的就是“我用你释放”这种糊涂账。大型循环里创建临时对象时确保每次迭代结束时释放干净。最典型的泄漏是线程池任务里每个任务 new 一个对象任务结束忘了 delete。启动阶段用工具做基线检查集成测试环境跑 72 小时压力测试每 24 小时记录一次内存快照。内存曲线只要有三次持续下降就开 JIRA 单驱动排查。6. 嵌入式内存面试高频题与应对逻辑6.1 十道高频题不再丢分嵌入式岗位面试内存部分是重灾区。很多候选人不是不会是答题没有层次。下面这十道题是我梳理的“嵌入式内存八股文”按出现频率排序题目考点答法要点堆和栈的区别内存分区从分配方式、生命周期、生长方向、大小限制四个维度答malloc 的底层实现内存管理brk、mmap、free list、碎片问题kmalloc 和 vmalloc 区别内核内存物理连续性、上下文限制、最大分配量什么是内存对齐硬件特性CPU 访问粒度、结构体 padding、pragma pack 的代价内存泄漏如何排查调试能力valgrind、kmemleak、slabinfo 三板斧cache 一致性是什么体系结构cache 刷新/invalidate、DMA 场景什么是伙伴系统内核核心幂次分配、页面迁移、碎片避免什么是 slab 分配器内核核心对象缓存、避免频繁初始化用户态虚拟地址怎么变成物理地址MMU页表、缺页异常、TLBOOM killer 怎么触发的内核机制bdflush、内存水位线、oom_score选题必须由浅入深先用一句话给结论再展开机制最后举一个实际项目例子。很多候选人只做到第一层面完就挂。6.2 面试官真正想听什么以“malloc 的底层实现”这题为例我建议的回答结构是先说结论malloc 通过 brk 扩展堆或 mmap 创建匿名映射把虚拟地址空间划给进程实际物理页在首次访问时才分配。再展开glibc 维护空闲链表分配时按 best-fit 或 first-fit 算法找合适 block释放时做相邻合并。小内存走 brk大内存走 mmap。内核侧最终通过伙伴系统和 slab 分配物理页。最后补一个嵌入式场景例子我在某项目中用 malloc 分配一个 1MB 的 buffermemset后系统 OOM。因为 malloc 成功时只是虚拟映射memset 时才真正触碰物理页系统内存不足就在那一刻暴露。这个结构面试官一听就知道你不仅背过书还踩过坑。面试题没有标准答案有现场感才有分数。7. 嵌入式内存学习路线与开源项目推荐7.1 学习路线的三个阶段第一阶段基础夯实期。学习 C 语言时就把指针、结构体、内存布局学好不要急着碰 Linux。手动实现一个简单 malloc 是很好的练习题能帮助你理解 free list、内存碎片、对齐这些概念。同时把《深入理解计算机系统》第三章、第九章啃下来这两章把地址翻译和虚拟内存讲得比较透。第二阶段SoC 认知期。找一个具体的开发板先裸机编程读芯片手册的 Memory Map 部分点亮 LED 时理清寄存器地址怎么来的。然后在板子上跑 U-Boot看它怎么初始化 DDR、怎么搬运镜像。这阶段目标是把“物理地址、总线地址、虚拟地址”三者的关系彻底搞明白。第三阶段Linux 驱动期。动手写一个字符设备驱动用 kmalloc、mmap、(也就是 file_operations 的 mmap 接口) 把内核内存映射给用户态。再写一个虚拟 DMA 驱动体验 dma_alloc_coherent 和 cache 维护 API。把 Linux 设备树里的 reserved-memory 节点配置一遍理解 CMA 内存是怎么预留和释放的。7.2 值得反复读的嵌入式内存相关源码很多同学问“开源项目怎么选”我的建议是别贪多把下面几个项目的内存相关部分吃透就足够构建一个嵌入式内存的完整认知。U-Boot启动时 DDR 初始化和镜像搬运里面是裸机视角下的内存管理。Linux Kernel 的 mm 目录这是内存管理的宝库。mm/page_alloc.c看伙伴系统mm/slab.c看 slab 缓存mm/vmalloc.c看 vmalloc 实现mm/cma.c看 CMA 迁移机制。不用全读完每年精读两三个关键函数就够。RT-Thread小型嵌入式 RTOS内存管理非常清晰有空闲链表和 memheap 两套机制适合交叉对比。Zephyr模块化系统内存分配可配置性强对理解“可裁剪性”很有帮助。7.3 从蓝桥杯嵌入式到工业项目的衔接搜热词时看到很多人在关注竞赛和考证比如蓝桥杯嵌入式。这类比赛确实能把基础操作逼着你练会尤其是 STM32 裸机寄存器操作、中断配置、外设初始化这些硬功夫。但竞赛和工业项目有一个明显区别竞赛板子资源充足代码量小内存管理基本停留在全局变量阶段而工业产品要面对的是 Linux、多任务、DMA、内存泄漏这些复杂问题。所以我的建议是竞赛可以打但别把它当作终点。打完蓝桥杯立刻转向带 MMU 的嵌入式 Linux 平台比如 imx6ull 或全志 V3s 这种性价比高的板子把内存管理在更大的系统里重新理解一遍。这时候你回头看竞赛里的全局变量会发现那只是冰山一角。8. 常见问题与排查技巧速查表这堂课的最后我把这么多年嵌入式内存问题排查的常见场景整理成一个速查表方便你直接对照常见现象可能原因排查手段解决方案系统运行几天后随机 OOM用户态内存泄漏或内核 slab 泄漏监控 MemFree 趋势、slabinfo用 valgrind 定位用户态kmemleak 定位内核态驱动 DMA 读到的数据全是 0xFFDMA 缓冲区物理不连续或 cache 未刷新打印 dma_handle、物理地址对比 DMA 区域改用 dma_alloc_coherent 或调用 cache 维护 API双核共享内存数据不同步cache 一致性检查共享区域 cache 属性标记 non-cacheable 或手动 flush/invalidatemalloc 后 memset 导致死机虚拟地址未映射物理页内存耗尽查看 dmesg 中的 OOM 信息初始化阶段预分配并 touch设备反复插拔后内存上涨引用计数未释放/proc/slabinfo 观察 usb 等对象计数补 put 操作修复引用计数大块连续内存分配失败CMA 区域不足或碎片化/proc/buddyinfo 观察碎片程度增大 CMA 或改用预先分配池进程地址空间不断增长堆没有收缩或线程栈泄漏/proc/pid/status 观察 VmSize检查线程栈属性、mmap 对象释放最后一条也是我最有体感的一条经验排查内存问题不要一上来就怀疑内核。先把你自己的应用层代码用静态分析工具过一遍再查驱动再查硬件。我遇到的嵌入式内存事故里九成最后都是自己那几行 malloc/new 没配对。这堂课的容量有限但把你最常撞的那几面墙都指了出来。剩下的去板子上踩一踩比看十篇文章都管用。
返回列表