🙌

SIGNAL発生時にデストラクタが起動しないことに対する対策

12 min read 2

はじめに

C/C++のようにポインタを扱う場合に、
事前に決めていた範囲を超えてアクセス、未初期化変数を間違ってアクセスしてしまうことで、
SIGSEGV(Segmantation Fault)を発生させてしまう...というのはありがちなミスかと思います。
私だけ?

勿論SIGSEGVを起こさないように作るべきじゃないの?という疑問もありますが、
他作プログラムをマージして動かす場合もあり、回避するのが難しい場合もあります。

そんな時、プログラムにてデバイスにアクセスするような、
例えば、クラスのコンストラクタ(初期化処理時)にdevice create、デストラクタ(処理終了時)にdevice release、といったプログラムを書いている場合、
SIGSEGVによってdevice releaseが行われないまま異常終了してしまい、プログラムからデバイスにアクセスできなくなる...なんてこともあります。
ホットプラグ機能が備わっていることが多いUSBデバイスならまだしも、汎用的には未対応なPCI Expressデバイスだと再起動を要することもあります。

実際に上記のような場面に出くわし、私が対処した方法を記録しておきます。

SIGNALってそもそも何よ?という人はこちらを参照ください。

SIGSEGV発生時の挙動

まずはこんなプログラムを作ったと仮定します。

[test.cpp]

#include<iostream>
using namespace std;

class TestClass{
public:
    TestClass(){ cout << "constructer." << endl;}
    ~TestClass(){ cout << "destructor." << endl;}
};

int* segfault_gen = nullptr;
TestClass* globalptr = new TestClass;

int main()
{
    cout << *segfault_gen << endl;

    return 0;
}

このプログラムを実行すると、nullptrのsegfault_genにアクセスしようとして、SIGSEGVが発生します。

$ ./test.elf
constructer.
Segmentation fault (コアダンプ)

ここで見てわかるように、SIGSEGVが発生した場合、terminateによる強制終了によって、
デストラクタが呼ばれることなくプログラムが終了してしまいます。
try-catchによる例外とは異なるため、例外処理でも対処はできないです。

SIGSEGV発生時にデストラクタを呼び出す

SIGSEGVが出たとして、どうしてもデストラクタを呼び出したい、という場面も当然あるかと思います。
その時に使われるのがシグナルハンドラです。
signalシステムコール, sigactionシステムコールによって実装できますが、signal関数は移植性の問題がありPOSIX非推奨である。
ここでは両手法について紹介。

signal実装

使い方は下記。

#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t sighandler);  

試し実装がこちら。

[test2_signal.cpp]

#include<iostream>
#include<signal.h>
using namespace std;

class TestClass{
public:
    TestClass(){ cout << "constructer." << endl;}
    ~TestClass(){ cout << "destructor." << endl;}
};

int* segfault_gen = nullptr;
TestClass* globalptr = new TestClass;

void sigsegv_handler(int signal_number){
    cout << "SIGNAL: " << signal_number << endl;
    delete globalptr;
    exit(EXIT_FAILURE);
}

int main()
{
    if(SIGERR == signal(SIGSEGV, sigsegv_handler)){
        cerr << "cannot adapter signal handler." << endl;
        return -1;
    }
    cout << *segfault_gen << endl;
    return 0;
}

このコードを実行すると、下記のように出力されます。

$ ./test2_signal.elf 
constructer.
SIGNAL: 11
destructor.

上記を見てわかるようにSIGNAL: 11というSIGSEGVのsignal番号が出力され、デストラクタが起動されていることがわかります。

ちなみに、シグナルハンドラとしてsignalを使うのは避けるべきであり、
移植性の問題無しにsignalを使う場合は、SIG_DFL(デフォルト動作)かSIG_IGN(無視)だけが動作保証されることになっています。
基本的にはsigactionを使うと良いでしょう。

sigaction実装

使い方は下記。

#include <signal.h>

int sigaction(int signum, const struct sigaction *act,
                struct sigaction *oldact);

試し実装がこちら。

[test2_sigaction.cpp]
#include<iostream>
#include<signal.h>
#include<string.h>
using namespace std;

class TestClass{
public:
    TestClass(){ cout << "constructer." << endl;}
    ~TestClass(){ cout << "destructor." << endl;}
};

int* segfault_gen = nullptr;
TestClass* globalptr = new TestClass;

void sigsegv_handler(int signal_number, siginfo_t* info, void* ctx){
    cout << "SIGNAL: " << info->si_signo << endl;
    delete globalptr;
    exit(EXIT_FAILURE);
}

int main()
{
    struct sigaction sa_sigsegv;
    memset(&sa_sigsegv, 0, sizeof(sa_sigsegv)); // if not bss section, init value is indefinite.
    sa_sigsegv.sa_sigaction = sigsegv_handler;  // signal handler
    sa_sigsegv.sa_flags = SA_SIGINFO;           // if use signal handler, set SIGINFO.

    if( sigaction(SIGSEGV, &sa_sigsegv, NULL) < 0 ){
        cerr << "cannot adapt signal handler." << endl;
        return -1;
    }

    cout << *segfault_gen << endl;
    return 0;
}

このコードを実行すると、下記のように出力されます。

$ ./test2_sigaction.elf 
constructer.
SIGNAL: 11
destructor.

signalに比べると少し実装量が増しますが、移植性を考えて実装するとなれば、
こちらの実装を採用するのが確実だと考えられます。

local変数への対処

今までのコードでは、global変数のデストラクタを呼ぶようにしています。
しかし、実際のところglobal変数の使用は避けられていることが殆どですし、
ましてやclassを扱う場合ならなおさら使われないかと思います。
ローカル変数のデストラクタを呼び出したい場合の対処法を考えてみます。

handler呼び出し用のglobal変数ポインタで呼び出す

簡単に実施する方法です。

[test3_global.cpp]

#include<iostream>
#include<signal.h>
#include<string.h>
using namespace std;

class TestClass{
public:
    TestClass(){ cout << "constructer." << endl;}
    ~TestClass(){ cout << "destructor." << endl;}
};

class TestClass* sig_handle = nullptr;
int* segfault_gen = nullptr;

void sigsegv_handler(int signal_number, siginfo_t* info, void* ctx){
    cout << "SIGNAL: " << info->si_signo << endl;
    if(sig_handle != nullptr) delete sig_handle;
    exit(EXIT_FAILURE);
}

int main()
{
    struct sigaction sa_sigsegv;
    memset(&sa_sigsegv, 0, sizeof(sa_sigsegv)); // if not bss section, init value is indefinite.
    sa_sigsegv.sa_sigaction = sigsegv_handler;  // signal handler
    sa_sigsegv.sa_flags = SA_SIGINFO;           // if use signal handler, set SIGINFO.

    if( sigaction(SIGSEGV, &sa_sigsegv, NULL) < 0 ){
        cerr << "cannot adapt signal handler." << endl;
        return -1;
    }

    TestClass* autoptr = new TestClass;
    sig_handle = autoptr;

    cout << *segfault_gen << endl;
    return 0;
}

このコードを実行すると、下記のように出力されます。

$ ./test3_global.elf 
constructer.
SIGNAL: 11
destructor.

ただし、この方法だと以下の懸念点があります。

  • 知らない間にglobalなsig_handleが別のものを指してしまう恐れがある。
  • 不用意にglobal変数を生み出す。namespaceなどで対処もできるがあまり増やしたくない。
  • template classへの対処が難しい。

特に3番目は面倒で、
例えば下記のようにtemplate class実装されていると面倒な処置が必要になります。

[test3_global.cpp NG]
#include<iostream>
#include<signal.h>
#include<string.h>
using namespace std;

template<typename T>
class TestClass{
public:
    TestClass(){ cout << "constructer." << endl;}
    ~TestClass(){ cout << "destructor." << endl;}
};

class TestClass<float>* sig_handle = nullptr;
int* segfault_gen = nullptr;

void sigsegv_handler(int signal_number, siginfo_t* info, void* ctx){
    cout << "SIGNAL: " << info->si_signo << endl;
    if(sig_handle != nullptr) delete sig_handle;
    exit(EXIT_FAILURE);
}

int main()
{
    struct sigaction sa_sigsegv;
    memset(&sa_sigsegv, 0, sizeof(sa_sigsegv)); // if not bss section, init value is indefinite.
    sa_sigsegv.sa_sigaction = sigsegv_handler;  // signal handler
    sa_sigsegv.sa_flags = SA_SIGINFO;           // if use signal handler, set SIGINFO.

    if( sigaction(SIGSEGV, &sa_sigsegv, NULL) < 0 ){
        cerr << "cannot adapt signal handler." << endl;
        return -1;
    }

    TestClass<int>* autoptr = new TestClass<int>;
    sig_handle = autoptr;

    cout << *segfault_gen << endl;
    return 0;
}

当然ですが型が不一致なのでコンパイルエラーになります。

$ g++ test3_global.cpp -o test3_global.elf
test3_global.cpp: In function ‘int main()’:
test3_global.cpp:35:18: error: cannot convert ‘TestClass<int>*’ to ‘TestClass<float>*’ in assignment
     sig_handle = autoptr;

今回のケースならTestClass<int>だけ作っておけば対処できますが、
実際には考えられるインスタンス分だけ用意し、それぞれをデストラクタ呼び出ししないといけない、と考えるとかなり不便です。
下記で試しに実装してみることにします。

[test3_global_class.cpp]
#include<iostream>
#include<signal.h>
#include<string.h>
using namespace std;

template<typename T>
class TestClass{
public:
    TestClass(){ cout << "constructer." << endl;}
    ~TestClass(){ cout << "destructor." << endl;}
};

class TestClass<float>* sig_handle_float = nullptr;
class TestClass<int>* sig_handle_int = nullptr;

int* segfault_gen = nullptr;

void sigsegv_handler(int signal_number, siginfo_t* info, void* ctx){
    cout << "SIGNAL: " << info->si_signo << endl;
    if(sig_handle_float != nullptr) delete sig_handle_float;
    if(sig_handle_int != nullptr) delete sig_handle_int;
    exit(EXIT_FAILURE);
}

int main()
{
    struct sigaction sa_sigsegv;
    memset(&sa_sigsegv, 0, sizeof(sa_sigsegv)); // if not bss section, init value is indefinite.
    sa_sigsegv.sa_sigaction = sigsegv_handler;  // signal handler
    sa_sigsegv.sa_flags = SA_SIGINFO;           // if use signal handler, set SIGINFO.

    if( sigaction(SIGSEGV, &sa_sigsegv, NULL) < 0 ){
        cerr << "cannot adapt signal handler." << endl;
        return -1;
    }

    TestClass<int>* autoptr = new TestClass<int>;
    sig_handle_int = autoptr;

    TestClass<float>* autoptr2 = new TestClass<float>;
    sig_handle_float = autoptr2;

    cout << *segfault_gen << endl;
    return 0;
}

このコードを実行すると、下記のように出力されます。

$ ./test3_global_template.elf 
constructer.
constructer.
SIGNAL: 11
destructor.
destructor.

動くには動きますが、動けばいいだけのコード、という状態です。

ローカル関数化したシグナルハンドラを定義して呼び出す

通常、C++では関数内にローカル関数を定義することはできません。
ですが、ローカル構造体やローカルclassを定義することは問題ないため、
構造体やclassをWrapperにして無理やりローカルなシグナルハンドラを作り出すことはできます。

注意点としては、

  • ローカル構造体内から構造体外の変数に触れる際にはstaticな変数を用意する。そのため静的領域の制約に注意。
  • シグナルハンドラもstaticにしておかないとコンパイルエラーが生じる。これは関数ポインタによるアクセスにおいてstaticなことが必須なため。
[test3_localfunc.cpp]
#include<iostream>
#include<signal.h>
#include<string.h>
using namespace std;

class TestClass{
public:
    TestClass(){ cout << "constructer." << endl;}
    ~TestClass(){ cout << "destructor." << endl;}
};

int* segfault_gen = nullptr;

int main()
{
    static TestClass* autoptr = new TestClass;

    struct LocalFunc{
        static void sigsegv_handler(int signal_number, siginfo_t* info, void* ctx){
            cout << "SIGNAL: " << info->si_signo << endl;
            if(autoptr != nullptr) delete autoptr;
            exit(EXIT_FAILURE);
        }
    };

    struct sigaction sa_sigsegv;
    memset(&sa_sigsegv, 0, sizeof(sa_sigsegv));           // if not bss section, init value is indefinite.
    sa_sigsegv.sa_sigaction = LocalFunc::sigsegv_handler; // signal handler
    sa_sigsegv.sa_flags = SA_SIGINFO;                     // if use signal handler, set SIGINFO.

    if( sigaction(SIGSEGV, &sa_sigsegv, NULL) < 0 ){
        cerr << "cannot adapt signal handler." << endl;
        return -1;
    }

    cout << *segfault_gen << endl;
    return 0;
}

このコードを実行すると、下記のように出力されます。

$ ./test3_localfunc.elf 
constructer.
SIGNAL: 11
destructor.

これだけだとあまり旨味がない気もしますが、template使用時にはそこそこ便利なシグナルハンドラが定義できます。

[test3_localfunc_template.cpp]
#include<iostream>
#include<signal.h>
#include<string.h>
using namespace std;

template<typename T>
class TestClass{
public:
    TestClass(){ cout << "constructer." << endl;}
    ~TestClass(){ cout << "destructor." << endl;}
};

int* segfault_gen = nullptr;

template<typename T>
void test_func(){
    static TestClass<T>* autoptr = new TestClass<T>;

    struct LocalFunc{
        static void sigsegv_handler(int signal_number, siginfo_t* info, void* ctx){
            cout << "SIGNAL: " << info->si_signo << endl;
            if(autoptr != nullptr) delete autoptr;
            exit(EXIT_FAILURE);
        }
    };

    struct sigaction sa_sigsegv;
    memset(&sa_sigsegv, 0, sizeof(sa_sigsegv));           // if not bss section, init value is indefinite.
    sa_sigsegv.sa_sigaction = LocalFunc::sigsegv_handler;  // signal handler
    sa_sigsegv.sa_flags = SA_SIGINFO;                     // if use signal handler, set SIGINFO.

    if( sigaction(SIGSEGV, &sa_sigsegv, NULL) < 0 ){
        cerr << "cannot adapt signal handler." << endl;
        return;
    }

    cout << *segfault_gen << endl;
}

int main()
{
    test_func<float>();
    return 0;
}

このコードを実行すると、下記のように出力されます。

$ ./test3_localfunc_template.elf 
constructer.
SIGNAL: 11
destructor.

この場合、例え呼び出し元をfloatからintに変える場合でも特に追加対応が不要です。

なお、上記のコードは非最適化前提になっているので、
最適化実行時は先にexit処理されることがあります。
optimize pragmaディレクティブなどで該当箇所だけ防いでおくと良いでしょう。

最後に

これは実際に私が直面したトラブルの対処方法を忘れないように備忘録としてまとめました。
私自身もまだまだ勉強中の身ですので、より最適な方法もあるかと思います。
より良い方法を知っている等、詳しい方がいたらコメントいただけると幸いです。

Discussion

シグナルハンドラによる回復処理はアテにできないケースが数多くあります。プロセスのクラッシュに対処する外部の仕組み(systemdに再起動させるとか)も検討する価値があるかもしれません。

シグナルハンドラによる回復処理に頼らずに再実行できれば、 CTRL+C で止めてプロセスだけ再起動するとかも可能になり、開発効率の面でもメリットが有りそうです。

シグナルハンドラがうまく作用しないケースとしては、例えば:

  1. スタックオーバーフローでシグナルハンドラが起動できないケースSIGSEGV の場合シグナルハンドラは発生スレッドのスタック上で起動されるため、SIGSEGVの要因自体がスタックオーバーフローだったりするとシグナルハンドラが起動できないというオチがあり得ます。 SA_ONSTACK やsigaltstack(2)で問題を緩和できます。
  2. ハンドラ内で使用されるシステムコールが非同期シグナル安全でないケース 。Linuxであれば signal-safety(7) で解説されていますが、シグナルハンドラ内で呼んで良いシステムコールは限定されています。ハンドラ内で実行されるシステムコールやライブラリの把握が難しくなるので、ハンドラの記述を必要以上に抽象化するのは好ましくないケースが多いと思います。
  3. そもそもライブラリが脱出に安全でないケース 。たとえば SIG30-C. シグナルハンドラ内では非同期安全な関数のみを呼び出す などで触れられていますが、シグナルハンドラでは本来 printf さえ呼出してはいけません。C++例外は関数を脱出する前にデストラクタや例外ハンドラを実行する機会を与えますが、(longjmpや、)シグナル発生による脱出はそうではないので、想定外の事態を発生させる可能性があります。
  4. (マルチスレッドで同発するケース、メモリ不足ではシグナルハンドラは起動できない、 ...)

... 現実にはこの手のシグナルハンドラの制約を無視してでも(最後のあがきとして)何かハンドリングしようとしているプログラムは多いです。そのような必要性がある場合でも、シグナルハンドラの制約を知っているとトラブルシュートに役立つと思います。

コメントありがとうございます!
割り込みハンドラでも気にすべきケースはありますし、勿論シグナルハンドラが最適でないケースがあるという話は伺ってましたが、関数呼び出しであることそのものがアテにならない、というケースは盲点でした。
プロセス管理側で回復させる方向での手法も調べてみようと思います。

ログインするとコメントできます