C言語におけるプログラミングにおいて、実行中のプロセスに対して外部からイベントを通知する仕組みとして「シグナル」は非常に重要な役割を担っています。
シグナルは、実行中のプログラムを一時的に中断させ、特定の処理を実行させるための非同期的な割り込み通知です。
Unix系オペレーティングシステムにおいて、プロセス管理や例外処理、ユーザーとのインタラクションを実現するために不可欠な要素となっています。
しかし、シグナル処理は正しく理解して実装しなければ、デッドロックや未定義動作などの深刻なバグを引き起こす原因となります。
本記事では、古典的なsignal関数に代わる標準的な手法であるsigactionを用いた、安全で堅牢なシグナルハンドリングの実践方法について詳しく解説します。
C言語におけるシグナル処理の基礎知識
シグナルとは何か
シグナルとは、実行中のプロセスに特定の事象が発生したことを知らせるためのソフトウェア割り込みです。
カーネルがプロセスに対して送信する場合もあれば、あるプロセスが別のプロセスに対して送信する場合もあります。
例えば、ユーザーがキーボードから「Ctrl+C」を入力した際には、実行中のプロセスにSIGINTというシグナルが送信されます。
また、プログラムが不正なメモリ参照を行った場合にはSIGSEGV(セグメンテーション違反)が発生します。
プロセスはシグナルを受け取ると、デフォルトの動作を実行するか、あるいはあらかじめ登録しておいた特定の関数(シグナルハンドラ)を呼び出します。
主なシグナルの種類と役割
システムには多くのシグナルが存在しますが、開発者が頻繁に扱う代表的なシグナルを以下の表にまとめました。
| シグナル名 | デフォルトの動作 | 発生する主な要因 |
|---|---|---|
| SIGINT | 終了 | キーボードからの割り込み(Ctrl+C) |
| SIGTERM | 終了 | システム終了やkillコマンドによる終了要求 |
| SIGKILL | 強制終了 | プロセスの強制的な停止(捕捉・無視不可) |
| SIGSEGV | コアダンプ | 不正なメモリへのアクセス |
| SIGALRM | 終了 | alarm関数で設定したタイマーの満了 |
| SIGUSR1 / SIGUSR2 | 終了 | ユーザー定義の目的で使用可能 |
これらのシグナルの中で、特にSIGKILLとSIGSTOPについては、プログラム側でハンドラを設定したり、無視したりすることができないという特性があります。
なぜsignal関数ではなくsigactionを使うのか
signal関数の限界と移植性の問題
初期のC言語ではsignal関数がシグナル処理の主流として使われてきました。
しかし、signal関数には、ハンドラが一度呼び出されると設定がリセットされる仕様や、シグナルのブロックが不十分であるといった歴史的な問題が存在します。
特に、OSのバージョンや種類によって挙動が異なるため、高い信頼性が求められるシステム開発では推奨されません。
これに対し、sigaction関数はPOSIX標準で定義されており、より詳細で一貫性のある制御が可能です。
sigactionが提供する高度な制御
sigactionを使用することで、シグナルハンドラの実行中に他のシグナルを一時的にブロックすることができます。
また、システムコールが中断された際に、それを自動的に再開するかどうかのフラグ制御も可能です。
これにより、予測困難な割り込みによるバグを防ぎ、安定した動作を保証することができます。
sigaction構造体と関数の使い方
sigaction構造体の詳細
sigaction関数を使用する際は、専用の構造体を用いて動作を設定します。
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
sa_handlerには、従来のシグナルハンドラ関数を指定します。
sa_maskは、ハンドラの実行中にブロックしたいシグナルの集合を指定するために使われます。
sa_flagsには、動作をカスタマイズするためのオプション(SA_RESTARTやSA_SIGINFOなど)を指定します。
sigactionを用いた基本的な実装例
以下に、SIGINTを安全に捕捉する基本的なコード例を示します。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
// シグナルハンドラ関数
void handle_sigint(int sig) {
// 非同期シグナルセーフな関数のみを使用する
const char *msg = "SIGINTを受信しました。終了します。\n";
write(STDOUT_FILENO, msg, 36);
exit(0);
}
int main() {
struct sigaction sa;
// ハンドラ関数の登録
sa.sa_handler = handle_sigint;
// ハンドラ実行中はすべてのシグナルをブロックしない(初期化)
sigemptyset(&sa.sa_mask);
// システムコールの再開を有効化
sa.sa_flags = SA_RESTART;
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction");
return 1;
}
printf("Ctrl+Cを入力してください(無限ループ中)...\n");
while (1) {
sleep(1);
}
return 0;
}
Ctrl+Cを入力してください(無限ループ中)...
^CSIGINTを受信しました。終了します。
シグナル処理における安全性と注意点
非同期シグナルセーフ(Async-Signal-Safe)な関数
シグナルハンドラ内では、呼び出しても安全な関数が限定されています。
これを「非同期シグナルセーフ」な関数と呼び、printfやmallocなどはこれに含まれません。
printfなどの関数は内部でバッファを管理しており、シグナル割り込みによってデータ構造が壊れる可能性があるためです。
シグナルハンドラ内で文字を出力したい場合は、システムコールであるwriteを使用するのが鉄則です。
volatile sig_atomic_t型の活用
メインループとシグナルハンドラ間でフラグを共有する場合、通常のint型を使用すると最適化によって正しく動作しないことがあります。
このような場合には、volatile sig_atomic_t型を使用する必要があります。
sig_atomic_tは、読み書きがアトミック(不可分)に行われることが保証されている型です。
volatileを付与することで、コンパイラによる最適化を抑制し、常に最新のメモリ値を参照するように強制します。
リエントラント(再入可能)な設計
シグナルはいつ発生するか予測できないため、ハンドラ内で実行されるコードはリエントラントである必要があります。
静的変数やグローバル変数を使用する関数をハンドラから呼び出すと、データの整合性が失われる危険があります。
極力、ハンドラ内ではフラグを立てるだけの最小限の処理にとどめ、実際の重い処理はメインループ側で行う設計が望ましいです。
高度なシグナルハンドリング技術
SA_SIGINFOフラグと詳細情報の取得
sa_flagsにSA_SIGINFOを指定すると、シグナルに関する詳細な情報を受け取ることができます。
この場合、sa_handlerではなくsa_sigactionに関数を登録します。
void complex_handler(int sig, siginfo_t *info, void *ucontext) {
// 送信元プロセスのPIDなどを取得できる
printf("シグナル %d をプロセス %d から受信しました。\n", sig, info->si_pid);
}
この仕組みを利用することで、どのプロセスからシグナルが送られてきたかを特定し、よりきめ細やかな処理が可能になります。
シグナルセットとブロッキングの管理
特定の区間でシグナルが発生してほしくない場合、sigprocmaskを使用してシグナルをブロック(マスク)できます。
クリティカルセクションの保護に役立ちますが、ブロックしたシグナルは後で解除するまで保留されることに注意してください。
sigset_t型を操作するには、sigaddsetやsigdelset、sigfillsetなどの専用関数を使用します。
実戦的なコード例:クリーンアップ処理の自動化
プログラムの終了時に一時ファイルを削除するなど、クリーンアップが必要な場合の例を紹介します。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdbool.h>
// 終了フラグ
volatile sig_atomic_t keep_running = 1;
void termination_handler(int signum) {
keep_running = 0;
}
int main() {
struct sigaction new_action;
new_action.sa_handler = termination_handler;
sigemptyset(&new_action.sa_mask);
new_action.sa_flags = 0;
// SIGINTとSIGTERMの両方を捕捉
sigaction(SIGINT, &new_action, NULL);
sigaction(SIGTERM, &new_action, NULL);
printf("サーバープロセスを開始しました。PID: %d\n", getpid());
while (keep_running) {
// メインの処理を実行
usleep(500000);
}
// 安全なクリーンアップ
printf("\nクリーンアップ処理を実行中...\n");
printf("一時ファイルを削除しました。\n");
printf("正常に終了しました。\n");
return 0;
}
サーバープロセスを開始しました。PID: 1234
^C
クリーンアップ処理を実行中...
一時ファイルを削除しました。
正常に終了しました。
この設計では、ハンドラ内で複雑な処理を行わず、フラグの変更のみを行っている点がポイントです。
これにより、標準入出力関数の使用によるデッドロックのリスクを完全に回避しています。
まとめ
C言語におけるシグナル処理は、プロセスの制御を高度に行うために避けては通れない技術です。
従来のsignal関数は挙動の不安定さがあるため、現代のシステム開発ではsigaction関数の使用が強く推奨されます。
シグナルハンドラの実装においては、非同期シグナルセーフな関数の使用を徹底し、データの競合を避ける設計が極めて重要です。
volatile sig_atomic_tを活用したフラグ管理や、適切なシグナルマスクの設定を行うことで、堅牢なアプリケーションを構築できます。
本記事で紹介した手法を参考に、安全で効率的な割り込み制御をプログラムに取り入れてみてください。
