背景

嵌入式设备多款设备使用同一套代码,部分设备的内存较小,在主线维护一段时间后,经常出现OOM(out of memery)问题,经过排查,确认了确实没有出现内存泄漏问题,基本可以确认是由于设备内存本身不足导致的OOM问题。本文记录了一下设备内存优化的思路。

 

优化思路

优化思路有两个:

  • 排查新增的功能代码,查看是否有比较明显的新增内存
  • 分析当前整体的内存使用情况,进行优化

由于多种设备共用一条主线,新增代码量较多,且不一定都熟悉。使用第一种方法工作量一般比较大,而且不容易确认问题。另外虽然是新增代码导致的内存增加出现OOM,但实际上新增代码的内存使用可能是合理的而之前由于内存较多使用时反而可能出现浪费的情况。所以,直接分析整体代码使用情况,针对性的进行优化。

针对内存优化一般会分析全局存储区和堆内存使用情况,一般很少会分析栈内存使用情况,编码规范也要求控制局部变量大小,一般不会出现过大的情况。

静态内存分析

对于全局存储区,主要是使用readelf命令进行分析,readelf 命令可接受参数如下所示:

Usage: readelf <option(s)> elf-file(s)
 Display information about the contents of ELF format files
 Options are:
  -a --all               Equivalent to: -h -l -S -s -r -d -V -A -I
  -h --file-header       Display the ELF file header
  -l --program-headers   Display the program headers
     --segments          An alias for --program-headers
  -S --section-headers   Display the sections' header
     --sections          An alias for --section-headers
  -g --section-groups    Display the section groups
  -t --section-details   Display the section details
  -e --headers           Equivalent to: -h -l -S
  -s --syms              Display the symbol table
     --symbols           An alias for --syms
  --dyn-syms             Display the dynamic symbol table
  -n --notes             Display the core notes (if present)
  -r --relocs            Display the relocations (if present)
  -u --unwind            Display the unwind info (if present)
  -d --dynamic           Display the dynamic section (if present)
  -V --version-info      Display the version sections (if present)
  -A --arch-specific     Display architecture specific information (if any)
  -c --archive-index     Display the symbol/file index in an archive
  -D --use-dynamic       Use the dynamic section info when displaying symbols
  -x --hex-dump=<number|name>
                         Dump the contents of section <number|name> as bytes
  -p --string-dump=<number|name>
                         Dump the contents of section <number|name> as strings
  -R --relocated-dump=<number|name>
                         Dump the contents of section <number|name> as relocated bytes
  -z --decompress        Decompress section before dumping it
  -w[lLiaprmfFsoRtUuTgAckK] or
  --debug-dump[=rawline,=decodedline,=info,=abbrev,=pubnames,=aranges,=macro,=frames,
               =frames-interp,=str,=loc,=Ranges,=pubtypes,
               =gdb_index,=trace_info,=trace_abbrev,=trace_aranges,
               =addr,=cu_index,=links,=follow-links]
                         Display the contents of DWARF debug sections
  --dwarf-depth=N        Do not display DIEs at depth N or greater
  --dwarf-start=N        Display DIEs starting with N, at the same depth
                         or deeper
  -I --histogram         Display histogram of bucket list lengths
  -W --wide              Allow output width to exceed 80 characters
  @<file>                Read options from <file>
  -H --help              Display this information
  -v --version           Display the version number of readelf

使用-S参数可以列出可执行文件或者动态库的区段

-S --section-headers   Display the sections' header

使用如下c程序编译后的可执行文件作为例子

#include<stdio.h>

char databuf[1024 * 1024] = {1};
char bssbuf[1024 * 1024];
int main()
{
    static char stabuf[1024*1024] = {2};

    databuf[1] = 1;
    bssbuf[1] = 1;
    stabuf[1] = 1;    
}

 

执行后输出如下:

$ readelf -S a.out
There are 28 section headers, starting at offset 0x1948:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
 
  [21] .got              PROGBITS         0000000000200fc0  00000fc0
       0000000000000040  0000000000000008  WA       0     0     8
  [22] .data             PROGBITS         0000000000201000  00001000
       0000000000200020  0000000000000000  WA       0     0     32
  [23] .bss              NOBITS           0000000000401020  00201020
       0000000000100020  0000000000000000  WA       0     0     32
  [24] .comment          PROGBITS         0000000000000000  00001010
       000000000000002b  0000000000000001  MS       0     0     1
  [25] .symtab           SYMTAB           0000000000000000  00001040
       0000000000000600  0000000000000018          26    43     8
  [26] .strtab           STRTAB           0000000000000000  00001640
       000000000000020b  0000000000000000           0     0     1
  [27] .shstrtab         STRTAB           0000000000000000  0000184b
       00000000000000f9  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)

输出中的,name列表表示区段的名字,size列表示该区段的大小。其他列可以先不关注。

其他区段可以先不关注,主要关注.data和.bss两个区段,.data区段是定义时就初始化了的全局变量和静态局部变量的存储区域,.bss是存储未初始化的全局变量和静态局部变量的存储区域。

示例中,git程序.data字段一共使用了0x200020字节的存储空间,.bss一共使用了0x100020字节的内存。.data区段占用的内存在程序一加载的时候就会进行初始化,因此必然会占用实际物理内存,而.bss段的内容是在初始化的时候才会分配物理内存,可以查看这两个区段的内存占用情况,若占用比较大或者比预想的多很多,则可以分析对应段变量分配内存的大小是否有不合理的地方,是否存在可以优化的点。

使用-s 参数可以列出可执行文件或者动态库的全部符号信息,由于关注的是内存使用情况,只需要过滤出变量信息即可,输出如下:

readelf -s a.out|grep OBJECT
 Num:    Value          Size Type    Bind   Vis      Ndx Name
•    29: 0000000000401020     1 OBJECT  LOCAL  DEFAULT   23 completed.7697
•    30: 0000000000200df8     0 OBJECT  LOCAL  DEFAULT   19 __do_global_dtors_aux_fin
•    32: 0000000000200df0     0 OBJECT  LOCAL  DEFAULT   18 __frame_dummy_init_array_
•    34: 0000000000301020 0x100000 OBJECT  LOCAL  DEFAULT   22 stabuf.2251
•    36: 00000000000007e4     0 OBJECT  LOCAL  DEFAULT   17 __FRAME_END__
•    39: 0000000000200e00     0 OBJECT  LOCAL  DEFAULT   20 _DYNAMIC
•    42: 0000000000200fc0     0 OBJECT  LOCAL  DEFAULT   21 _GLOBAL_OFFSET_TABLE_
•    44: 0000000000201020 0x100000 OBJECT  GLOBAL DEFAULT   22 databuf
•    52: 0000000000201008     0 OBJECT  GLOBAL HIDDEN    22 __dso_handle
•    53: 00000000000006a0     4 OBJECT  GLOBAL DEFAULT   15 _IO_stdin_used
•    59: 0000000000401020     0 OBJECT  GLOBAL HIDDEN    22 __TMC_END__
•    61: 0000000000401040 0x100000 OBJECT  GLOBAL DEFAULT   23 bssbuf

第三列就是变量的大小,bind列有表示该变量类型,GLOBAL 表示为全局变量,LOCAL表示该变量为局部变量

在过滤局部变量是可以通过正则表达式来过滤内存较大的变量,如下所示,正则表达式“0x[0-9a-f]{5,} OBJECT”表示过滤出大小在0x10000byte以上的变量。

readelf -s a.out|grep -E "0x[0-9a-f]{5,} OBJECT"
    34: 0000000000301020 0x100000 OBJECT  LOCAL  DEFAULT   22 stabuf.2251
    44: 0000000000201020 0x100000 OBJECT  GLOBAL DEFAULT   22 databuf
    61: 0000000000401040 0x100000 OBJECT  GLOBAL DEFAULT   23 bssbuf

和上面的例子对照可以看到,该程序内存在0x10000以上的变量有三个,每个大小都是1M(0x100000)。其中databuf是全局变量,stabuf.2251是静态局部变量,bssbuf是未初始化的局部变量。在这个例子中我们很容易对照代码看出变量是否有初始化,但如果没有代码的时候如何查看该变量是在data段还是bss段呢?

readelf -s 的输出内容中第2列value表示的是该变量在可执行文件中的地址(偏移量),在readelf -S输出中我们可以看到 data段起始地址为0x0000000000201000,bss段起始地址为0x0000000000401020,

Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align

.data             PROGBITS         0000000000201000  00001000
       0000000000200020  0000000000000000  WA       0     0     32
[23] .bss              NOBITS           0000000000401020  00201020
       0000000000100020  0000000000000000  WA       0     0     32

bssbuf的地址为0x0000000000401040 很明显是在.bss段,而databuf,stabuf.2251地址分别为0x0000000000201020,0x0000000000301020都在.data段。

技巧:使用GDB进行详细内容分析

对于结构体或者结构体中的变量无法直接通过符号看出来大小的情况可以通过gdb挂进程的方式查看,方式为gdb -p pid。使用p 命令输出变量或者结构体大小,如p sizeof(databuf) 或者 p sizeof(int)

通过上面的方法可以比较容易的看出全局变量内存的使用是否出现了明显的增加,如果增加了是那部分增加的。在当前存在的全局变量中,主要是那些变量占的内存较多,是否比预期的更多,是否可以有明显的优化。

推荐:《程序员的自我修养:链接、装载与库》 之前看过,对可执行文件的分析,动态库的加载链接讲的比较好,C语言开发进阶值得一看。

动态申请内存分析

对于动态申请的内存,可以通过mtrace进行跟踪分析。一般实际使用时主要关注的是申请了那些较大的内存块,而对应的内存实际是可以裁剪的。mtrace跟踪内存使用方式如下文介绍:

[mtrace-内存使用追踪] https://www.jianshu.com/p/d9e12b66096amtrace-内存使用追踪

实际工作中使用mtrace进行内存使用情况分析较少,可以用mtrace对比两个版本中大内存块申请情况,查看是否有明显的增加,另外就是可以看较大块的内存申请中是否有可以优化的点。仅仅比较大块内存申请情况的原因主要是因为在没有内存泄漏的情况下,分析小块内存的使用是否可以优化性价比太低,优先考虑大块内存的优化情况更合理。

动态内存申请的内存是否占用实际物理内存也需要考虑,如果使用malloc申请后不会使用的内存实际上不会占用物理内存,因此优化后对实际内存使用无特别的帮助。

 

总结:

在这次内存优化的过程中,主要优化了部分缓存的使用(由于多产品共用一条代码主线,存在部分缓存分配及定义不合理的情况,定义时没有考虑小内存设备,导致小内存设备虽然不需要这些功能但实际上却占用了相应的内存)