《嵌入式linux内存使用与性能优化》读书笔记

2019-07-12 14:22发布

《嵌入式linux内存使用与性能优化》读书笔记

前言

本书的重点分为系统内存和性能优化,前4章着重内存使用,尽量减少进程的内存使用量,定位和发现内存泄漏;后5章着重与如何让系统性能优化,加快执行速度。 要优化进程的内存用量,首先需要使用工具评估内存使用量; 查看进程的哪些部分耗费内存,有针对性进行优化; 从系统方面查找方法进行优化; 如何定位进程的内存泄漏,如何解决。

第1章 内存的测量

作者对于内存使用优化采用的方法论,按照明确目标->寻找评估方法->系统优化->重新测量评估,来研究系统的内存使用与优化。
  • 明确目标,针对系统内存优化,目标有两个: (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可以查看与进程内存相关的节点,比如statmmapssmaps等。 # 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都可以被回收。

第2章 进程内存优化

本章分为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 ", pid); printf("global variable address=%p ", &n); printf("const global address=%p ", &n1); printf("global uninitialization variable address=%p ", &m);; printf("static variable address=%p ", &s1); printf("stack variable address=%p ", &s); printf("heap variable address=%p ", &p); printf("malloc address=%p ", 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的内存管理,这里暂且不表,后期结合奔跑卷内存管理再来细看。

第3章 系统内存优化

笔者建议将守护进程区分为需常驻和非常驻两部分,尽量简单化,了解inetd守护进程架构。
  • tmpfs分区
在linux中,为了加快对文件的读写,基于内存建立了一个文件系统,称为ramdisk或tmpfs,对于该文件系统的访问,都将直接操作物理内存,因此要比flash这类存储设备快得多。这部分分区的文件,不需要的要及时删除。
  • cache和buffer
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就开始启动,此时会根据算法来决定杀死某些进程,基本上系统已经不能正常使用了。

第4章 内存泄漏

  • 如何确定是否存在内存泄漏
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检测内存泄漏 $ 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)

第5章 性能优化的流程

  • 性能评价 优化也要考虑到可移植性以及普适性,不要因为优化过度导致其他问题的出现。
  • 优化流程 Created with Raphaël 2.1.2确定性能指标性能测量,获取数据分析数据,查找瓶颈修改程序是否满足要求成功yesno
  • 性能分析
系统相关 # 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]

第6章 进程启动速度

进程启动分为两部分:
(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进程 提前加载、延迟退出

第7章 性能优化的方法

本章主要介绍软件优化的方法,通过检测工具对程序本身进行优化,涉及到软件模块化设计以及实现算法等。

第8章 代码优化的境界

本章主要讲解arm gcc优化,借助汇编方法解析软件流程,从而实现编码最优解,内容可参考《ARM嵌入式系统开发—-软件设计与优化》部分章节。

第9章 系统性能优化

  • 脚本优化
    去掉脚本中无用的代码 尽可能不适用pipe 尽量不适用”`”
  • 使用preload预先加载进程
  • 调整进程的优先级
  • 关注守护进程的数量
  • 优化系统启动速度 https://elinux.org/Boot_Time