2017-11-06 9 views
2

Linux上でPOSIXタイマーシステム用のC++ "ラッパー"を作成しようとしています。そのため、C++プログラムはシステムクロックを使用せずに(ネットワーク経由でメッセージが到着するのを待つなど)タイムアウトを設定できます。 POSIXの醜いCインタフェースを扱います。ほとんどの場合はうまくいくと思われますが、プログラムが正常に実行されてから数分後にセグメンテーションが失敗することがあります。問題は、私のLinuxTimerManagerオブジェクト(またはメンバオブジェクトの1つ)がメモリを破壊しているようですが、残念ながら、Valgrindの下でプログラムを実行すると問題が現れないので、私のコードを見て、何が間違っているのか。POSIXタイマーはC++ STLオブジェクトを安全に変更できますか?

ここに私のタイマー・ラッパーの実装のコアがあります:

LinuxTimerManager.h

namespace util { 

using timer_id_t = int; 

class LinuxTimerManager { 
private: 
    timer_id_t next_id; 
    std::map<timer_id_t, timer_t> timer_handles; 
    std::map<timer_id_t, std::function<void(void)>> timer_callbacks; 
    std::set<timer_id_t> cancelled_timers; 
    friend void timer_signal_handler(int signum, siginfo_t* info, void* ucontext); 
public: 
    LinuxTimerManager(); 
    timer_id_t register_timer(const int delay_ms, std::function<void(void)> callback); 
    void cancel_timer(const timer_id_t timer_id); 
}; 

void timer_signal_handler(int signum, siginfo_t* info, void* ucontext); 
} 

LinuxTimerManager.cpp

namespace util { 

LinuxTimerManager* tm_instance; 

LinuxTimerManager::LinuxTimerManager() : next_id(0) { 
    tm_instance = this; 
    struct sigaction sa = {0}; 
    sa.sa_flags = SA_SIGINFO; 
    sa.sa_sigaction = timer_signal_handler; 
    sigemptyset(&sa.sa_mask); 
    int success_flag = sigaction(SIGRTMIN, &sa, NULL); 
    assert(success_flag == 0); 
} 

void timer_signal_handler(int signum, siginfo_t* info, void* ucontext) { 
    timer_id_t timer_id = info->si_value.sival_int; 
    auto cancelled_location = tm_instance->cancelled_timers.find(timer_id); 
    //Only fire the callback if the timer is not in the cancelled set 
    if(cancelled_location == tm_instance->cancelled_timers.end()) { 
     tm_instance->timer_callbacks.at(timer_id)(); 
    } else { 
     tm_instance->cancelled_timers.erase(cancelled_location); 
    } 
    tm_instance->timer_callbacks.erase(timer_id); 
    timer_delete(tm_instance->timer_handles.at(timer_id)); 
    tm_instance->timer_handles.erase(timer_id); 
} 

timer_id_t LinuxTimerManager::register_timer(const int delay_ms, std::function<void(void)> callback) { 
    struct sigevent timer_event = {0}; 
    timer_event.sigev_notify = SIGEV_SIGNAL; 
    timer_event.sigev_signo = SIGRTMIN; 
    timer_event.sigev_value.sival_int = next_id; 

    timer_t timer_handle; 
    int success_flag = timer_create(CLOCK_REALTIME, &timer_event, &timer_handle); 
    assert(success_flag == 0); 
    timer_handles[next_id] = timer_handle; 
    timer_callbacks[next_id] = callback; 

    struct itimerspec timer_spec = {0}; 
    timer_spec.it_interval.tv_sec = 0; 
    timer_spec.it_interval.tv_nsec = 0; 
    timer_spec.it_value.tv_sec = 0; 
    timer_spec.it_value.tv_nsec = delay_ms * 1000000; 
    timer_settime(timer_handle, 0, &timer_spec, NULL); 

    return next_id++; 
} 


void LinuxTimerManager::cancel_timer(const timer_id_t timer_id) { 
    if(timer_handles.find(timer_id) != timer_handles.end()) { 
     cancelled_timers.emplace(timer_id); 
    } 
} 

} 

私のプログラムがクラッシュし、 segfaultは常にtimer_signal_handler()から来ており、通常はtm_instance->timer_callbacks.erase(timer_id)またはtm_instance->timer_handles.erase(timer_id)。実際のセグメンテーションはstd::map実装の深いどこか(つまりstl_tree.h)からスローされます。

同じLinuxTimerManagerを変更している異なるタイマー信号間の競合状態によって、メモリが破損する可能性がありますか?一度に1つのタイマー信号しか届かないと思ったのですが、マニュアルページを誤解しているかもしれません。 std::mapのような複雑なC++オブジェクトをLinuxシグナルハンドラで変更するのは、一般的に危険ですか?

+2

シグナルハンドラから安全に呼び出せるのは、[async-signal-safe functions](https://stackoverflow.com/questions/8493095/what-c​​onstitutes-asynchronous-safeness)だけです。シグナルハンドラがアクセスするオブジェクトは必ずしも一貫した状態にあるとは限りません。 –

+1

シグナルハンドラはasync-signal-safe関数のみを呼び出すことがあります。関数が明示的にそのようにリストされていない限り(STLメンバ関数がメモリ割り当てを行っている場合は特に)、非同期シグナルセーフ関数。あなたができることは、揮発性のフラグを設定し、後でそれを通常のプログラムのコンテキストでチェックし、あなたのアクションをそこで実行することです。 – PSkocik

答えて

3

信号は、例えば、 mallocまたはfreeなので、コンテナで面白いことをするほとんどの呼び出しは、そのデータ構造が任意の状態にある間にメモリ割り当てサポートに再入力する可能性があります。 (コメントで指摘されているように、ほとんどの関数は非同期シグナルハンドラを呼び出すのに安全ではありません。mallocfreeは単なる例です)。このようにコンポーネントを再入力すると、かなりの任意の失敗が発生します。

ライブラリ内の操作中にプロセス全体のシグナルをブロックすることなく、この動作に対してライブラリを安全にすることはできません。そうすることは、信号マスクを管理するオーバーヘッドと信号がブロックされる時間の両方において、非常に高価です。 (シグナルハンドラがロックをブロックしてはならないので、プロセス全体でなければなりません。シグナルハンドラが必要とするmutexを他のスレッドが保持している間にmutexによって保護されたライブラリにシグナルを呼び出す場合、ハンドラはブロックします。これが起こるとデッドロックを回避するのは非常に難しい)。

これを回避する設計では、通常、特定のイベントをリッスンしてから処理を行うスレッドがあります。スレッドとシグナルハンドラを同期させるには、セマフォを使用する必要があります。

関連する問題