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

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

前言

本书的重点分为系统内存和性能优化,前4章着重内存使用,尽量减少进程的内存使用量,定位和发现内存泄漏;后5章着重与如何让系统性能优化,加快执行速度。

  • 《嵌入式linux内存使用与性能优化》读书笔记
    • 前言
    • 第1章 内存的测量
    • 第2章 进程内存优化
    • 第3章 系统内存优化
    • 第4章 内存泄漏
    • 第5章 性能优化的流程
    • 第6章 进程启动速度
    • 第7章 性能优化的方法
    • 第8章 代码优化的境界
    • 第9章 系统性能优化

要优化进程的内存用量,首先需要使用工具评估内存使用量;

查看进程的哪些部分耗费内存,有针对性进行优化;

从系统方面查找方法进行优化;

如何定位进程的内存泄漏,如何解决。

第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\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的内存管理,这里暂且不表,后期结合奔跑卷内存管理再来细看。

第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 确定性能指标 性能测量,获取数据 分析数据,查找瓶颈 修改程序 是否满足要求 成功 yes no
  • 性能分析

系统相关

# 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

你可能感兴趣的:(ARM,Linux,嵌入式linux优化)