1. Shared Library的优势
共享库,又称动态库或so文件,顾名思义,它可以在可执行文件启动时加载或进程运行期被调用。使用共享库有很多好处,例如(包含但不限于下面提到的场景):
1) 减少了依赖共享库的模块的大小,因为它们不必把共享库提供的功能的实现代码静态编译到自己的模块代码中。
2) 在同一台机器上运行的多个进程会在内存中共享同一份动态库,操作系统采用的这种内存布局方式可以极大地节省机器内存资源。
3) 若很多模块依赖了以共享库形式提供的同一个底层库,则底层库升级时,只需升级该so文件即可,无需重新编译应用模块;而若底层库以静态编译形式集成在上层模块内部时,则需要重新编译每个依赖了该库的模块。
4) 即使某些so库新版本不满足后向兼容也可以升级,某些不支持新版so库的应用程序可以不受影响,继续使用旧版本。
5) 应用程序可以利用so库覆盖某些库,还可以覆盖库中的某些函数(即仍使用某库,但该库中的某些函数被so库的同名函数覆盖)。
由于shared library有其独特的场景应用优势,因此*NIX操作系统的底层库基本都以共享库(*.so)形式提供,其实,windows操作系统底层库也是以共享库(*.dll)形式提供的。
2. Shared Library的命名和部署约定
为方便管理依赖关系,创建或部署共享库时,必须遵循统一约定的规则才行,其中包括动态库的命名规则及其部署方式。
2.1 共享库命名约定
1) 每个动态库都有一个以"lib"为前缀且以".so.x"为结尾的被称为soname的特定名称,其中x为主版本号,soname命名格式通常为libxxx.so.x。例如,我的linux系统机器上zlib共享库的soname为libz.so.1
2) 每个动态库还有一个包含了真正的库代码的文件名,通常被称为库的realname,与soname相比,它增加了副版本号(minor number)和发行版本号(release number),命名格式通常为libxxx.so.x.y.z,其中so后缀中的x为主版本号,y为副版本号,z为发行版本号。例如,我的linux系统机器上zlib共享库的realname为libz.so.1.2.8
3) 此外,链接或启动依赖了共享库的应用模块时,链接器(linker)或loader只认不带任何版本号的共享库名,这篇文档中把供linker/loader用的库名称作"linker
name"。也即,某个依赖了zlib库的模块在链接或启动时,linker或loader只会查找名为libz.so的共享库,查找不到就会报错。
上面提到的realname/soname/linker name这3个命名约定是linux系统管理共享库的关键,具体而言:
1) 当库开发者创建共享库时,通常以realname为该库命名
2) 该共享库的某个版本被安装时,安装脚本通常会下载对应版本命名为realname的库文件,然后调用linux系统内置的ldconfig工具为名为realname的库文件生成名为soname的软链且把该软链关系更新至/etc/ld.so.cache中
3) 安装脚本创建一个不带版本号的库名(即共享库的linker name),它是一个指向该库soname的symbolic link
4) 更新新版共享库时,安装脚本重复上述第2步
当然,我们完全可以手动完成上述步骤中的两次软链设定。还以我的linux系统机器上zlib共享库为例,它有一个供linker在链接时查找用的名为libz.so的库名,该库名是一个指向libz.so.1的软链,而libz.so.1是一个指向libz.so.1.2.8的软链。
两层软链的部署约定为同一系统下同一个共享库不同版本间的共存或共享库升级提供了方便:依赖了某共享库的上层模块无需关心当前系统下该共享库的最新版本是多少,只要最新版本已成功安装且soname指向了最新版本的realname,则上层模块下次启动时会由loader自动加载最新版本的共享库。
关于类UNIX平台开源软件版本号的几点补充说明:
1) 若发布的新版共享库升级对外接口导致其无法与上个旧版本的接口兼容时,则主版本号需要加1
2) 若发布的新版共享库只是修改了内部实现逻辑(如bug fix),其对外接口与旧版本保持兼容时,则主版本号不变,副版本号加1
2.2 共享库部署路径
开源软件部署路径通常遵循GNU默认约定或FHS标准(Filesystem
Hierarchy Standard)。具体的部署路径视库的用途而定,对大多数的共享库使用者来说,其实不用了解太多细节,这里不再赘述。感兴趣的话,可以查阅相关资料。
3. 上层模块如何使用共享库
3.1 共享库默认查找路径
在linux系统中,ELF格式的2进制可执行文件启动时,操作系统内置的program loader(如/lib/ld-linux.so.2)会自动查找并加载该模块依赖的共享库(如果未在编译期显示指定依赖的共享库的路径,则模块启动时loader的查找顺序可以参考这篇文档DESCRIPTION部分的说明)。默认情况下,loader会在/etc/ld.so.conf中查找候选集,若查找失败,则会报出类似于"cannot
open shared object file: No such file or directory"的错误。
因此,在有root权限的情况下,我们将依赖的共享库路径加入/etc/ld.so.conf中即可。
3.2 如何覆盖某个库的部分函数
如果我们想以某些函数覆盖共享库中的同名函数(如想用更高效的内存分配函数代替系统自带的malloc函数),则我们可以把包含新版函数的目标文件路径(xxxpath/xxx.o)配置到/etc/ld.so.preload中,在编译上层模块时,这些preload的xxx.o文件中包含的函数会覆盖掉其它库的同名函数。当然,也可以把已编译好的共享库路径配置在这里,依赖的模块运行时,这里配置的共享库中的函数会覆盖操作系统基础库的同名函数。
特别需要说明的是:在ld.so.preload文件中修改配置可能会影响到机器上的所有用户,故配置过程需要root权限。当然,即使是root用户,在修改这些可能影响到全局的配置文件时也必须谨慎才行。
3.3 如何使loader高效查找共享库
显然,上层模块启动时,在/etc/ld.so.conf或/etc/ld.so.preload中遍历查找所依赖共享库的过程并不高效,因此,linux系统引入了cache机制。我们可以借助ldconfig命令为在/etc/ld.so.conf中配置的共享库在/etc/ld.so.cache中创建cache item(ldconfig命令的另一个功能是为realname创建soname软链,本文2.1节提到过这一点)。ld.so.cache中列出的共享库会被loader高效地查找到。
3.4 影响共享库查找路径的典型环境变量
1) LD_LIBRARY_PATH
在linux系统中,可以用LD_LIBRARY_PATH指定以冒号分隔的多个共享库路径。依赖这些库的模块链接或启动时,由LD_LIBRARY_PATH指定的路径会被linker或loader优先搜索(优先于/lib或/usr/lib这些默认搜索路径或由/etc/ld.so.conf指定的搜索路径)。
注意1:用LD_LIBRARY_PATH指定共享库搜索路径的方法对于开发/调试模块非常方便,但多数情况下,不应该在~/.bash_profile中将LD_LIBRARY_PATH
export为某账号下的全局环境变量。因为一旦LD_LIBRARY_PATH成为某个账号默认的全局环境变量,就有可能会被很多模块依赖,而一旦有很多模块已经依赖了这个变量指定的路径去查找共享库,那么,如果这个变量被其他人改动或删除时,那些依赖该全局环境变量的模块将无法正常启动!
当然,如果每次启动依赖共享库的模块前,在当前shell终端或脚本中临时export LD_LIBRARY_PATH的内容,则即使通过LD_LIBRARY_PATH指定搜索路径也不会产生严重的后果(因为这个非全局的变量设定只在当前shell及其subshell中有效,不会对其它模块产生影响),只是,这样做不太优雅而已。
优雅的解决方法是:编译模块前,在Makefile中通过ld的-rpath选项为模块指定启动时loader搜索共享库的路径。如此,在部署模块时,只要共享库路径与Makefile中指定的-rpath路径一致,loader就会在该路径下搜索共享库。
另一种解决方法:用下面的命令启动依赖了共享库的应用程序
/lib/ld-linux.so.2 --library-path SO_PATH EXECUTABLE [arg1, arg2, ...]
其中,SO_PATH是共享库的路径,EXECUTABLE是要启动的应用程序,arg1 - argn是程序的输入参数
注意2:在有些Unix-like的系统下,实现同样功能的环境变量名可能不是LD_LIBRARY_PATH(如HP-UX系统中定义的环境变量名为SHLIB_PATH),使用时需要注意这一点。
2) LD_PRELOAD
该环境变量用于指定.o文件或共享库的路径,linker对上层模块进行链接或loader加载上层模块的启动信息时,会以由LD_PRELOAD指定的.o文件中或共享库的函数覆盖其依赖的其它库的同名函数。
关于模块启动时,如何通过LD_PRELOAD指定自定义库来覆盖其它库同名函数的方法,可以参考这里的例子。
3) LD_DEBUG
通过设置该环境变量,可以输出模块进程启动时loader处理过程的verbose信息,如loader查找到模块依赖的哪些共享库及哪个共享库被加载等信息。
这种调试日志可以帮我们定位一些共享库相关的异常问题。
4) 其它环境变量
除以上3个常用环境变量外,还有一些会影响共享库行为的环境变量,不过这些变量偏底层,对底层库开发者意义比较大,对上层的库调用者来说,接触机会其实不是很多。感兴趣的可以在shell终端通过man ld.so来查看这些变量名,这里不赘述。
【参考资料】
1. The Linux Document Project: Shared Libraries
3. A Simple LD_PRELOAD Tutorial
4. Linux Programmer‘s Manual: ld.so
======================= EOF ====================