为什么要动态链接,动态链接库和静态连接的区别与优势

本文摘抄于程序员的自我修养-链接装载与库7.1节,这段写的很好,直接拿过来来收藏
http://www.wq3028.top/technology/compile/20180727124/

静态链接使得不同的程序开发者和部门能够相对独立地开发和测试自己的程序模块,从某种意义上来讲大大促进了程序开发的效率,原先限制程序的规模也随之扩大。但是慢慢地静态链接的诸多缺点也逐步暴露出来,比如浪费内存和磁盘空间、模块更新困难等问题,使得人们不得不寻找一种更好的方式来组织程序的模块。

内存和磁盘空间
静态链接这种方法的确很简单,原理上很容易理解,实践上很难实现,在操作系统和硬件不发达的早期,绝大部分系统采用这种方案。随着计算机软件的发展,这种方法的缺点很快就暴露出来了,那就是静态连接的方式对于计算机内存和磁盘的空间浪费非常严重。特别是多进程操作系统情况下,静态链接极大地浪费了内存空间,想象一下每个程序内部除了都保留着printf()函数、scanf()函数、strlen()等这样的公用库函数,还有数量相当可观的其他库函数及它们所需要的辅助数据结构。在现在的Linux系统中,一个普通程序会使用到的C语言静态库至少在1 MB以上,那么,如果我们的机器中运行着100个这样的程序,就要浪费近100 MB的内存;如果磁盘中有2 000个这样的程序,就要浪费近2 GB的磁盘空间,很多Linux的机器中,/usr/bin下就有数千个可执行文件。

比如图7-1所示的Program1和Program2分别包含Program1.o和Program2.o两个模块,并且它们还共用Lib.o这两模块。在静态连接的情况下,因为Program1和Program2都用到了Lib.o这个模块,所以它们同时在链接输出的可执行文件Program1和Program2有两个副本。当我们同时运行Program1和Program2时,Lib.o在磁盘中和内存中都有两份副本。当系统中存在大量的类似于Lib.o的被多个程序共享的目标文件时,其中很大一部分空间就被浪费了。在静态链接中,C语言静态库是很典型的浪费空间的例子,还有其他数以千计的库如果都需要静态链接,那么空间浪费无法想象。

这里写图片描述
图7-1 静态链接时文件在内存中的副本

程序开发和发布
空间浪费是静态链接的一个问题,另一个问题是静态链接对程序的更新、部署和发布也会带来很多麻烦。比如程序Program1所使用的Lib.o是由一个第三方厂商提供的,当该厂商更新了Lib.o的时候(比如修正了lib.o里面包含的一个Bug),那么Program1的厂商就需要拿到最新版的Lib.o,然后将其与Program1.o链接后,将新的Program1整个发布给用户。这样做的缺点很明显,即一旦程序中有任何模块更新,整个程序就要重新链接、发布给用户。比如一个程序有20个模块,每个模块1 MB,那么每次更新任何一个模块,用户就得重新获取这个20 MB的程序。如果程序都使用静态链接,那么通过网络来更新程序将会非常不便,因为一旦程序任何位置的一个小改动,都会导致整个程序重新下载。

动态链接
要解决空间浪费和更新困难这两个问题最简单的办法就是把程序的模块相互分割开来,形成独立的文件,而不再将它们静态地链接在一起。简单地讲,就是不对那些组成程序的目标文件进行链接,等到程序要运行时才进行链接。也就是说,把链接这个过程推迟到了运行时再进行,这就是动态链接(Dynamic Linking)的基本思想。

还是以Program1和Program2为例,假设我们保留Program1.o、Program2.o和Lib.o三个目标文件。当我们要运行Program1这个程序时,系统首先加载Program1.o,当系统发现Program1.o中用到了Lib.o,即Program1.o依赖于Lib.o,那么系统接着加载Lib.o,如果Program1.o或Lib.o还依赖于其他目标文件,系统会按照这种方法将它们全部加载至内存。所有需要的目标文件加载完毕之后,如果依赖关系满足,即所有依赖的目标文件都存在于磁盘,系统开始进行链接工作。这个链接工作的原理与静态链接非常相似,包括符号解析、地址重定位等,我们在前面已经很详细地介绍过了。完成这些步骤之后,系统开始把控制权交给Program1.o的程序入口处,程序开始运行。这时如果我们需要运行Program2,那么系统只需要加载Program2.o,而不需要重新加载Lib.o,因为内存中已经存在了一份Lib.o的副本(见图7-2),系统要做的只是将Program2.o和Lib.o链接起来。很明显,上面的这种做法解决了共享的目标文件多个副本浪费磁盘和内存空间的问题,可以看到,磁盘和内存中只存在一份Lib.o,而不是两份。另外在内存中共享一个目标文件

这里写图片描述

图7-2 动态链接时文件在内存中的副本

模块的好处不仅仅是节省内存,它还可以减少物理页面的换入换出,也可以增加CPU缓存的命中率,因为不同进程间的数据和指令访问都集中在了同一个共享模块上。上面的动态链接方案也可以使程序的升级变得更加容易,当我们要升级程序库或程序共享的某个模块时,理论上只要简单地将旧的目标文件覆盖掉,而无须将所有的程序再重新链接一遍。当程序下一次运行的时候,新版本的目标文件会被自动装载到内存并且链接起来,程序就完成了升级的目标。当一个程序产品的规模很大的时候,往往会分割成多个子系统及多个模块,每个模块都由独立的小组开发,甚至会使用不同的编程语言。动态链接的方式使得开发过程中各个模块更加独立,耦合度更小,便于不同的开发者和开发组织之间独立进行开发和测试。

程序可扩展性和兼容性
动态链接还有一个特点就是程序在运行时可以动态地选择加载各种程序模块,这个优点就是后来被人们用来制作程序的插件(Plug-in)。比如某个公司开发完成了某个产品,它按照一定的规则制定好程序的接口,其他公司或开发者可以按照这种接口来编写符合要求的动态链接文件。该产品程序可以动态地载入各种由第三方开发的模块,在程序运行时动态地链接,实现程序功能的扩展。动态链接还可以加强程序的兼容性。一个程序在不同的平台运行时可以动态地链接到由操作系统提供的动态链接库,这些动态链接库相当于在程序和操作系统之间增加了一个中间层,从而消除了程序对不同平台之间依赖的差异性。比如操作系统A和操作系统B对于printf()的实现制不同,如果我们的程序是静态链接的,那么程序需要分别链接成能够在A运行和在B运行的两个版本并且分开发布;但是如果是动态链接,只要操作系统A和操作系统B都能提供一个动态链接库包含printf(),并且这个printf()使用相同的接口,那么程序只需要有一个版本,就可以在两个操作系统上运行,动态地选择相应的printf()的实现版本。当然这只是理论上的可能性,实际上还存在不少问题,我们会在后面继续探讨关于动态链接模块之间兼容性的问题。

从上面的描述来看,动态链接是不是一种“万能膏药”,包治百病呢?很遗憾,动态链接也有诸多的问题及令人烦恼和费解的地方。很常见的一个问题是,当程序所依赖的某个模块更新后,由于新的模块与旧的模块之间接口不兼容,导致了原有的程序无法运行。这个问题在早期的Windows版本中尤为严重,因为它们缺少一种有效的共享库版本管理机制,使得用户经常出现新程序安装完之后,其他某个程序无法正常工作的现象,这个问题也经常被称为“DLLHell”。

动态链接的基本实现
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有的程序模块都链接成一个个单独的可执行文件。那么我们能不能按照前面例子中所描述的那样,直接使用目标文件进行动态链接呢?这个问题的答案是:理论上是可行的,但实际上动态链接的实现方案与直接使用目标文件稍有差别。我们将在后面分析目标文件和动态链接文件的区别。动态链接涉及运行时的链接及多个文件的装载,必需要有操作系统的支持,因为动态链接的情况下,进程的虚拟地址空间的分布会比静态链接情况下更为复杂,还有一些存储管理、内存共享、进程线程等机制在动态链接下也会有一些微妙的变化。目前主流的操作系统几乎都支持动态链接这种方式,在Linux系统中,ELF动态链接文件被称为动态共享对象(DSO,Dynamic Shared Objects),简称共享对象,它们一般都是以“.so”为扩展名的一些文件;而在Windows系统中,动态链接文件被称为动态链接库(Dynamical Linking Library),它们通常就是我们平时很常见的以“.dll”为扩展名的文件。

从本质上讲,普通可执行程序和动态链接库中都包含指令和数据,这一点没有区别。在使用动态链接库的情况下,程序本身被分为了程序主要模块(Program1)和动态链接库(Lib.so),但实际上它们都可以看作是整个程序的一个模块,所以当我们提到程序模块时可以指程序主模块也可以指动态链接库。在Linux中,常用的C语言库的运行库glibc,它的动态链接形式的版本保存在“/lib”目录下,文件名叫做“libc.so”。整个系统只保留一份C语言库的动态链接文件“libc.so”,而所有的C语言编写的、动态链接的程序都可以在运行时使用它。当程序被装载的时候,系统的动态链接器会将程序所需要的所有动态链接库(最基本的就是libc.so)装载到进程的地址空间,并且将程序中所有未决议的符号绑定到相应的动态链接库中,并进行重定位工作。程序与libc.so之间真正的链接工作是由动态链接器完成的,而不是由我们前面看到过的静态链接器ld完成的。也就是说,动态链接是把链接这个过程从本来的程序装载前被推迟到了装载的时候。可能有人会问,这样的做法的确很灵活,但是程序每次被装载时都要进行重新进行链接,是不是很慢?的确,动态链接会导致程序在性能的一些损失,但是对动态链接的链接过程可以进行优化,比如我们后面要介绍的延迟绑定(Lazy Binding)等方法,可以使得动态链接的性能损失尽可能地减小。据估算,动态链接与静态链接相比,性能损失大约在5%以下。当然经过实践的证明,这点性能损失用来换取程序在空间上的节省和程序构建和升级时的灵活性,是相当值得的。

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