Linux上跨共享库的多个单例实例

Multiple instances of singleton across shared libraries on Linux

本文关键字:单例 实例 共享 Linux      更新时间:2023-10-16

正如标题所提到的,我的问题很明显,我详细描述了这个场景。在文件singleton.h:

中有一个名为singleton的类,由singleton模式实现如下
/*
 * singleton.h
 *
 *  Created on: 2011-12-24
 *      Author: bourneli
 */
#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_H_ */

那么,有一个名为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;
}

你可以看到插件调用了单例,并且改变了单例中的属性num。

最后,有一个主函数使用单例和插件如下:

#include <iostream>
#include <dlfcn.h>
#include "singleton.h"
int main() {
    using std::cout;
    using std::cerr;
    using std::endl;
    singleton::instance().num = 100; // call singleton
    cout << "singleton.num in main : " << singleton::instance().num << endl;// call singleton
    // 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': " << dlerror() << 'n';
        dlclose(handle);
        return 1;
    }
    hello(); // call plugin function hello
    cout << "singleton.num in main : " << singleton::instance().num << endl;// call singleton
    dlclose(handle);
}

和makefile如下:

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

那么,输出是什么?我认为有以下内容:

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

证明单例类有两个实例。

为什么?

首先,在构建共享库时通常应该使用-fPIC标志。

不使用

在32位Linux上"工作",但在64位Linux上会失败,并出现类似以下错误:

/usr/bin/ld: /tmp/ccUUrz9c.o: relocation R_X86_64_32 against `.rodata' can not be used when making a shared object; recompile with -fPIC
第二,在主可执行文件的链接行中添加-rdynamic后,程序将按预期工作:

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

为了理解为什么需要-rdynamic,您需要了解动态链接器解析符号的方式,以及动态符号表。

首先,让我们看一下hello.so的动态符号表:

$ nm -C -D hello.so | grep singleton
0000000000000b8c W singleton::instance()
0000000000201068 B singleton::pInstance
0000000000000b78 W singleton::singleton()

这告诉我们有两个弱函数定义和一个全局变量singleton::pInstance对动态链接器可见。

现在让我们看看原始example1(链接中没有-rdynamic)的静态和动态符号表:

$ nm -C  example1 | grep singleton
0000000000400d0f t global constructors keyed to singleton::pInstance
0000000000400d38 W singleton::instance()
00000000006022e0 B singleton::pInstance
0000000000400d24 W singleton::singleton()
$ nm -C -D example1 | grep singleton
$ 

这是正确的:即使singleton::pInstance作为一个全局变量出现在可执行文件中,该符号也不存在于动态符号表中,因此对动态链接器是"不可见的"。

因为动态链接器"不知道"example1已经包含了singleton::pInstance的定义,所以它不会将hello.so中的变量绑定到现有的定义(这是您真正想要的)。

当我们将-rdynamic添加到链接行:

$ nm -C  example1-rdynamic | grep singleton
0000000000400fdf t global constructors keyed to singleton::pInstance
0000000000401008 W singleton::instance()
00000000006022e0 B singleton::pInstance
0000000000400ff4 W singleton::singleton()
$ nm -C -D  example1-rdynamic | grep singleton
0000000000401008 W singleton::instance()
00000000006022e0 B singleton::pInstance
0000000000400ff4 W singleton::singleton()

现在主可执行文件中的singleton::pInstance的定义是可见的动态链接器,所以它将"重用"该定义时加载hello.so:

LD_DEBUG=bindings ./example1-rdynamic |& grep pInstance
     31972: binding file ./hello.so [0] to ./example1-rdynamic [0]: normal symbol `_ZN9singleton9pInstanceE'

在使用运行时加载的共享库时必须小心。严格来说,这样的构造不是c++标准的一部分,您必须仔细考虑这样一个过程的语义是什么。

首先,发生的事情是共享库看到了自己的独立全局变量singleton::pInstance。为什么呢?在运行时加载的库本质上是一个单独的、独立的程序,只是碰巧没有入口点。但是其他的东西就像一个单独的程序,动态加载器会这样对待它,例如初始化全局变量等。

动态加载器是一个运行时工具,与静态加载器无关。静态加载器是c++标准实现的一部分,它在主程序启动之前解析所有主程序的符号。另一方面,动态加载器只在主程序已经启动之后才运行。特别是,主程序的所有符号都必须被解析!简单的没有方法可以动态地自动替换主程序中的符号。原生程序不以任何允许系统链接的方式进行"管理"。(也许有些东西可以被黑,但不是以系统的、可移植的方式。)

所以真正的问题是如何解决你正在尝试的设计问题。这里的解决方案是将所有全局变量的句柄传递给插件函数。让主程序定义全局变量的原始(且唯一)副本,并使用指向该全局变量的指针初始化库。 例如,您的共享库可能如下所示。首先,向单例类添加一个指针对指针:
class singleton
{
    static singleton * pInstance;
public:
    static singleton ** ppinstance;
    // ...
};
singleton ** singleton::ppInstance(&singleton::pInstance);

现在用*ppInstance代替pInstance

在插件中,将单例配置为主程序的指针:

void init(singleton ** p)
{
    singleton::ppInsance = p;
}

和main函数,调用插件初始化:

init_fn init;
hello_fn hello;
*reinterpret_cast<void**>(&init) = dlsym(lib, "init");
*reinterpret_cast<void**>(&hello) = dlsym(lib, "hello");
init(singleton::ppInstance);
hello();

现在插件与程序的其他部分共享同一个指向单例实例的指针

我认为简单的答案是:http://www.yolinux.com/TUTORIALS/LibraryArchives-StaticAndDynamic.html

当您有一个静态变量时,它存储在对象(.o,。A and/or .so)

如果要执行的最终对象包含该对象的两个版本,则行为是不可预期的,例如,调用单例对象的析构函数。

使用适当的设计,例如在主文件中声明静态成员,使用-rdynamic/fpic和使用"编译器指令将为您完成技巧部分。

示例makefile语句:

$ g++ -rdynamic -o appexe $(OBJ) $(LINKFLAGS) -Wl,--whole-archive -L./Singleton/ -lsingleton -Wl,--no-whole-archive $(LIBS) 

希望这有效!

谢谢大家的回答!

作为Linux的后续,您还可以根据man dlopen(及其示例)使用RTLD_GLOBALdlopen(...)。我在这个目录下做了一个OP示例的变体:github树输出示例:output.txt

又快又脏:

  • 如果你不想手动链接每个符号到你的main,保持共享对象。(例如,如果你让*.so对象导入Python)
  • 你可以初始加载到全局符号表,或者做一个NOLOAD + GLOBAL重新打开。
代码:

#if MODE == 1
// Add to static symbol table.
#include "producer.h"
#endif
...
    #if MODE == 0 || MODE == 1
        handle = dlopen(lib, RTLD_LAZY);
    #elif MODE == 2
        handle = dlopen(lib, RTLD_LAZY | RTLD_GLOBAL);
    #elif MODE == 3
        handle = dlopen(lib, RTLD_LAZY);
        handle = dlopen(lib, RTLD_LAZY | RTLD_NOLOAD | RTLD_GLOBAL);
    #endif

模式:

  • 模式0:名义延迟加载(不工作)
  • 模式1:包含要添加到静态符号表的文件
  • 模式2:初始加载使用RTLD_GLOBAL
  • 模式3:使用RTLD_NOLOAD | RTLD_GLOBAL重新加载