背景介绍
前段时间,我的同事宁哥遇到了一个奇怪的需求:一个进程需要调用一个库的不同版本。
宁哥是负责UA业务的开发。核心工作就是对设备进行程序升级。其中涉及到两个核心功能。
-
压缩/解压。对于数据包,在不失真的前提下。进行压缩/解压操作,可以提高传输速度以及节约网络资源。
-
差分/还原。在现在的fota业务中,差分/还原也属于基本功能了。很多智能件的固件包可能会有一个1GB或更大。但是新旧版本的固件包往往差别并不大,可能只有几MB的差别。差分/还原的功能就是计算出新版本固件与旧版本固件的差别,得到patch文件(差分)。再同比旧版本固件、patch文件计算,得到新固件版本(还原)。最终大大提高传输效率,以及节约网络资源。
无论是压缩/解压,还是差分/还原,都需要采用特定的算法。但是因为历史原因,差分/还原功能和压缩/解压功能采用的是同一个库,但是不同的版本,但是新项目中要求两个功能更都需要集成。
解决方案:
一、采用同一个库版本
优点:一劳永逸,后续方便进行代码管理。
风险:对应修改库版本的功能,需要花费较长时间进行适配,调试,验证。
二、不同的功能指定库版本接口
优点:不需要再进行验证功能的可靠性。因为对应库已经在多个项目中实施。
风险:存在一定的技术难度。如何解决符号冲突,指定特定版本等问题。
理论分析
最终考虑到项目时间因素,决定采用方案二。但是我们需要认识到并解决以下几个问题。
举例:
为方便后续讨论,拟定开源库为libfunc.so
,分别对应V1和V2版本。工程如下:
yihua@ubuntu:~/20$ tree
.
├── main.c
├── V1
│ └── myfunc.c
└── V2
└── myfunc.c
2 directories, 3 files
其中V1、V2目录表示相同算法库,不同的版本。
代码如下:
// V1/myfunc.c
#include<stdio.h>
#include<stdlib.h>
void callself(void)
{
printf("i'am callself v1'\n");
return;
}
int myfunc()
{
printf("i'm myfunc V1\n");
callself();
return 0;
}
// V2/myfunc.c
#include<stdio.h>
#include<stdlib.h>
void callself(void)
{
printf("i'am callself v2'\n");
return;
}
int myfunc()
{
printf("i'm myfunc V2\n");
callself();
return 0;
}
// main.c
#include<stdio.h>
extern int myfunc();
int main()
{
myfunc();
return 0;
}
- 符号冲突问题
虽然V1和V2属于不同的版本,但是他们的接口名称都是一样的。这样就导致在可执行程序链接时,是否会出现重复定义错误?multiple definition of “xxx”
。如下:
yihua@ubuntu:~/20/V1$ gcc -fPIC --shared myfunc.c -o libmyfunc.so
yihua@ubuntu:~/20/V1$ cd ../V2/
yihua@ubuntu:~/20/V2$ gcc -fPIC --shared myfunc.c -o libmyfunc.so
yihua@ubuntu:~/20/V2$ cd ..
yihua@ubuntu:~/20$ gcc main.c -lmyfunc -L ./V1/ -L ./V2/ -o main
yihua@ubuntu:~/20$
由上可知,并不会存在重定义的错误。因为编译阶段,对动态库链接的目的是确认符号的处理方式。此时并不会将动态库的代码段等加载可执行程序中,若是算法库为静态库,则会存在重定义错误。进一步的理解,可以参考另一篇文章程序员的自我修养08】精华!!!动态库的由来及其实现原理。
- 全局符号介入
在动态库章节,我们曾讨论过全局符号介入的问题:当一个符号需要被加入到全局符号表时,如果相同的符号名已经存在,则后加入的符号被忽略。有兴趣可以参考文章【程序员的自我修养09】动态链接过程的场景补充及其思考、坑惨啦!!!——符号冲突案例分析。
如下:
yihua@ubuntu:~/20$ export LD_LIBRARY_PATH=V1/
yihua@ubuntu:~/20$ ./main
i'm myfunc V1
i'am callself v1'
yihua@ubuntu:~/20$ export LD_LIBRARY_PATH=V2/
yihua@ubuntu:~/20$ ./main
i'm myfunc V2
i'am callself v2'
yihua@ubuntu:~/20$
最终main
接口中应用到哪一个版本的接口,取决于最先加载哪一个版本的库。头疼~~~
结论:动态加载动态库,会存在全局符号介入的问题。导致应用程序最终只能加载一个版本的算法库。
- 如何指定特定版本
由于全局GOT表的原因,动态加载无法加载多个版本的库。那么是否可以通过运行时加载呢?比如当我需要使用V1版本的算法接口时,dlopen
V1/libmyfunc.so
,引用相应接口;当我需要使用V2版本的算法接口时,dlopen
V2/libmyfunc.so
。该方案理论上是成立的。
实践
将代码修改如下:
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <assert.h>
#include <pthread.h>
#include <unistd.h>
//动态链接库路径
#define LIB_MYFUNCV1_PATH "./V1/libmyfunc.so"
#define LIB_MYFUNCV2_PATH "./V2/libmyfunc.so"
//函数指针
typedef int (*MY_FUNC)();
void* UAdiffernce(void* arg)
{
void *handlev1;
char *error;
MY_FUNC myfuncv1 = NULL;
//打开动态链接库
//handlev1 = dlopen(LIB_MYFUNCV1_PATH, RTLD_LAZY|RTLD_GLOBAL);
handlev1 = dlopen(LIB_MYFUNCV1_PATH, RTLD_LAZY);
if (!handlev1) {
fprintf(stderr, "%s\n", dlerror());
exit(EXIT_FAILURE);
}
//清除之前存在的错误
dlerror();
//获取一个函数
*(void **) (&myfuncv1) = dlsym(handlev1, "myfunc");
if ((error = dlerror()) != NULL) {
fprintf(stderr, "%s\n", error);
exit(EXIT_FAILURE);
}
myfuncv1();
printf("myfunc address=%p\n",myfuncv1);
sleep(5);
//关闭动态链接库
dlclose(handlev1);
printf("UAdiffrence end\n");
return NULL;
}
void* UARestore(void* arg)
{
void *handlev2;
char *error;
MY_FUNC myfuncv2 = NULL;
//打开动态链接库
//handlev2 = dlopen(LIB_MYFUNCV2_PATH, RTLD_LAZY|RTLD_GLOBAL);
handlev2 = dlopen(LIB_MYFUNCV2_PATH, RTLD_LAZY);
if (!handlev2) {
fprintf(stderr, "%s\n", dlerror());
exit(EXIT_FAILURE);
}
//清除之前存在的错误
dlerror();
//获取一个函数
*(void **) (&myfuncv2) = dlsym(handlev2, "myfunc");
if ((error = dlerror()) != NULL) {
fprintf(stderr, "%s\n", error);
exit(EXIT_FAILURE);
}
myfuncv2();
printf("myfunc address=%p\n",myfuncv2);
sleep(5);
//关闭动态链接库
dlclose(handlev2);
printf("UARestore end\n");
return NULL;
}
int main()
{
pthread_t tid1;
int res = pthread_create(&tid1,NULL,UAdiffernce,NULL);
assert(res == 0);
pthread_t tid2;
res = pthread_create(&tid2,NULL,UARestore,NULL);
assert(res == 0);
sleep(10);
return 0;
}
编译:
yihua@ubuntu:~/20$ gcc main.c -o main -ldl -lpthread
yihua@ubuntu:~/20$
运行:
yihua@ubuntu:~/20$ ./main
i'm myfunc V1
i'am callself v1'
myfunc address=0x7f5f55a5c150
i'm myfunc V2
i'am callself v2'
myfunc address=0x7f5f55a57150
UAdiffrence end
UARestore end
yihua@ubuntu:~/20$
很完美,结果如预期。不同的线程调用了不同的版本的库接口。完结,撒花~~~
问题:为什么运行时加载不会出现全局符号介入问题呢?
那是因为dlopen
采用的属性不同。dlopen(LIB_MYFUNCV2_PATH, RTLD_LAZY);
其中:
RTLD_LAZY
表示使用延迟绑定,可加快动态库加载速度。其对应的是RTLD_NOW
,表示当模块被加载时及完成所有函数的绑定工作,如果有任何未定义的符号引用的绑定工作没法完成,则会返回错误。RTLD_GLOBAL
表示被加载的模块的全局符号合并到进程的全局符号表中。可以使得后续加载的模块也可以使用该模块内的符号。
因此,若采用RTLD_GLOBAL
属性,打开动态库。依然会存在全局符号介入问题。
总结
本文分享了工作中的一个案例,由于历史原因。我们不得不在一个进程中采用两个不同的版本的相同算法库。
识别到了潜在问题:符号重定义、全局符号介入、如何指定版本符号。并从理论与实践上分析解决。更深入了解运行时模块加载的好处与特殊场景。
若我的内容对您有所帮助,还请关注我的公众号。不定期分享干活,剖析案例,也可以一起讨论分享。
我的宗旨:
踩完您工作中的所有坑并分享给您,让你的工作无bug,人生尽是坦途。