C++ std::signalの罠?よくあるエラーとトラブルシューティング
std::signal
の基本的な使い方
std::signal
関数の基本的な構文は以下の通りです。
#include <csignal> // ヘッダファイル
// シグナルハンドラ関数の型
typedef void (*__sighandler_t)(int);
__sighandler_t std::signal(int sig, __sighandler_t func);
引数は以下の通りです。
func
: そのシグナルが発生したときに呼び出される関数へのポインタ(シグナルハンドラ)です。この関数はint
型の引数を1つ取り、void
を返します。sig
: 処理したいシグナルを表す整数値です。<csignal>
ヘッダで定義されているマクロを使用します。例:SIGINT
,SIGTERM
,SIGSEGV
など。
戻り値は、以前にそのシグナルに設定されていたシグナルハンドラへのポインタです。エラーが発生した場合はSIG_ERR
を返します。
シグナルハンドラの種類
func
には以下のいずれかを指定できます。
SIG_DFL
: シグナルに対するデフォルトの動作を設定します。各シグナルにはOSが定義するデフォルトの動作があります(例: プログラムの終了、コアダンプの生成など)。SIG_IGN
: シグナルを無視します。そのシグナルが発生しても、プログラムは何も処理せず続行されます。- カスタム関数へのポインタ: ユーザーが定義した関数をシグナルハンドラとして設定します。シグナルが発生すると、この関数が呼び出されます。
例
Ctrl+C (SIGINT) が押されたときに "Ctrl+Cが押されました!" と表示して終了するプログラムの例です。
#include <iostream>
#include <csignal> // std::signal を使うために必要
#include <chrono> // 時間関連
#include <thread> // スレッド関連
// シグナルハンドラ関数
void signalHandler(int signum) {
std::cout << "\nCtrl+Cが押されました!シグナル番号: " << signum << std::endl;
// ここでクリーンアップ処理などを行う
std::exit(signum); // プログラムを終了
}
int main() {
// SIGINT (Ctrl+C) のシグナルハンドラを設定
// 以前のハンドラを保存しておくと、後で元に戻すことも可能
if (std::signal(SIGINT, signalHandler) == SIG_ERR) {
std::cerr << "SIGINTのシグナルハンドラ設定に失敗しました。" << std::endl;
return 1;
}
std::cout << "プログラム実行中... Ctrl+Cを押してください。" << std::endl;
// 無限ループでプログラムを継続させる
while (true) {
std::cout << "作業中..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2)); // 2秒待機
}
return 0;
}
このプログラムを実行中にCtrl+Cを押すと、signalHandler
関数が呼び出され、メッセージが表示されてからプログラムが終了します。
std::signal
はC言語から引き継がれた伝統的なシグナル処理メカニズムであり、C++での非同期シグナルセーフティに関する厳密な制約があります。
- ポータビリティ:
std::signal
の動作は、オペレーティングシステムによって微妙に異なる場合があります。 - 競合状態: マルチスレッド環境で
std::signal
を使用すると、競合状態が発生する可能性があります。 - 非同期シグナルセーフティ (Async-signal-safety): シグナルハンドラ関数内で呼び出しが許可されている関数は非常に限られています。ほとんどの標準ライブラリ関数(
std::cout
なども含む)は非同期シグナルセーフではないため、シグナルハンドラ内での呼び出しは未定義動作を引き起こす可能性があります。上記の例のstd::cout
やstd::exit
は、厳密には安全ではありませんが、簡単なデモ目的でよく使用されます。安全なシグナル処理のためには、volatile sig_atomic_t
型のフラグを設定するなどの非常に制限された操作のみを行うべきです。
現代のC++プログラミングでは、より堅牢で安全なシグナル処理が必要な場合、std::signal
ではなく、POSIX標準で定義されているsigaction
関数を使用することが推奨されます。sigaction
は、シグナルハンドラに渡される情報が豊富で、より詳細な制御が可能です。しかし、sigaction
はC++標準ライブラリの一部ではなく、POSIXシステム(Linux, macOSなど)に限定されます。
非同期シグナルセーフティ (Async-signal-safety) の問題
これはstd::signal
における最も重要で一般的な問題です。シグナルハンドラが非同期に(プログラムの任意の時点で)呼び出される可能性があるため、シグナルハンドラ内から呼び出して安全な関数は非常に限られています。
一般的なエラー/問題
- 多くの標準ライブラリ関数の呼び出し
std::vector
、std::string
、std::mutex
など、ほとんどの標準ライブラリ関数は非同期シグナルセーフではありません。- トラブルシューティング
シグナルハンドラでは、非同期シグナルセーフな関数(リストは非常に短い)のみを使用するようにします。一般的には、volatile sig_atomic_t
に値をセットし、メインスレッドがそのフラグを定期的にチェックして、安全な場所で適切な処理を行うように設計します。
- 動的メモリ割り当て(new、delete、malloc、free)の呼び出し
- ヒープ管理は複雑な内部状態を持つため、シグナルハンドラ内でこれらの関数を呼び出すと、ヒープが破損したり、クラッシュしたりする可能性があります。
- トラブルシューティング
シグナルハンドラ内では動的メモリ割り当てを行わないでください。必要なリソースは事前に割り当てておくか、非同期シグナルセーフな方法で通信する仕組みを構築します。
- std::coutやprintfなどのI/O関数の呼び出し
- これらの関数は内部でバッファリングやロックを行っており、シグナルハンドラが呼び出された時点で、これらの関数が別のスレッドで実行中である場合、デッドロックやデータの破損を引き起こす可能性があります。
- トラブルシューティング
シグナルハンドラ内では、volatile sig_atomic_t
型のフラグを設定するなどの最小限の操作に留めるべきです。デバッグ目的でメッセージを出力したい場合は、write
システムコールのような非同期シグナルセーフな関数を使用することを検討しますが、これはプラットフォーム依存です。
シグナルハンドラの再入可能性 (Reentrancy)
シグナルハンドラが実行中に同じシグナルが再度発生した場合、ハンドラが再入可能でないと問題が発生します。
一般的なエラー/問題
- ハンドラが終了する前に同じシグナルが再送される
- 特に処理に時間がかかるハンドラの場合、この問題が発生しやすくなります。
- トラブルシューティング
シグナルハンドラは可能な限り短く、シンプルに保つべきです。また、シグナルハンドラの冒頭でシグナルをブロックし、処理の最後にブロックを解除することで、再入を防止できます(ただし、これはより高度なシグナル処理(sigaction
など)で一般的に行われる手法です)。
シグナルハンドラでの例外 (Exceptions)
シグナルハンドラから例外をスローすると、未定義動作を引き起こす可能性があります。
一般的なエラー/問題
- シグナルハンドラ内で例外をスローする、または例外をスローする可能性のある関数を呼び出す
- シグナルハンドラが呼び出される時点でのプログラムのスタックの状態は予測不能であり、例外機構が正しく動作しない可能性があります。
- トラブルシューティング
シグナルハンドラからは絶対に例外をスローしないでください。代わりに、フラグを設定してメインスレッドにエラーを通知するなどの方法を取ります。
シグナル設定のライフサイクルと競合
一般的なエラー/問題
- マルチスレッド環境でのシグナル処理
std::signal
はスレッドセーフではありません。特定のシグナルはどのスレッドで処理されるかが不定であったり、複数のスレッドでシグナル処理を行う場合に競合状態が発生したりする可能性があります。- トラブルシューティング
POSIXシステムでは、pthread_sigmask
を使って特定のスレッドのシグナルマスクを変更したり、sigwaitinfo
を使って特定のスレッドでシグナルを同期的に待機したりする、より高度な方法があります。std::signal
はシングルスレッド環境での使用が推奨されます。
- 複数の場所で同じシグナルハンドラを設定する(または意図せず上書きする)
std::signal
を複数回呼び出すと、最後の呼び出しが以前の設定を上書きします。- トラブルシューティング
シグナルハンドラの設定は、プログラムの起動時に一度だけ行い、慎重に管理する必要があります。std::signal
は以前のハンドラを返すので、その値を保存しておき、後で元に戻すことも可能です。
SIG_ERRの確認不足
std::signal
はシグナルハンドラの設定に失敗した場合にSIG_ERR
を返します。
一般的なエラー/問題
- std::signalの戻り値をチェックしない
- シグナルハンドラの設定が成功したかどうかを確認せずに、プログラムが続行されると、シグナルが発生しても期待通りに動作しない可能性があります。
- トラブルシューティング
std::signal
の戻り値は常に確認し、SIG_ERR
が返された場合は適切なエラー処理を行うべきです。
特定のシグナルの扱い
- SIGKILLやSIGSTOPの処理を試みる
- これらのシグナルはOSによって直接処理されるため、プログラムがこれらのシグナルを捕捉したり無視したりすることはできません。
- トラブルシューティング
これらは捕捉不能なシグナルであることを理解し、対応しようとしないようにします。
- 最小限のシグナルハンドラ
まず、シグナルハンドラ内でvolatile sig_atomic_t
型のフラグを立てるだけの最小限のコードでテストし、シグナルが正しく捕捉されているかを確認します。 - ログ出力の制限
デバッグのためにシグナルハンドラから何かを出力したい場合は、write
システムコールのような非同期シグナルセーフな関数を使用するか、デバッグビルドでのみ限定的に使用するようにします。 - OS/コンパイラ固有のドキュメント参照
std::signal
の動作はプラットフォームによって異なる場合があるため、使用しているOS(Linux, Windows, macOSなど)やコンパイラ(GCC, Clang, MSVCなど)のドキュメントを確認することが重要です。 - sigactionの検討
複雑なシグナル処理やマルチスレッド環境での安全なシグナル処理が必要な場合は、C++標準ではなくPOSIX標準で提供されるsigaction
関数(Linux/Unix系OSの場合)の使用を強く推奨します。sigaction
は、シグナルマスクの制御、SA_RESTARTフラグによるシステムコールの中断防止など、より詳細な制御を可能にします。
例1: Ctrl+C (SIGINT) を捕捉して終了する
最も一般的なstd::signal
の利用例です。ユーザーがプログラム実行中にCtrl+Cを押すと、SIGINT
シグナルが発生し、それに対するカスタムハンドラが呼び出されます。
#include <iostream> // std::cout, std::cerr を使うために必要
#include <csignal> // std::signal, SIGINT, SIG_ERR を使うために必要
#include <chrono> // 時間関連 (std::chrono::seconds)
#include <thread> // スレッド関連 (std::this_thread::sleep_for)
// グローバルなシグナルステータスフラグ
// シグナルハンドラから安全にアクセスできるように volatile sig_atomic_t を使用
volatile std::sig_atomic_t g_signal_status = 0;
// シグナルハンドラ関数
// シグナルハンドラは非同期シグナルセーフでなければならないため、
// 非常に限られた操作しかできません。
// ここではフラグを設定するだけにとどめています。
extern "C" void signal_handler(int signum) {
g_signal_status = signum;
}
int main() {
// SIGINT (Ctrl+C) のシグナルハンドラを設定
// 以前のハンドラは不要なので保存しませんが、エラーチェックは重要です。
if (std::signal(SIGINT, signal_handler) == SIG_ERR) {
std::cerr << "エラー: SIGINT のシグナルハンドラ設定に失敗しました。" << std::endl;
return 1; // 異常終了
}
std::cout << "プログラム実行中... Ctrl+C を押して終了してください。" << std::endl;
// g_signal_status が設定されるまでループ
while (g_signal_status == 0) {
std::cout << "作業中..." << std::endl;
// 実際にはもっと複雑な処理を行う
std::this_thread::sleep_for(std::chrono::seconds(2)); // 2秒待機
}
// シグナルが捕捉された後の処理
std::cout << "\nシグナル " << g_signal_status << " が捕捉されました。クリーンアップ処理を実行します。" << std::endl;
// ここでファイルクローズ、リソース解放などのクリーンアップ処理を行う
std::cout << "プログラムを終了します。" << std::endl;
return 0; // 正常終了
}
解説
- メインループでのフラグ確認: シグナルハンドラ内では、非同期シグナルセーフな操作のみを行うべきです。
std::cout
などの多くの標準ライブラリ関数は非同期シグナルセーフではありません。そのため、シグナルハンドラではg_signal_status
フラグをセットするだけにし、メインスレッドがそのフラグを定期的にチェックして、安全な場所でクリーンアップなどの重い処理を行うのが一般的なプラクティスです。 std::signal(SIGINT, signal_handler) == SIG_ERR
:std::signal
関数は、設定に失敗した場合にSIG_ERR
を返します。エラーチェックは重要です。std::signal(SIGINT, signal_handler)
:SIGINT
(Interrupt Signal)が発生した場合にsignal_handler
関数を呼び出すように設定しています。extern "C" void signal_handler(int signum)
: シグナルハンドラは通常、Cリンケージ(extern "C"
)で宣言されます。C++の関数名マングリングを防ぎ、OSが正しく関数を呼び出せるようにするためです。ハンドラ関数はint
型の引数(シグナル番号)を取り、void
を返します。volatile std::sig_atomic_t g_signal_status
: シグナルハンドラは非同期に呼び出されるため、他のスレッドやメイン処理との間でデータ競合が発生する可能性があります。volatile
キーワードはコンパイラに最適化を抑制させ、変数が予期せぬタイミングで変更される可能性があることを伝えます。std::sig_atomic_t
型は、アトミックな書き込み・読み出しが保証される整数型であり、シグナルハンドラ内で安全にアクセスできる数少ない型の一つです。#include <csignal>
:std::signal
関数やシグナル番号(SIGINT
など)、std::sig_atomic_t
が定義されています。
例2: シグナルを無視する (SIG_IGN
)
特定のシグナルをプログラムが無視するように設定する例です。
#include <iostream>
#include <csignal>
#include <chrono>
#include <thread>
int main() {
// SIGINT (Ctrl+C) を無視するように設定
// これにより、Ctrl+C を押してもプログラムは終了しません
if (std::signal(SIGINT, SIG_IGN) == SIG_ERR) {
std::cerr << "エラー: SIGINT の無視設定に失敗しました。" << std::endl;
return 1;
}
std::cout << "プログラム実行中... Ctrl+C を押しても何も起こりません。" << std::endl;
std::cout << "強制終了するには、タスクマネージャーなどからプロセスを終了してください。" << std::endl;
// 無限ループでプログラムを継続
while (true) {
std::cout << "稼働中..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(3)); // 3秒待機
}
return 0; // ここには到達しない
}
解説
std::signal(SIGINT, SIG_IGN)
:SIGINT
シグナルが発生しても、プログラムがそれを無視するように設定します。これにより、Ctrl+Cを押してもプログラムは終了しなくなります。これは、デーモンプロセスやバックグラウンドサービスなどで、ユーザーからの通常の終了要求を受け付けない場合に利用されることがあります。
例3: シグナルのデフォルト動作に戻す (SIG_DFL
)
カスタムハンドラを設定した後に、そのシグナルのデフォルト動作に戻す例です。
#include <iostream>
#include <csignal>
#include <chrono>
#include <thread>
// シグナルハンドラ関数
extern "C" void temp_signal_handler(int signum) {
std::cout << "\n一時的なハンドラがシグナル " << signum << " を捕捉しました。" << std::endl;
// ここでデフォルト動作に戻す
if (std::signal(SIGINT, SIG_DFL) == SIG_ERR) {
std::cerr << "エラー: SIGINT をデフォルトに戻せませんでした。" << std::endl;
// エラー処理
} else {
std::cout << "SIGINT のハンドラがデフォルトに戻されました。" << std::endl;
}
// ここで、一時的なハンドラでの処理を完了
// 戻り値がvoidなので、直接終了はしない
}
int main() {
// まず、一時的なシグナルハンドラを設定
if (std::signal(SIGINT, temp_signal_handler) == SIG_ERR) {
std::cerr << "エラー: 一時ハンドラ設定に失敗しました。" << std::endl;
return 1;
}
std::cout << "プログラム実行中... Ctrl+C を一度押してみてください。" << std::endl;
std::cout << "一時ハンドラが呼び出され、その後デフォルト動作に戻ります。" << std::endl;
// 最初は一時ハンドラで処理される
std::this_thread::sleep_for(std::chrono::seconds(5));
std::cout << "一時ハンドラの呼び出し後、Ctrl+C を再度押してください。" << std::endl;
std::cout << "今度はデフォルト動作(プログラム終了)になります。" << std::endl;
// 無限ループでプログラムを継続
while (true) {
std::cout << "ループ中..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
}
return 0;
}
解説
std::signal(SIGINT, SIG_DFL)
:SIGINT
に対する処理を、オペレーティングシステムが定めるデフォルトの動作に戻します。この例では、一度Ctrl+Cが押されるとtemp_signal_handler
が呼び出され、その中でSIG_DFL
に設定し直されます。その後、再度Ctrl+Cを押すとプログラムが通常通り終了します。
プログラム自身でシグナルを発生させるためにstd::raise()
関数を使用する例です。
#include <iostream>
#include <csignal>
#include <chrono>
#include <thread>
volatile std::sig_atomic_t g_my_signal_received = 0;
// カスタムシグナルハンドラ
extern "C" void my_custom_signal_handler(int signum) {
std::cout << "\nカスタムシグナルハンドラ: シグナル " << signum << " を受け取りました。" << std::endl;
g_my_signal_received = signum;
}
int main() {
// カスタムシグナル (SIGUSR1など、OSに依存) を捕捉するように設定
// 通常、SIGUSR1はユーザー定義のシグナルとして使用されます
// WindowsではSIGUSR1が存在しないため、SIGTERMなど他のシグナルを使用するか、
// Windows固有のシグナル処理メカニズムを使う必要があります。
// この例は主にUnix/Linux向けです。
// Windowsで試す場合は SIGINT や SIGTERM に変更してください。
#ifdef _WIN32
if (std::signal(SIGTERM, my_custom_signal_handler) == SIG_ERR) {
std::cerr << "エラー: SIGTERM のシグナルハンドラ設定に失敗しました。" << std::endl;
return 1;
}
std::cout << "SIGTERM のカスタムハンドラを設定しました。" << std::endl;
#else // Unix/Linux
if (std::signal(SIGUSR1, my_custom_signal_handler) == SIG_ERR) {
std::cerr << "エラー: SIGUSR1 のシグナルハンドラ設定に失敗しました。" << std::endl;
return 1;
}
std::cout << "SIGUSR1 のカスタムハンドラを設定しました。" << std::endl;
#endif
std::cout << "5秒後にプログラム自身でシグナルを発生させます..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(5));
// プログラム自身でシグナルを発生させる
#ifdef _WIN32
std::cout << "std::raise(SIGTERM) を呼び出します。" << std::endl;
std::raise(SIGTERM);
#else // Unix/Linux
std::cout << "std::raise(SIGUSR1) を呼び出します。" << std::endl;
std::raise(SIGUSR1);
#endif
// シグナルハンドラが処理するのを待つ
while (g_my_signal_received == 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
std::cout << "シグナルが処理されました。プログラムを終了します。" << std::endl;
return 0;
}
- プラットフォーム依存のシグナル:
SIGUSR1
はPOSIXシステム(Unix/Linux/macOSなど)でユーザー定義シグナルとしてよく使われますが、Windowsには存在しません。WindowsではSIGTERM
のような既存のシグナルを使用する必要があります。このように、シグナルの種類はOSに依存する場合があることに注意が必要です。 std::raise(int sig)
: 指定されたシグナルを現在のプロセスに送信します。これにより、そのシグナルに対するハンドラが呼び出されます。これは、特定の条件下でシグナルハンドラをテストしたり、内部エラー状態をシグナルとして扱う場合に便利です。
POSIX sigaction
Unix系システム(Linux, macOSなど)で利用可能な最も推奨される代替手段です。std::signal
よりもはるかに多くの機能と制御を提供し、非同期シグナルセーフティに関する問題をより適切に扱えます。
特徴
- スレッドセーフティの考慮
pthread_sigmask
と組み合わせることで、マルチスレッド環境でのシグナル処理をより安全に設計できます。 - 再起動可能なシステムコール
SA_RESTART
フラグを設定することで、シグナルによって中断されたシステムコールを自動的に再開させることができます。 - シグナル情報の取得
sa_sigaction
メンバーを使用すると、シグナルを発生させたプロセスIDやユーザーID、エラーコードなど、より詳細なシグナル情報を取得できます。 - 詳細な制御
シグナルハンドラが呼び出される際のシグナルマスク(ハンドラ実行中にブロックするシグナル)を制御できます。
簡単な例
#include <iostream>
#include <csignal> // for SIGINT etc.
#include <unistd.h> // for sleep, getpid etc. (POSIX specific)
#include <string> // for std::string
// シグナルハンドラ関数
// extern "C" はCリンケージを保証し、OSからの呼び出しを可能にする
extern "C" void sigaction_handler(int signum, siginfo_t *info, void *context) {
// シグナルハンドラ内で安全な操作のみを行う (非同期シグナルセーフ)
// ここで直接 std::cout を使うのは厳密には非同期シグナルセーフではないが、
// デモンストレーション目的で簡略化
const char* msg = "\nSigactionハンドラ: シグナルを受信しました。\n";
write(STDOUT_FILENO, msg, strlen(msg)); // write()は非同期シグナルセーフ
// siginfo_t から詳細情報を取得
// ここで printf を使うのは非同期シグナルセーフではないため、
// 実際のアプリケーションでは別の方法を検討
// std::string builder;
// builder += " シグナル番号: " + std::to_string(signum) + "\n";
// if (info->si_code == SI_USER) { // si_code の例
// builder += " 送信元プロセスID: " + std::to_string(info->si_pid) + "\n";
// builder += " 送信元ユーザーID: " + std::to_string(info->si_uid) + "\n";
// }
// write(STDOUT_FILENO, builder.c_str(), builder.length()); // この文字列構築も複雑で危険
// メインループに通知するためのフラグを設定
volatile sig_atomic_t* status_flag = static_cast<volatile sig_atomic_t*>(context);
if (status_flag) {
*status_flag = signum;
}
}
int main() {
struct sigaction sa;
volatile sig_atomic_t global_status = 0; // シグナルハンドラとメインループ間の安全な通信用
// sigaction構造体の設定
sa.sa_handler = sigaction_handler; // シグナルハンドラの関数ポインタ
sa.sa_flags = SA_SIGINFO; // siginfo_t を受け取るハンドラ形式を使用
// (sa_sigactionではなくsa_handlerを使用する場合はSA_SIGINFOなし)
sa.sa_flags |= SA_RESTART; // 中断されたシステムコールを自動的に再開
sigemptyset(&sa.sa_mask); // ハンドラ実行中にブロックするシグナルをなしに設定
// sigaction_handlerの3番目の引数(void* context)にglobal_statusのアドレスを渡す場合:
// これは標準的な方法ではなく、一般的にはグローバル変数やアトミックフラグを直接使用します。
// デモンストレーションのため、sa_handlerの代わりにsa_sigactionを使う場合に
// ucontext_t 構造体の一部として渡される情報を利用するイメージ。
// この例ではsa_handlerを使っているので、context引数は使われず、global_statusに直接アクセスします。
// SIGINT のシグナルハンドラを設定
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction"); // エラーの場合は errno を出力
return 1;
}
std::cout << "プログラム実行中... Ctrl+C を押してください。" << std::endl;
std::cout << "プロセスID: " << getpid() << std::endl;
while (global_status == 0) {
std::cout << "メインループで作業中..." << std::endl;
sleep(2); // POSIX sleep関数 (シグナルで中断される可能性がある)
}
std::cout << "\nメインループ: シグナル " << global_status << " を検知しました。終了します。" << std::endl;
return 0;
}
// ... 以前のコード ...
extern "C" void sigaction_handler_full_info(int signum, siginfo_t *info, void *context) {
const char* msg = "\nSigactionハンドラ: シグナルを受信しました。\n";
write(STDOUT_FILeno, msg, strlen(msg));
char buf[100];
int len = snprintf(buf, sizeof(buf), " シグナル番号: %d\n", signum);
write(STDOUT_FILENO, buf, len);
if (info->si_code == SI_USER) { // ユーザーによって生成されたシグナル
len = snprintf(buf, sizeof(buf), " 送信元プロセスID: %d, 送信元ユーザーID: %d\n", info->si_pid, info->si_uid);
write(STDOUT_FILENO, buf, len);
}
// ... 他の情報も取得可能 ...
// メインループに通知するためのフラグを設定
volatile sig_atomic_t* status_flag = static_cast<volatile sig_atomic_t*>(info->si_value.sival_ptr);
if (status_flag) {
*status_flag = signum;
}
}
int main() {
struct sigaction sa;
volatile sig_atomic_t global_status = 0;
// sigaction構造体の設定
// SA_SIGINFO を設定すると、sa_sigaction メンバのハンドラが呼び出される
sa.sa_sigaction = sigaction_handler_full_info;
sa.sa_flags = SA_SIGINFO | SA_RESTART; // SA_RESTART も有効
sigemptyset(&sa.sa_mask); // ハンドラ実行中にブロックするシグナルをなしに設定
// SIGINT のシグナルハンドラを設定
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction");
return 1;
}
std::cout << "プログラム実行中... Ctrl+C を押してください。" << std::endl;
std::cout << "プロセスID: " << getpid() << std::endl;
while (global_status == 0) {
std::cout << "メインループで作業中..." << std::endl;
sleep(2);
}
std::cout << "\nメインループ: シグナル " << global_status << " を検知しました。終了します。" << std::endl;
return 0;
}
シグナル駆動型I/O (signalfd / kqueue)
Linuxのsignalfd
やBSD/macOSのkqueue
のようなOS固有の機能は、シグナルをファイルディスクリプタとして扱うことを可能にします。これにより、シグナルを通常のI/Oイベント(ソケットの受信、ファイルの読み書きなど)と同じイベントループで処理できます。これは非同期処理を扱うアプリケーションで非常に強力なアプローチです。
特徴
- マルチスレッド環境での安全性
シグナルをブロックし、専用のスレッドでsignalfd
を監視することで、シグナル処理を安全に一元化できます。 - 同期的なシグナル処理
シグナルハンドラが非同期に割り込むのではなく、イベントループでシグナルを同期的に「読み取る」ことができます。これにより、非同期シグナルセーフティの制約を回避できます。 - イベントループとの統合
epoll
(Linux)やkqueue
(BSD/macOS)などのI/O多重化メカニズムにシグナルイベントを組み込むことができます。
簡単な概念例 (Linux signalfd)
#include <iostream>
#include <csignal>
#include <unistd.h>
#include <sys/signalfd.h> // signalfd に必要
#include <sys/epoll.h> // epoll に必要 (イベントループの例として)
#include <vector>
int main() {
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT); // SIGINT をマスクに追加
sigaddset(&mask, SIGTERM); // SIGTERM をマスクに追加
// プロセス全体でこれらのシグナルをブロック
// これにより、デフォルトのシグナルハンドラや他のスレッドがシグナルを受け取らないようにする
if (pthread_sigmask(SIG_BLOCK, &mask, NULL) == -1) {
perror("pthread_sigmask");
return 1;
}
// signalfd を作成し、マスクされたシグナルを監視
int sfd = signalfd(-1, &mask, 0);
if (sfd == -1) {
perror("signalfd");
return 1;
}
// epoll インスタンスを作成 (イベントループの一部として)
int epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("epoll_create1");
close(sfd);
return 1;
}
// signalfd を epoll に追加
struct epoll_event event;
event.events = EPOLLIN; // 読み込み可能イベントを監視
event.data.fd = sfd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sfd, &event) == -1) {
perror("epoll_ctl");
close(sfd);
close(epoll_fd);
return 1;
}
std::cout << "プログラム実行中... Ctrl+C または SIGTERM を送信してください。" << std::endl;
std::cout << "プロセスID: " << getpid() << std::endl;
struct epoll_event events[1]; // イベントバッファ
bool running = true;
while (running) {
int num_events = epoll_wait(epoll_fd, events, 1, -1); // 無限に待機
if (num_events == -1) {
if (errno == EINTR) { // シグナルによって中断された場合
continue;
}
perror("epoll_wait");
break;
}
for (int i = 0; i < num_events; ++i) {
if (events[i].data.fd == sfd) {
struct signalfd_siginfo fdsi;
ssize_t bytes = read(sfd, &fdsi, sizeof(struct signalfd_siginfo));
if (bytes != sizeof(struct signalfd_siginfo)) {
std::cerr << "signalfd からの読み込みエラーまたは不完全なデータ" << std::endl;
running = false;
break;
}
std::cout << "\nsignalfd: シグナル " << fdsi.ssi_signo << " を受け取りました。" << std::endl;
if (fdsi.ssi_signo == SIGINT || fdsi.ssi_signo == SIGTERM) {
running = false; // 終了シグナルであればループを終了
}
}
// 他のファイルディスクリプタのイベント処理もここに追加
}
}
std::cout << "クリーンアップ処理を実行します..." << std::endl;
close(sfd);
close(epoll_fd);
std::cout << "プログラムを終了します。" << std::endl;
return 0;
}
Boost.Asioは、ネットワークI/Oやタイマーなど、さまざまな非同期操作を扱うためのクロスプラットフォームライブラリです。signal_set
クラスを提供しており、これは内部的にOS固有のシグナル処理メカニズム(Unix系ではsignalfd
/kqueue
、Windowsでは独自のメカニズム)を抽象化し、シグナルをイベント駆動型で処理できるようにします。
特徴
- スレッドセーフティ
Boost.Asioの非同期モデルはスレッドセーフティを考慮して設計されており、適切に使用すれば安全なシグナル処理が可能です。 - 非同期処理モデルへの統合
シグナルを他の非同期イベント(ネットワークソケットの読み書き、タイマーなど)と同じio_context
(またはio_service
)で処理できます。 - クロスプラットフォーム
Linux, Windows, macOSなど、複数のプラットフォームで同じコードを使用できます。
簡単な例
#include <iostream>
#include <boost/asio.hpp> // Boost.Asio ヘッダ
#include <boost/bind/bind.hpp> // boost::bind を使う場合
#include <thread> // std::this_thread::sleep_for
// シグナルハンドラ関数
void signal_handler(const boost::system::error_code& error, int signal_number) {
if (!error) {
// シグナルが正常に受信された場合
std::cout << "\nBoost.Asio: シグナル " << signal_number << " を受け取りました。" << std::endl;
if (signal_number == SIGINT || signal_number == SIGTERM) {
// io_context を停止して、メインループを終了させる
// 実際には、この後でクリーンアップなどを行う
std::cout << "io_context を停止します。" << std::endl;
// io_context.stop(); // 外部から io_context にアクセスする必要がある
// この例ではシンプルにするため、直接 io_context を停止せず、
// main 関数でフラグを介して制御します。
}
} else {
// エラーが発生した場合 (例: キャンセルされた場合)
std::cerr << "Boost.Asio: シグナルハンドラでエラー: " << error.message() << std::endl;
}
}
int main() {
boost::asio::io_context io_context;
// シグナルセットを構築し、SIGINT (Ctrl+C) と SIGTERM を登録
boost::asio::signal_set signals(io_context, SIGINT, SIGTERM);
// シグナルが発生したときに非同期的にハンドラを呼び出すように設定
signals.async_wait(boost::bind(signal_handler,
boost::asio::placeholders::error,
boost::asio::placeholders::signal_number));
std::cout << "プログラム実行中... Ctrl+C または SIGTERM を送信してください。" << std::endl;
std::cout << "プロセスID: " << getpid() << std::endl;
// io_context を実行し、登録された非同期操作(シグナル待機など)を処理
// ここで io_context.run() は、登録された作業がなくなるまでブロックします。
// シグナルを受信し、signal_handler内で io_context.stop() を呼び出すことで、
// run() が終了し、メインスレッドに戻ることができます。
// 別のスレッドで io_context を実行する例
// この例では簡略化のためメインスレッドで実行
// std::thread t([&io_context](){ io_context.run(); });
// io_context が停止するまでループ
// signal_handler で io_context.stop() を呼び出すことで終了
std::cout << "io_context が実行を継続します..." << std::endl;
io_context.run(); // この関数がブロックし、シグナルによって停止される
// io_context.run() が戻ってきた後、クリーンアップ処理を行う
std::cout << "io_context が停止しました。クリーンアップ処理を実行します。" << std::endl;
return 0;
}
- マルチスレッドとシグナルマスク
マルチスレッド環境では、プロセス全体のシグナルマスクと各スレッドのシグナルマスクを適切に管理することが重要です。一般的には、すべてのスレッドでシグナルをブロックし、一つの専用スレッドでシグナルを同期的に待機する(例:sigwaitinfo
、signalfd
を監視)パターンが推奨されます。 - 非同期シグナルセーフティ
上記の代替方法、特にsigaction
とsignalfd
は、std::signal
に比べて非同期シグナルセーフティに関する制約を緩和しますが、完全に解決するわけではありません。シグナルハンドラ(またはsignalfd
から読み取った後の処理)内では、依然として安全な関数のみを呼び出すよう注意が必要です。