预计阅读本页时间:-
3.5.6 共享库
可以使用其他的粒度取代单个页面来实现共享。如果一个程序被启动两次,大多数操作系统会自动共享所有的代码页面,而在内存中只保留一份代码页面的副本。代码页面总是只读的,因此这样做不存在任何问题。依赖于不同的操作系统,每个进程都拥有一份数据页面的私有副本,或者这些数据页面被共享并且被标记为只读。如果任何一个进程对一个数据页面进行修改,系统就会为此进程复制这个数据页面的一个副本,并且这个副本是此进程私有的,也就是说会执行“写时复制”。
现代操作系统中,有很多大型库被众多进程使用,例如,处理浏览文件以便打开文件的对话框的库和多个图形库。把所有的这些库静态地与磁盘上的每一个可执行程序绑定在一起,将会使它们变得更加庞大。
一个更加通用的技术是使用共享库(在Windows中称作DLL或动态链接库)。为了清楚地表达共享库的思想,首先考虑一下传统的链接。当链接一个程序时,要在链接器的命令中指定一个或多个目标文件,可能还包括一些库文件。以下面的UNIX命令为例:
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
ld*.o-lc-lm
这个命令会链接当前目录下的所有的.o(目标)文件,并扫描两个库:/usr/lib/libc.a和/usr/lib/libm.a。任何在目标文件中被调用了但是没有被定义的函数(比如,printf),都被称作未定义外部函数(undefined externals)。链接器会在库中寻找这些未定义外部函数。如果找到了,则将它们加载到可执行二进制文件中。任何被这些未定义外部函数调用了但是不存在的函数也会成为未定义外部函数。例如,printf需要write,如果write还没有被加载进来,链接器就会查找write并在找到后把它加载进来。当链接器完成任务后,一个可执行二进制文件被写到磁盘,其中包括了所需的全部函数。在库中定义但是没有被调用的函数则不会被加载进去。当程序被装入内存执行时,它需要的所有函数都已经准备就绪了。
假设普通程序需要消耗20~50MB用于图形和用户界面函数。静态链接上百个包括这些库的程序会浪费大量的磁盘空间,在装载这些程序时也会浪费大量的内存空间,因为系统不知道它可以共享这些库。这就是引入共享库的原因。当一个程序和共享库(与静态库有些许区别)链接时,链接器没有加载被调用的函数,而是加载了一小段能够在运行时绑定被调用函数的存根例程(stub routine)。依赖于系统和配置信息,共享库或者和程序一起被装载,或者在其所包含函数第一次被调用时被装载。当然,如果其他程序已经装载了某个共享库,就没有必要再次装载它了——这正是关键所在。值得注意的是,当一个共享库被装载和使用时,整个库并不是被一次性地读入内存。而是根据需要,以页面为单位装载的,因此没有被调用到的函数是不会被装载到内存中的。
除了可以使可执行文件更小、节省内存空间之外,共享库还有一个优点:如果共享库中的一个函数因为修正一个bug被更新了,那么并不需要重新编译调用了这个函数的程序。旧的二进制文件依然可以正常工作。这个特性对于商业软件来说尤为重要,因为商业软件的源码不会分发给客户。例如,如果微软发现并修复了某个标准DLL中的安全错误,Windows更新会下载新的DLL来替换原有文件,所有使用这个DLL的程序在下次启动时会自动使用这个新版本的DLL。
不过,共享库带来了一个必须解决的小问题,如图3-27所示。我们看到有两个进程共享一个20KB大小的库(假设每一方框为4KB)。但是,这个库被不同的进程定位在不同的地址上,大概是因为程序本身的大小不相同。在进程1中,库从地址36K开始;在进程2中则从地址12K开始。假设库中第一个函数要做的第一件事就是跳转到库的地址16。如果这个库没有被共享,它可以在装载的过程中重定位,就会跳转(在进程1中)到虚拟地址的36K+16。注意,库被装载到的物理地址与这个库是否为共享库是没有任何关系的,因为所有的页面都被MMU硬件从虚拟地址映射到了物理地址。

但是,由于库是共享的,因此在装载时再进行重定位就行不通了。毕竟,当进程2调用第一个函数时(在地址12K),跳转指令需要跳转到地址12K+16,而不是地址36K+16。这就是那个必须解决的小问题。解决它的一个办法是写时复制,并为每一个共享这个库的进程创建新页面,在创建新页面的过程中进行重定位。当然,这样做和使用共享库的目的相悖。
一个更好的解决方法是:在编译共享库时,用一个特殊的编译选项告知编译器,不要产生使用绝对地址的指令。相反,只能产生使用相对地址的指令。例如,几乎总是使用向前(或向后)跳转n个字节(与给出具体跳转地址的指令不同)的指令。不论共享库被放置在虚拟地址空间的什么位置,这种指令都可以正确工作。通过避免使用绝对地址,这个问题就可以被解决。只使用相对偏移量的代码被称作位置无关代码(position-independent code)。