【Android Linux內存及性能優化】(九) 進程啓動速度優化

本文接着
【Android Linux內存及性能優化】(一) 進程內存的優化 - 堆段
【Android Linux內存及性能優化】(二) 進程內存的優化 - 棧段 - 環境變量 - ELF
【Android Linux內存及性能優化】(三) 進程內存的優化 - ELF執行文件的 數據段-代碼段
【Android Linux內存及性能優化】(四) 進程內存的優化 - 動態庫- 靜態庫
【Android Linux內存及性能優化】(五) 進程內存的優化 - 線程
【Android Linux內存及性能優化】(六) 系統內存的優化
【Android Linux內存及性能優化】(七) 程序內存泄漏檢查工具
【Android Linux內存及性能優化】(八) 系統性能分析工具

三、進程啓動速度

  • 在實際開發過程中,經常會遇到這樣的情況:
    由於對用戶事件響應速度要求比較高,而當前的程序無法達到,程序員不得不把它們改成守護進程,在一開機便將其啓動,
    守候在系統中,來提高用戶的響應速度,這樣便會導致系統中守護進程的數量越來越多。
    這些進程不光會佔用大量的內存,而且還容易造成內存泄漏,
    同時系統裏存活的進程過多,也會導致系統的整體性能下降。

解決這個問題的關鍵在於提高進程的啓動速度,減少守護進程的數量。

進程的啓動主要包括兩個部分:

  • (1)進程啓動,加載動態庫,直到main 函數之前。 (涉及前面的動態庫優化)
  • (2)main 函數之後,直到對用啓的操作有響應。(涉及自身編寫的代碼的優化)

3.1 查看進程的啓動過程

要想優化進程的啓動速度,先來看下進程在啓動時都做了什麼事情。
可以使用兩個工具 strace 和 LD_DEBUG 來查看進程的啓動過程。

先來寫個測試程序:

// 編寫 hello.c 文件
#include <stdlib.h>
#include <stdio.h>

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 */]) = 0
11:02:14.863058 brk(NULL)               = 0x173d000
11: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) = 3
11:02:14.863867 fstat(3, {st_mode=S_IFREG|0644, st_size=125755, ...}) = 0
11:02:14.864035 mmap(NULL, 125755, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f018ca5b000
11:02:14.864226 close(3)                = 0
11:02:14.864373 openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
11: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) = 832
11:02:14.864641 fstat(3, {st_mode=S_IFREG|0755, st_size=1824496, ...}) = 0
11:02:14.864762 mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f018ca59000
11:02:14.864899 mmap(NULL, 1837056, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f018c898000
11:02:14.865016 mprotect(0x7f018c8ba000, 1658880, PROT_NONE) = 0
11:02:14.865141 mmap(0x7f018c8ba000, 1343488, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x22000) = 0x7f018c8ba000
11:02:14.865273 mmap(0x7f018ca02000, 311296, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x16a000) = 0x7f018ca02000
11:02:14.865391 mmap(0x7f018ca4f000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1b6000) = 0x7f018ca4f000
11:02:14.865525 mmap(0x7f018ca55000, 14336, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f018ca55000
11:02:14.865663 close(3)                = 0
11:02:14.865827 arch_prctl(ARCH_SET_FS, 0x7f018ca5a500) = 0
11:02:14.866092 mprotect(0x7f018ca4f000, 16384, PROT_READ) = 0
11:02:14.866214 mprotect(0x600000, 4096, PROT_READ) = 0
11:02:14.866341 mprotect(0x7f018caa1000, 4096, PROT_READ) = 0
11:02:14.866459 munmap(0x7f018ca5b000, 125755) = 0
11:02:14.866742 fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 2), ...}) = 0
11:02:14.866972 brk(NULL)               = 0x173d000
11:02:14.867096 brk(0x175e000)          = 0x175e000
11:02:14.867228 write(1, "hello, this is test!", 20hello, this is test!) = 20
11: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 -h
usage: 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 descriptors

Statistics:
  -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 path

Tracing:
  -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 setgid

Miscellaneous:
  -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 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.


如果要查看一個進程啓動過程中動態庫的搜索和加載過程,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 共享庫的搜索路徑

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章