1. 动态库与静态库
一般情况下,库文件的开发者会同时提供两个版本,它们都各自的优缺点。静态库
在链接阶段,会被直接链接
进最终的二进制文件中,因此最终生成的二进制文件体积会比较大,但是可以不再依赖于库文件。而动态库
并不是被链接到文件中的,只是保存了依赖关系
,因此最终生成的二进制文件体积较小,但是在运行阶段需要加载动态库。
2. 编译生成和使用动态库
首先,我们来编译并生成一个动态库:
#include <stdlib.h>
#include <stdio.h>
void dynamic_lib_call(void)
{
printf("dynamic lib call\n");
}
编译生成动态库与编译普通的可执行程序不同,如下所示:
gcc -Wall -shared 4_5_2_dlib.c -o libdlib.so
其中多了一个-shared
选项,该选项用于指示gcc
生成动态库。
然后在编写一个简单的例子,来使用这个动态库,代码如下:
#include <stdlib.h>
#include <stdio.h>
extern void dynamic_lib_call(void);
int main(void)
{
dynamic_lib_call();
return 0;
}
下面我们利用前面的动态库来生成最终的可执行文件gcc-Wall4_5_2_main.c-o test_dlib-L./-ldlib
。其中,-l
用于指示生成文件依赖的库,本例依赖于libdlib.so
,因此为-ldlib
;-L
与-I
类似,-L
用于指示gcc
在哪个目录中查找依赖的库文件。
运行这个test_dlib
结果如何:
[fgao@ubuntu chapter3]#./test_dlib
./test_dlib: error while loading shared libraries: libdlib.so: cannot open sh
为什么会报错,找不到这个libdlib.o
呢?前面明明已经使用-L制定了库文件在当前目录中,并且这库文件也确实存在当前目录中。
用ldd
来查看test_lib
的依赖库,代码如下:
[fgao@ubuntu chapter3]#ldd test_dlib
linux-gate.so.1 => (0xb7785000)
libdlib.so => not found
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb75ce000)
/lib/ld-linux.so.2 (0xb7786000)
确实显示无法找到libdlib.so
。原因在于-L
只是在gcc
编译的过程中指示库的位置,而在程序运行的时候,动态库的加载路径默认为/lib
和/usr/lib
。在Linux环境下,还可以通过/etc/ld.so.conf
配置文件和环境变量LD_LIBRARY_PATH
指示额外的动态库路径。
为了简单起见,我们在这里将libdlib.so
复制到/usr/lib
目录下,在运行test_dlib
试试:
[root@ubuntu lib]#cp /home/fgao/works/my_git_codes/my_books/understanding_apue/sample_codes/chapter3/libdlib.so .
[fgao@ubuntu chapter3]#./test_dlib
dynamic lib call
现在./test_dlib
顺利执行了,并成功调用了动态库中的dynamic_lib_call
函数。
上面的例子中,动态库是由系统自动加载的,所以需要将动态库放在指定的目录下。然而,C库
还提供了dlopen
等接口来支持手工加载动态库的功能,代码如下:
#include <stdlib.h>
#include <stdio.h>
#include <dlfcn.h>
int main()
{
void *dlib = dlopen("./libdlib.so", RTLD_NOW);
if (!dlib) {
printf("dlopen failed\n");
return -1;
}
void (*dfunc) (void) = dlsym(dlib, "dynamic_lib_call");
if (!dfunc) {
printf("dlsym failed\n");
return -1;
}
dfunc();
dlclose(dlib);
return 0;
}
编译代码gcc-Wall 4_5_2_main_mlib.c-ldl-o test_mlib
,需要使用-ldl
选项来指定依赖的动态libdl.so
。
下面来看一下输出结果:
[fgao@ubuntu chapter3]#./test_mlib
dynamic lib call
可以看出,我们已经成功地使用手工来加载动态库,并完成了动态库中的函数调用。
介绍完动态库的两种加载方法,我们可以对比一下两者的优缺点。对于自动加载,处理起来比较简单;而手工加载需要编写额外的代码,但正是这些额外的代码提供了更多的动态库的可控性。
3. 程序的“平滑无缝”升级
对比了动态库和静态库的优缺点。其中动态库的一个重要优点就是,可执行程序并不包含动态库中的任何指令,而是在运行时加载动态库并完成调用。这就给我们提供了升级动态库的机会。只要保证接口不变,使用新版本的动态库替换原来的动态库,就完成了动态库的升级。更新完库文件以后启动的可执行程序都会使用新的动态库。
这样的更新方法只能够影响更新以后启动的程序,对于正在运行的程序则无法产生效果,因为程序在运行时,旧的动态库文件已经加载到内存中了。我们只能更新位于磁盘上的动态库的物理文件,而不能影响已经位于内存中的库了。
我们是否可以做得更好呢?对于服务程序来说,重启会付出很大的代价并带来糟糕的用户体验。那么,能否让运行中的服务程序也能在升级库以后使用新的指令,做到“平滑无缝”的升级呢?这就需要使用前面介绍的手工加载动态库的方法了。
下面的伪代码将给出一个比较简单的解决方案。
1)使用一个结构体来管理动态库的接口:
struct dlib_manager {void *dlib_handle; //保存动态库的句柄int (service_func) (void *);
} g_dlib_manager;
/* g_dlib_manager作为动态库接口的全局变量 */
struct dlib_manager *g_dlib_manager;
2)利用dlopen、dlsym等来加载动态库,更新接口。重新申请新
的内存,来保存新的动态库接口:
/* 更新动态库接口 */
struct dlib_manager *new_manager = malloc(sizeof(*new_manager));
new_manager->dlib_handle = dlopen("libupgrade.so", RTLD_LAZY);
new_manager->service_func = dlsym(g_dlib_handle, "service_call");
new_manager->service_func2 = dlsym(g_dlib_handle, "service_call2");
/* 在多核环境下,使用内存屏障,以保证在交换new_manager和g_dlib_manager时,new_manager已经完成了赋值 */
wmb();
/*交换新指针与当前正在使用的接口指针因为目前,无论是新指针还是旧指针都是有效的接口,所以并不会对业务产生影响
*/
swap(new_manager, g_dlib_manager);
/*交换完成以后,新的请求都会交由新接口来处理由于当前旧接口仍然可能正在使用中,所以要使用推迟释放或是等待正在服务的接口完成
*/
delay_free(new_manager);
3)在调用服务接口时,要利用局部变量保存服务接口:
struct dlib_manager *local_dlib_manager = g_dlib_manager;
local_dlib_manager->service_func1(data);
local_dlib_manager->service_func2(data);
之所以这里使用局部变量来进行接口调用,是为了避免在调用了一部分接口后,g_dlib_manager才发生更新,从而导致前后的服务接口属于不同的动态库,造成不可预料的问题。通过临时变量来保存服务接口,能确保所有接口的一致性。
4)释放旧接口的关键在于,要保证没有旧接口正在被使用。根据自己的业务,找到一个时间点——在这个时间点上,所有的线程(准确地说是请求流程)都已经服务过一次。这时,新来的请求就会使用新的接口,于是我们也就可以安全地释放旧接口了。
其实整个实现方案是借鉴了Linux内核的
RCU
实现方式。通过这种方法,可以进行程序的“平滑无缝”
的升级,而不影响程序在运行状态下的业务功能。