Safe C and C ++ Programming、2nd Editionの作者であるRobert Sikordは、シグナルハンドラーの共有オブジェクトへのアクセスが、データの不整合を引き起こす可能性のある競合につながる方法について説明しています。 歴史的に、シグナルハンドラから共有オブジェクトにアクセスする唯一の適切な方法は、 volatile型sig_atomic_tの変数を読み書きすることでした 。 C11の出現により、アトミックオブジェクトは、シグナルハンドラで共有オブジェクトにアクセスするための最良の選択肢になりました。
CERT Cコーディング標準、第2版:安全、信頼性、および安全なシステムを開発するための98の規則、第2版は、C11およびC ISO / IEC TS 17961に準拠するように更新されました。 、SIG31-C:「シグナルハンドラで共有オブジェクトにアクセスしないでください。」 このルールが存在するのは、シグナルハンドラ内の共有オブジェクトへのアクセスが競合を引き起こし、データの不整合を引き起こす可能性があるためです。 この記事では、シグナルハンドラから共有オブジェクトにアクセスするための追加情報を提供します。 本のルールと例の説明を超えて行きます。
この規則は、CERT C Secure Coding Standardの第1版に存在していましたが、その本のトピックはC99であり、アトミックオブジェクトはまだ定義されていないため、シグナルハンドラーから共有オブジェクトにアクセスする唯一の適切な方法は、型の変数の読み取りまたは書き込みでしたvolatile sig_atomic_t 次のプログラムは、 揮発性sig_atomic_t変数のe_flagを決定するSIGINTハンドラーをインストールし、終了する前にハンドラーが呼び出されたかどうかを確認します。
#include <signal.h> #include <stdlib.h> #include <stdio.h> volatile sig_atomic_t e_flag = 0; void handler(int signum) { e_flag = 1; } int main(void) { if (signal(SIGINT, handler) == SIG_ERR) { return EXIT_FAILURE; } /* */ if (e_flag) { puts("SIGINT ."); } else { puts("SIGINT ."); } return EXIT_SUCCESS; }
C11、5.1.2.3、条項5では、シグナルハンドラーが非ブロッキングアトミックオブジェクトの読み取りと書き込みを行うこともできます。 以下は、アトミックフラグへのアクセスの単純な(ただし非標準の)例です。 atomic_flagタイプは、古典的なチェックインストール機能を提供します。 設定とクリアの2つの状態があり、C標準では、 atomic_flag型のオブジェクトに対する操作がブロックされないようにします。
#include <signal.h> #include <stdlib.h> #include <stdio.h> #if __STDC_NO_ATOMICS__ != 1 #include <stdatomic.h> #endif atomic_flag e_flag = ATOMIC_FLAG_INIT; void handler(int signum) { (void)atomic_flag_test_and_set(&e_flag); } int main(void) { if (signal(SIGINT, handler) == SIG_ERR) { return EXIT_FAILURE; } /* */ if (atomic_flag_test_and_set(&e_flag)) { puts("SIGINT ."); } else { puts("SIGINT ."); } return EXIT_SUCCESS; }
atomic_flagタイプは、アトミックオブジェクトのサポートがある場合にのみ、保証された非ブロッキングタイプです。 atomic_flagタイプは 、シグナルハンドラーから使用できることが保証されている唯一のタイプでもあります。 それにもかかわらず、このタイプのオブジェクトは、アトミック関数の呼び出しに対してのみ確実にアクセスでき、そのような呼び出しは許可されていません。 標準C 7.14.1.1の条項5に従って、シグナルハンドラーが標準ライブラリの関数を呼び出す場合、未処理の動作が発生します。ただし、_abort、_Exit、quick_exit 、およびハンドラーを呼び出したシグナルに対応するシグナル番号に等しいシグナル番号を持つシグナル関数は例外です。
ほとんどのCライブラリ関数は非同期環境で安全に実行する必要がないため、この制限が存在します。 標準を変更せずにこの問題を解決するには、別のアトミックタイプ( atomic_intなど)を使用して例を書き換える必要があります 。
#include <signal.h> #include <stdlib.h> #if __STDC_NO_ATOMICS__ != 1 #include <stdatomic.h> #endif atomic_int e_flag = ATOMIC_VAR_INIT(0); void handler(int signum) { e_flag = 1; } int main(void) { if (signal(SIGINT, handler) == SIG_ERR) { return EXIT_FAILURE; } /* */ if (e_flag) { puts("SIGINT ."); } else { puts("SIGINT ."); } return EXIT_SUCCESS; }
このソリューションは、 atomic_int型が常に非ブロッキングであるプラットフォームで成功します。 次のコードにより、アトミックタイプがサポートされていない場合、またはatomic_intタイプが非ブロッキングでない場合、コンパイラーは診断メッセージを出力します。
#if __STDC_NO_ATOMICS__ == 1 #error " " #elif ATOMIC_INT_LOCK_FREE == 0 #error "int " #endif
ATOMIC_INT_LOCK_FREEマクロには次のものがあります。
値0は、このタイプが非ブロッキングではないことを示します。
値1は、このタイプが非ブロッキングであることを示します。
値2は、このタイプが常に非ブロッキングであることを示します。
タイプが非ブロッキングの場合がある場合、 atomic_is_lock_free関数を実行時に呼び出して、タイプが非ブロッキングかどうかを判断する必要があります。
#if ATOMIC_INT_LOCK_FREE == 1 if (!atomic_is_lock_free(&e_flag)) { return EXIT_FAILURE; } #endif
一部のアーキテクチャでは、一部のプロセッサオプションがExchangeとの非ブロック比較をサポートする一方で、他のサポートはサポートしないため(80386や80486など)、アトミックタイプは非ブロッキングである場合があります。 プロセッサの種類によっては、アプリケーションが特定の動的ライブラリに関連付けられている場合があります。 したがって、 ATOMIC_INT_LOCK_FREE == 1の実装の動的検証を有効にする必要があります。 このプログラムは、 atomic_int型がブロックされていない実装で機能します。
#include <signal.h> #include <stdlib.h> #if __STDC_NO_ATOMICS__ != 1 #include <stdatomic.h> #endif #if __STDC_NO_ATOMICS__ == 1 #error " " #elif ATOMIC_INT_LOCK_FREE == 0 #error "int " #endif atomic_int e_flag = ATOMIC_VAR_INIT(0); void handler(int signum) { e_flag = 1; } int main(void) { #if ATOMIC_INT_LOCK_FREE == 1 if (!atomic_is_lock_free(&e_flag)) { return EXIT_FAILURE; } #endif if (signal(SIGINT, handler) == SIG_ERR) { return EXIT_FAILURE; } /* */ if (e_flag) { puts("SIGINT ."); } else { puts("SIGINT ."); } return EXIT_SUCCESS; }
変数e_flagが volatileとして宣言されていない理由については、引き続き議論します。 volatile sig_atomic_tを使用した最初の例とは異なり、アトミックタイプオブジェクトの読み込みと保存は、 memory_order_seq_cstのセマンティクスを使用して行われます。 一貫性のある一貫したプログラムは、複合スレッドによって実行される操作が単純に交互に行われるかのように動作し、オブジェクトの値の各計算は、このローテーションで保存される最後の値です。 アトミック操作の引数はvolatile A *として定義されており、アトミックオブジェクトを必要とせずにvolatileとして宣言できます。
C規格委員会(WG14)は、一般に、並行性サポートの定義においてC ++規格委員会(WG21)の例に従いました。 WG21委員会の目的は、ノンブロッキングアトミックタイプをC ++ 11のシグナルハンドラに適用できるようにすることでした。 残念ながら、WG21がC ++ 14で修正しようとしているいくつかのエラーが発生しました。 C ++でシグナルハンドラの動作を定義するための最新の提案は、WG21 / N3910です。 国際標準C ++ 14ドラフトに次のエントリが追加されました。
「raise関数を呼び出した結果として実行されるシグナルハンドラは、raise関数呼び出しと同じ実行スレッドに属します。 その他の場合、どの実行スレッドにシグナルハンドラー呼び出しが含まれているかは示されません。
POSIXでは、プロセスに対して、またはプロセス内の特定のスレッドに対してシグナルを生成するかどうかを決定する必要があります。 特定のストリームに関連する何らかのアクション(ハードウェアの誤動作など)によって生成された信号は、信号を生成させたストリームに対して生成されます。 プロセスID、プロセスグループID、またはターミナルアクティビティなどの非同期イベントに関連して生成された信号は、プロセスに対して生成されます。
可変オブジェクトへのアクセスは、抽象マシンのルールに従って厳密に評価されます。 揮発性オブジェクトのアクションは、実装を使用して最適化できません。 アトミックオブジェクトが利用可能になる前に、 volatileは、シグナルプロセッサと共有されるオブジェクトに必要なセマンティクスに最も近い一致を提供しました。 アトミックオブジェクトは、シグナルハンドラーで共有オブジェクトにアクセスするのに最適な選択肢です。これは、 volatileが他のスレッドとの関連で可視性の範囲を編成しないため、異なるスレッドでの動作方法の定義を大幅に複雑にするためです。 したがって、 volatile sig_atomic_tは、同じスレッドで実行されているハンドラーとの通信にのみ使用できます。
標準Cでは、マルチスレッドプログラムにシグナルハンドラをインストールできません。 特に、C11は、マルチスレッドプログラムでシグナル関数を使用することは未定義の動作であると主張しているため、マルチスレッドプログラムでのシグナル処理に関するほとんどの議論は、Cセマンティクスに対応するマルチスレッドプログラムの純粋な理論です。
次の例は、このプログラムの最もコンパクトなバージョンです。 この例では型置換が使用されているため、コンパイル段階ですべてを知っておく必要があります。 この例では、非ブロッキングアトミックタイプの可用性がコンパイル段階で決定できる場合、アトミックタイプを使用します。 それ以外の場合は、 volatile sig_atomic_tを使用します。 したがって、値ATOMIC_INT_LOCK_FREE == 1の場合、これはゼロであるかのように扱われます。
#include <signal.h> #include <stdlib.h> #include <stdio.h> #if __STDC_NO_ATOMICS__ != 1 #include <stdatomic.h> #endif #if __STDC_NO_ATOMICS__ == 1 typedef volatile sig_atomic_t flag_type; #elif ATOMIC_INT_LOCK_FREE == 0 || ATOMIC_INT_LOCK_FREE == 1 typedef volatile sig_atomic_t flag_type; #else typedef atomic_int flag_type; #endif flag_type e_flag; void handler(int signum) { e_flag = 1; } int main(void) { if (signal(SIGINT, handler) == SIG_ERR) { return EXIT_FAILURE; } /* */ if (e_flag) { puts("SIGINT ."); } else { puts("SIGINT ."); } return EXIT_SUCCESS; }
C標準によると、静的またはローカルスレッドストレージ領域を持つオブジェクトのデフォルトの初期化(ゼロ)は、有効な状態を生成することが保証されています。 これは、この例または他の例でe_flagオブジェクトを明示的に初期化する必要がないことを意味します。
結論
シグナルハンドラからの共有オブジェクトへのアクセスは、現在CとC ++の両方で問題があります(C ++ 14でこれらの問題を解決したい)。 現時点では、シグナルハンドラからアトミックフラグ関数を呼び出すことができるように標準Cを修正する意見があり、そのような提案はWG14で行われました。 オースティングループは、リリース8のC11とPOSIXの統合に取り組んでいます。マルチスレッドプログラムでsignal関数を使用することは未定義の動作であるため、POSIXは公式に未定義の動作を定義することで言語を強化できます。 長期的には、CおよびC ++標準委員会は、 揮発性sig_atomic_tから遠ざかる傾向があります。これは、マルチスレッド実行をサポートしておらず、また現在アトミック型が最良の代替手段であるためです。
この記事に貢献してくれてありがとう:アーロン・ボールマン、ジョン・ベニート、ハンス・ベーム、ジェフ・クレア、ロビン・ドレイク、イェンス・ガステッド、デヴィッド・キートン、キャロル・ラリエ、ダニエル・プラコシュ、マーティン・セボール、ダグラス・ウォールズ。
Pearson Educationの許可を得て公開。
オリジナル記事 。
Robert Sikordのマスタークラスは、 11月26〜27日にオンライン形式で開催され 、CおよびC ++での安全なプログラミングに専念します。