C言語によるシステムプログラミングにおいて、プログラムの外部やOSから送られてくる通知を受け取り、適切に処理を行う仕組みが「シグナル」です。
シグナルは非同期的に発生するイベントであり、実行中のプロセスに対して特定の動作を要求したり、異常を知らせたりするために利用されます。
Unix系オペレーティングシステムやWindows環境において、プログラムの終了処理やユーザーの割り込み操作を制御するためには、このシグナルの理解が欠かせません。
本記事では、C言語の標準ライブラリで提供されているsignal関数に焦点を当て、その基本的な使い方から実装上の重要な注意点までを詳しく説明します。
シグナルハンドラを正しく実装することで、不意の強制終了を防ぎ、安全なリソースの解放やログの保存といった処理を実現できるようになります。
シグナルとは何か
シグナルとは、プロセスに対して送信される「非同期の通知メッセージ」です。
プログラムの実行中に、キーボードからCtrl+Cが入力されたり、メモリ割り当ての失敗が発生したりした際に、OSはプロセスにシグナルを送ります。
シグナルを受け取ったプロセスは、その種類に応じてあらかじめ定義された動作を実行します。
一般的には、プログラムの強制終了やコアダンプの出力が行われますが、開発者が独自の処理を定義することも可能です。
C言語のプログラムにおいて、特定のシグナルを受け取った際に実行される関数のことを「シグナルハンドラ」と呼びます。
以下に、主要なシグナルの種類とその意味をまとめました。
| シグナル名 | 内容 | 標準的な動作 |
|---|---|---|
| SIGINT | 割り込み信号(Ctrl+Cなどで発生) | プロセスの終了 |
| SIGTERM | 終了信号(システム終了時など) | プロセスの終了 |
| SIGSEGV | 不正なメモリ参照(セグメンテーション違反) | プロセスの終了とコアダンプ |
| SIGALRM | タイマーによる通知 | プロセスの終了 |
| SIGFPE | 算術演算エラー(ゼロ除算など) | プロセスの終了とコアダンプ |
signal関数の基本仕様
C言語でシグナルを扱うための最も基本的な手段が、signal関数です。
この関数を使用することで、特定のシグナルが発生した際に呼び出す関数をOSに登録できます。
signal関数はsignal.hヘッダファイルに定義されており、プラットフォームを問わず広く利用されてきました。
signal関数のプロトタイプ
signal関数の宣言は、初見では少し複雑に見えるかもしれません。
#include <signal.h>
void (*signal(int sig, void (*func)(int)))(int);
この宣言は、「整数値sigと、整数を引数に取る関数ポインタfuncを受け取り、以前のシグナルハンドラへの関数ポインタを返す」という意味です。
第一引数のsigには、処理したいシグナルの番号を指定します。
第二引数のfuncには、実行したいシグナルハンドラの関数名を指定するか、あるいは以下の定数を指定します。
- SIG_DFL:シグナルの動作をデフォルトの設定に戻します。
- SIG_IGN:対象のシグナルを無視するように設定します。
もし設定に失敗した場合、signal関数はSIG_ERRを返します。
シグナルハンドラの実装方法
具体的に、ユーザーがCtrl+Cを入力したときに発生するSIGINTを捕捉するプログラムを作成してみましょう。
シグナルハンドラとして定義する関数は、必ずvoidを戻り値とし、int型の引数を1つ取る必要があります。
基本的なプログラム例
以下のコードは、Ctrl+Cが押された際にメッセージを表示し、プログラムを安全に終了させる例です。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
// シグナルハンドラ関数の定義
void handle_sigint(int sig) {
printf("\nシグナル %d (SIGINT) を受信しました。プログラムを終了します。\n", sig);
// 終了処理をここに記述できる
exit(0);
}
int main() {
// SIGINTに独自のハンドラを登録
if (signal(SIGINT, handle_sigint) == SIG_ERR) {
perror("signal関数の登録に失敗しました");
return 1;
}
printf("プログラム実行中... Ctrl+C を押すと停止します。\n");
// 無限ループで待機
while (1) {
sleep(1);
}
return 0;
}
プログラム実行中... Ctrl+C を押すと停止します。
^C
シグナル 2 (SIGINT) を受信しました。プログラムを終了します。
このプログラムを実行し、ターミナルでCtrl+Cを入力すると、標準の「即座に強制終了」という動作が上書きされ、handle_sigint関数内のメッセージが表示されるようになります。
シグナルハンドリングにおける注意点
signal関数は非常に手軽ですが、実際の運用においては深刻な問題を引き起こす可能性があるため、注意が必要です。
シグナルはメインプログラムの処理を中断して「割り込む」形で実行されるため、ハンドラ内で行う処理には厳しい制限があります。
非同期シグナル安全(Async-Signal-Safe)
シグナルハンドラ内では、「非同期シグナル安全」と呼ばれる関数しか呼び出してはいけません。
printf関数やmalloc関数は、非同期シグナル安全ではないため、本来はハンドラ内で使用すべきではありません。
これらの関数は内部でグローバルなロックや静的なバッファを使用していることがあり、メイン処理中にシグナルが割り込むと、デッドロックやデータの破損を引き起こす恐れがあります。
安全にメッセージを出力したい場合は、システムコールのwrite関数を使用することが推奨されます。
sig_atomic_t型の使用
シグナルハンドラとメイン処理の間で変数を共有する場合は、データの一貫性を保証しなければなりません。
CPUが変数へのアクセスを分割して行うと、中途半端な状態でシグナル処理が割り込む可能性があります。
この問題を回避するために、volatile sig_atomic_t型の変数を使用します。
sig_atomic_tは、その変数へのアクセスがアトミック(不可分)であることを保証する型です。
#include <stdio.h>
#include <signal.h>
// volatile sig_atomic_t を使用してフラグを管理
volatile sig_atomic_t keep_running = 1;
void handle_sigterm(int sig) {
keep_running = 0; // ハンドラ内ではフラグを立てるだけにとどめる
}
int main() {
signal(SIGTERM, handle_sigterm);
while (keep_running) {
// メインループの処理
}
printf("安全に終了しました。\n");
return 0;
}
signal関数とsigaction関数の違い
現代的なC言語開発(特にPOSIX準拠のシステム)では、signal関数ではなくsigaction関数の使用が強く推奨されています。
signal関数には、OSの種類によって動作が異なるという重大な欠点があるためです。
例えば、一部の古いシステムでは、一度シグナルを受け取るとハンドラの設定が自動的にデフォルト(SIG_DFL)に戻ってしまう仕様があります。
そのため、再度signal関数を呼び出して再登録する必要があり、そのわずかな隙間に別のシグナルが来ると制御できなくなります。
一方、sigaction関数は以下のような利点を提供します。
- ハンドラ実行中に他のシグナルをブロックできる。
- 一度設定したハンドラが永続的に有効。
- シグナルが発生した際の状態(詳細な原因など)を構造体で取得可能。
信頼性の高いアプリケーションを開発する場合は、sigactionを選択するようにしましょう。
実践的な活用シーン:終了処理の実装
シグナルハンドリングが最も活用される場面の一つが、「クリーンアップ処理」です。
例えば、プログラムがネットワーク接続を保持していたり、一時ファイルを作成したりしている場合、不意の終了時にそれらを放置するとシステムに悪影響を及ぼします。
以下のような手順で実装するのが一般的です。
- プログラム開始時に、SIGINTやSIGTERMに対してハンドラを登録する。
- ハンドラ内ではフラグ変数を変更する。
- メインループがフラグを検知してループを抜ける。
- ループの外側に記述されたメモリ解放、ファイルクローズ、一時ファイル削除などの終了処理を実行する。
このように、「シグナルハンドラ内で直接重い処理を行わない」という設計が、バグの少ない堅牢なプログラムへの近道となります。
まとめ
C言語におけるsignal関数は、外部からの割り込みイベントを制御するための基本的かつ強力なツールです。
本記事では、シグナルの種類やsignal関数の構文、そして具体的な実装例について解説しました。
シグナルハンドラを実装する際には、「非同期シグナル安全」な関数のみを使用することや、「volatile sig_atomic_t」によるフラグ管理を徹底することが極めて重要です。
また、より高度な制御や移植性を求める場合には、sigaction関数への移行も検討すべき選択肢となります。
シグナルの仕組みを正しく使いこなし、予期せぬ動作にも柔軟に対応できる高品質なプログラムを構築していきましょう。
