admin 管理员组文章数量: 1184232
转载来源:
进程启动速度
在实际开发过程中,经常会遇到这样的情况:由于对用户事件响应速度要求比较高,而当前的程序无法达到,程序员便不得不把它们改成守护进程,在一开机便将其启动,守候在系统中,来提高用户的响应速度,这样便导致了系统中守护进程的数量越来越多。这些守护进程不光会占用大量的内存,而且还容易造成内存泄漏,同时系统里存活的进程过多,也会导致系统的整体性能下降。
解决这个问题的关键是提高进程的启动速度,减少守护进程的数量。
进程的启动主要包括两个部分:
(1)进程启动,加载动态库,直到main函数之前。这时还没有运行到程序员编写的代码,其性能优化有其特殊的方法,这部分将是本章讲述的重点。
(2)main函数之后,直到对用户的操作有所响应。这时主要涉及自身编写代码的优化,笔者将在第7章和第8章详细叙述。
6.1 查看进程的启动过程
要想优化进程的启动速度,先要看看进程在启动时都做了什么事情。可以使用两个工具-strace和LD_DEBUG来查看进程的启动过程。例如:
- #include <stdlib.h>
- #include <stdio.h>
- int main() {
- printf("hello world\n");
- return 0;
- }
编译程序:
strace工具本身主要用来查看进程在运行过程中的系统调用。
- >gcc -o hello -O2 hello.c
- # strace -tt ./hello
- 23:31:02.618348 execve("./hello", ["./hello"],
- [/* 9 vars */]) = 0
- 23:31:02.624391 uname({sys="Linux", node="(none)", ...}) =
- 0
- 23:31:02.628785 brk(0) = 0x11000
- 23:31:02.631655 access("/etc/ld.so.preload", R_OK) =
- -1 ENOENT (No such file or directory)
- 23:31:02.634035 open("/etc/ld.so.cache", O_RDONLY) =
- -1 ENOENT (No such file or directory)
- 23:31:02.636079 open("/lib/tls/v6l/libc.so.6",
- O_RDONLY) = -1 ENOENT (No such file or directory)
- 23:31:02.638063 stat64("/lib/tls/v6l", 0xbefff1e8)
- = -1 ENOENT (No such file or directory)
- 23:31:02.639985 open("/lib/tls/libc.so.6", O_RDONLY)
- = -1 ENOENT (No such file or directory)
- 23:31:02.642000 stat64("/lib/tls", 0xbefff1e8)
- = -1 ENOENT (No such file or directory)
- 23:31:02.643892 open("/lib/v6l/libc.so.6",
- O_RDONLY) = -1 ENOENT (No such file or directory)
- 23:31:02.645784 stat64("/lib/v6l", 0xbefff1e8)
- = -1 ENOENT (No such file or directory)
- 23:31:02.647646 open("/lib/libc.so.6", O_RDONLY) = 3
- 23:31:02.649538 read(3, "\177ELF\1\1\1\0\0\0\0\0
- \0\0\0\0\3\0(\0\1\0\0\0
- \264\271"..., 512) = 512
- 23:31:02.651887 fstat64(3, {st_mode=S_IFREG
- |0644, st_size=17592186044416, ...}) = 0
- 23:31:02.654481 mmap2(0x411b8000, 1071140,
- PROT_READ|PROT_EXEC, MAP_PRIVATE| MAP_DENYWRITE,
- 3, 0) = 0x411b8000
- 23:31:02.656343 mprotect(0x412b0000, 55332,
- PROT_NONE) = 0
- 23:31:02.658082 mmap2(0x412b8000, 16384,
- PROT_READ|PROT_WRITE, MAP_PRIVATE| MAP_FIXED|
- MAP_DENYWRITE, 3, 0xf8) = 0x412b8000
- 23:31:02.662325 mmap2(0x412bc000, 6180,
- PROT_READ|PROT_WRITE, MAP_PRIVATE| MAP_FIXED|
- MAP_ANONYMOUS, -1, 0) = 0x412bc000
- 23:31:02.664339 close(3) = 0
- 23:31:02.667817 mprotect(0x412b8000, 4096,
- PROT_READ) = 0
- 23:31:02.670321 fstat64(1, {st_mode=S_IFCHR|
- 0600, st_rdev=makedev(136, 1), ...}) = 0
- 23:31:02.672884 mmap2(NULL, 4096, PROT_READ|
- PROT_WRITE, MAP_PRIVATE|MAP_ ANONYMOUS, -1, 0)
- = 0x40000000
通过显示系统调用,就能够得知进程在加载动态库时的大概过程。通过使用"-tt"选项,能够将进程运行过程中系统调用的时间戳打印出来,就可以知道进程在加载动态库过程中所用的时间,注意这个时间要比进程实际所用的时间要大。
如果想更加详细地了解strace工具,可以通过只输入strace来显示其帮助信息。
- # strace
- usage: strace [-dffhiqrtttTvVxxn] [-a column] [-e expr]
- ... [-o file]
- [-p pid] ... [-s strsize] [-u username]
- [-E var=val] ...
- [command [arg ...]]
- or: strace -c [-e expr] ... [-O overhead]
- [-S sortby] [-E var=val] ...
- [command [arg ...]]
- -c -- count time, calls, and errors for each
- syscall and report summary
- -f -- follow forks, -ff -- with output into
- separate files
- -F -- attempt to follow vforks, -h --
- print help message
- -i -- print instruction pointer at time of syscall
- -q -- suppress messages about attaching, detaching,
- etc.
- -r -- print relative timestamp, -t --
- absolute timestamp, -tt -- with usecs
- -T -- print time spent in each syscall,
- -V -- print version
- -v -- verbose mode: print unabbreviated argv,
- stat, termio[s], etc. args
- -x -- print non-ascii strings in hex,
- -xx -- print all strings in hex
- -a column -- alignment COLUMN for printing
- syscall results (default 40)
- -e expr -- a qualifying expression: option=
- [!]all or option=[!]val1[,val2]...
- options: trace, abbrev, verbose, raw, signal,
- read, or write
- -o file -- send trace output to FILE instead of stderr
- -O overhead -- set overhead for tracing
- syscalls to OVERHEAD usecs
- -p pid -- trace process with process id PID,
- may be repeated
- -s strsize -- limit length of print strings
- to STRSIZE chars (default 32)
- -S sortby -- sort syscall counts by: time, calls,
- name, nothing (default time)
- -u username -- run command as username
- handling setuid and/or setgid
- -E var=val -- put var=val in the environment for command
- -E var -- remove var from the environment for command
- -n -- print print the syscall number of each call.
下面,笔者再介绍一个更加专业的工具LD_DEBUG,它只是glibc中的loader为了方便自身的调试,而设置的一个环境变量。通过设置这个环境变量,可以打印出在进程加载过程中loader都做了哪些事情。
- # LD_DEBUG=libs ./hello
- 432: find library=libc.so.6; searching
- 432: search cache=/etc/ld.so.cache
- 432: search path=/lib/tls/v6l:/lib/tls:/lib/v6l:
- /lib:/usr/lib/tls/v6l:/usr/lib/tls:/usr/lib/v6l: /usr/lib
- (system search path)
- 432: trying file=/lib/tls/v6l/libc.so.6
- 432: trying file=/lib/tls/libc.so.6
- 432: trying file=/lib/v6l/libc.so.6
- 432: trying file=/lib/libc.so.6
- 432:
- 432:
- 432: calling init: /lib/libc.so.6
- 432:
- 432:
- 432: initialize program: ./hello
- 432:
- 432:
- 432: transferring control: ./hello
如果查看loader都搜索哪些路径、加载哪些动态库,使用这个工具将更加直观、方便。另外,还可以通过下面的命令,来熟悉LD_DEBUG的相关选项:
- # LD_DEBUG=help ./hello
- 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
- all all previous options combined
- statistics display relocation statistics
- unused determined unused DSOs
- help display this help message and exit
strace和LD_DEBUG的比较如下:
如果要查看一个进程启动过程中动态库的搜索和加载过程,那么无疑LD_DEBUG将更加直观。
如果要查看一个进程加载动态库所花费的时间,LD_DEBUG并没有提供类似的功能,只能通过strace -tt来完成。
从上面LD_DEBUG=lib ./hello的运行结果,可以清楚地看到,进程启动过程中系统都做了哪些事情:
(1)搜索其所依赖的动态库。
(2)加载动态库。
(3)初始化动态库。
(4)初始化进程。
(5)将程序的控制权交给main函数。
现在清楚了进程启动的过程,就可以想办法进行优化,缩短进程启动的时间。
在做进程的代码优化时,程序员往往忽视进程加载动态库所需要的时间,往往想当然地认为这段时间与进程的启动过程相比不值一提。这在早期Linux下进程功能相对简单,只是加载少量的动态库时是正确的,可是随着程序复杂性的越来越大,一个进程往往依赖几十个,甚至接近100个动态库时,耗时就不可忽视了。笔者做过测试:以一个应用为例,其在启动过程中需要加载72个动态库,而加载这72个动态库,所用时间大约为0.8898s,将近1s,已经很长了,开始影响用户的操作体验。
因此,优化加载动态库是提高进程启动速度很重要的一部.
减少加载动态库的数量
谈到优化,映射到我们头脑中的第一个想法就是:少做事!减少进程启动过程中的动态库数量,就成了当务之急。这里介绍几个方法:
(1)将一些无用的动态库去掉。有些程序员为了自己编程方便,把一些动态库不管是否真的使用全都链接上,这导致进程启动过程中加载了一些无用的动态库,浪费了时间。这些无用的动态库应坚决去掉。
(2)重新组织动态库的结构,力争将进程加载动态库的数量减到最少。对于使用标准C编写的动态库,可以考虑将几个小动态库合并为一个大的动态库,减少进程加载动态库的数量。
对于使用C++编写的动态库,由于涉及全局对象初始化的问题,笔者建议将大的动态库拆分为若干个小的动态库,进程根据自己的需要灵活加载所需要动态库。对于那些经常一同出现的动态库,可以考虑将其进行合并。
关于这点,可以参考2.1.5节,那里有更加详细的论述。
(3)将一些动态库编译成静态库,与进程或其他动态库合并,从而减少加载动态库的数量。其优点是:
减少了加载动态库的数量。
在与其他动态库(或进程)合并之后,动态库内部之间的函数调用不必再进行符号查找、动态链接,从而提高速度。
缺点是:
该动态库如果被多个动态库或进程所依赖的话,那么该动态库将被复制多份合并到新的动态库中,导致整体的文件大小增加,占用更多的Flash。
失去了动态库原有的代码段内存共享,因此可能会导致代码段内存使用上的增加。
如果该动态库被多个守护进程所使用,那么其代码段很多代码已经被加载到物理内存,那么进程在运行该动态库的代码时产生的page fault就少;如果该动态库被编译成静态库与其他动态库合并,那么其代码段被其他多个守护进程运行到的机会就少,在进程启动过程中运行到新的动态库时所产生的page fault就多,从而有可能影响进程的加载速度。
基于此,在考虑将动态库改为静态库时,有以下原则:
对于那些只被很少进程加载的动态库,要将其编译成静态库,从而减少进程启动时加载动态库的数量;同时由于该动态库代码段很少被多个进程共享,所以不会增加内存方面的开销。
对于那些守护使用的动态库,其代码段大多已经被加载到内存,运行时产生的page fault要少,故其为动态库反而有可能要比静态库速度更快。
(4)使用dlopen动态加载动态库。进程所依赖的动态库,并不一定在进程启动时都要用到。不需要的动态库,要在进程启动时加载动态库的清单中去掉,从而加快进程的启动速度。在需要该动态库时,再使用dlopen来动态加载动态库。
dlopen的优点是:可以精确控制动态库的生存周期,一方面可以减少动态库数据段的内存使用,另一方面可以减少进程启动时加载动态库的时间。
其缺点是:程序员编写程序将变得很麻烦。
动态库的高度
根据动态库的依赖关系,可以画出动态库之间的依赖关系,这个依赖关系可能是一棵树,也可能是一个线性结构。当前动态库到最底层动态库之间的最长路径,称为该动态库的高度。
有一种说法,改变动态库的依赖关系,降低动态库的高度,使动态库之间的依赖关系扁平的话,可以减少加载动态库的时间。有些人尝试做了这样的优化,也的确收到了效果,可为什么呢?
笔者为此做了一个试验:
测试1:创建一个可执行文件hello和82个动态库,这82个动态库之间不相互依赖。可以说从执行文件hello到最底层的libc.so的距离为2。
使用strace -tt 来获取加载库的时间,为0.645112s。
测试2:创建了一个可执行文件hello和82个动态库,这82个动态库相互依赖,lib1依赖于lib2,lib2依赖于lib3,….,lib81依赖lib82。可以说从hello到最底层的libc.so的距离为83。
使用strace -tt来获取加载库的时间,为0.650055s。
从结果可以看出,Test2的确比Test1的时间要少,但仅少0.004943s,几乎可以忽略不计。
那为什么有些人降低了动态库的高度,的确缩短了加载动态库的时间呢?
那是因为在降低动态库的高度时,经常采用的手段是合并动态库库、使用dlopen来动态加载库等方法,这样的做的效果会减少进程加载动态库的数量,因此真正缩短加载库时间的是减少动态库的数量,而不是动态库之间的依赖关系。
动态库的初始化
在loader完成了对动态库的内存映射之后,需要运行动态库的一些初始化函数,来完成设置动态库的一些基本环境。这些初始化函数主要包含两个部分:
(1)动态库的构造和析构函数机制。
(2)动态库的全局变量初始化工作。
1.动态库的构造和析构函数机制
在Linux中,提供了一个机制:在加载和卸载动态库时,可以编写一些函数,处理一些相应的事物,我们称这些函数为动态库的构造和析构函数,其代码格式如下:
- void __attribute__ ((constructor)) my_init(void);
- void __attribute__ ((destructor)) my_fini(void);
在编译共享库时,不能使用"-nonstartfiles"或"-nostdlib"选项,否则,构建与析构函数将不能正常执行(除非你采取一定措施)。
注意,构造函数的参数必须为空,返回值也必须为空。
举个例子,动态库文件a、c的代码如下:
- void __attribute__ ((constructor)) my_init(void) {
- printf("init library\n");
- }
编译成动态库:
主程序hello.c如下:
- >gcc -fPIC -shared a.c -o liba.so
- #include
- #include
- int main() {
- pause();
- return 0;
- }
编译:
- >gcc -L./ -la hello.c -o hello
- 运行hello:# ./hello
- init library
也就是说,在运行hello时,加载完liba.so后,自动运行liba.so的初始化函数。
全局变量初始化
在介绍全局变量初始化之前,先举个例子:
- b2.c
- #include <stdlib.h>
- #include <stdio.h>
- int reti() {
- printf("reti\n");
- return 10;
- }
- int g1=reti();
g1是个全局变量。
使用GCC对其进行编译:
使用G++对其进行编译:
- >gcc -o hello b2.c
- b2.c:23: error: initializer element is not constant
- >g++ -o hello b2.c
编译成功!
可见GCC和G++对于这种全局变量初始化的方法,支持力度是不一样的。
运行hello:
- # ./hello
- reti
这说明,进程在加载liba.so后,为了初始化全局变量g1,其会运行reti来初始化g1。
再来看一个C++的例子:
编译动态库:
- b2.c
- class Myclass {
- public:
- Myclass();
- int i;
- };
- Myclass::Myclass() {
- printf("construct Myclass\n");
- };
- Myclass g1;
- >gcc -fPIC -shared b2.c -o liba.so
在动态库liba.so中,声明了一个类型为Myclass的全局变量g1。
主程序
编译执行文件:
- hello.c
- #include <stdlib.h>
- #include <stdio.h>
- int main() {
- pause();
- return 0;
- }
- >gcc -L./ -la hello.c -o hello
- 运行hello:
- # ./hello
- construct Myclass
这说明,进程在加载liba.so后,为了初始化全局变量g1,将会运行Myclass的构造函数。这些构造函数的运行,必然会导致加载动态库时间缓慢。如果构造函数修改了成员变量的话,其还会产生dirty page,有时候你会发现程序刚进入main函数,就已经产生了大量的dirty page。
为什么非内置类型的全局变量,需要在main函数之前就构造出来呢?
在C语言中,其全局变量保存在.data段。在启动过程中,loader只是简单地使用mmap将数据段映射到内存中,这些全局变量只有在第一次使用到的时候才会为其分配物理内存,其在启动过程中是不需要运行什么构造函数的。
而在C++语言中,对于非内置类型的全局变量,一方面需要在main函数之前就准备好,需要的时候马上就可以使用;另一方面,全局对象内部的成员变量,不能像C语言那样一个简单的内存映射就可以了,故系统在bss节为全局对象分配了内存,运行构造函数,来初始化其值。这也就决定了,对于非内置类型的全局对象,系统要在main函数之前,运行其构造函数,完成全局变量的初始化。
总的来讲,对于非内置类型的全局变量,无论在进程启动时是否会用到该全局变量,都需要运行其构造函数创建该全局变量,其对进程启动有如下影响:
(1)由于运行了一些不必要的构造函数,减缓了进程的启动速度。
(2)构造函数修改了类的成员变量,这时一方面会产生page fault,从而减缓进程的启动速度;另一方面,也会产生一些不必要的dirty page,造成内存上的浪费。
在系统优化过程中,笔者曾经在一个进程的main函数中第一条指令前增加了一个pause语句,很惊奇地发现,在进程还没有做任何事情的时候,仅仅是加载动态库,进程和动态库的数据段就使用了800KB的物理内存,就是这个原因。
从优化的角度来讲,要尽量减少全局对象的使用。
对于一个给定的动态库(或进程),首要的问题是如何来查看其包含了哪些全局对象,以便程序员对其进行优化。可以通过查看动态库(或进程)的符号表,来查找全局对象。请看以下代码:
- #include <stdlib.h>
- #include <stdio.h>
- class Myclass {
- public:
- int n;
- int m;
- Myclass() {
- m=10;
- n=20;
- }
- };
- Myclass obj;
- Myclass *pobj;
- int func() {
- static Myclass sobj;
- return sobj.n++;
- }
- int main() {
- Myclass mobj;
- mobj.n++;
- return 0;
- }
在上面的例子中,全局对象obj才会导致在main函数之前,运行其构造函数。
pobj只是一个对象的指针,并不需要运行构造函数。
在函数func内部的sobj,在第一次进入func时,才会运行构造函数。
main函数中的mobj,将在程序运行到此时,才会运行构造函数。
通过符号表来定位全局对象:
- >nm -f sysv hello | grep bss
- _ZGVZ4funcvE4sobj |000106ac| b |
- OBJECT |00000004| |.bss
- _ZZ4funcvE4sobj |000106b0| b |
- OBJECT |00000008| |.bss
- __bss_end__ |000106b8| A |
- NOTYPE | | |*ABS*
- __bss_start |0001069c| A |
- NOTYPE | | |*ABS*
- __bss_start__ |0001069c| A |
- NOTYPE | | |*ABS*
- _bss_end__ |000106b8| A |
- NOTYPE | | |*ABS*
- completed.0 |0001069c| b |
- OBJECT |00000001| |.bss
- obj |000106a4| B |
- OBJECT |00000008| |.bss
- pobj |000106a0| B |
- OBJECT |00000004| |.bss
mobj不是全局变量,也不是静态变量,故其不在符号表中。
sobj在函数func中,故G++在编译过程中,会将其改名,在对象的名称前面加上函数名。并且可以看到有两个符号ZGVZ4funcvE4sobj、ZZ4funcvE4sobj,都包含sobj,其中第一个符号是用来标识sobj静态对象是否被创建;ZZ4funcvE4sobj则指向静态对象sobj。详情可以参见8.4.5节。
obj 是全局对象,并且其大小为8。
pobj是指向全局的对象指针,其大小为4。
故要想查找全局对象,可以遵从下面的原则:
在bss节,类型为OBJECT,不包含函数名,对象大小>4,基本可以认为是全局对象。
在bss节,类型为OBJECT,不包含函数名,对象大小=4,有可能是全局对象,需要你到代码中去搜索、确认。
版权声明:本文标题:优化进程启动速度与动态库管理 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.roclinux.cn/p/1774378676a3571054.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论