RT-Thread "骚操作"之内存泄漏定位

https://www.rt-thread.org/qa/thread-11925-1-1.html


所谓动态内存分配(Dynamic Memory Allocation)就是指在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。动态内存分配不像数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小。

1、嵌入式中是否应该使用动态内存?

1.1、静态和动态内存的特点

在探讨这个问题之前我们先来对比下静态内存和动态内存的特点:
1. 创建的时间不同:静态分配发生在程序编译和连接的时候。动态分配则发生在程序调入和执行的时候。
2. 创建的空间不同:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由函数malloc进行分配。不过栈的动态分配和堆不同,他的动态分配是由编译器进行释放,无需我们手工实现。

1.2、静态和动态内存的优缺点

在了解了静态内存和动态内存不同的特点后可以分析出它们在不同的方面具有不同的优缺点:
1. 静态内存编译阶段就分配好了,不会存在分配失败的情况,除非系统内存被分配完了,但是在编译阶段就可以查出。动态内存在运行中分配,在正常运行的情况下可能够正常分配,在某种环境下可能分配失败 ,这样就对开发者的代码水平要求更高,要求开发者需要对代码的健壮性有要求。
2. 静态内存由于是编译阶段分配好的,而动态内存是由运行中分配,这样动态内存可以被高效复用,当某段程序使用完毕后,可以将内存交换给内存池中等待下次程序的分配,由于这种机制,开发者可以利用更小的内存开发更大型的程序。
3. 动态内存由于是动态分配,所以涉及到分配和释放的问题,假设X程序在分配了ABC3块内存,程序执行完毕后AC区域内存被释放,但是由于开发者的疏忽导致B区域的内存被遗留。而X程序又是会被反复调用的,这样我们的内存区域由于有没有释放的区域导致每次调用就会占用一段区域,最终系统就没有内存可以使用。这种情况一般在产品实际体现为最开始的时候设备正常工作,但是运行了几个小时或者几天后死机。这种情况一般被称作内存泄漏(Memory Leak)。
4. 在嵌入式RTOS中线程函数中使用静态内存,也就是在线程中定义局部变量,会加大线程栈空间的开销。

1.3、裸机开发和RTOS开发的特点

1. 代码规模:裸机开发的项目一般代码规模度都比较小,对于一般开发者都难于驾驭使用裸机开发超大规模项目,人力成本和时间成本都太高。反而由于RTOS的模块化特性,更容易开发出代码规模大的项目。
2. 系统稳定性:在机电,航空,汽车等行业都是需要高实时性、高稳定性和高安全性的。对于RTOS来说,系统任务的划分调度都是系统管理,当开发者对其机制了解不清楚的情况下反而带来了部分风险。除此之外RTOS上的内存管理也是很大影响系统的,对于RTOS上一般不会存在MMU内存管理单元,也就代表大部分运行RTOS的芯片上都没有虚拟地址映射,那么就无法利用物理地址和虚拟地址的特性进行内存整理,避免内存泄漏。所以说在RTOS动态内存这块内存有风险。

1.4、嵌入式中是否应该使用动态内存

明白不同内存的优缺点和裸机RTOS的特点之后,我们在回头来分析 “嵌入式中是否应该使用动态内存?” 这个问题。在实时嵌入式系统中,由于系统软件并不像PC上复杂,一般都是很简单的内存使用,所以一般也不使用动态内存管理,但是随着RTOS面临的任务越来越重,尤其是IoT和AIoT的到来,让许多消费级产品运行的RTOS的负担越来越重。在这种情况下继续使用静态内存开发会极大的限制开发进度,整个开发的灵活性非常低。同时静态的内存无法复用使用同一片内存区域,相比与动态内存在大型项目中一般会使用更多的内存,在内存大小不容乐观的嵌入式系统是很难接受的。但是针对一些高稳定性和高安全性的项目来说,使用动态内存就需要好好斟酌下,由于RTOS上基本没有MMU的硬件支持或者是系统软件支持,所以不可避免的系统中会出现内存碎片,从而有可能导致系统异常。最终说了这么多总结成以下几点:
1. 没有使用RTOS的项目:不建议使用动态内存,这种项目一般很简单,不必使用动态内存增大系统开销和复杂度。
2. 使用RTOS的消费级项目:建议使用动态内存,一般这种项目代码复杂度比裸机高,合理使用动态内存会有效降低内存的开销。同时很多Github项目的linux程序都是会使用内存管理,有这套机制能更加方便的移植代码。
3. 使用RTOS的军工、航天、医疗等高稳定性和高安全性项目:建议尽量降低对动态内存的使用与依赖以确保系统的稳定性。当然也不代表动态内存就无法在这些项目上使用,只是要求系统开发者对系统内存的把控要求更高。

1.5、RT-Thread 的内存管理

RT-Thread 操作系统在内存管理上,根据上层应用及系统资源的不同,有针对性地提供了不同的内存分配管理算法。总体上可分为两类:内存堆管理与内存池管理,而内存堆管理又根据具体内存设备划分为三种情况:
第一种是针对小内存块的分配管理(小内存管理算法);
第二种是针对大内存块的分配管理(slab 管理算法);
第三种是针对多内存堆的分配情况(memheap 管理算法)。

小内存管理算法是一个简单的内存分配算法。初始时,它是一块大的内存。当需要分配内存块时,将从这个大的内存块上分割出相匹配的内存块,然后把分割出来的空闲内存块还回给堆管理系统中。每个内存块都包含一个管理用的数据头,通过这个头把使用块与空闲块用双向链表的方式链接起来。

RT-Thread 的 slab 分配器是在 DragonFly BSD 创始人 Matthew Dillon 实现的 slab 分配器基础上,针对嵌入式系统优化的内存分配算法。最原始的 slab 算法是 Jeff Bonwick 为 Solaris 操作系统而引入的一种高效内核内存分配算法。

memheap 管理算法适用于系统含有多个地址可不连续的内存堆。使用 memheap 内存管理可以简化系统存在多个内存堆时的使用:当系统中存在多个内存堆的时候,用户只需要在系统初始化时将多个所需的 memheap 初始化,并开启 memheap 功能就可以很方便地把多个 memheap(地址可不连续)粘合起来用于系统的 heap 分配。

关于更多的关于 RT-Thread 内存的实现与细节我们就不在这里探讨了,有需求的小伙伴可以在 https://www.rt-thread.org/document/site/programming-manual/memory/memory/ 了解更多。

2、在RT-Thread上如何查找内存泄漏

前面探讨了这么多关于动态内存的东西,那么我们回到本次的主题内存泄漏。笔者前面说到使用RTOS无法避免的会有内存泄漏,所以我们需要找一种手段来查找内存泄漏并修复。
笔者多次提到了内存泄漏,那内存泄漏到底是什么呢?
简单的来说内存泄漏就是应该释放的内存没有释放,那么我们回头来考虑在没有MMU的RTOS,由于分配的地址都是真实的物理地址,一段代码的所有内存都没有释放,系统的内存就会变得越来越少,最终没有内存可以使用。

 

最终的现象是在RT-Thread的msh上执行free命令可以看到系统没有可用的内存,效果如下:

 
  1. memheap   pool size  max used size available size
  2. -------- ---------- ------------- --------------
  3. sram1    131072     131000        72
  4. heap     413872     413000        872
复制代码

除此之外还有一种泄漏,一段代码的部分内存没有释放,随着这块代码反复调用,虽然系统中可能有内存,但是内存区域被划分成了n个小块,如果这个时候分配一个大内存的区域就会分配时候,但是系统总内存是大于你需要分配的内存大小的。在RT-Thread的内存策略中只有连续的小块内存会在空闲线程中被整合成一块大内存,同时由于没有MMU所以无法利用物理与虚拟地址的转换在后台将小块内存自动整合成大内存。所以这种内存泄漏也是比较难查的问题之一,哪怕一段代码中分配了100块不同大小的内存,其中有一块没有释放也会导致系统没有大内存可用,只是时间问题。

 

 
  1. memheap  pool size  max used size available size
  2. -------- ---------- ------------- --------------
  3. sram1    131072     31000         100072
  4. heap     413872     313000        300872
复制代码

根据上图我们可用知道,系统中虽然有400K的内存,但是这400K的内存可能是被分成了100块区域,最终的区域可能只有4K左右,那么这个时候我去分配8K 10K 20K都肯定是失败的,这种内存泄漏一般在设备运行的早期不会出现,随着运行的时候越长就越容易出现。
在了解了内存泄漏的原理后,我们在回想一下在1.5中笔者说到小内存管理算法。其实在 RT-Thread 上小内存管理算法就是前面一段内存管理块+真实用户可以使用的的内存区域,具体如下图所示:

 

既然每块内存都有管理块,那么我们就可用将内存的具体情况遍历打印出来。包括内存使用的情况和内存的大小。这样就可用使用打印信息协助我们某一块内存区域是否有内存泄漏,并能够看到实际的内存情况,并合理的调整内存分配的策略。

在RT-Thread的小内存管理算法中已经支持了该功能,在RT-Thread中该功能被称作memtrace,下图是memtrace的具体打印:

 
  1. memory information
  2. [0x009000a4 -    20 00000000] NONE    : USED
  3. [0x009000dc -    64 00000000] NONE    : USED
  4. [0x00900140 -    20 00000000] NONE    : USED
  5. [0x00900178 -   128 00000000] NONE    : USED
  6. [0x0090021c -    20 00000000] NONE    : USED
  7. [0x00900254 -    64 00000000] NONE    : USED
  8. [0x009002b8 -    20 00000000] NONE    : USED
  9. [0x009002f0 -    16 00000001] main    : USED
  10. [0x00900324 -   108 00000001] main    : USED
  11. [0x009003b4 -   116 00000001] main    : USED
  12. [0x0090044c -    68 00000225] main    : USED
  13. [0x009004b4 -    12 00000225] main    : USED
  14. [0x009004e4 -   100 00000225] main    : USED
  15. [0x0090056c -    12 00000225] main    : USED
  16. [0x0090059c -    2K 00000227] main    : USED
  17. [0x00900dc0 -    28 00000227] main    : USED
  18. [0x00900e00 -    80 00000227] main    : USED
  19. [0x00900e74 -    5K 00000233] main    : USED
  20. [0x00902298 -    72 00000233] main    : USED
  21. [0x00902304 -    72 00000240] main    : USED
  22. [0x00902370 -   428 00000298] main    : USED
  23. [0x00902540 -    16 00000298] main    : USED
  24. [0x00902574 -    32 00000298] main    : USED
  25. [0x009025b8 -    12 00000298]         : FREED
  26. [0x009025e8 -    12 00000298] main    : USED
  27. [0x00902618 -    2K 00000298] main    : USED
  28. [0x00902e48 -    12 00000298]         : FREED
  29. [0x00902e78 -    12 00000298] main    : USED
  30. [0x00902ea8 -    12 00000298] main    : USED
  31. [0x00902ed8 -    12 00000298] main    : USED
  32. [0x00902f08 -   520 00000298] main    : USED
  33. [0x00903134 -    12 00000304] main    : USED
  34. [0x00903164 -    12 00000322] main    : USED
  35. [0x00903194 -    4K 00000322] main    : USED
  36. [0x009041f4 -    16 00000355] init_thr: USED
  37. [0x00904228 -    80 00000355] init_thr: USED
  38. [0x0090429c -    16 00000355] main    : USED
  39. [0x009042d0 -    1K 00000355] main    : USED
  40. [0x009047f4 -   16K 00000355] main    : USED
  41. [0x00908868 -  128K 00000355] main    : USED
  42. [0x0092888c -    48 00000355]         : USED
  43. [0x009288e0 -    16 00000355] main    : USED
  44. [0x00928914 -   120 00000361]         : USED
  45. [0x009289b0 -    40 00000361] main    : USED
  46. [0x009289fc -   120 00000361]         : USED
  47. [0x00928a98 -    36 00000411] user mai: USED
  48. [0x00928ae0 -    16 00000411] user mai: USED
  49. [0x00928b14 -    40 00000411] user mai: USED
  50. [0x00928b60 -   93K 00000429]         : FREED
复制代码

以上打印使用memtrace功能打印了整个系统的当前内存情况。打印的每部分意义可以参考下图理解。

 

那么有同学有疑问了,我们可以看到内存情况对修复系统内存泄漏有什么帮助呢?解决这个疑问之前我们需要了解下动态内存分配的特点:
1. 系统启动常驻内存:一般系统初始化的时候会分配大量的内存,这部分会在系统运行的时候就创建伴随系统运行的整个生命周期;
2. 系统当前正在运行内存:由于动态内存运行时创建的特点,所以系统在当前时间节点前后会存在一些正在使用的动态内存,但是这块内存在程序运行完毕后就会被释放,所以这块内存一般也不是内存泄漏;
3. 系统运行中间时间节点内存:由于系统一般只会存在常驻内存和当前使用的动态内存,这种在系统运行中间时间节点内存出现的内存一般都很有可能是内存泄漏的段。但是也不一定就是内存泄漏,由于系统部分功能可能在运行了一段时间后才会初始化,这部分也可能算做常驻内存。

了解了动态内存分配的特点后,我们得出一些结论:
1. 系统刚刚启动时分配的大部分内存一般不属于内存泄漏,哪怕是泄漏的,只要是不会重复有相同大小相同线程分配的,我们都可以不予处理,因为他最多泄漏一次对系统稳定性不会太大影响。
2. 系统当前正在运行内存一般也不予考虑,这部分内存无法知道是否有泄漏;
3. 系统运行中间时间节点内存需要保持高度怀疑,这个时间区域有相同线程和重复内存大小的反复出现一般都可能是内存泄漏;
4. 整个系统中有间接性的使用的内存和没有使用的内存逐渐把内存分成n个小块,这种一般也是由于申请了一片内存有一个没有释放导致。
5. 虽然是n个大小相同的内存且同一线程分配的内存也不一定是内存泄漏,这个时候还需要看这大小相同的且同一线程分配的的n个内存是否是同一时间分配,如果是同一时间分配一般都不是泄漏。

根据以上规则可以定位到大部分内存泄漏,剩下的工作就是找到这些泄漏的内存是在哪里分配的了。

3、在RT-Thread上使用memtrace功能

目前memtrace只支持Small Memory Algorithm算法。我们在bsp下使用menuconfig配置如下,生成工程后编译下载就可以在msh中看到对应的命令了。

 

最后的工作就是定位到是哪里的代码分配的这块泄漏内存,那么内存泄漏的代码点你就找到了!!!!

只要掌握了这套内存泄漏的调试方法就可以愉快的在RT-Thread使用动态内存了。
  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值