案例分享:进程如何加载相同库的不同版本

背景介绍

前段时间,我的同事宁哥遇到了一个奇怪的需求:一个进程需要调用一个库的不同版本。

宁哥是负责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,人生尽是坦途。

在这里插入图片描述

  • 14
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

谢艺华

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值