背景
嵌入式设备多款设备使用同一套代码,部分设备的内存较小,在主线维护一段时间后,经常出现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申请后不会使用的内存实际上不会占用物理内存,因此优化后对实际内存使用无特别的帮助。
总结:
在这次内存优化的过程中,主要优化了部分缓存的使用(由于多产品共用一条代码主线,存在部分缓存分配及定义不合理的情况,定义时没有考虑小内存设备,导致小内存设备虽然不需要这些功能但实际上却占用了相应的内存)