動態庫之間單例模式出現多個實例(Linux)


本文目的

前幾天在開發中遇到一個古怪的問題,定位了兩天左右的時間才發現問題。該問題正如題目所描述:單一模式在動態鏈接庫之間出現了多個實例。由於該實例是一個配置管理器,許多配置信息都在這個實例的初始化過程中讀取,一旦出錯,系統的其他地方都無法正確運行,所以給問題定位帶來一定難度。為了避免敏感信息的泄漏,同時為了便於大家理解,將問題簡化,在此與大家分享。

 

問題描述

首先,編寫一個簡單的單一模式的類,文件singleton.h內容如下。

#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文件中,如下:

#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文件:

#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在同一個目錄中):

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(注意第二行末尾與原來的區別):

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM