【Android Linux内存及性能优化】(四) 进程内存的优化 - 动态库- 静态库
发布日期:2021-06-29 14:52:03 浏览次数:2 分类:技术文章

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

【Android Linux内存及性能优化】四 进程内存的优化 - 动态库 - 静态库

本文接着

《》
《》
《》

一、内存篇

1.1 系统当前可用内存

1.2 进程的内存使用

1.3 进程内存优化

1.3.1 ELF执行文件

1.3.2 动态库

动态库技术是当前程序中经常采用的技术,其目的是减小程序的大小,节省空间,提高效率,具有很高的灵活性。

与静态库不同,动态库里面的函数不是执行程序本身的一部分,
而是根据执行需要按需载入,其执行代码可以同时在多个程序中共享。

动态库加载方式有两种:

  1. 静态加载

    在程序编译时,加上“ -l ”选项,指定其所依赖的动态库,这个库名字将记录在 ELF 文件的 .dynamic 节。
    在程序运行时,loader 会预先将程序所依赖的所有动态库都加载在进程空间中。
    优点: 动态库的接口调用简单,可以直接调用。
    缺点: 动态库的生存周期等于进程的生存周期,其加载时机不灵活。

  2. 动态加载

    在程序中调用函数(dlopen、dlclose)来控制动态库加载与卸载。
    优点:动态库加载的时机非常灵活,可以非常细致地定义动态库搞错生存周期。
    缺点:动态库的接口调用起来比较麻烦,同时还要关注动态库的生存周期。

前面介绍进程时,分别包括: 只读的代码段、可修改的数据段、堆段 和 栈段。

对于共享库来说,分为两个段:只读的代码段 、 可修改的数据段。
如果你在共享库的函数里,动态分配一块内存,这段内存将被算在调用该函数的进程的堆中。

对于共享库的代码段 和 数据段:

  • 代码段由于其是只读的,内容 不会改变,对每个进程都是一样的,所以它在系统中是唯一的,系统只为其分配一块内存,多个进程之间共享。
  • 数据段由于其内容在进程运行期间是变化的,每个进程都要对其进行改写,所以在链接到进程空间后,系统会为每个进程创建相应的数据段。也就是说如果一个共享库,被 N 个进程链接,当这N 个进程同时运行时,同时共享一个代码段,每个进程拥有一个数据段,系统中共有该动态链接库的1 个代码段 和 N 个数据段。

1.3.2.1 数据段

1.3.2.1.1 共享库中的.bss 节

前面讲过,在进程中的bss,如果数据段容不下,它将使用mmap 在堆段内存分配大内存。

那 共享库中是怎么分配的呢?

例:

a.c#include 
#include
int bss[1024*128] = {
0};void a1(){
printf("bss: %p\n",bss); return;}编译成动态库: gcc -shared -fPIC a.c -o liba.sohello.c#include
#include
extern int bss[1024*128];int main(){
a1(); // 调用a1() int pid = getpid(); printf("pid: %d\n",pid); funca(); pause();}编译成可执行文件: gcc -L./ -Ia hello.c -o hell

在这里插入图片描述

编译执行后,使用 cat maps 和 cat memmap 可以看看出,

对于共享库的 bss 节的数据,如果数据段不能容纳的话,进程将会创建一内存段来容纳bss 节的数据,
其中 bss 数组起始地址位于动态库的数据段。
loader 在加载动态库时会自动将数据段最后一个页面剩余的地址自动清零,留给 bss节的变量使用,故其数据段使用了一个物理页而。

如果在进程中引用了共享库的全局变量,进程将会扩展它的堆栈段,并将 bss 段的数据复制到堆栈段中来。

因此,不要在进程中通过extern 的方式,引用共享库中的变量,一旦引用,不论其是否使用,都会将占用物理内存。

同时还会增加系统启动时内存复制的代价,会导致性能下降

1.3.2.1.2 共享库中的.data 节

对于未赋值或初值为0 的全局变量在共享库中的声明,

在进程中使用,则该变量被复制到进程的数据段,同时修改使用该变量的共享库的指向。

当主程序链接了一个共享库的全局变量时,它会为该变量定义一个地址,但它不会影响数据段的大小,将该值复制到这个地址上。如果地址段不够用,它将占用堆段,系统将调用brk 来扩展堆段。

总之,可执行文件(动态库)尽量不要直接去操作位于其他动态库的全局变量,跨动态库的直接访问全局变量的代价很高,可以在动态库中编写接口函数来操作全局变量,并将这些接口导出,供其他进程(或动态库)使用。

1.3.2.2 代码段

动态库的代码段会被多个不同的进程所引用,所以动态库的代码段与执行文件的代码段有所不同。

1.3.2.2.1 符号解析

前面讲解过的ELF文件的主体结构,ELF 其中有两个section:.rel.dyn 和 .rel.plt 。

主要负责共享库的重定向工作,现在就来看看它们的内容 。
在这里插入图片描述
可以看到变量 b 在 .rel.dyn 中,而 hello中用到的外部函数 funca 、printf 在 .rel.plt 中。

在加载 liba.so 后,以及查找全局变量b 时,会将liba.so 的b 复制到自已的数据段,并且修改 liba.so 中的 b 的指向。

主要是因为进程不会为这些共享库的变量做重定向,它只是把该数据复制到自已的数据段,然后要求对应的共享库修改其对应的指向。

  • 如果全局变量声明在进程中,其共享库中使用,则该变量位于进程的数据段。
  • 如果全局变量声明在共享库中,在进程中使用,则该变量被复制到进程的数据段。
  • 如果该变量在共享中声明,在共享库中使用,则该变量位于声明它的共享库的数据段中。
1.3.2.2.2 导出函数对代码段的影响

在共享库中所定义的函数,缺省都是导出函数。

通过 readelf -a liba.so ,可以看出各节的大小。

  1. 每增加一个函数

    .hash = .hash + 4
    .dynsym = .dynsym + 16
    .dynstr = .dynstr + 函数名长度 + 1
    .gnu.version = .gnu.version + 2
    .text 节按函数代码长度有所增长

  2. 每增加一个全局变量

    .hash = .hash + 4
    .dynsym = .dynsym + 16
    .dynstr = .dynstr + 变量名长度 + 1
    .gnu.version = .gnu.version + 2
    .data 节按函数代码长度有所增长

从这里可以看出,每增加一个导出函数,除了自身代码变化外,需要增加 24 + 函数名长度个字节。

而实际上,在共享库代码中有很多函数和变量,只是在共享库内部使用,不需要导出,因此,只要可以精确定义出外部使用的函数,那么就可以节省 .dynsym 和 .dynstr 等section 的大小,从而节省内存。

  1. 第一种 方法是:使用 static 修饰。

    使用static 修饰过的函数和全局变量,都将具有内部链接的属性,只在其所在的编译单元有效,编译时,不会作为导出符号。

  2. 第二种 方法是:使用 GCC 的 --version-script 选项。

    需要定义一个文件,标明所要导出符号,示例如下:

    编译时,命令如下: gcc -o liba.so -shared -fPIC -WI,--version-script=a.map a.c

//   a.map{
global: functa; local: *;}
1.3.2.2.3 查看依赖关系

在查看依赖关系时,我们经常使用工具 ldd,它能打印出该动态库所依赖的所有动态库,包括直接依赖和间接依赖。

如果想直接看直接依赖的动态库,使用 readelf -d liba.so

示例如下,可以看到,该库依赖了哪些库。

ciellee@sh:~$ readelf  -d libnative-platform.so Dynamic section at offset 0x4b70 contains 25 entries:  Tag        Type                         Name/Value 0x0000000000000001 (NEEDED)             Shared library: [libstdc++.so.6] 0x0000000000000001 (NEEDED)             Shared library: [libm.so.6] 0x0000000000000001 (NEEDED)             Shared library: [libgcc_s.so.1] 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6] 0x000000000000000e (SONAME)             Library soname: [libnative-platform.so] 0x000000000000000c (INIT)               0x1fc0 0x000000000000000d (FINI)               0x3f88

1.3.2.3 动态库的优化

由于一个动态库的数据段所占内存会随着依赖它的进程的数量线性增长,而其代码段则是系统共享,所以应该将优化的重点放在动态库的数据段。

1.3.2.3.1 减少 .bss 节的数据

在做动态库数据段优化的时候,有这么一个概念:

在加载动态库的时候,.bss 节的数据将被单独放在一个数据段中,试想,1kb 的data 数据 和 1kb 的bss 数据, 要分别占用2 个 4kb 的物理页面,浪引费了 6kb。 如果将bss 的数据全部赋上初值,转化为 data 段,这样就可以转化为 1 个2kb 的数据段,占用一个物理页面,从而节省出一个物理页面。

这种优化思路:

关键在于加载动态库的时候 bss 数据单独分出一坐个内存段来。可实际上,在是最后一个页面,哪果还有剩余的话,会使用 bss 去补齐, 1KB 的 data 数据 和 1 KB的 bss 数据, 最的合在一起只占用1个页面,总共4 kb,并不省出一个物理页面。

loader 在补齐的时候,采用了将这这些bss 变量所 对应的肉存直接清0 的方式,这会造成动态库数据段最后一个页面的写操作,从而分配物理内存。因此,在大多数情况下,你会必以现数据段的最后一个页面被分配了物理页面。

因此,通过将 bss 变量赋初值,将 bss 段与数据段合并的方式,并不能优化内存。

1.3.2.3.2 无用的动态库

loader 在加载动态库时 ,需要做很多事情,

对于代码段来讲其要做一些重定位的工作,至占用一个物理页面;
对于数据段来讲,首先要做一些重定位的工作,在映射数据段时,还要在最后一个页面,将.data 节未填充完的剩余字节填充为0,其最少要占用一个物理页面。
也就是说,链接一个无用的共享库,至少损失了8kb 的物理内存。
对于每个无论是直接依赖还是间接依赖该动态库的进程,都将损失 8 KB 的物理内存。

  1. 使用readelf 读取动态库A 的dynamic 节,获取其所有的直接依赖的动态库。
  2. 读取动态库A 中所有weak 和 undefine 的符号。
  3. 读取动态库 A 所直接依赖动态库所有已经定义的符号。
  4. 将上面的结果取个交集,剔除一些系统的符号,如果交集不为空,就认为两个动态库有实际的依赖关系,否则就是链接了无用的动态库。
1.3.2.3.3 动态库的合并

试想一个场景,动态库A 的数据段为 1kb,动态库B的数据段为 1kb ,进程加载两个动态库,就需要2个页面 8kb 的内存。如果将这两个动态库合并到一起,那么合并动态库的数据段所占内存为 4kb ,节省出一个物理页面。

问题的关链是:

将很多小动态库合并成一个大动态库还是将一个大动态库拆分成 为若干个小动态库。

  1. 将很多小动态库合并成一个大动态库

    优点: 数据段都合并到一起,节约内存。
    缺点:进程可能会因此引入很多无用的内容,
    对于C 语言的动态库来讲,在加载动态库时,只是为数据段分配了一起虚内存,没有用到的全局变量并不会占用更多的内存。
    而对于 C++ 来讲,对于非内置类型的全局变量,在加载完后需要调用其构造函数,会产生 dirty page ,反而有可能增加内存使用。

  2. 将一个大动态库拆分成若干个小动态库

    优点:进程可以只加载那些它所需要的内容,对于C++ 的库来讲可能会节省内存。
    缺点:需要加载更多的动态库,而产生很的内存段,造成每个动态库数据段最后一个页面部分空间浪费。

因此,对于 C语言写的库,动态库的合并会通过合并数据段来节省内存;

对于 C++ 编写的动态库,有可能会由于无用的非内置类型的全局变量的增加而增加内存消耗。

如果几个动态库同时出现的话,那么动态库的合并从内存角度来讲,对进程有益无害。

关于动态库的合并,可以总结如下几点:

(1) 对于C语言编写的动态库,合并动态库没有什么害处。
(2) 对于C++ 编写的动态库,可以考虑将一些不常用的功能分拆,使用 dlopen 的方式加载,来节省内存。
(3) 防止由于动态库之间的合并,导致进程加载很多无关的内容。
(4) 对于经常一同出现的动态库,可以合并。

1.3.2.3.4 仅被依赖一次的动态库

下面再来考虑一种特殊情形:

一个动态库A 只被某一个动态库(或执行文件)B 所依赖,我们可以说这个动态库A 和 动态库B 是100% 同时出现的,
那可以考虑将动态库A 做成一个静态库,然后将其与动态库 B 进行合并,从内存的角度来讲这是有益无害的。

查找仅使用一次的动态库方法为:

(1)使用 readelf 读取动态库的 dynamic 节,打到其所有直接依赖的动态库 。
(2)为每一个动态库建立一个依赖于它的动态库和执行文件表。表长度为 1 的动态库,即是仅被依赖一次的动态库。

1.3.2.3.5 使用 dlopen 来控制动态库的生存周期

在启动进程时,进程会加载了很多的动态库 ,有些动态库是很少用到的,有些可能只用一次,而进程一启动就全部加载进来,每个动态库最少使用8KB(4KB代码段,4KB数据段)内存,实在是非常浪费。

更严重的是,动态库的数据段一旦使用使用后,便无法释放,将会导致动态库所占用的内存越来越多。

使用dlopen 加载一个共享库时:

(1)进程会加载该动态库的txt 段和 数据段,同时为这个代享库讲数 +1 .
(2)进程查找该共享库的dynamic 节,查看其所依赖的共享库。
(3) 首先检查所依赖库是否已经被加载,如果已被加载,则为这个 这个共享库计数 +1.
如果未被加载,则加载其 txt段 和 data段,然后为这个共享库计数+1.
(4)再查找这些库所依赖的库,重复上面的占骤。
最终进程会为每个加载的共享库维护一个依赖的计数。

使用dlclose 卸载共享库时:

(1)首先将该共享库的计数减1,如是该共享库依赖计数为0,则卸载该共享库。
(2)在dynamic 节中,查找其所依赖的共享库。
(3)为每个共享库的计数减1,如果该共享库依赖计数为0,则卸载该共享库。
(4)重复上面的步骤。

dlopen的优点在于:

(1)可以在程序启动的时候,减少加载库的数量,这样可以加快进程的启动速度和减少加载库的内存使用。
(2)为进程提供了卸载共享库的机会,达到回收共享库的代码段和数据段所占用内存的目的。

缺点在于:

对于程序员编码来讲,很不方便。

现在使用dlopen 来提高程序性能和节约内存的程序员越来越多。下面举例一些采用 dlopen 优化时,经常遇到的问题。

  1. 由于dlopen 的嵌套,导致一些动态库没有卸载

    比如:
    进程 p:
    共享库 liba.so : 依赖于 libb.so
    共享库 libc.so : 依赖于 libd.so
    进程P 的执行过程为:
    (1)进程P,通过 dlopen 调用liba.so ,进程将liba.so 和 libb.so 加载到进程中。
    (2)进程P,通过liba.so 的库函数,dlopen 加载libc.so ,进程将libc.so 和libd.so 加载到进程中。
    (3)这时,进程P ,调用dlclose 来卸载 liba.so,dlclose 会查找其所依赖的库,那么它只能查到libb.so, 所以它将卸载 libb.so 及 liba.so.
    这样虽然进程P关闭了 liba.so ,但是进程的所加载的共享库却增加了 libc.so 和 libd.so ,导致共享库并没有完全卸载。
    这个问题的根源在于,进程P 在卸载liba.so 之前,应该先调用liba.so 里面的函数卸载 libc.so。

    这个问题的难点在于,需要一个时机,在dlclose 一个动态库时,将这个动态库所有 dlopen 的动态库关闭。

    如果每次 dlclose 的进候,去检查这个库是否又打开了其他的库,那么就需要上层的函数了解该动态库详细的实现细节,这不符合软件的封装的概念。

    好在glibc 提供了一个这样的时机: 程序员可以将自已定义的函数声明为动态库的构造函和析构函数:

    void __attribute__ ((constructor))my_init(void);
    void __attribute__ ((destructor)) my_fini(void);

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

    可以在libs.so 的析构函数中,检测自已都dlopen 了哪些动态库,并将其全部dlclose。

  1. dlopen 有可能导致内存泄漏
    比如: 在动态库中要使用进程中唯一的对象 mInstance ,标准写法是:
// liba.sostatic Myclass * mInstance; // 声明一个静态Myclass 的对象指针Myclass getInstance(){
if(NULL == mInstance) mInstance = new Myclass; return mInstance;}

上面的代码,引入dlopen 后,有可能会导致内存泄漏

dlopen(liba.so);			// 进程加载动态库liba.so 时,同时初始化了其数据段,这时mInstance 应该为空pInstance = getInstance();	// getInstannce 时,进程在堆中分配了一块内存,生成一个Myclase  实例,同时为数据段的 mInstance 赋值 ......dlclose(liba.so)//卸载 liba.so 后,这时mInstance是不存在的,也就是丢失了在堆中生成的Myclass 的对象实例......dlopen(liba.so);	//进程进程加载动态库liba.so 时,同时初始化了其数据段,这时mInstance 还是为空pInstance = getInstance()// getInstannce 时,进程在堆中分配了一块内存,生成一个Myclase  实例,同时为数据段的 mInstance 赋值 ......dlclose(liba.so)......结查,如果重复循环的话,堆中会有越来越多的 Myclass 的对象实例,导致 Myclass 对象内存卸漏。

这个 问题的实质是:、

在程序员的心目中,一个static 对象的生存周期是贯穿进程始终的。
实际上并非如此,在动态库中的static 对象,其生命周期等于该动态库的生命周期。
如果采用静态链接的方式,动态库的生命周期等于进程的声明周期;
而采用动态加载的方式,则是不同的,其生命周期等于该动态库的生命周期。
为了避免上面的问题出现,需要在动态卸载的时候,释放该块内存。

有两个方法如下:

(1)写一个释放内存的函数,在调用dlclose 函数之前,调用该函数。
(2)在动态库的析构函数中,释放这块内存。 void __attribute__ ((destructor)) my_fini(void);

1.3.3 静态库

相信大家都比较清楚动态库和静态库的不同:

  • 静态库在程序编译时会被链接到目标代码中,程序运行时将不再需要加载该静态库。
  • 动态库在程序编译时并不会被链接到目标代码中,而是在程序运行时才被载入,因此程序运行时还需要动态库存在。

在编译程序、链接静态库时,GCC编译器并不会选择性地只复制在目标代码中用的全局变量和函数,

而是把静态库的所有全局变量和函数 全部复制到目标代码中。

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

上一篇:【Android Linux内存及性能优化】(五) 进程内存的优化 - 线程
下一篇:【Android Linux内存及性能优化】(三) 进程内存的优化 - ELF执行文件的 数据段-代码段

发表评论

最新留言

逛到本站,mark一下
[***.202.152.39]2024年04月11日 18时45分03秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章

ElasticSearch5.4.3 环境搭建 2017 (2-集群搭建) 2019-04-29
ElasticSearch5.4.3 环境搭建 2017 (3-可视化分析工具Kibana) 2019-04-29
ElasticSearch5.4.3 环境搭建 2017 (4-安全插件-监控工具x-pack) 2019-04-29
ElasticSearch5.4.3 环境搭建 2017 (5-Java Client Security Api x-pack) 2019-04-29
Cause: couldn‘t make a guess for 解决方法 2019-04-29
java.lang.NoSuchMethodError: No static method create()Lorg/webrtc/EglBase 2019-04-29
小米手机相册选取后的intent为空? 2019-04-29
Android SurfaceView预览相机黑屏问题解决方案 2019-04-29
Android HTTP 设置UA(User-Agent)及自定义 2019-04-29
如何快速融入团队并成为团队核心?(九) 2019-04-29
一文读懂jar包的小秘密 2019-04-29
没项目经验难就业?推荐你参加“大学生就业特训营” | 100个免费名额,先到先得!... 2019-04-29
年薪100万和10万程序员的差距 2019-04-29
王者荣耀背后的实时大数据平台用了什么黑科技? 2019-04-29
太厉害了!目前 Redis 可视化工具最全的横向评测 2019-04-29
为什么在做微服务设计的时候需要DDD? 2019-04-29
Redis——由分布式锁造成的重大事故 2019-04-29
某程序员毕业进UC,被阿里收购!跳去优酷土豆,又被阿里收购!再跳去饿了么,还被阿里收购!难道阿里想收购的是他?... 2019-04-29
漫谈何时从单体架构迁移到微服务? 2019-04-29
为什么阿里不允许用Executors创建线程池 2019-04-29