シグナルハンドラーで共有データを処理すると、ほとんどの場合、スレッドを処理するだけでなく、混乱することになります。
デフォルトでは、シグナルハンドラーの実行中はシグナルがブロックされるため (少なくとも Linux では、これは一般的に当てはまらない可能性があります)、少なくともシグナルハンドラー自体がプリエンプトされることはありません。ただし、複数のスレッドがあり、シグナルが他のスレッドでブロックされていない場合、シグナル ハンドラーは複数のスレッド内で同時に実行される可能性があります。
1 つのスレッドがシグナルを受信してハンドラーを実行します。どのスレッドになるかは多かれ少なかれランダムですが、シグナルを処理したくないすべてのスレッドでシグナルをブロックすることで制御できます。
ただし、シグナルを処理するスレッドを妨げる他のスレッドは、並行して実行される可能性があります。シグナルを処理するスレッドは、プログラム内のほぼすべての時点でシグナル ハンドラーを実行できます (シグナルがブロックされない限り)。そのため、そのデータを保護するために何らかのロックが必要になります。問題は、通常のスレッド ロック プリミティブを使用できないことです。これらは、シグナル非同期セーフではありません。たとえば、シグナル ハンドラで pthread_mutex_t を取得しようとすると、プログラムが簡単にデッドロックしてしまいます。
シグナル ハンドラで安全に呼び出すことができる唯一の関数は、ここにリストされているものです。共有データの保護に関しては、一種の保護として sigblock()/sigunblock() を使用して、その共有データにアクセスしている間はシグナル ハンドラが実行されないようにすることができます。そうしないと、ブロックされていないスレッドのいずれかで実行されます。その道を進むのは狂気です。
シグナルハンドラで安全にアクセスできる唯一の共有データはほとんどsig_atomic_t
型であり、実際には他の種類のプリミティブ型も通常は安全です。
シグナルハンドラで本当にすべきことは、
- グローバル フラグを設定する
- 適切な場合はコードの他の場所でそのフラグを確認し、アクションを実行します
または
- select()/poll() などを使用してイベントのファイル記述子を監視する何らかのメインループがあります。
- パイプを作成し、メインループでそれを監視します
- write() シグナル ハンドラのパイプへのバイト
- メインループがそのパイプでイベントを検出したときに共有データを保護するなど、コードを実行してシグナルを処理します
または
- 予備のスレッドを保持する
- すべてのスレッドで指定されたシグナルをブロックします
- そのシグナルの配信を保証するシグナルマスクを使用して sigsuspend() を呼び出すときに、予備のスレッドループを用意します。
- sigsuspend() が返されたときにシグナルを処理するために共有データを保護するなど、コードを実行します