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