web-dev-qa-db-ja.com

Linux上の共有ライブラリにまたがるシングルトンの複数のインスタンス

タイトルが述べたように、私の質問は明白であり、シナリオを詳細に説明します。 singleton.hファイルには、次のようにシングルトンパターンによって実装されたシングルトンという名前のクラスがあります。

/*
 * 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

シングルトンクラスのインスタンスが2つあることがわかります。

どうして?

38
bourneli

最初に、共有ライブラリをビルドするときは通常、-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が必要な理由を理解するには、ダイナミックリンカーがシンボルを解決する方法、およびdynamicシンボルテーブルについて知っておく必要があります。

まず、hello.soの動的シンボルテーブルを見てみましょう。

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

これは、動的リンカーに表示される2つの弱い関数定義と1つのグローバル変数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が実行可能ファイルにグローバル変数として存在していても、そのシンボルはdynamicシンボルテーブルには存在しないため、ダイナミックリンカーには「見えません」。

ダイナミックリンカーは、example1singleton::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の定義が動的リンカーに対してvisibleとなり、hello.soをロードするときにその定義を「再利用」します。

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

ランタイムロードの共有ライブラリを使用する場合は注意が必要です。このような構成は厳密にはC++標準の一部ではないため、そのようなプロシージャのセマンティクスがどうなるかを慎重に検討する必要があります。

まず、何が起こっているかというと、共有ライブラリが独自の個別のグローバル変数singleton::pInstanceを見ているということです。何故ですか?実行時にロードされるライブラリは、本質的には、たまたまエントリポイントがない、独立した独立したプログラムです。しかし、他のすべては本当に別のプログラムのようであり、ダイナミックローダーはそれをそのように扱います。グローバル変数などを初期化する.

動的ローダーは、静的ローダーとは何の関係もないランタイム機能です。静的ローダーはC++標準実装の一部であり、メインプログラムのすべてのシンボルを解決しますbeforeメインプログラムが開始します。一方、ダイナミックローダーはafterのみを実行します。メインプログラムはすでに開始されています。特に、メインプログラムのすべての記号はすでに解決されている必要があります!メインプログラムからのシンボルを動的に自動的に置き換える単純なno方法があります。ネイティブプログラムは、体系的な再リンクを可能にするような方法で「管理」されません。 (おそらく何かがハッキングされる可能性がありますが、体系的で移植可能な方法ではありません。)

したがって、本当の問題は、どのように解決しようとしている設計の問題なのかです。ここでの解決策は、すべてのグローバル変数へのハンドルをプラグイン関数に渡すことです。メインプログラムにグローバル変数の元の(そして唯一の)コピーを定義させ、それへのポインターでライブラリーを初期化します。

たとえば、共有ライブラリは次のようになります。まず、シングルトンクラスにポインターツーポインターを追加します。

class singleton
{
    static singleton * pInstance;
public:
    static singleton ** ppinstance;
    // ...
};

singleton ** singleton::ppInstance(&singleton::pInstance);

どこでもpInstanceではなく*ppInstanceを使用してください。

プラグインで、メインプログラムからのポインタへのシングルトンを構成します。

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

そしてメイン関数は、プラグインの初期化を呼び出します:

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

init(singleton::ppInstance);
hello();

これで、プラグインは、他のプログラムと同じシングルトンインスタンスへのポインタを共有します。

5
Kerrek SB

私は簡単な答えがここにあると思います: http://www.yolinux.com/TUTORIALS/LibraryArchives-StaticAndDynamic.html

静的変数がある場合、それはオブジェクト(.o、.aや.so)に保存されます

実行される最後のオブジェクトに2つのバージョンのオブジェクトが含まれている場合、たとえば、SingletonオブジェクトのDestructorを呼び出すなど、予期しない動作が発生します。

メインファイルで静的メンバーを宣言したり、-rdynamic/fpicを使用したり、 ""コンパイラーディレクティブを使用したりするなど、適切な設計を使用すると、トリックの部分が役立ちます。

Makefileステートメントの例:

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

これがうまくいくことを願っています!

2
Paulo Lellis

ご回答ありがとうございます!

Linuxのフォローアップとして、_RTLD_GLOBAL_(およびその例)ごとに、dlopen(...)とともに_man dlopen_を使用することもできます。このディレクトリにOPの例のバリアントを作成しました: github tree 出力例: _output.txt_

素早く汚い:

  • 各シンボルをmainに手動でリンクする必要がない場合は、共有オブジェクトをそのままにしておきます。 (たとえば、Pythonにインポートする_*.so_オブジェクトを作成した場合)
  • 最初にグローバルシンボルテーブルに読み込むか、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
0
Eric Cousineau