【问题标题】:Multiple instances of singleton across shared libraries on LinuxLinux 上跨共享库的多个单例实例
【发布时间】:2011-12-24 08:59:29
【问题描述】:

正如标题所提到的,我的问题很明显,我详细描述了这个场景。 在singleton.h文件中有一个由单例模式实现的名为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;
}

可以看到插件调用了singleton并改变了singleton中的属性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);
}

生成文件如下:

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

证明单例类有两个实例。

为什么?

【问题讨论】:

  • 你想让这个单例成为你正在执行的一个进程的单例吗?还是违反所有受保护内存为我们提供的“系统范围”单例?
  • 除非明确说明,否则问题通常并不明显。您是否希望共享库共享单例?您是否对任何一种行为进行理论化或实际体验?除非您告诉我们,否则无法知道。
  • @sarnold:有一种众所周知的系统范围的单例模式不受地址空间的限制:它被称为服务器。但在原发帖人说出他的代码的用途之前,很难判断这种模式是否适合。
  • @9000:哈哈!好点子。 :) 这里仍然没有类似服务器行为的证据,所以我认为不是这样。 :)

标签: c++ singleton dlopen


【解决方案1】:

首先,在构建共享库时,您通常应该使用-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'

【讨论】:

  • -rdynmaic 选项解决了这个问题,谢谢。感谢您的帮助:)
  • 你能解释一下如何做相反的事情吗?我有一个共享基库,两个插件链接到,共享库导出一个单例;但我希望每个插件都维护自己的单例副本。我没有将 -rdynamic 列为编译标志。
  • @DanielMoodie 我确实知道您的问题的答案适用于某些操作系统。但在我提供这个答案之前,你需要问一个(单独的)问题。
  • 我已经在这里发布了我的问题stackoverflow.com/questions/40685771/…
  • 除了使用-rdynamic之外,还要检查你的构建系统没有添加-fvisibility=hidden选项! (因为它会完全丢弃-rdynamic的效果)
【解决方案2】:

在使用运行时加载的共享库时必须小心。这种构造严格来说不是 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();

现在插件与程序的其余部分共享指向单例实例的相同指针。

【讨论】:

  • 如果你强制“singleton”用某个全局地址初始化,那么它就不再是一个singleton了。您的答案在许多细节上都不正确,您提出的解决方案是(恕我直言)伪造的。
  • 我同意。这个解决方案失去了使用单例模式的全部目的。
【解决方案3】:

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

当你有一个静态变量时,它存储在对象中(.o、.a 和/或 .so)

如果要执行的最终对象包含该对象的两个版本,则会出现意外行为,例如调用 Singleton 对象的析构函数。

使用正确的设计,例如在主文件中声明静态成员并使用 -rdynamic/fpic 和使用 "" 编译器指令将为您解决问题。

makefile 语句示例:

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

希望这行得通!

【讨论】:

    【解决方案4】:

    谢谢大家的回答!

    作为 Linux 的后续,您还可以使用 RTLD_GLOBALdlopen(...),根据 man dlopen(以及它的示例)。我在此目录中制作了 OP 示例的变体:github tree 示例输出: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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-11-11
      • 1970-01-01
      • 1970-01-01
      • 2022-01-12
      • 1970-01-01
      • 1970-01-01
      • 2011-09-03
      • 2020-12-05
      相关资源
      最近更新 更多