动态库之间单例模式出现多个实例(Linux)

简介:

本文目的

前几天在开发中遇到一个古怪的问题,定位了两天左右的时间才发现问题。该问题正如题目所描述:单一模式在动态链接库之间出现了多个实例。由于该实例是一个配置管理器,许多配置信息都在这个实例的初始化过程中读取,一旦出错,系统的其他地方都无法正确运行,所以给问题定位带来一定难度。为了避免敏感信息的泄漏,同时为了便于大家理解,将问题简化,在此与大家分享。

 

问题描述

首先,编写一个简单的单一模式的类,文件singleton.h内容如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef SINGLETON_H_
#define SINGLETON_H_
class  singleton{
private :
     singleton() {num = -1;}
     static  singleton* pInstance;
public :
     static  singleton& instance(){
         if  (NULL == pInstance){
             pInstance = new  singleton();
         }
         return  *pInstance;
     }
public :
     int  num;
};
singleton* singleton::pInstance = NULL;
#endif

 

接下来,编写一个简单的插件,调用该singleton,插件内容在hello.cpp文件中,如下:

1
2
3
4
5
6
7
8
9
#include <iostream>
#include "singleton.h"
extern  "C"  void  hello() {
std::cout << "singleton.num in hello.so : "
             << singleton::instance().num << std::endl;
     ++singleton::instance().num;
std::cout << "singleton.num in hello.so after ++ : "
             << singleton::instance().num << std::endl;
}

最后,编写一个main函数,调用singleton和插件,如下面的example1.cpp文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
#include <dlfcn.h>
#include "singleton.h"
int  main() {
using  namespace  std;
 
     // call singleton firstly
     singleton::instance().num = 100;   
cout << "singleton.num in main : "
       << singleton::instance().num << endl;
 
     // open the library
     void * handle = dlopen( "./hello.so" , RTLD_LAZY);
     if  (!handle) {
         cerr << "Cannot open library: "  << dlerror() << '\n' ;
         return  1;
     }
     
     // load the symbol
     typedef  void  (*hello_t)();
 
     // reset errors
     dlerror();
     hello_t hello = (hello_t) dlsym(handle, "hello" );
     const  char  *dlsym_error = dlerror();
     if  (dlsym_error) {
         cerr << "Cannot load symbol 'hello': "
               << dlsym_error << '\n' ;
         dlclose(handle);
         return  1;
     }
     
     hello();  // call method in the plugin
     // call singleton secondly
cout << "singleton.num in main : "
       << singleton::instance().num << endl;
     dlclose(handle);
}

好了,问题重现的简化版本构建完成,接下来,我们需要编译并执行它。编写makefile,内容如下(PS: singleton.h,hellp.cpp和example1.cpp在同一个目录中):

1
2
3
4
5
6
7
example1: main.cpp hello.so
     $(CXX) $(CXXFLAGS) -o example1 main.cpp -ldl
hello.so: hello.cpp
     $(CXX) $(CXXFLAGS) -shared -o hello.so hello.cpp
clean:
     rm  -f example1 hello.so
.PHONY: clean

 

make完后,运行example1文件。读到这里,你认为会输出什么?不读题目,可能,你认为会输出这些内容:

singleton.num in main : 100

singleton.num in hello.so : 100

singleton.num in hello.so after ++ : 101

singleton.num in main : 101

可是,小黑框中输出了如下的内容(注意红色部分):

singleton.num in main : 100

singleton.num in hello.so : -1

singleton.num in hello.so after ++ : 0

singleton.num in main : 100

从输出内容中,可以看出singleton出现了两个实例,一个实例在main函数中,另一个实例在插件hello.so中。

 

原因分析和解决方法

上面的现象,与现实中的项目一样,为单例构造了两个实例,这与我的初衷(使用单一的配置)相悖。为什么会出现这个现象呢?

究其原因,是由于插件的动态链接引起的。hello.so在动态链接过程中,没有发现example1中有singleton::instance这个实例(但是事实上,该实例已经存在了),那么就会自己创建一个实例(正如单例的逻辑实现),这样就导致了两个实例。

可以通过工具nm查看example1的符号表(symbol table),看看其中是否包含singleton::instance符号(dynamic )。

(PS: google一下”symbol table”和”nm”可以了解更多细节)

$ nm -C example1 | grep singleton

080488fa t global constructors keyed to _ZN9singleton9pInstanceE

08048ab6 W singleton::instance()

08049ff0 B singleton::pInstance

08048aa8 W singleton::singleton()

$ nm –C –D example1 | grep singleton

$

D选项用于查看动态符号(dynamic symbol),你会发现singleton::pInstance在静态表中存在,在动态表中不存在,这也就是说,动态连接器(dynamic linker)在加载hello.so的时候,无法找到singleton::pInstance的唯一实例,只能构造一个新的实例,该实例在hello.so中是唯一的,但是不能保证在example1和hello.so中唯一。

现在,解决问题的关键在于如何暴露example1中的singleton::pInstance。好在,GNU make有一个链接选项-rdynamic,可以帮我们解决这个问题,看看修改后的makefile(注意第二行末尾与原来的区别):

1
2
3
4
5
6
7
example1: main.cpp hello.so
     $(CXX) $(CXXFLAGS) -o example1 main.cpp –ldl -rdynamic
hello.so: hello.cpp
     $(CXX) $(CXXFLAGS) -shared -o hello.so hello.cpp
clean:
     rm  -f example1 hello.so
.PHONY: clean

修改后,重新编译example1(make clean && make)。

此时,再看看example1的符号表(注意红色高亮部分):

$ nm -C example1 | grep singleton

08048ada t global constructors keyed to _ZN9singleton9pInstanceE

08048c96 W singleton::instance()

08049280 B singleton::pInstance

08048c88 W singleton::singleton()

$ nm -C -D example1 | grep singleton

08048c96 W singleton::instance()

08049280 B singleton::pInstance

08048c88 W singleton::singleton()

静态符号没有什么变化,但是动态符号却显示了更多的内容,其中包括我们想要的singleton::pInstance,也就是singleton的唯一实例。

OK,此时上面的问题已经解决,执行example1,输出结果如下:

singleton.num in main : 100

singleton.num in hello.so : 100

singleton.num in hello.so after ++ : 101

singleton.num in main : 101

此结果表明singleton在example1和hello.so之间只产生了一个实例。

 

插件设计建议

到目前为止,上面的问题已经解决,似乎这边文章应该结束了。但是,针对上面的问题,虽然从技术层面可以解决,但是此问题最好从设计层面上避免,养成良好的程序设计风格。

首先,我们需要明白一点,插件其实是一个独立的程序,它与主程序的不同在于他没有一个像main函数一样的入口,而是被主程序动态加载并调用相关接口。打个比喻,插件与主程序的关系好比主人(主程序)与仆人(插件)。主人通过接口向仆人发出命令,也就是调用仆人的相关函数。而仆人在执行命令的时候,不应该打扰主人,也就是不应该去调用主人的单例或其他全局变量。为了做到这一点,主人在命令中应该给出足够的信息,以便仆人能够顺利完成任务,也就是应该传入一些参数,这些参数可以由主程序统一读取,然后传给插件。这样,就不会出现类似上面两个单例实例的问题。

小结上面的比喻:主程序读取所有配置,将配置传给插件,插件在执行任务时,不要调用主程序的全局变量,而是通过局部变量,也就是参数和返回值的方式,与主程序交互。

 

参考资料

上面的解决方案是通过在stackOverflow中提问,得到的,本人只是将其翻译并总结,所以最后需要感谢一下stackOverflow中的热心的朋友,BourneLi是我的stackOverflow中的ID。问题链接如下:

http://stackoverflow.com/questions/8623657/multiple-instances-of-singleton-across-shared-libraries-on-linux

声明:如有转载本博文章,请注明出处。您的支持是我的动力!文章部分内容来自互联网,本人不负任何法律责任。
本文转自bourneli博客园博客,原文链接:http://www.cnblogs.com/bourneli/archive/2011/12/28/2305280.html ,如需转载请自行联系原作者
相关文章
|
4天前
|
缓存 Linux
Linux 文件IO简单实例
Linux 文件IO简单实例
15 1
|
4天前
|
Linux vr&ar C语言
|
4天前
|
存储 Linux 编译器
【Linux】详解动态库链接和加载&&对可执行程序底层的理解
【Linux】详解动态库链接和加载&&对可执行程序底层的理解
|
4天前
|
存储 Linux 编译器
【探索Linux】P.13(文件系统 | 软硬链接 | 动态库和静态库)
【探索Linux】P.13(文件系统 | 软硬链接 | 动态库和静态库)
16 0
|
4天前
|
存储 Linux 编译器
Linux静态库与动态库的使用
Linux静态库与动态库的使用
|
4天前
|
XML 安全 Linux
【Linux】深入探究CentOS防火墙(Firewalld):基础概念、常用命令及实例操作
【Linux】深入探究CentOS防火墙(Firewalld):基础概念、常用命令及实例操作
|
4天前
|
Linux 编译器 vr&ar
【库函数】Linux下动态库.so和静态库.a的生成和使用
【库函数】Linux下动态库.so和静态库.a的生成和使用
14 1
|
4天前
|
存储 Linux Windows
Linux挂载硬盘的详细步骤与实例
Linux挂载硬盘的详细步骤与实例
19 0
|
4天前
|
Linux 数据安全/隐私保护
Linux常用命令实例带注释
Linux常用命令实例带注释
36 0
|
4天前
|
Linux C语言
【Linux】C语言动态库和静态库的制作和使用
【Linux】C语言动态库和静态库的制作和使用