程序的链接和装入及Linux下动态链接的实现--发展史
- UID
- 1066743
|
程序的链接和装入及Linux下动态链接的实现--发展史
链接和装入技术的发展史一个程序要想装入内存运行必然要先经过编译、链接和装入这三个阶段,虽然是这样一个大家听起来耳熟能详的概念,在操作系统发展的过程中却已经经历了多次重大变革。简单来讲,可以将其划分为以下三个阶段:
1. 静态链接、静态装入
这种方法最早被采用,其特点是简单,不需要操作系统提供任何额外的支持。像C这样的编程语言从很早开始就已经支持分别编译了,程序的不同模块可以并行开发,然后独立编译为相应的目标文件。在得到了所有的目标文件后,静态链接、静态装入的做法是将所有目标文件链接成一个可执行映象,随后在创建进程时将该可执行映象一次全部装入内存。举个简单的例子,假设我们开发了两个程序Prog1和Prog2,Prog1由main1.c、utilities.c以及errhdl1.c三部分组成,分别对应程序的主框架、一些公用的辅助函数(其作用相当于库)以及错误处理部分,这三部分代码编译后分别得到各自对应的目标文件main1.o、utilities.o以及errhdl1.o。同样,Prog2由main2.c、utilities.c以及errhdl2.c三部分组成,三部分代码编译后分别得到各自对应的目标文件main2.o、utilities.o以及errhdl2.o。值得注意的是,这里Prog1和Prog2使用了相同的公用辅助函数utilities.o。当我们采用静态链接、静态装入的方法,同时运行这两个程序时内存和硬盘的使用情况如图1所示:
可以看到,首先就硬盘的使用来讲,虽然两个程序共享使用了utilities,但这并没有在硬盘保存的可执行程序映象上体现出来。相反,utilities.o被链接进了每一个用到它的程序的可执行映象。内存的使用也是如此,操作系统在创建进程时将程序的可执行映象一次全部装入内存,之后进程才能开始运行。如前所述,采用这种方法使得操作系统的实现变得非常简单,但其缺点也是显而易见的。首先,既然两个程序使用的是相同的utilities.o,那么我们只要在硬盘上保存utilities.o的一份拷贝应该就足够了;另外,假如程序在运行过程中没有出现任何错误,那么错误处理部分的代码就不应该被装入内存。因此静态链接、静态装入的方法不但浪费了硬盘空间,同时也浪费了内存空间。由于早期系统的内存资源十分宝贵,所以后者对早期的系统来讲更加致命。
2. 静态链接、动态装入
既然采用静态链接、静态装入的方法弊大于利,我们来看看人们是如何解决这一问题的。由于内存紧张的问题在早期的系统中显得更加突出,因此人们首先想到的是要解决内存使用效率不高这一问题,于是便提出了动态装入的思想。其想法是非常简单的,即一个函数只有当它被调用时,其所在的模块才会被装入内存。所有的模块都以一种可重定位的装入格式存放在磁盘上。首先,主程序被装入内存并开始运行。当一个模块需要调用另一个模块中的函数时,首先要检查含有被调用函数的模块是否已装入内存。如果该模块尚未被装入内存,那么将由负责重定位的链接装入器将该模块装入内存,同时更新此程序的地址表以反应这一变化。之后,控制便转移到了新装入的模块中被调用的函数那里。
动态装入的优点在于永远不会装入一个使用不到的模块。如果程序中存在着大量像出错处理函数这种用于处理小概率事件的代码,使用这种方法无疑是卓有成效的。在这种情况下,即使整个程序可能很大,但是实际用到(因此被装入到内存中)的部分实际上可能非常小。
仍然以上面提到的两个程序Prog1和Prog2为例,假如Prog1运行过程中出现了错误而Prog2在运行过程中没有出现任何错误。当我们采用静态链接、动态装入的方法,同时运行这两个程序时内存和硬盘的使用情况如图2所示:
图 2采用静态链接、动态装入方法,同时运行Prog1和Prog2时内存和硬盘的使用情况
可以看到,当程序中存在着大量像错误处理这样使用概率很小的模块时,采用静态链接、动态装入的方法在内存的使用效率上就体现出了相当大的优势。到此为止,人们已经向理想的目标迈进了一部,但是问题还没有完全解决――内存的使用效率提高了,硬盘呢?
3. 动态链接、动态装入
采用静态链接、动态装入的方法后看似只剩下硬盘空间使用效率不高的问题了,实际上内存使用效率不高的问题仍然没有完全解决。图2中,既然两个程序用到的是相同的utilities.o,那么理想的情况是系统中只保存一份utilities.o的拷贝,无论是在内存中还是在硬盘上,于是人们想到了动态链接。
在使用动态链接时,需要在程序映象中每个调用库函数的地方打一个桩(stub)。stub是一小段代码,用于定位已装入内存的相应的库;如果所需的库还不在内存中,stub将指出如何将该函数所在的库装入内存。
当执行到这样一个stub时,首先检查所需的函数是否已位于内存中。如果所需函数尚不在内存中,则首先需要将其装入。不论怎样,stub最终将被调用函数的地址替换掉。这样,在下次运行同一个代码段时,同样的库函数就能直接得以运行,从而省掉了动态链接的额外开销。由此,用到同一个库的所有进程在运行时使用的都是这个库的同一份拷贝。
下面我们就来看看上面提到的两个程序Prog1和Prog2在采用动态链接、动态装入的方法,同时运行这两个程序时内存和硬盘的使用情况(见图3)。仍然假设Prog1运行过程中出现了错误而Prog2在运行过程中没有出现任何错误。
图 3采用动态链接、动态装入方法,同时运行Prog1和Prog2时内存和硬盘的使用情况
图中,无论是硬盘还是内存中都只存在一份utilities.o的拷贝。内存中,两个进程通过将地址映射到相同的utilities.o实现对其的共享。动态链接的这一特性对于库的升级(比如错误的修正)是至关重要的。当一个库升级到一个新版本时,所有用到这个库的程序将自动使用新的版本。如果不使用动态链接技术,那么所有这些程序都需要被重新链接才能得以访问新版的库。为了避免程序意外使用到一些不兼容的新版的库,通常在程序和库中都包含各自的版本信息。内存中可能会同时存在着一个库的几个版本,但是每个程序可以通过版本信息来决定它到底应该使用哪一个。如果对库只做了微小的改动,库的版本号将保持不变;如果改动较大,则相应递增版本号。因此,如果新版库中含有与早期不兼容的改动,只有那些使用新版库进行编译的程序才会受到影响,而在新版库安装之前进行过链接的程序将继续使用以前的库。这样的系统被称作共享库系统。 |
|
|
|
|
|