• 动态库和静态库,混合使用下的单例bug


    不同的动态库在加载同一个静态库时,静态库会将代码编译到不同的动态库中,若静态库中有单例或者静态变量等,则会生成多份副本,则达不到单例的作用,严重者则会出现系统问题,不建议使用静态库。

    问题介绍

    最近半年,项目里出现多次奇怪的crash。看crash堆栈都是进程退出的时候静态变量的销毁core掉了。从堆栈里看不出来哪里逻辑不对,从代码里也看不出来哪里改坏了这个变量,但是像std::unordered_map析构时候挂掉这种真是不知道怎么查。只能上asan去看是不是哪里把这个变量的内存写坏了,然而费时费力也没查出来什么。

    最后没办法,二分回滚所有相关提交,最后定位到出问题的那次。可是看代码仍然看不出来哪里有问题,只好在这个提交的所有修改里进行二分注释来测试是否会引发关服core,最后定位到调用的一个库的一个接口就会引发core。既然定位到了问题,那我就将代码提前return,不再调用这个接口验证一下是否解决。WTF,即使这个函数不被执行到也会出现core,只有彻底把这个函数的调用注释掉才能彻底解决core的发生。幸亏这个库是我们自己写的,有源代码,于是对这个函数的源代码进行二分注释,发现这个函数内的一个静态变量被销毁了两次,从而引发了core

    静态单例

    c++11里,我们已经被教育了,单例模式直接上static的这个完美解决方案,保证多线程模式下单例数据只会初始化一次。

    1. //sinleton_lib.h
    2. #pragma once
    3. extern int global_var;
    4. class singleton_test
    5. {
    6. singleton_test();
    7. singleton_test(const singleton_test& other) = delete;
    8. public:
    9. static singleton_test& instance();
    10. ~singleton_test();
    11. };
    12. //sinleton_lib.cpp
    13. #include "singleton_lib.h"
    14. #include
    15. int global_var = 1314;
    16. singleton_test::singleton_test()
    17. {
    18. std::cout<<"singleton_test create at "<<this<
    19. }
    20. singleton_test::~singleton_test()
    21. {
    22. std::cout<<"singleton_test destroyed at "<<this<
    23. }
    24. singleton_test& singleton_test::instance()
    25. {
    26. static singleton_test the_one;
    27. return the_one;
    28. }

    优雅,简单,易懂, perfect。但是在下面我设计出来的情况里这个单例就不再是单例,而会出现多份数据。

    多个动态库链接同一个静态库

    1. 首先我们把上面的代码封装为一个静态库
    2. 然后我们新建两个内容相同的动态库连接到这个静态库, 一个是libdyn_test_1.so
    1. #include "singleton_lib.h"
    2. #include
    3. extern "C" void dyn_test()
    4. {
    5. auto& cur_singleton = singleton_test::instance();
    6. std::cout<<"dyn_test_1 get singleton at "<<&cur_singleton<
    7. std::cout<<"dyn_test_1 get global var at "<<&global_var<
    8. }

    一个是libdyn_test_2.so

    1. #include "singleton_lib.h"
    2. #include
    3. extern "C" void dyn_test()
    4. {
    5. auto& cur_singleton = singleton_test::instance();
    6. std::cout<<"dyn_test_2 get singleton at "<<&cur_singleton<
    7. std::cout<<"dyn_test_2 get global var at "<<&global_var<
    8. }
    1. 最后我们建立一个可执行文件load_test,连接到singleton_test,然后再运行时加载上面的两个动态库,执行对应的dyn_test接口,观察输出
    1. //load_test.cpp
    2. #include
    3. #include
    4. #include "singleton_lib.h"
    5. void call_dyn_test(const std::string& lib_name, int load_flag)
    6. {
    7. void (*foo)();
    8. void *handle = dlopen(lib_name.c_str(), load_flag);
    9. if(!handle)
    10. {
    11. std::cout<<"cant open lib "<
    12. return;
    13. }
    14. *(void **) (&foo) = dlsym(handle, "dyn_test");
    15. const char* error;
    16. if((error = dlerror()) != NULL)
    17. {
    18. std::cout<<"fail to dlsym "<
    19. return;
    20. }
    21. (*foo)();
    22. }
    23. int main()
    24. {
    25. int flag = RTLD_LOCAL|RTLD_NOW;
    26. const auto& the_one = singleton_test::instance();
    27. call_dyn_test("./libdyn_test_1.so", flag);
    28. call_dyn_test("./libdyn_test_2.so", flag);
    29. return 1;
    30. }

    最后的输出很是令人绝望:

    1. singleton_test create at 0x564624be8159
    2. singleton_test create at 0x7f6eca7fa091
    3. dyn_test_1 get singleton at 0x7f6eca7fa091
    4. dyn_test_1 get global var at 0x7f6eca7fa078
    5. singleton_test create at 0x7f6eca7f5091
    6. dyn_test_2 get singleton at 0x7f6eca7f5091
    7. dyn_test_2 get global var at 0x7f6eca7f5078
    8. singleton_test destroyed at 0x7f6eca7f5091
    9. singleton_test destroyed at 0x7f6eca7fa091
    10. singleton_test destroyed at 0x564624be8159

    从这里的输出可以看出,load_testlibdyn_test_1libdyn_test_2里对于全局变量global_var和静态单例singleton_test都各自拥有一份数据,分配在不同的地址上。这样导致了singleton_test单例被构造了三次,同时析构了三次。

    多个静态单例实例带来的问题

    如果这个单例对象一个接口是申请资源并设置内部指针,一个接口是释放资源并销毁内部指针。但是释放的调用和销毁的调用在不同的库里,逻辑不严谨直接free掉对应的数据指针,遇到上面的情况就崩掉了。 还有另外一种就是一个动态库里对静态库的全局变量做的修改,其他动态库是看不到的。例如上面的global_var变量,load_testlibdyn_test_1libdyn_test_2任意一个进行的修改都无法在其他库里观察到,这样就会引发逻辑错误。

    不同的动态库加载模式触发的不同问题

    上面的不同动态库引用的静态库全局变量多实例的问题可以通过更换dlopen传入的flag来解决,我们把上面的RTLD_LOCAL切换为RTLD_GLOBAL,输出就不一样了:

    1. singleton_test create at 0x564b6ee46159
    2. singleton_test create at 0x7fcd07dd5091
    3. dyn_test_1 get singleton at 0x7fcd07dd5091
    4. dyn_test_1 get global var at 0x7fcd07dd5078
    5. dyn_test_2 get singleton at 0x7fcd07dd5091
    6. dyn_test_2 get global var at 0x7fcd07dd5078
    7. singleton_test destroyed at 0x7fcd07dd5091
    8. singleton_test destroyed at 0x564b6ee46159

    可以看出,两个动态库里面引用的静态库的变量都指向了同一个地址。不过load_test里的地址还是跟动态库里不一样,前面我们提到的问题仍然还存在。

    然而RTLD_GLOBAL只是解决了同一个地址的问题,静态库里全局变量数据还是会进行多次构造和析构。我们将global_var的类型替换一个非pod类型

    1. //singleton_lib.h
    2. struct raii_test
    3. {
    4. int i = 0;
    5. raii_test();
    6. ~raii_test();
    7. };
    8. extern raii_test global_var;
    9. //singleton_lib.cpp
    10. raii_test::raii_test()
    11. {
    12. std::cout<<"raii_test ctor at "<<this<
    13. }
    14. raii_test::~raii_test()
    15. {
    16. std::cout<<"raii_test dtor at "<<this<
    17. }
    18. raii_test global_var;

    重新编译执行一下程序,下面是输出:

    1. raii_test ctor at 0x56113df4b158
    2. singleton_test create at 0x56113df4b15d
    3. raii_test ctor at 0x7fe5ac87e090
    4. singleton_test create at 0x7fe5ac87e095
    5. dyn_test_1 get singleton at 0x7fe5ac87e095
    6. dyn_test_1 get global var at 0x7fe5ac87e090
    7. raii_test ctor at 0x7fe5ac87e090
    8. dyn_test_2 get singleton at 0x7fe5ac87e095
    9. dyn_test_2 get global var at 0x7fe5ac87e090
    10. raii_test dtor at 0x7fe5ac87e090
    11. singleton_test destroyed at 0x7fe5ac87e095
    12. raii_test dtor at 0x7fe5ac87e090
    13. singleton_test destroyed at 0x56113df4b15d
    14. raii_test dtor at 0x56113df4b158

    从上面的输出可以看出,两个动态链接库指向的singleton global_var的地址现在是同一个了, 但是global_var在同一个地址0x7fe5ac87e090构造了两次,析构了两次。这种情况,业务逻辑再怎么严谨也救不回来,里面嵌入一个stl容器就会直接崩溃。

    最终解决方案

    库这个东西就应该做成动态库,别听别人说静态库比动态库快1%这种可有可无的优势。如果做成静态库,你无法控制后续是不是又被多个动态库引用,或者动态库静态库混合引用,出现各种奇奇怪怪的问题。

  • 相关阅读:
    verilog学习笔记(1)module实例化
    牛客编程题--必刷101之动态规划(一文彻底了解动态规划)
    二叉树与堆
    密码学基础:搞懂Hash函数SHA1、SHA-2、SHA3(1)
    实战+代码!Selenium + Phantom JS爬取天天基金数据
    Spring IoC和DI详解
    在 Linux 上使用 Systemd 运行 Java Jar 应用程序
    计算机毕业设计JavaVue框架电商后台管理系统(源码+系统+mysql数据库+lw文档)
    【博客483】prometheus-----告警处理源码剖析
    海外代理ip有什么作用,使用场景是什么?
  • 原文地址:https://blog.csdn.net/liuying263/article/details/127692389