【Android Linux内存及性能优化】(九) 进程启动速度优化篇
发布日期:2021-06-29 14:52:12 浏览次数:2 分类:技术文章

本文共 24833 字,大约阅读时间需要 82 分钟。

【Android Linux内存及性能优化】九 进程启动速度篇

本文接着

《》
《》
《》
《》
《》
《》
《》
《》

三、进程启动速度

  • 在实际开发过程中,经常会遇到这样的情况:
    由于对用户事件响应速度要求比较高,而当前的程序无法达到,程序员不得不把它们改成守护进程,在一开机便将其启动,
    守候在系统中,来提高用户的响应速度,这样便会导致系统中守护进程的数量越来越多。
    这些进程不光会占用大量的内存,而且还容易造成内存泄漏,
    同时系统里存活的进程过多,也会导致系统的整体性能下降。

解决这个问题的关键在于提高进程的启动速度,减少守护进程的数量。

进程的启动主要包括两个部分:

  • (1)进程启动,加载动态库,直到main 函数之前。 (涉及前面的动态库优化)
  • (2)main 函数之后,直到对用启的操作有响应。(涉及自身编写的代码的优化)

3.1 查看进程的启动过程

要想优化进程的启动速度,先来看下进程在启动时都做了什么事情。

可以使用两个工具 strace 和 LD_DEBUG 来查看进程的启动过程。

先来写个测试程序:

// 编写 hello.c 文件#include 
#include
int main(){
printf("hello, this is test!"); return 0;}使用 gcc -o hello -O2 hello.c 编译成可执行文件

3.1.1 查看进程启动时间 strace -tt ./hello

使用 strace -tt ./hello 查看进程启动过程。

ciellee@sh:~/Desktop/hello_test$ strace -tt ./hello 11:02:14.861467 execve("./hello", ["./hello"], [/* 71 vars */]) = 011:02:14.863058 brk(NULL)               = 0x173d00011:02:14.863399 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)11:02:14.863700 openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 311:02:14.863867 fstat(3, {
st_mode=S_IFREG|0644, st_size=125755, ...}) = 011:02:14.864035 mmap(NULL, 125755, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f018ca5b00011:02:14.864226 close(3) = 011:02:14.864373 openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 311:02:14.864513 read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\260A\2\0\0\0\0\0"..., 832) = 83211:02:14.864641 fstat(3, {
st_mode=S_IFREG|0755, st_size=1824496, ...}) = 011:02:14.864762 mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f018ca5900011:02:14.864899 mmap(NULL, 1837056, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f018c89800011:02:14.865016 mprotect(0x7f018c8ba000, 1658880, PROT_NONE) = 011:02:14.865141 mmap(0x7f018c8ba000, 1343488, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x22000) = 0x7f018c8ba00011:02:14.865273 mmap(0x7f018ca02000, 311296, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x16a000) = 0x7f018ca0200011:02:14.865391 mmap(0x7f018ca4f000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1b6000) = 0x7f018ca4f00011:02:14.865525 mmap(0x7f018ca55000, 14336, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f018ca5500011:02:14.865663 close(3) = 011:02:14.865827 arch_prctl(ARCH_SET_FS, 0x7f018ca5a500) = 011:02:14.866092 mprotect(0x7f018ca4f000, 16384, PROT_READ) = 011:02:14.866214 mprotect(0x600000, 4096, PROT_READ) = 011:02:14.866341 mprotect(0x7f018caa1000, 4096, PROT_READ) = 011:02:14.866459 munmap(0x7f018ca5b000, 125755) = 011:02:14.866742 fstat(1, {
st_mode=S_IFCHR|0620, st_rdev=makedev(136, 2), ...}) = 011:02:14.866972 brk(NULL) = 0x173d00011:02:14.867096 brk(0x175e000) = 0x175e00011:02:14.867228 write(1, "hello, this is test!", 20hello, this is test!) = 2011:02:14.867424 exit_group(0) = ?11:02:14.867634 +++ exited with 0 +++ciellee@sh:~/Desktop/hello_test$

通过上面的打印信息,就可以知道进程在加载动态时的大概过程。

通过使用 "-tt" 选项,能够将进程运行过程中系统调用的时间戳打印出来,就可以知道 进程在加载动态库过程中所用的时间,
注意这个 时间要比进程实际所用的时间要大。

strace 使用方法如下:

ciellee@sh:~/Desktop/hello_test$ strace -husage: strace [-CdffhiqrtttTvVwxxy] [-I n] [-e expr]...              [-a column] [-o file] [-s strsize] [-P path]...              -p pid... / [-D] [-E var=val]... [-u username] PROG [ARGS]   or: strace -c[dfw] [-I n] [-e expr]... [-O overhead] [-S sortby]              -p pid... / [-D] [-E var=val]... [-u username] PROG [ARGS]Output format:  -a column      alignment COLUMN for printing syscall results (default 40)  -i             print instruction pointer at time of syscall  -o file        send trace output to FILE instead of stderr  -q             suppress messages about attaching, detaching, etc.  -r             print relative timestamp  -s strsize     limit length of print strings to STRSIZE chars (default 32)  -t             print absolute timestamp  -tt            print absolute timestamp with usecs  -T             print time spent in each syscall  -x             print non-ascii strings in hex  -xx            print all strings in hex  -y             print paths associated with file descriptor arguments  -yy            print ip:port pairs associated with socket file descriptorsStatistics:  -c             count time, calls, and errors for each syscall and report summary  -C             like -c but also print regular output  -O overhead    set overhead for tracing syscalls to OVERHEAD usecs  -S sortby      sort syscall counts by: time, calls, name, nothing (default time)  -w             summarise syscall latency (default is system time)Filtering:  -e expr        a qualifying expression: option=[!]all or option=[!]val1[,val2]...     options:    trace, abbrev, verbose, raw, signal, read, write  -P path        trace accesses to pathTracing:  -b execve      detach on execve syscall  -D             run tracer process as a detached grandchild, not as parent  -f             follow forks  -ff            follow forks with output into separate files  -I interruptible     1:          no signals are blocked     2:          fatal signals are blocked while decoding syscall (default)     3:          fatal signals are always blocked (default if '-o FILE PROG')     4:          fatal signals and SIGTSTP (^Z) are always blocked                 (useful to make 'strace -o FILE PROG' not stop on ^Z)Startup:  -E var         remove var from the environment for command  -E var=val     put var=val in the environment for command  -p pid         trace process with process id PID, may be repeated  -u username    run command as username handling setuid and/or setgidMiscellaneous:  -d             enable debug output to stderr  -v             verbose mode: print unabbreviated argv, stat, termios, etc. args  -h             print help message  -V             print version

3.1.2 查看进程启动过程 LD_DEBUG=libs ./hello

LD_DEBUG 是 glibc 中的loader 为了方便自身调试,而设置的一个环境变量。

通过设置这个 环境变量,可以打印出在进程加载过程中 loader 都做了哪些事情。

ciellee@sh:~/Desktop/hello_test$ LD_DEBUG=libs ./hello     14368:	find library=libc.so.6 [0]; searching     14368:	 search cache=/etc/ld.so.cache     14368:	  trying file=/lib/x86_64-linux-gnu/libc.so.6     14368:	     14368:	     14368:	calling init: /lib/x86_64-linux-gnu/libc.so.6     14368:	     14368:	     14368:	initialize program: ./hello     14368:	     14368:	     14368:	transferring control: ./hello     14368:	     14368:	     14368:	calling fini: ./hello [0]     14368:	hello, this is test!

从运行结果,可以清楚的看到,进程启动过程中系统都做了哪些事情:

  • (1)搜索其所依赖的动态库
  • (2)加载动态库
  • (3)初始化动态库
  • (4)初始化进程
  • (5)将程序控制权交给main 函数

LD_DEBUG 不仅仅局限于这个, 还可以有如下使用方法,用于不同的调试需求:

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 exitTo direct the debugging output into a file instead of standard outputa filename can be specified using the LD_DEBUG_OUTPUT environment variable.

如果要查看一个进程启动过程中动态库的搜索和加载过程,LD_DEBUG 是更加直观的的。

但要查看一个进程启运过程中加载动态库所花费的时间,LD_DEBUG 并没有类似的功能,只能通过 strace -tt 来完成。

3.2 减少动态库的加载数量

说到优化,我们第一个想法是:少做事。

减少进程启动过程中的动态库数量就成了当务之急,有如下几个方法:

  • (1)将一些无用的动态库去掉。

  • (2)重新组织动态库的结构,力争将进程加载动态库的数量减到最少。 对于标准C 编写的动态库,可以考虑将几个小动态库合并为一个大的动态库,减少进程加载动态库的数量。

  • (3)将一些动态库编写成静态库,与进程或其他动态库合并,从而减少加载动态库的数量。

  • 其优点是:

    • a. 减少了动态库加载的数量。
    • b. 在与其他动态库(或进程)合并之后,动态库内部之间的函数调用不必再进行符号查找、动态链接,从而提高速度。
  • 其缺点是:

    • 该动态库如果被多个动态库或进程所依赖的话,那么该动态库将被复制多份合并到新的动态库中,导致整体的文件大小增加,占用更多的flash 内存。
    • 失动了动态库原有的代码段内存共享,困此可能导致代码段内存使用上的增加。
    • 如果该动态库被多个守护进程所使用,那么其代码段很多代码已经被 加载到特理内存,那么进程在运行该动态库的代码产生的page fault 就少; 但如果该动态库被编译成静态库与其他动态库合并,那么其代码段被其他多个守护进程运行到的机会就少,在进程启动过程中运行到新的动态库时所产生的 page fault 就多,从而可能影响进程的加载速度。

基于此,在考虑将动态库改为静态库时,有以下原则:

  • 对于那此只被很少进程加载的动态库,要将其编译成为静态库,从而减少进程启动时加载动态库的数量;同时该运态库代码段很少被多个进程共享,所以不会增加内存方面的开销。
  • 对于那些守护使用的动态库,其代码段大多已经被 加载到内存,运行时产生的page fault 要少,故其为动态库反而有可能要比静态库速度更快。
  • (4)使用dlopen 动态库载动态库
    进程所依赖的动态库,并不一定要在进程启动时都要用到。
    不需要的动态库,要在进程启动时加载动态库的清单中去掉,从而加快进程的启动速度。
    在需要调用到动态库时,再使用dlopen 来动态加载动态库。
    • dlopen 的优点是: 可以精确控制动态库的生存周期,一方面可以减少动态库数据段的内存使用,另一方面可以减少进程启动时加载动态库的时间。
    • dlopen 的缺点是: 程序员编写程序将变得很麻烦。

3.3 优化共享库的搜索路径 (屏蔽HWCAP机制)

在进程加载动态库时,loader 首先要去很多路径搜索动态库,其搜索顺序是:

  • (1)DT_NEED 入口中包含的路径
  • (2)DT_RPATH 入口给出的路径(存在的话)
  • (3)环境变量LD_LIBRATY_PATH 路径(setuid 类的程序排除)
  • (4)LD_RUNPATH 入口中给出的路径(存在的话)
  • (5)动态库高速缓存文件 ld.so.conf 中给出的路径
  • (6)/lib//usr/lib 目录

其中DT_RPATHLD_RUNPATH 是在程序编译时加的选项,这些信息会记录在进程中,在loader 分析过程文件时,获取这些信息。

可以使用 -rpath 来设置 DT_RPATH ,如:

gcc -o test test.c -L. -Ifoo -Ibar -WI,-rpath=/home/app

实际上,上面的搜索路径还不完全,有一种比DT_RPATH 更高优先级的目录搜索机制 HWCAP 。

HWCAP 机制意思如下:

在不同的系统中,系统的硬件功能是不一致的,有的系统支持浮点运算,有的则不支持。这样由于系统硬件功能的不同,软件需要加载不同的共享库。loader 的HWCAP 就是为了这个 功能设计的,根据不同的硬件特性,到不同的目录去搜完结动态库,来实现前面提到的需求。

HWCAP 这个功能虽然带来了灵活,但也导致进程在加载动态库时,搜索了很多无效的路径,浪费了宝贵的时间,

可以通过环境变量 LD_HWCAP_MASK 来屏蔽一些硬件抱定项,从而减少搜索路径。

配置方法如下:

# export LD_HWCAP_MASK=0x00000000
# LD_DEBUG=libs ./hello

总而言之,优化共享库的搜索路径 主要的思路就是在进程加载共享库时,能最快地找到动态库,减少搜索时间。

可以使用的方法有:

  • (1)设置 LD_HWCAP_MASK ,禁掉一些不用的硬件特性
  • (2)将所有的动态库放在一个目录下,并且在 LD_LIBRARY_PATH 的第一个搜索路径指向它,从而减少搜索的次数,降彽搜索动态库的时间。
  • (3)不能放在一个目录的动态库,应在进程中加入“-rpath” 选项,指这下搜索路径。

3.4 动态库的高度

根据动态库的依赖关系,可以画出动态库之间的依赖关系,这个依赖关系可能是一棵树,也可能是一个线性结构。

当前动态库到最底层动态库之间最长的路径,称为动态库的高度。

有一种说法,改变动态库的依赖关稍顷,降低动态库的高度,使动态库之间的依赖关系扁平的话,可以减少加载动态库的时间。

但实际上,这种说法的解释并不完全正确,并不是减少动态库的主要原因。

因为在降低动态库的高度时,经常采用的手段是合并动态库,使用dlopen 来动态加载库等方法,这样做的效果会减少进程加载动态库的数量,因此真正缩短加载库时间的是减少动态库的数量,而不是动态库之间的依赖关系

3.5 优化动态库的初始化时间

在loader 完成了对动态库的内存映射之后,需要运行动态库的一些初始化函数,来完在设置动态库的一些基本环境。

这些初始化函数主要包含两个部分:

  • (1)动态库的构造和析构函数机制
  • (2)动态库的全局变量初始化工作

3.5.1 动态库的构造和析构函数机制

在LInux 中,提供了一个机制:

在加载和卸载动态库时,可以编写一些函数,处理一些相应的事物,我们称这些函数为动态库的构造和析构函数。

其代码格式如下:

void __attribute__ ((constructor)) my_init(void);

void __attribute__ ((destructor)) my_fini(void);

在编译共享库时,不能使用“-nonstartfiles”“-nostdlib” 选项,否则,构建与析构函数将不能正常执行。

3.5.2 优化全局变量初始化

进程在加载 共享库liba.so 后,为了初始化共享库 liba 中的全局变量,将会运行 共享库liba 中的类构造函数,这些构造函数的运行,必然会导致加载时间缓慢。

如果构造函数修改了成员变量的话,其还会产生 dirty page,
有时间你会发现,程序刚进入main函数,就已经产生了大量的dirty page.

非内置类型的全局变量,需要在main 函数之前就构造出来的原因:

  • 在C语言中,全局变量保存在 .data 段,

    在启动过程中,loader 只是简单地使用mmap 交将数据段映射到内存上,这些全局变量只有在第一次使用到的时睺才会为其分配物理内存。其存启动过程中不需要动行什么构造函数。

  • 在 C++ 语言中

    对于非内置类型的全局变量,一方面需要 main 函数之前就准备好,需要的时候可以直接用。
    另一方面,全局对象内部的成员变量,不能像C语言那样简单的内存映射就可以了,故系统在bss 节为全局对象分配了内存,运行构造 函数,来初始化其值。
    这也就决定了,对于非内置类型的全局对象,系统要在 main 函数之前,运行其构造 函数,完成全局变量的实始化。

总的来讲,对于非内置类型 的全局变量,无论进程是否会用到该全局变量,都 需要运行其构造函数创建 该全局变量,其对进程启动有如下影响:

  • (1)由于运行了一些不必要的构造函数,减缓了进程的启动速度。
  • (2)构造函数修改了类的成员变量,这时一方面会产生page fault ,从而减缓进程的启动速度; 另一方面,也会以产生一些不必要的dirty page,造成内存上的浪费。
    因此,从优化的角度来讲,要尽量减少全局对象的使用。

对于 C++ 的可执行程序,可以通过 nm -f sysv xxxx | grep bss 来查看有哪些全局变量。

查找全局变量,原则如下:

  • (1)在bss 节,类型为 OBJECT ,不包含函数名,对象大小 > 4 ,基本可以认为是全局变量。
  • (2)在bss 节,类型为 OBJECT ,不包含函数名,对象大小 = 4 ,有可能是全局对象,需要到代码中去搜索确认。

3.6 优化动态链接时间

动态库的确给程序带来良好的扩充性,并减少了内存的使用量,但这是有代价的。

如果是静态编译的话,函数的地址在编译时就是已知的了,使用简单的一句地址转移,就可以进行函数调用。

如果是动态编译的话,在程序编译阶段,编译器就无法得知 printf 的函数地址,因为动态库加载的内存地址是随机的。

在程序运行调用函数时,程序会将处理权交给 linker ,由其负责在执行文件以及其链接的动态库中查找函数地址。
由于linker 不知道具体函数是在哪个动态库,所以它将整个执行文件和动态库的范围内查找。
更糟糕的是在C++ 程序中,符号的命名是 类名+函数名, 这导致了在做安符串比较时,往往直到字符串的结尾才能获得结果。

这就导致了在进程启动过程中,符号查找往往占据了大部分时间。

在Linux 的KDE 进程启动过程中,符号查找竟占据了进程启动80% 时间。

可以使用 LD_DEBUG=statistics ./hello 来查看程序启动过程中符号链接情况。

例:

ciellee@sh:~/Desktop/hello_test$ LD_DEBUG=statistics ./hello      12121:	     12121:	runtime linker statistics:     12121:	  total startup time in dynamic loader: 1212535 cycles     12121:		    time needed for relocation: 387063 cycles (31.9%)     12121:	                 number of relocations: 83     12121:	      number of relocations from cache: 3     12121:	        number of relative relocations: 1272     12121:		   time needed to load objects: 437809 cycles (36.1%)     12121:	     12121:	runtime linker statistics:     12121:	           final number of relocations: 88     12121:	final number of relocations from cache: 3hello, this is test!ciellee@sh:~/Desktop/hello_test$

可以看出,前面代码中,仅仅pintf 打印一句话,在启动过程中,就查找链接了83个符号,时间占用了 31.9%。

优化的方法:

  • (1)减少导出符号的数量

    在动态库编译和生成时,默认所有的函数和全局变量都 是导出的,
    而实际上有很多函数只在动态库内部使用。
    通过去掉那些动态库中不必导出的符号,从而减少动态库在做链接时所查找的符号的数量,可以加快动态链接的速度。
    可以使用GCC 的“--version-script” 选项,来实现定义导出符号的功能。
    首先生成一个定义导出符号的文件,格式如下:

    编译时加上sym.map , 如 gcc -o liba.so -shared -fPIC -Wl,--version-script=a.map a.c

// sym.map{
global: 导出函数名; local: *;}
  • (2)减少符号的长度
    因为在做符号链接时,linker 将做字符串的匹配,符号名字越长,其查找匹配的时间也将越长,因些缩短导出符号的长度,也会减少符号动态链接的时间。
    但这是以降低代码的可读性为代价,不推荐。
  • (3)使用perlink

    如果动态库在编译的时候就能确定运行时的加载地址,那么动态库函数调用的地址就应该是已知的,
    在进程运行的时候就没有必要再进行符号的查找和链接,从而节省进程的启动时间。

    实际上perlink 也正是这么做的。prelink 最早是在 Redhat 中使用的,用来加速KDE 的启动速度。

    那时perlink 作为系统的一个进程,不定期地启动,对系统中的进程和动态库进行优化,这对于进程和动态库不怎么变化的系统非常实用。

    而对于嵌入式linux 系统来讲,可以在编译、创建完所有的文件后,运行 prelink 来预先链接进程和动态库,再将修改后的进程和动态库做成img 烧写到设备上,优化效果非常明显。

    prelink 的使用方法如下:

    a. 配置 prelink.conf,在该文件中增加所要做预先链接的执行文件和动态库所在的路径。
    下面提 供一个prelink.conf 的范例。

// prelink.conf# System libraries-l /lib-l /usr/li-l /usr/local/lib# System binaries-l /bin-l /usr/bin-l /usr/local/bin-l /sbin-l /usr/sbin-l /usr/local/sbin# Qtopia-l /opt/Qtopia/lib-l /opt/Qtopia/bin-l /opt/Qtopia/plugins-l /opt/Qtopia/qt_plugins

运行prelink 命令,将 prelink.conf 里面路径下所有的ELF ,进行预先链接

prelink -afmR -c /etc/prelink.conf

每个选项的含义如下:

-a "ALL": 对所有执行文件和动态库进进行“预先链接”
-f : 强制 prelink 重新预先链接已经做过“预先链接”的执行文件和动态库,加上这个先项是因为 prelink 在看见 做过“预先链接”的执行文件和动态库的时候会中止执行,即使它们改动过。
-m:节省虚拟定址分配,如果你有很多动态库要“预先链接”,就会需要这个选项。
-R Random: 用随机数进行内存地址分配,这样可以增进安全性,提升缓冲区溢出攻击的抵抗能力。

3.6.1 prelink 的原理

在做prelink 时,prelink 为程序员做了以下几件事情:

  • (1)分析所有的执行文件和动态库,为每个动态库指定一块唯一的内存地址。
  • (2)分析执行文件和动态库中所有需要重定位的函数,全局变量等 ,使用 linker 进行符号查找,对其地址进行解析。
  • (3)修改执行文件和动态库的二进制文件。

3.6.1.1 ubuntu prelink 安装及实测(hello可执行程序实测)

  • (1) 安装 prelink 使用 sudo apt-get install prelink
ciellee@sh:~/Desktop/hello_test$ sudo apt-get install prelinkReading package lists... DoneBuilding dependency tree       Reading state information... DoneThe following NEW packages will be installed:  execstack prelink0 upgraded, 2 newly installed, 0 to remove and 4 not upgraded.Need to get 1,031 kB of archives.After this operation, 2,140 kB of additional disk space will be used.
  • (2)prelink hello可执行程序实测
    先来编写一个hello.c
//hello.c#include 
#include
int main(){
printf("hello, this is test!"); return 0;}

编译后,先来看下,运行它的时候,查找了多少次符号链接:

注意:

需要被 Prelink 的 ELF 文件(被别人加载的),无论是共享库还是可执行文件,编译时必须加 -fpic/-fPIC 参数,生成目标无关地址代码。
对于可执行文件,不能使用 -fpie/-fPIE 加 –pie 生成地址无关可执行文件,否则无法被 prelink。

ciellee@sh:~/Desktop/hello_test$ gcc -o hello hello.c ciellee@sh:~/Desktop/hello_test$ LD_DEBUG=statistics ./hello      30015:	     30015:	runtime linker statistics:     30015:	  total startup time in dynamic loader: 1122030 cycles     30015:		    time needed for relocation: 352786 cycles (31.4%)     30015:	                 number of relocations: 83			// 可以看出,查找了83次符号链接     30015:	      number of relocations from cache: 3     30015:	        number of relative relocations: 0     30015:		   time needed to load objects: 399827 cycles (35.6%)     30015:	     30015:	runtime linker statistics:     30015:	           final number of relocations: 88			// 总共查找了88次符号链接     30015:	final number of relocations from cache: 3hello, this is test!

开始做 prelink,命令为 sudo prelink -h hello -vm,然后使用同一个命令执行测试,

ciellee@sh:~/Desktop/hello_test$ sudo prelink -h hello -vmAssigned virtual address space slots for libraries:/lib64/ld-linux-x86-64.so.2                                  0000003301400000-0000003301429190/lib/x86_64-linux-gnu/libc.so.6                              0000003301600000-00000033017c0800/lib/x86_64-linux-gnu/libblkid.so.1.1.0                      0000003964000000-00000039642408e8/lib/x86_64-linux-gnu/libcom_err.so.2.1                      0000003963600000-00000039638031c8/lib/x86_64-linux-gnu/libdbus-1.so.3.14.6                    0000003963e00000-000000396404b2b0/lib/x86_64-linux-gnu/libgcc_s.so.1                          0000003963600000-0000003963815910/lib/x86_64-linux-gnu/libkeyutils.so.1.5                     0000003964200000-0000003964403010/lib/x86_64-linux-gnu/liblzma.so.5.0.0                       0000003962c00000-0000003962e21088/lib/x86_64-linux-gnu/libpcre.so.3.13.2                      0000003961e00000-000000396206f108/lib/x86_64-linux-gnu/libselinux.so.1                        0000003963200000-0000003963427ab0/lib/x86_64-linux-gnu/libusb-1.0.so.0.1.0                    0000003962600000-0000003962817460/lib/x86_64-linux-gnu/libuuid.so.1.3.0                       0000003963c00000-0000003963e04170/lib/x86_64-linux-gnu/libdl-2.28.so                          0000003963000000-0000003963004110/lib/x86_64-linux-gnu/libgcrypt.so.20.2.4                    0000003962200000-000000396231dfc8/lib/x86_64-linux-gnu/libgpg-error.so.0.26.1                 0000003962400000-0000003962422260/lib/x86_64-linux-gnu/libm-2.28.so                           0000003962a00000-0000003962b82148/lib/x86_64-linux-gnu/libmount.so.1.1.0                      0000003964400000-000000396445e528/lib/x86_64-linux-gnu/libpthread-2.28.so                     0000003961800000-00000039618204c0/lib/x86_64-linux-gnu/libresolv-2.28.so                      0000003963a00000-0000003963a19a80/lib/x86_64-linux-gnu/librt-2.28.so                          0000003961a00000-0000003961a09be0/lib/x86_64-linux-gnu/libsystemd.so.0.14.0                   0000003963c00000-0000003963c84080/lib/x86_64-linux-gnu/libudev.so.1.6.4                       0000003961c00000-0000003961c1fe60Prelinking /home/ciellee/Desktop/hello_test/hellociellee@sh:~/Desktop/hello_test$ LD_DEBUG=statistics ./hello      30110:	     30110:	runtime linker statistics:     30110:	  total startup time in dynamic loader: 988247 cycles     30110:		    time needed for relocation: 164735 cycles (16.6%)     30110:	                 number of relocations: 2			// 可以看出做了 2 次符号链接     30110:	      number of relocations from cache: 65     30110:	        number of relative relocations: 0     30110:		   time needed to load objects: 389798 cycles (39.4%)     30110:	     30110:	runtime linker statistics:     30110:	           final number of relocations: 4			// 总共做了 4 次符号链接     30110:	final number of relocations from cache: 65hello, this is test!ciellee@sh:~/Desktop/hello_test$

可以看出,从最开始的 88 次符号链接,优化到了 4 次符号链接,效果还是非常显著的。

3.6.1.2 ARM prelink 编译

下载命令如下:

git clone https://git.yoctoproject.org/git/prelink-cross
cd prelink-cross/
git checkout cross_prelink
autoreconf -if
sudo apt-get install autoconf
sudo apt-get install libtool
编译命令如下:
autoreconf -if
./configure --prefix=/home/ciellee/Desktop/hello_test/arm_prelink/prelink-cross/out --without-sysroot --target=arm-linux

详细参考:

《》
《》
《》

3.6.1.3 [常见问题] 指定动态库地址够用吗?

如果系统中动态库的数据非常多,为每个动态库指定一段唯一的内存地址,那地址够用吗?

在进程中,3GB 以上归操作系统内核使用,

在00000000~40000000 归进程的代码段、数据段 和堆段使用, 从3GB 往下归栈段使用。
基本可以认为从1GB~3GB 的地址空间可以用来指定动态库的加载地址,地址空间还是和很充裕的。

另外 prelink 关于这个问题,做了两个约定:

  • (1)总是一同出现的动态库,其动态库的加载地址一 定不能重叠。
  • (2)总是不同时出现的动态库,其动态库的加载地址可以重叠。

有了这两个约定之后,基本就能保证,为每个动态库指定加载地址,在动行前就能获知函数等的地址。

3.6.1.4 [常见问题] 如何指定加载地址?(hello.so 库实测)

还是使用前面的 hello.c ,我们编译一个 so 库出来验证下:

//hello.c#include 
#include
int main(){
printf("hello, this is test!"); return 0;}

使用命令 gcc hello.c -shared -fPIC -o hello.so 编译生成 hello.so 库,

读取一下内容 :
可以看出,PhysAddr LOAD=0x0000000000000000

ciellee@sh:~/Desktop/hello_test$ gcc hello.c  -shared -fPIC -o hello.sociellee@sh:~/Desktop/hello_test$ readelf -l hello.so Elf file type is DYN (Shared object file)Entry point 0x550There are 7 program headers, starting at offset 64Program Headers:  Type           Offset             VirtAddr           PhysAddr                 FileSiz            MemSiz              Flags  Align  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000                 0x000000000000070c 0x000000000000070c  R E    200000  LOAD           0x0000000000000e00 0x0000000000200e00 0x0000000000200e00                 0x0000000000000228 0x0000000000000230  RW     200000  DYNAMIC        0x0000000000000e18 0x0000000000200e18 0x0000000000200e18                 0x00000000000001c0 0x00000000000001c0  RW     8  NOTE           0x00000000000001c8 0x00000000000001c8 0x00000000000001c8                 0x0000000000000024 0x0000000000000024  R      4  GNU_EH_FRAME   0x000000000000068c 0x000000000000068c 0x000000000000068c                 0x000000000000001c 0x000000000000001c  R      4  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000                 0x0000000000000000 0x0000000000000000  RW     10  GNU_RELRO      0x0000000000000e00 0x0000000000200e00 0x0000000000200e00                 0x0000000000000200 0x0000000000000200  R      1ciellee@sh:~/Desktop/hello_test$

此时,经过prelink 处理,再次读取下内容 :

PhysAddr LOAD=0x0000003000000000

ciellee@sh:~/Desktop/hello_test$ sudo prelink -h hello.so -vmLaying out 1 libraries in virtual address space 0000003000000000-0000004000000000ciellee@sh:~/Desktop/hello_test$ readelf -l hello.so Elf file type is DYN (Shared object file)Entry point 0x3000000550There are 7 program headers, starting at offset 64Program Headers:  Type           Offset             VirtAddr           PhysAddr                 FileSiz            MemSiz              Flags  Align  LOAD           0x0000000000000000 0x0000003000000000 0x0000003000000000                 0x000000000000070c 0x000000000000070c  R E    200000  LOAD           0x0000000000000e00 0x0000003000200e00 0x0000003000200e00                 0x0000000000000228 0x0000000000000230  RW     200000  DYNAMIC        0x0000000000000e18 0x0000003000200e18 0x0000003000200e18                 0x00000000000001c0 0x00000000000001c0  RW     8  NOTE           0x00000000000001c8 0x00000030000001c8 0x00000030000001c8                 0x0000000000000024 0x0000000000000024  R      4  GNU_EH_FRAME   0x000000000000068c 0x000000300000068c 0x000000300000068c                 0x000000000000001c 0x000000000000001c  R      4  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000                 0x0000000000000000 0x0000000000000000  RW     10  GNU_RELRO      0x0000000000000e00 0x0000003000200e00 0x0000003000200e00                 0x0000000000000200 0x0000000000000200  R      1ciellee@sh:~/Desktop/hello_test$

3.7 提高进程启动速度

在做完上面所有的优化后,你也许会发现,进程的启动速度还是无法满足用户需要,

因此,很多进程不得不改为守护进程常驻内存,而守护进程的增多,会验系统带来很多的问题,
如内存的占用和CPU 的主调度的增加等 。

接下来,可以从软件的调度上做文章:

3.7.1 将进程改为线程

在LInux 中线程没有自已的内存空间,所有的线程共享进程的内存空间。

那么在启动线程的时候,就不必再去进行加载动态库等 费时的操作。

从这个 思路来看,可以把原来的进程分割为两个部分:

  • (1)常驻内存部分,其为守护进程,主要负责加载进程所需要的动态库,侦听用户信息,创建和销毁用户逻辑线程。
  • (2)完成用户逻辑部分,由常驻内存部分创建 ,按用户需求完成用户逻辑。

这样就省掉了加载动态库,初始化动态库 和全局变量部分,可以缩短进程的响应时间,来满足用户需求。

从本质上来讲,这种方法是明确区分出来常驻和非常驻部分。
还可以引申下,将原来多个守护进程的常驻部分进行合并,根据用户逻辑的要求,创建不同的线程。

将进程改为线程的过程中,需要注意几个问题:

  • (1)因为进程不会退出,所以动态库的数据段将会保留以前的变化,全局变量静态数据不会随着用户线程的销毁而重置。
  • (2)多个业务逻辑共享动态库,避免系统为不同业务逻辑的进程创建动态库的数据段,从而节省了大量的内存。

缺点:

  • (1)由原来的进程改为线程,工作量比较大,代码修改存在一定的风险。
  • (2)多个业务逻辑线程共享动态库,有可能会带来全局变量的冲突。
  • (3)由于还是存在常驻内存进程部分,所以其堆所占内存不会被释放,多个业务逻辑线程所存在的内存泄漏会纠缠在一起,从而使问题更加复杂。

3.7.2 prefork 进程

在Linux 创建一个进程主要分为两步:

  • (1)使用fork 创建子进程。
  • (2)使用exec 来执行新的执行程序,替换掉子进程的信息。

在创建新的进程过程中,fork 和exec 都是很花时间的操作。

我们注意到,不管系统创建什么样的子进程,它都要执行相同的第一步fork 出来个子进程,那么就可以采用prefork 的方法进行优化,
步骤如下:

  • (1)在系统空闲的时候,使用fork 创建一个子进程,预留在系统中。
  • (2)在需要执行进程时,先创建 一个定时器,以便在新进程执行完毕后,fork 一个子进程预留在系统中。
  • (3)直接在先前创建出来的空闲子进程上执行exec ,执行新的进程。
  • (4)在新进程启动完毕后,先前创建的定时器也将触发,这时再创建一个子进程,预留在系统中。

这样做的好处在于:

进程在启动过程中,就节省了fork 子进程的过程,缩短了进程启动时间。
这种方法,对于那些集中由某一个进程创建其他进程的系统来讲非常实用。

3.7.3 preload 进程

进程响应时间 = 进程加载动态库 + 用户逻辑部分。

那么可不可以将用户进程启动分为两个部分?

  • (1)在进程的main 函数中,插入一行语句: pause()

    这样当进程启动时,加载完动态库后,就会暂停,不会运行用户逻辑。

  • (2)当需要继续运行时,向该进程发送一个信号,这样进程就会继续前进,处理用户逻辑,这样就节省了进程加载动态库的过程。

    这里需要注册一个信号处理函数:

void sigCont(int unused){
return ;}int main(int argc, char ** argv){
signal(SIGCONT, sigCont); pause(); ......}
  • (3)当用户逻辑执行完成后,就退出进程,同时再启动该进程,这时进程会在加载完动态库后暂停。

优点:

  • (1)该方法对于目前的进程来讲,修改起来十分容易。
  • (2)由于没有了常驻部分,堆段在进程退出后会被回收,所以进程的内存泄漏影响不大。

缺点:

  • (1)由于在进程退出后,需要重新启动进程,会带来调度上的复杂性。
  • (2)在进程等待的时候,其加载的动态库还是要占用一部分内存。
  • (3)由于该方法只是将进程的启动部分分为了两个部分,并没有在整体上缩短进程的启动时间,故对于那些需要频繁启动、退出的进程效果并不理想。

3.7.4 提前加载,延后退出

在进程无法满足响应时间要求时,很多人想到了将它提前启动,却很少有人想到在什么时机将该进程退出,从而导致进程变成了一个常驻内存的进程,而这也是我们最不愿意看到的结果。

其实只要再往前走一步即可:

提前加载,延后退出,更加精确地控制进程的生命周期。

假设要完成如下任务:在进入菜单A ,选择菜单B 时,启动进程C,在退出进程C 后,返回菜单A。

为了加快进程的启动速度,可以考虑在进入菜单A 后,启动进程C,并把进程C 隐藏在后台; 在选择菜单项B 后,发送一个signal 给进程,
进程将界面显示在前台,这样就节省了进程启动时加载动态库和初始化的过程。在退出进程时,只是将进程隐藏在后台,返回菜单A ,在退出菜单A时,退出进程。

这样就达到了提高进程的启动速度,同时进程以不是守护进程的目的。

3.7.5 调整CPU的频率

在嵌入式设备中,为了节省宝贵的电力,降低功率,CPU 都采用了变频的方法,

一般来说,CPU 的主频越低,耗电量越低,同时代码执行的速度也将越慢。

从理论上讲,在启动某一个进程时,如果CPU 的主频调用最高,速度有可能提高到原来的2-4 倍,但这是以系统耗电量增加为代价。

在进程启动完成后,一定要把CPU 的主频降低下来,同时还要考虑进程启动有可能不成功的情 形。

3.7.6 总结进程启动度优化顺序

总成言之 ,优化进程的启动速度的顺序为:

  • (1)优化动态库的搜索路径。 -------------->优化动态库搜索时间
  • (2)检查进程中是否有无用的动态库。 -------------->优化无用动态库耗费的时间
  • (3)减少进程所依赖动态库的全局对象的数量。 -------------->优化动态库初始化时间
  • (4)使用prelink,预先链接进程和动态库。 -------------->优化进程和动态库函数匹配解析地址时间
  • (5)考虑重新组织动态库,争取减少进程加载动态库的数量。 -------------->减少动态库的加载时间
  • (6)考虑使用dlopen ,将一起启动时不需要的动态库从进程的依赖动态库中去除。 --------------> 减少动态库的加载时间

如果上述方法仍不能满足要求,可以考虑采用调试的方法:

  • (1)进程改为线程。 -------------->加快进程启动时间
  • (2)preload 进程。 -------------->预先加载,加快进程启动时间
  • (3)提前加载,延迟退出。 -------------->提前启动进程,加快用户体验。
  • (4)调整CPU 频率,以增加系统耗电量为代价,是最后的手段。

转载地址:https://ciellee.blog.csdn.net/article/details/106683356 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:20200615 身心健康,修身安神,先坚持一周
下一篇:高通 wlan 调试总结随笔

发表评论

最新留言

表示我来过!
[***.240.166.169]2024年04月30日 20时34分23秒