本书的重点分为系统内存和性能优化,前4章着重内存使用,尽量减少进程的内存使用量,定位和发现内存泄漏;后5章着重与如何让系统性能优化,加快执行速度。
要优化进程的内存用量,首先需要使用工具评估内存使用量;
查看进程的哪些部分耗费内存,有针对性进行优化;
从系统方面查找方法进行优化;
如何定位进程的内存泄漏,如何解决。
作者对于内存使用优化采用的方法论,按照明确目标->寻找评估方法->系统优化->重新测量评估,来研究系统的内存使用与优化。
明确目标,针对系统内存优化,目标有两个:
(1)每个守护进程使用的内存尽可能少
(2)长时间运行后,守护进程内存仍然保持较低使用量,没有内存泄露。
寻找评估方法
系统优化
系统内存测量
PC下使用free
total used free shared buffers cached
Mem: 3959100 3564900 394200 276968 479640 1973096
-/+ buffers/cache: 1112164 2846936
Swap: 12295156 264 12294892
嵌入式平台使用free
total used free shared buffers
Mem: 247676 59800 187876 0 1432
-/+ buffers: 58368 189308
Swap: 0 0 0
可用的物理内存=free+buffers+cached,但是在嵌入式平台没有cached,所以无法获取确切的空闲内存。那么可以通过proc
查看。
buffers是用来给linux系统中块设备做缓冲区,cached用来缓冲打开的文件。下面是通过cat /proc/meminfo
获取,可知实际可用内存=187884+1432+23968=213284,使用proc
更为准确。
# cat /proc/meminfo | grep -E "Free|Cached|Buffers"
MemFree: 187884 kB
Buffers: 1432 kB
Cached: 23968 kB
SwapCached: 0 kB
HighFree: 0 kB
LowFree: 187884 kB
SwapFree: 0 kB
进程内存测量
利用proc
可以查看与进程内存相关的节点,比如statm
、maps
和smaps
等。
# cat /proc/1/statm
587 156 136 155 0 77 0
# cat /proc/1/maps
00008000-000a3000 r-xp 00000000 1f:0a 10116 /bin/busybox
000ab000-000ac000 rw-p 0009b000 1f:0a 10116 /bin/busybox
000ac000-000cf000 rw-p 00000000 00:00 0 [heap]
b6e7c000-b6f50000 r-xp 00000000 1f:0a 610260 /lib/libc-2.15.so
b6f50000-b6f58000 ---p 000d4000 1f:0a 610260 /lib/libc-2.15.so
b6f58000-b6f5a000 r--p 000d4000 1f:0a 610260 /lib/libc-2.15.so
b6f5a000-b6f5b000 rw-p 000d6000 1f:0a 610260 /lib/libc-2.15.so
b6f5b000-b6f5e000 rw-p 00000000 00:00 0
b6f5e000-b6fbf000 r-xp 00000000 1f:0a 1300548 /lib/libm-2.15.so
b6fbf000-b6fc6000 ---p 00061000 1f:0a 1300548 /lib/libm-2.15.so
b6fc6000-b6fc7000 r--p 00060000 1f:0a 1300548 /lib/libm-2.15.so
b6fc7000-b6fc8000 rw-p 00061000 1f:0a 1300548 /lib/libm-2.15.so
b6fc8000-b6fdf000 r-xp 00000000 1f:0a 509412 /lib/ld-2.15.so
b6fe1000-b6fe5000 rw-p 00000000 00:00 0
b6fe5000-b6fe6000 r-xp 00000000 00:00 0 [sigpage]
b6fe6000-b6fe7000 r--p 00016000 1f:0a 509412 /lib/ld-2.15.so
b6fe7000-b6fe8000 rw-p 00017000 1f:0a 509412 /lib/ld-2.15.so
bec47000-bec68000 rw-p 00000000 00:00 0 [stack]
ffff0000-ffff1000 r-xp 00000000 00:00 0 [vectors]
statm
表示进程内存状态信息,文件中的值都是从系统启动开始累计到当前时刻。
各个字段的含义:
Size (pages) 任务虚拟地址空间的大小 VmSize/4
Resident(pages) 应用程序正在使用的物理内存的大小 VmRSS/4
Shared(pages) 共享页数
Trs(pages) 程序所拥有的可执行虚拟内存的大小 VmExe/4
Lrs(pages) 被映像到任务的虚拟内存空间的库的大小 VmLib/4
Drs(pages) 程序数据段和用户态的栈的大小 (VmData+ VmStk)/4
dt(pages) 脏页数量
Size、Trs、Lrs、Drs对应虚拟内存,Resident、Shared、dt对应物理内存。
maps
文件中各列的含义:
第一列,代表该内存段的虚拟地址。
第二列,r-xp,代表该段内存的权限,r读,w写,x执行,s共享,p私有。
第三列,代表在进程地址里的偏移。
第四列,映射文件的设备号。
第五列,映射文件的inode节点号。
第六列,映射文件名
内存回收
Linux存在一个守护进程kswapd,他是Linux内存回收机制,会定期监察系统中空闲呢村的数量,一旦发现空闲内存数量小于一个阈值的时候,就会将若干页面换出。
但是在嵌入式Linux系统中,却没有交换分区。没有交换分区的原因是:
1.一旦使用了交换分区,系统系能将下降的很快,不可接受。
2.Flash的写次数是有限的,如果在Flash上面建立交换分区,必然导致对Flash的频繁读写,影响Flash寿命。
那没有交换分区,Linux是如何做内存回收的呢?
对于那些没有被改写的页面,这块内存不需要写到交换分区上,可以直接回收。
对于已经改写了的页面,只能保留在系统中,没有交换分区,不能写到Flash上。
在Linux物理内存中,每个页面有一个dirty标志,如果被改写了,称之为dirty page,而所有非dirty page都可以被回收。
本章分为3部分说明内存优化
1.执行文件所占用的内存。
2.动态库对内存的影响。
3.线程对内存的影响。
执行文件
一个程序包括代码段、数据段、堆段和栈段。一个进程运行时,所占用的内存,可以分为如下几部分:
栈区(stack):由编译器自动分配释放,存放函数的参数、局部变量等
堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可有操作系统来回收
全局变量、静态变量:初始化的全局变量和静态变量在一块区域,未初始化的全局变量和静态变量在另一块区域,程序结束后由系统释放
文字常量:常量、字符串就是放在这里的,程序结束后有系统释放
程序代码:存放函数体的二进制代码
使用下面的例子:
#include
#include
int n=10;
const int n1=20;
int m;
int main()
{
int s=7;
static int s1=30;
char *p=(char *)malloc(20);
pid_t pid=getpid();
printf("pid:%d\n", pid);
printf("global variable address=%p\n", &n);
printf("const global address=%p\n", &n1);
printf("global uninitialization variable address=%p\n", &m);;
printf("static variable address=%p\n", &s1);
printf("stack variable address=%p\n", &s);
printf("heap variable address=%p\n", &p);
printf("malloc address=%p\n", p);
pause();
}
执行结果
pid:1234
global variable address=0x1077c
const global address=0x856c
global uninitialization variable address=0x10788
static variable address=0x10780
stack variable address=0xbe91bc48
heap variable address=0xbe91bc44
malloc address=0x11008
查看该进程的maps信息
00008000-00009000 r-xp 00000000 00:13 676676 /work/test
(只读全局变量n1属于进程的代码段)
00010000-00011000 rw-p 00000000 00:13 676676 /work/test
(全局初始化变量n、全局未初始化变量m、局部静态变量s1属于进程的数据段)
00011000-00032000 rw-p 00000000 00:00 0 [heap]
(malloc分配的区间位于堆区)
b6e75000-b6f49000 r-xp 00000000 00:13 692826 /lib/libc-2.15.so
b6f49000-b6f51000 ---p 000d4000 00:13 692826 /lib/libc-2.15.so
b6f51000-b6f53000 r--p 000d4000 00:13 692826 /lib/libc-2.15.so
b6f53000-b6f54000 rw-p 000d6000 00:13 692826 /lib/libc-2.15.so
b6f54000-b6f57000 rw-p 00000000 00:00 0
b6f57000-b6f6e000 r-xp 00000000 00:13 692833 /lib/ld-2.15.so
b6f70000-b6f72000 rw-p 00000000 00:00 0
b6f72000-b6f74000 rw-p 00000000 00:00 0
b6f74000-b6f75000 r-xp 00000000 00:00 0 [sigpage]
b6f75000-b6f76000 r--p 00016000 00:13 692833 /lib/ld-2.15.so
b6f76000-b6f77000 rw-p 00017000 00:13 692833 /lib/ld-2.15.so
be9e1000-bea02000 rw-p 00000000 00:00 0 [stack]
(局部变量s、指针变量p属于进程的栈区)
ffff0000-ffff1000 r-xp 00000000 00:00 0 [vectors]
glibc
笔者花了很大的篇幅讲解glibc的内存管理,这里暂且不表,后期结合奔跑卷内存管理再来细看。
笔者建议将守护进程区分为需常驻和非常驻两部分,尽量简单化,了解inetd守护进程架构。
在linux中,为了加快对文件的读写,基于内存建立了一个文件系统,称为ramdisk或tmpfs,对于该文件系统的访问,都将直接操作物理内存,因此要比flash这类存储设备快得多。这部分分区的文件,不需要的要及时删除。
cache和buffer也是内存中的数据,buffer是即将写入磁盘的,而cache是从磁盘中读出的。在系统内存不足时,内核会释放一部分cache和buffer。
查看内核文档Documentation/sysctl/vm.txt
,讲解了vm优化的方法。
block_dump
表示是否打开Block Debug模式,用于记录所有的读写及Dirty Block写回操作。0,表示禁用Block Debug模式;1,表示开启Block Debug模式。dirty_background_ratio
表示脏数据达到系统整体内存的百分比,此时触发pdflush进程把脏数据写回磁盘。dirty_expires_centisecs
表示脏数据在内存中驻留时间超过该值,pdflush进程在下一次将把这些数据写回磁盘。缺省值3000,单位是1/100s。dirty_ratio
表示如果进程产生的脏数据达到系统整体内存的百分比,此时进程自行吧脏数据写回磁盘。dirty_writeback_centisecs
表示pdflush进程周期性间隔多久把脏数据协会磁盘,单位是1/100s。vfs_cache_pressure
表示内核回收用于directory和inode cache内存的倾向;缺省值100表示内核将根据pagecache和swapcache,把directory和inode cache报纸在一个合理的百分比;降低该值低于100,将导致内核倾向于保留directory和inode cache;高于100,将导致内核倾向于回收directory和inode cache。min_free_kbytes
表示强制Linux VM最低保留多少空闲内存(KB)。nr_pdflush_threads
表示当前正在进行的pdflush进程数量,在I/O负载高的情况下,内核会自动增加更多的pdflush。overcommit_memory
指定了内核针对内存分配的策略,可以是0、1、2.
0 表示内核将检查是否有足够的可用内存供应用进程使用。如果足够,内存申请允许;反之,内存申请失败。
1 表示内核允许分配所有物理内存,而不管当前内存状态如何。
2 表示内核允许分配查过所有物理内存和交换空间总和的内存。overcommit_ratio
如果overcommit_memory=2,可以过在内存的百分比。page-cluster
表示在写一次到swap区时写入的页面数量,0表示1页,3表示8页。swapiness
表示系统进行交换行为的成都,数值(0~100)越高,越可能发生磁盘交换。legacy_va_layout
表示是否使用最新的32位共享内存mmap()系统调用。nr_hugepages
表示系统保留的hugetlg页数。
涉及到cache的文件
To free pagecache:
echo 1 > /proc/sys/vm/drop_caches
To free reclaimable slab objects (includes dentries and inodes):
echo 2 > /proc/sys/vm/drop_caches
To free slab objects and pagecache:
echo 3 > /proc/sys/vm/drop_caches
linux中有一个内核进程kswapd,其专门负责回收内存,这里涉及2个阈值,当空闲内存页数量低于pages_low的时候,kswap进程就会扫描内存并且每次释放32个free pages,直到free page的数量达到pages_high。
kswapd回收内存的原则:
1.如果物理页面不是dirty page,就将该物理页面回收。
- 代码段,只读不能被改写,所占内存都不是dirty page。
- 数据段,可读写,所占内存可能是dirty page,也可能不是。
- 堆段,没有对应的映射文件,内容都是通过修改程序改写的,所占物理内存都是dirty page。
- 栈段和堆段一样,所占物理内存都是dirty page。
- 共享内存,所占物理内存都是dirty page。
就是说,这条规则主要面向进程的代码段和未修改的数据段。
2.如果物理页面已经修改并且可以备份回文件系统,就调用pdflush将内存中的内容和文件系统进行同步。pdflush写回磁盘,主要针对Buffers。
3.如果物理页面已经修改但是没有任何磁盘的备份,就将其写入swap分区。
kswapd在回收内存过程中有2个方法:
LMR - Low on Memory Reclaiming
OMK - Out of Memory Killer
在分配内存失败的时候LMR会启用,因为没有足够的空闲物理页,LMR每次释放1024个脏页直到内存分配成功。但当LMR也不能释放物理页的时候,OMK就开始启动,此时会根据算法来决定杀死某些进程,基本上系统已经不能正常使用了。
1.模仿用户长时间使用设备,查看内存使用情况,对于那些内存大量增长的进程,可以初步怀疑其有内存泄露。
2.针对某个具体测试用例,检查是否有内存泄露。
mtrace
glibc库中提供了一个钩子函数mtrace
1.加入头文件
#include
#include
#include
int main(int argc, char **argv)
{
mtrace();
int *p = malloc(100);
return 0;
}
编译,设置环境变量,执行
$ gcc -g memtest.c -o memtest
$ export MALLOC_TRACE=/tmp/memtest
$ mtrace memtest $MALLOC_TRACE
Memory not freed:
-----------------
Address Size Caller
0x00000000017f9450 0x64 at /work/tmp/memtest.c:8
利用valgrind检测内存泄漏
$ valgrind --tool=memcheck ./memtest
==16014== Memcheck, a memory error detector
==16014== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==16014== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==16014== Command: ./memtest
==16014==
==16014==
==16014== HEAP SUMMARY:
==16014== in use at exit: 1,164 bytes in 3 blocks
==16014== total heap usage: 3 allocs, 0 frees, 1,164 bytes allocated
==16014==
==16014== LEAK SUMMARY:
==16014== definitely lost: 612 bytes in 2 blocks
==16014== indirectly lost: 0 bytes in 0 blocks
==16014== possibly lost: 0 bytes in 0 blocks
==16014== still reachable: 552 bytes in 1 blocks
==16014== suppressed: 0 bytes in 0 blocks
==16014== Rerun with --leak-check=full to see details of leaked memory
==16014==
==16014== For counts of detected and suppressed errors, rerun with: -v
==16014== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
性能评价
优化也要考虑到可移植性以及普适性,不要因为优化过度导致其他问题的出现。
优化流程
性能分析
系统相关
# cat /proc/stat
cpu 384 7 2290 2642657 3964 0 152 0 0 0
cpu0 384 7 2290 2642657 3964 0 152 0 0 0
intr 693125 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 36485 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 45314 0 0 0 0 57165 33074 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 511878 0 1020 0 8189 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0
ctxt 848641
btime 1536194949
processes 2555
procs_running 1
procs_blocked 0
softirq 352444 31623 211249 5303 90199 0 0 1663 0 74 12333
cpu0后面的数字单位是jiffy,分别对应:
user:从系统启动开始累计到当前时刻,用户态CPU时间,不包含nice值为负的进程。
nice:从系统启动开始累计到当前时刻,nice值为负的进程所占用的CPU时间。
system:从系统启动开始累计到当前时刻,内核所占用的CPU时间。
idle:从系统启动开始累计到当前时刻,除硬盘IO等待时间以外其他等待时间。
iowait:从系统启动开始累计到当前时刻,硬盘IO等待时间。
irq:从系统启动开始累计到当前时刻,硬中断时间。
softirq:从系统启动开始累计到当前时刻,软中断时间。
steal:从系统启动开始累计到当前时刻,involuntary wait
guest:running as a normal guest
guest_nice:running as a niced guest
由此可以粗略计算cpu运行时间,cpu的利用率和IO利用率
查看负载
# cat /proc/loadavg
0.00 0.01 0.05 1/55 2565
1、5、15分钟平均负载;
1/55:在采样时刻,运行队列任务数目和系统中活跃任务数目。
2565:最大pid值,包括线程。
查看进程
# top
Mem: 61368K used, 186308K free, 48K shrd, 1436K buff, 23688K cached
CPU: 0.0% usr 0.0% sys 0.0% nic 100% idle 0.0% io 0.0% irq 0.0% sirq
Load average: 0.00 0.01 0.05 1/55 2568
PID PPID USER STAT VSZ %VSZ CPU %CPU COMMAND
2568 1142 root R 2352 0.9 0 0.0 top
42 2 root SW 0 0.0 0 0.0 [kworker/0:1]
1120 1 root S 33132 13.3 0 0.0 /usr/sbin/minidlnad
1086 1 root S 13712 5.5 0 0.0 /usr/sbin/ntpd -g
1114 1 root S 4340 1.7 0 0.0 /usr/sbin/sshd
1100 1099 www-data S 4248 1.7 0 0.0 nginx: worker process
1090 1 mosquitt S 4116 1.6 0 0.0 /usr/sbin/mosquitto -c /etc/mosqui
1099 1 root S 4064 1.6 0 0.0 nginx: master process /usr/sbin/ng
1142 1 root S 2352 0.9 0 0.0 -/bin/sh
1052 1 root S 2348 0.9 0 0.0 /sbin/syslogd -n
1 0 root S 2348 0.9 0 0.0 init
1055 1 root S 2348 0.9 0 0.0 /sbin/klogd -n
1037 1 root S 2348 0.9 0 0.0 /sbin/udhcpc -i eth0
1071 1 dbus S 2096 0.8 0 0.0 dbus-daemon --system
1128 1 root S 2020 0.8 0 0.0 rpc.statd
1137 1 root S 1940 0.7 0 0.0 rpc.mountd
1079 1 root S 1604 0.6 0 0.0 /usr/bin/rpcbind
992 2 root SW< 0 0.0 0 0.0 [kworker/u3:2]
539 2 root SW 0 0.0 0 0.0 [kswapd0]
6 2 root SW 0 0.0 0 0.0 [kworker/u2:0]
进程启动分为两部分:
(1)进程启动,加载动态库,知道main函数之前。
(2)main函数之后,知道对用户操作响应。
查看进程启动过程
strace
利用strace
查看程序执行流程,-tt
可以打印微妙级别时间戳。
strace -tt xxx
LD_DEBUG
通过设置LD_DEBUG环境变量,可以打印程序启动流程。
比如LD_DEBUG=libs xxx,可以查看程序运行过程中查找哪些动态库。
# LD_DEBUG=help ls
Valid options for the LD_DEBUG environment variable are:
libs display library search paths
reloc display relocation processing
files display progress for input file
symbols display symbol table processing
bindings display information about symbol binding
versions display version dependencies
scopes display scope information
all all previous options combined
statistics display relocation statistics
unused determined unused DSOs
help display this help message and exit
To direct the debugging output into a file instead of standard output
a filename can be specified using the LD_DEBUG_OUTPUT environment variable.
减少加载动态库数量
移除一些不需要的动态库
使用dlopen动态加载动态库,可以精确控制动态库的生存周期,一方面可以减少动态库数据段的内存使用,另一方面可以减少进程启动时加载动态库的时间。
优化进程启动速度
优化动态库的搜索路径
考虑使用dlopen,将一起启动时不需要的动态库从进程的依赖动态库中去除
进程修改为线程
preload进程
提前加载、延迟退出
本章主要介绍软件优化的方法,通过检测工具对程序本身进行优化,涉及到软件模块化设计以及实现算法等。
本章主要讲解arm gcc优化,借助汇编方法解析软件流程,从而实现编码最优解,内容可参考《ARM嵌入式系统开发—-软件设计与优化》部分章节。
脚本优化
去掉脚本中无用的代码
尽可能不适用pipe
尽量不适用”`”
使用preload预先加载进程
调整进程的优先级
关注守护进程的数量
优化系统启动速度 https://elinux.org/Boot_Time