C++の応用 - シグナル
概要
シグナル (Signal) は、Unix系オペレーティングシステム (Linux含む) において、プロセスに対して非同期的にイベントを通知するための仕組みである。
シグナルは、プロセス間通信 (IPC) の一種として分類されるが、他のIPC機構と比較して、情報量が限定的である点が特徴である。
シグナルは、以下に示すような状況で使用される。
- カーネルからプロセスへの通知
- 例 : 不正なメモリアクセス、ゼロ除算等の例外
- ユーザからプロセスへの制御
- 例 : [Ctrl] + [C]キーによる割り込み、killコマンドによる終了要求
- プロセス間の通知
- 例 : 子プロセスの終了通知、タイマの満了通知
シグナルは、POSIXで標準化されており、移植性の高いプログラミングが可能である。
ただし、Linuxでは、POSIXで定義されていない独自のシグナルや拡張機能も提供されている。
C++でシグナルを扱う場合、C言語のAPIをそのまま使用することができるが、モダンC++の機能を活用することにより、より安全で保守性の高いソースコードを記述することができる。
シグナルの種類
Linuxでは、多数のシグナルが定義されており、それぞれ異なる意味と用途を持つ。
下表に、主要なシグナルを示す。
| シグナル名 | 番号 | デフォルト動作 | 説明 |
|---|---|---|---|
| SIGHUP | 1 | 終了 | ハングアップ検出、制御端末のクローズ、またはプロセスの死 |
| SIGINT | 2 | 終了 | キーボードからの割り込み (通常はCtrl+C) |
| SIGQUIT | 3 | 終了+コアダンプ | キーボードからの終了シグナル (通常はCtrl+\) |
| SIGILL | 4 | 終了+コアダンプ | 不正な命令 |
| SIGABRT | 6 | 終了+コアダンプ | abort()関数からの中止シグナル |
| SIGFPE | 8 | 終了+コアダンプ | 浮動小数点例外 |
| SIGKILL | 9 | 終了 | 強制終了 (キャッチ不可) |
| SIGSEGV | 11 | 終了+コアダンプ | 不正なメモリ参照 |
| SIGPIPE | 13 | 終了 | パイプ破壊: 読み手のないパイプへの書き込み |
| SIGALRM | 14 | 終了 | alarm()からのタイマーシグナル |
| SIGTERM | 15 | 終了 | 終了シグナル |
| SIGUSR1 | 10 | 終了 | ユーザ定義シグナル1 |
| SIGUSR2 | 12 | 終了 | ユーザ定義シグナル2 |
| SIGCHLD | 17 | 無視 | 子プロセスの停止または終了 |
| SIGCONT | 18 | 継続 | 停止していれば継続 |
| SIGSTOP | 19 | 停止 | プロセスの停止 (キャッチ不可) |
| SIGTSTP | 20 | 停止 | 端末で入力された停止 (通常はCtrl+Z) |
※注意
SIGKILL と SIGSTOPは、キャッチ、ブロック、または無視することができない。
これらは、システムがプロセスを確実に制御するために予約されているためである。
signal関数
基本的なシグナルハンドラの登録方法は、signal関数を使用する方法である。
#include <iostream>
#include <csignal>
#include <unistd.h>
// シグナルハンドラ関数
// シグナルハンドラは、シグナル番号を引数として受け取る
void signalHandler(int signum)
{
std::cout << "Interrupt signal (" << signum << ") received." << std::endl;
// プログラムを終了
exit(signum);
}
int main()
{
// SIGINTシグナル ([Ctrl] + [C]キー) に対してハンドラを登録
signal(SIGINT, signalHandler);
std::cout << "Press [Ctrl] + [C] to trigger signal handler." << std::endl;
// 無限ループで待機
while(true) {
std::cout << "Running..." << std::endl;
sleep(1);
}
return 0;
}
ただし、signal関数の動作は、POSIXで完全に標準化されておらず、システムによって微妙に異なる場合がある。
このため、移植性の高いソースコードを記述する場合は、sigaction関数の使用が推奨される。
sigaction関数
sigaction関数は、signal関数よりも詳細な制御が可能で、POSIXで完全に標準化されている。
#include <iostream>
#include <csignal>
#include <cstring>
#include <unistd.h>
// シグナルハンドラ関数
void signalHandler(int signum, siginfo_t* info, void* context)
{
std::cout << "Signal " << signum << " received from process " << info->si_pid << std::endl;
}
int main()
{
struct sigaction sa;
// sigaction構造体の初期化
std::memset(&sa, 0, sizeof(sa));
// シグナルハンドラを設定
sa.sa_sigaction = signalHandler;
// SA_SIGINFOフラグを設定して、拡張情報を取得
sa.sa_flags = SA_SIGINFO;
// SIGINTに対してハンドラを登録
if (sigaction(SIGINT, &sa, nullptr) == -1) {
std::cerr << "Error setting up signal handler" << std::endl;
return 1;
}
std::cout << "Press [Ctrl] + [C] to trigger signal handler." << std::endl;
// 無限ループで待機
while(true) {
std::cout << "Running..." << std::endl;
sleep(1);
}
return 0;
}
| メンバ名 | 説明 |
|---|---|
| sa_handler | 従来型のシグナルハンドラ関数へのポインタ |
| sa_sigaction | 拡張情報を受け取るシグナルハンドラ関数へのポインタ |
| sa_mask | ハンドラ実行中にブロックするシグナルのマスク |
| sa_flags | シグナル処理の動作を制御するフラグ |
| フラグ名 | 説明 |
|---|---|
| SA_SIGINFO | 拡張情報を含むハンドラ (sa_sigaction) を使用 |
| SA_RESTART | 一部のシステムコールを自動的に再開 |
| SA_NOCLDSTOP | 子プロセスが停止したときにSIGCHLDを生成しない |
| SA_NOCLDWAIT | 子プロセスの終了時にゾンビプロセスを作成しない |
| SA_NODEFER | ハンドラ実行中に同じシグナルをブロックしない |
| SA_RESETHAND | ハンドラ実行後にシグナルの動作をデフォルトに戻す |
C++でのシグナル処理
std::signal
C++11以降では、std::signal関数が標準ライブラリに含まれている。
これは、C言語のsignal関数のラッパーである。
以下の例では、std::atomicを使用してシグナルハンドラとメインスレッド間で安全にフラグを共有している。
シグナルハンドラ内では、複雑な処理を避け、アトミック変数の設定のみを行うことが推奨される。
#include <iostream>
#include <csignal>
#include <atomic>
#include <thread>
#include <chrono>
// グローバルなアトミック変数でシグナル受信を管理
std::atomic<bool> signalReceived(false);
void signalHandler(int signum)
{
signalReceived.store(true);
}
int main()
{
// シグナルハンドラを登録
std::signal(SIGINT, signalHandler);
std::signal(SIGTERM, signalHandler);
std::cout << "Running... Press Ctrl+C to stop." << std::endl;
// メインループ
while (!signalReceived.load()) {
std::cout << "Working..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
std::cout << "Signal received. Cleaning up..." << std::endl;
return 0;
}
signalfd関数を使用した同期的なシグナル処理
Linuxでは、signalfdシステムコールを使用して、シグナルをファイルディスクリプタ経由で処理することができる。
これにより、select関数、poll関数、epoll関数等の多重化機構と統合することが可能になる。
signalfd関数を使用するメリットは、以下の通りである。
- シグナルを同期的に処理できる。
- 複数のイベントソース (ファイル、ソケット、シグナル等) を統一的に処理できる。
- 非同期シグナル安全性の問題を回避できる。
#include <iostream>
#include <sys/signalfd.h>
#include <signal.h>
#include <unistd.h>
#include <cstring>
int main()
{
sigset_t mask;
int sfd;
struct signalfd_siginfo fdsi;
ssize_t s;
// SIGINTとSIGTERMをマスクに追加
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);
// これらのシグナルをブロック
// signalfd関数は、ブロックされたシグナルのみを処理できる
if (sigprocmask(SIG_BLOCK, &mask, nullptr) == -1) {
std::cerr << "sigprocmask error" << std::endl;
return 1;
}
// signalfd関数でファイルディスクリプタを作成
sfd = signalfd(-1, &mask, 0);
if (sfd == -1) {
std::cerr << "signalfd error" << std::endl;
return 1;
}
std::cout << "Waiting for signals... Press Ctrl+C to trigger." << std::endl;
// メインループ
while (true) {
// シグナルを待機
s = read(sfd, &fdsi, sizeof(struct signalfd_siginfo));
if (s != sizeof(struct signalfd_siginfo)) {
std::cerr << "read error" << std::endl;
break;
}
// 受信したシグナルを処理
if (fdsi.ssi_signo == SIGINT) {
std::cout << "Got SIGINT" << std::endl;
break;
}
else if (fdsi.ssi_signo == SIGTERM) {
std::cout << "Got SIGTERM" << std::endl;
break;
}
else {
std::cout << "Read unexpected signal" << std::endl;
}
}
close(sfd);
return 0;
}
シグナルマスクの操作
シグナルマスクは、プロセスがどのシグナルをブロックするかを制御する。
ブロックされたシグナルは、配信が保留され、ブロックが解除されるまで処理されない。
sigprocmask関数の第1引数で指定できる操作を以下に示す。
- SIG_BLOCK
- 指定されたシグナルを現在のマスクに追加
- SIG_UNBLOCK
- 指定されたシグナルを現在のマスクから削除
- SIG_SETMASK
- シグナルマスクを指定されたセットで置き換え
#include <iostream>
#include <csignal>
#include <unistd.h>
#include <cstring>
void criticalSection()
{
sigset_t newmask, oldmask;
// 新しいシグナルマスクを作成
sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);
sigaddset(&newmask, SIGTERM);
// シグナルをブロックし、古いマスクを保存
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) == -1) {
std::cerr << "sigprocmask error" << std::endl;
return;
}
std::cout << "Critical section: signals blocked" << std::endl;
// クリティカルセクション
for (int i = 0; i < 5; i++) {
std::cout << "Working in critical section... " << i << std::endl;
sleep(1);
}
std::cout << "Critical section done" << std::endl;
// 元のシグナルマスクを復元
if (sigprocmask(SIG_SETMASK, &oldmask, nullptr) == -1) {
std::cerr << "sigprocmask restore error" << std::endl;
}
}
void signalHandler(int signum)
{
std::cout << "Signal " << signum << " received" << std::endl;
}
int main()
{
// シグナルハンドラを登録
signal(SIGINT, signalHandler);
signal(SIGTERM, signalHandler);
std::cout << "Before critical section - signals are not blocked" << std::endl;
sleep(3);
criticalSection();
std::cout << "After critical section - signals are unblocked again" << std::endl;
sleep(3);
return 0;
}
タイマシグナルの使用
シグナルを使用して、アラームやタイマ機能を定義することができる。
alarm関数を使用した単純なタイマ
#include <iostream>
#include <csignal>
#include <unistd.h>
void alarmHandler(int signum)
{
std::cout << "Timer expired!" << std::endl;
}
int main()
{
// SIGALRMハンドラを登録
signal(SIGALRM, alarmHandler);
std::cout << "Setting alarm for 5 seconds..." << std::endl;
// 5秒後にSIGALRMを送信
alarm(5);
std::cout << "Waiting..." << std::endl;
// タイマを待機
pause(); // シグナルを受信するまで待機
std::cout << "Done" << std::endl;
return 0;
}
setitimer関数を使用した周期的なタイマ
setitimer関数では、3種類のタイマを指定することができる。
- ITIMER_REAL
- 実時間で計測し、SIGALRMを送信
- ITIMER_VIRTUAL
- プロセスのユーザ時間で計測し、SIGVTALRMを送信
- ITIMER_PROF
- プロセスのユーザ時間とシステム時間の合計で計測し、SIGPROFを送信
#include <iostream>
#include <csignal>
#include <sys/time.h>
#include <unistd.h>
volatile int timerCount = 0;
void timerHandler(int signum)
{
timerCount++;
std::cout << "Timer fired: " << timerCount << std::endl;
}
int main()
{
struct itimerval timer;
// SIGALRMハンドラを登録
signal(SIGALRM, timerHandler);
// 最初のタイマ満了時間を設定 (2秒後)
timer.it_value.tv_sec = 2;
timer.it_value.tv_usec = 0;
// タイマの周期を設定 (1秒ごと)
timer.it_interval.tv_sec = 1;
timer.it_interval.tv_usec = 0;
// タイマを開始
if (setitimer(ITIMER_REAL, &timer, nullptr) == -1) {
std::cerr << "Error setting timer" << std::endl;
return 1;
}
std::cout << "Timer started. Will fire 10 times." << std::endl;
// タイマイベントが10回実行するまで待機
while (timerCount < 10) {
sleep(1);
}
// タイマを停止
timer.it_value.tv_sec = 0;
timer.it_value.tv_usec = 0;
timer.it_interval.tv_sec = 0;
timer.it_interval.tv_usec = 0;
setitimer(ITIMER_REAL, &timer, nullptr);
std::cout << "Timer stopped" << std::endl;
return 0;
}
子プロセスの終了処理
子プロセスが終了すると、親プロセスにSIGCHLDシグナルが送信される。
これを適切に処理することで、ゾンビプロセスの発生を防ぐことができる。
waitpid関数のオプションを以下に示す。
- WNOHANG
- 終了した子プロセスがない場合、ブロックせずに即座に戻る
- WUNTRACED
- 停止した子プロセスの状態も返す
- WCONTINUED
- 再開した子プロセスの状態も返す
#include <iostream>
#include <csignal>
#include <sys/wait.h>
#include <unistd.h>
#include <cstring>
void sigchldHandler(int signum)
{
int status;
pid_t pid;
// 全ての終了した子プロセスを回収
// WAITNOHANGを指定することで、ノンブロッキングで処理
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
if (WIFEXITED(status)) {
std::cout << "Child process " << pid << " exited with status " << WEXITSTATUS(status) << std::endl;
}
else if (WIFSIGNALED(status)) {
std::cout << "Child process " << pid << " killed by signal " << WTERMSIG(status) << std::endl;
}
}
}
int main()
{
struct sigaction sa;
// sigaction構造体の初期化
std::memset(&sa, 0, sizeof(sa));
sa.sa_handler = sigchldHandler;
// SA_RESTARTフラグを設定して、システムコールを自動再開
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
// SIGCHLDハンドラを登録
if (sigaction(SIGCHLD, &sa, nullptr) == -1) {
std::cerr << "sigaction error" << std::endl;
return 1;
}
std::cout << "Creating child processes..." << std::endl;
// 3つの子プロセスを作成
for (int i = 0; i < 3; i++) {
pid_t pid = fork();
if (pid == -1) {
std::cerr << "fork error" << std::endl;
return 1;
}
else if (pid == 0) {
// 子プロセス
std::cout << "Child process " << getpid() << " started" << std::endl;
sleep(i + 1); // 異なる時間だけスリープ
std::cout << "Child process " << getpid() << " exiting" << std::endl;
exit(i);
}
}
// 親プロセス
std::cout << "Parent process waiting for children..." << std::endl;
// 全ての子プロセスが終了するまで待機
sleep(5);
std::cout << "Parent process done" << std::endl;
return 0;
}
非同期シグナル安全性
シグナルハンドラ内では、非同期シグナル安全 (async-signal-safe) な関数のみを呼び出すことができる。
これは、シグナルハンドラが、プログラムの任意の時点で実行される可能性があるためである。
非同期シグナル安全な関数の例を以下に示す。
- write関数
- _exit関数
- signal関数
- sigaction関数
- kill関数
- getpid関数
非同期シグナル安全でない関数の例を以下に示す。
- printf関数 / std::cout
- 内部でロックを使用している
- malloc関数 / new
- メモリアロケータが非再入可能
- pthread関数
- 多くのpthread関数が非再入可能
sig_atomic_t型は、シグナルハンドラとの間で安全に値を交換できることが保証された整数型である。
ただし、volatileキーワードを付加することにより、コンパイラの最適化を防ぐ必要がある。
以下の例では、安全にシグナルハンドラを定義している。
#include <unistd.h>
#include <csignal>
#include <cstring>
// グローバルフラグ (volatileとして宣言)
volatile sig_atomic_t signalFlag = 0;
void signalHandler(int signum)
{
// sig_atomic_t型の変数への代入は安全
signalFlag = 1;
// write関数は非同期シグナル安全
const char msg[] = "Signal received\n";
write(STDOUT_FILENO, msg, sizeof(msg) - 1);
}
int main()
{
signal(SIGINT, signalHandler);
while (!signalFlag) {
// メインループ
sleep(1);
}
// シグナルフラグがセットされたら、メインプログラムで処理
// ここでは非同期シグナル安全でない関数を使用できる
// std::cout等の使用が可能
return 0;
}
リアルタイムシグナル
POSIXリアルタイムシグナルは、従来のシグナルに対する拡張機能である。
Linuxでは、SIGRTMIN から SIGRTMAX までの範囲のシグナル番号が、リアルタイムシグナルとして使用できる。
リアルタイムシグナルの特徴を以下に示す。
- キューイング
- 複数の同じシグナルがキューに入る
- 優先順位
- シグナル番号の小さいものから順に配信される
- データ送信
- シグナルと共に整数値やポインタを送信できる
sigqueue関数は、リアルタイムシグナルを送信するために使用される。
これにより、シグナルと共にデータを送信することができる。
#include <iostream>
#include <csignal>
#include <unistd.h>
#include <cstring>
void realtimeSignalHandler(int signum, siginfo_t* info, void* context)
{
std::cout << "Received real-time signal " << signum << std::endl;
std::cout << "Value: " << info->si_value.sival_int << std::endl;
std::cout << "From PID: " << info->si_pid << std::endl;
}
int main()
{
struct sigaction sa;
// sigaction構造体の初期化
std::memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = realtimeSignalHandler;
sa.sa_flags = SA_SIGINFO;
// リアルタイムシグナルのハンドラを登録
int rtsig = SIGRTMIN + 5;
if (sigaction(rtsig, &sa, nullptr) == -1) {
std::cerr << "sigaction error" << std::endl;
return 1;
}
std::cout << "Real-time signal handler registered for signal " << rtsig << std::endl;
// 自分自身にシグナルを送信
union sigval value;
value.sival_int = 42;
if (sigqueue(getpid(), rtsig, value) == -1) {
std::cerr << "sigqueue error" << std::endl;
return 1;
}
// シグナルを待機
sleep(1);
return 0;
}
RAIIパターンを使用したシグナルマスク管理
現代のC++では、RAIIパターンを使用して、シグナルマスクの管理を自動化することができる。
#include <iostream>
#include <csignal>
#include <unistd.h>
// シグナルマスクを管理するRAIIクラス
class SignalMaskGuard
{
private:
sigset_t oldmask_;
bool valid_;
public:
// コンストラクタでシグナルをブロック
SignalMaskGuard(const sigset_t& mask) : valid_(false)
{
if (sigprocmask(SIG_BLOCK, &mask, &oldmask_) == 0) {
valid_ = true;
}
}
// デストラクタで元のマスクを復元
~SignalMaskGuard()
{
if (valid_) {
sigprocmask(SIG_SETMASK, &oldmask_, nullptr);
}
}
// コピー禁止
SignalMaskGuard(const SignalMaskGuard&) = delete;
SignalMaskGuard& operator=(const SignalMaskGuard&) = delete;
};
void criticalSection()
{
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
// スコープを抜けると自動的にマスクが復元される
SignalMaskGuard guard(mask);
std::cout << "In critical section - signals blocked" << std::endl;
sleep(3);
std::cout << "Leaving critical section" << std::endl;
}
void signalHandler(int signum)
{
std::cout << "Signal " << signum << " received" << std::endl;
}
int main()
{
signal(SIGINT, signalHandler);
std::cout << "Before critical section" << std::endl;
sleep(2);
criticalSection();
std::cout << "After critical section - signals unblocked" << std::endl;
sleep(2);
return 0;
}
その他
シグナルハンドラの定義
- シグナルハンドラは可能な限り単純に記述する。
- フラグを設定するだけの最小限の処理が推奨される。
- 非同期シグナル安全な関数のみを使用する。
- printf関数、malloc関数等は使用してはならない。
- グローバル変数を使用する場合は、
sig_atomic_t型 とvolatile修飾子を使用する。- これにより、アトミックな操作とコンパイラの最適化防止が保証される。
シグナルマスクの管理
- クリティカルセクションでは、必要なシグナルをブロックする。
- 重要な処理中に割り込まれないようにする。
- sigprocmask関数の戻り値を必ず確認する。
- エラーが発生した場合の処理を適切に行う。
- RAIIパターンを使用して、マスクの復元を自動化する。
- スコープを抜ける際に、確実に元のマスクに戻す。
子プロセスの管理
- SIGCHLDハンドラを必ず実装する。
- ゾンビプロセスの発生を防ぐ。
- waitpid関数をループで呼び出す。
- 複数の子プロセスが同時に終了する可能性がある。
- WNOHANGオプションを使用する。
- ハンドラ内でブロックしないようにする。
エラーハンドリング
- 全てのシステムコールの戻り値を確認する。
- エラーが発生した場合の処理を実装する。
- errnoを適切に処理する。
- システムコールのエラー原因を特定する。
マルチスレッド環境
- シグナルマスクはスレッドごとに独立している。
- 各スレッドで適切にマスクを設定する必要がある。
pthread_sigmask関数を使用する。- マルチスレッドプログラムでは、
sigprocmask関数 ではなくpthread_sigmask関数を使用する。
- マルチスレッドプログラムでは、
- シグナルを特定のスレッドで処理する設計を検討する。
- 専用のシグナルハンドリングスレッドを作成する方法もある。
CMakeを使用したビルド設定
シグナル処理を使用するプログラムのCMakeLists.txtファイルの設定例を以下に示す。
cmake_minimum_required(VERSION 3.16)
project(<任意のプロジェクト名>)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# ...略
# 実行可能ファイルの作成
add_executable(<任意の実行可能ファイル名>
# ...略
)
# システムライブラリのリンク
# 通常、特別なライブラリは不要
# リアルタイム拡張を使用する場合のみ -lrt が必要な場合がある
if(UNIX AND NOT APPLE)
target_link_libraries(<任意の実行可能ファイル名> PRIVATE rt)
endif()