C++におけるsignal関数の活用方法8選

C++プログラミングでsignal関数を使用する方法のイメージC++
この記事は約19分で読めます。

 

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

基本的な知識があればカスタムコードを使って機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

C++を学び始めた方や、すでにある程度の知識を持っている方にとって、signal関数の理解と適用はプログラミングスキルを一段階引き上げるチャンスです。

この記事では、C++のsignal関数について、その基本から応用まで詳しく解説します。

プログラムの安定性を高め、予期せぬエラーからデータを守るための重要な機能であるこの関数の使い方をマスターすることで、より高度なプログラミングが可能になります。

●signal関数とは

C++でプログラムを書いていると、外部からのさまざまなシグナルに対応する必要が出てきます。

プロセスに対する外部の割り込みや、システムからの通知など、さまざまなシグナルがプログラムに影響を与えることがあります。

signal関数は、これらのシグナルを捕捉し、指定した関数(シグナルハンドラ)を実行することで、プログラムの振る舞いをコントロールします。

具体的には、SIGINTやSIGTERMなど、特定のシグナルに対してカスタムの応答を設定することができるのです。

○signal関数の基本理解

signal関数の使用方法は、非常に直感的です。

具体的には、signal関数は二つの引数を取ります。

一つ目の引数はシグナルの種類(例えばSIGINT)、二つ目の引数はそのシグナルを捕捉した際に呼び出される関数(シグナルハンドラ)です。

シグナルハンドラは、シグナルがプログラムに送られたときに実行される関数で、この設定によりプログラムはユーザーからの割り込みに対応したり、シャットダウンプロセスを安全に管理することができます。

例として、プログラムがCTRL+C(SIGINT)を受け取った際に安全に終了するためのシグナルハンドラを設定するコードは下記のようになります。

#include <csignal>
#include <iostream>
#include <unistd.h>

void signalHandler(int signum) {
    std::cout << "Interrupt signal (" << signum << ") received.\n";
    // クリーンアップとシャットダウン処理をここに記述
    exit(signum);  
}

int main () {
    // シグナル SIGINT とシグナルハンドラを結び付ける
    signal(SIGINT, signalHandler);

    // 無限ループでプログラムを動かし続ける
    while(true) {
        std::cout << "Program is running..." << std::endl;
        sleep(1);
    }

    return 0;
}

このコードでは、signal関数を使ってSIGINTが発生した場合にsignalHandler関数が呼ばれるように設定しています。

ユーザーがCTRL+Cを押したときにプログラムが適切な処理を行って終了することができます。

●signal関数の使い方

前述したように、signal関数はC++において重要な役割を果たします。

この関数を利用することで、プログラムは外部からの様々なシグナルに適切に反応し、適切なハンドラ関数を呼び出して対応することが可能になります。

主にプログラムの異常終了を防ぐため、または特定のシグナルに対してカスタマイズされた挙動を設定するために使用されます。

○サンプルコード1:シグナル処理の基本設定

C++においてシグナル処理を設定する最も基本的な方法は、signal()関数を使用することです。

下記のサンプルコードは、SIGINTシグナル(通常はCtrl+Cによって発生)を捕捉し、カスタム関数を呼び出してプログラムを終了する基本的な構成を表しています。

#include <csignal>
#include <iostream>
#include <cstdlib>

void handleInterrupt(int signal) {
    std::cout << "Caught signal " << signal << ", exiting now." << std::endl;
    std::exit(signal);
}

int main() {
    signal(SIGINT, handleInterrupt);
    while(true) {
        std::cout << "Running... Press Ctrl+C to stop." << std::endl;
        sleep(1);
    }
    return 0;
}

このコードは、プログラムがCtrl+Cを受け取ったときに、handleInterrupt関数を呼び出し、プログラムを安全に終了させる方法を表しています。

このようにsignal関数を使うことで、プログラムは外部の割り込みに対応しやすくなります。

○サンプルコード2:外部割り込みのハンドリング

外部からの割り込みを効果的に扱うためには、シグナルハンドラの設計が重要です。

下記のコードは、複数のシグナルに対応するための一例を表しています。

#include <csignal>
#include <iostream>
#include <unistd.h>

void signalHandler(int signum) {
    std::cout << "Received signal " << signum << std::endl;
    if (signum == SIGTERM) {
        std::cout << "SIGTERM received, shutting down." << std::endl;
        std::exit(signum);
    }
}

int main() {
    signal(SIGINT, signalHandler);
    signal(SIGTERM, signalHandler);
    while (true) {
        sleep(1);
    }
    return 0;
}

このコードでは、SIGINTとSIGTERMの二つのシグナルに対して同じハンドラ関数を割り当てています。

これにより、プログラムはこれらのシグナルを受け取った際に適切に反応し、必要に応じてプログラムを終了します。

○サンプルコード3:カスタムシグナルの作成

特定のカスタムシグナルを生成して、プログラム内で特定のイベントをトリガすることも可能です。

下記のコードは、カスタムシグナルを使用して特定の処理を行う方法を表しています。

#include <csignal>
#include <iostream>
#include <unistd.h>

void customSignalHandler(int signum) {
    std::cout << "Custom signal " << signum << " received, performing custom action." << std::endl;
}

int main() {
    signal(SIGUSR1, customSignalHandler);
    while (true) {
        sleep(1);
        raise(SIGUSR1); // 自身にカスタムシグナルを送信
    }
    return 0;
}

このコードでは、SIGUSR1というユーザー定義のシグナルを用いて、定期的にカスタムアクションを実行しています。

raise()関数を使用することで、プログラムは自身にシグナルを送信することができます。

○サンプルコード4:プログラムの安全な終了

シグナルを利用してプログラムの終了時にクリーンアップ処理を行う例を紹介します。

#include <csignal>
#include <iostream>
#include <cstdlib>

void terminateHandler(int signal) {
    std::cout << "Termination signal received, cleaning up..." << std::endl;
    // 必要なリソース解放やファイルクローズ処理をここで行う
    std::exit(signal);
}

int main() {
    signal(SIGTERM, terminateHandler);
    while(true) {
        std::cout << "Program is running. Send SIGTERM to terminate." << std::endl;
        sleep(1);
    }
    return 0;
}

このコードでは、SIGTERMシグナルを受け取った際に、リソースの解放やファイルのクローズなどのクリーンアップ処理を行うことで、プログラムを安全に終了させています。

○サンプルコード5:エラーシグナルのキャッチと対応

プログラムで予期せぬエラーが発生した場合、シグナルハンドリングを通じて適切に対応することが重要です。

下記のコードは、シグナルによるエラーハンドリングの一例を表しています。

#include <csignal>
#include <iostream>
#include <cstdlib>

void errorHandler(int signal) {
    std::cerr << "Error signal " << signal << " received, performing error handling." << std::endl;
    std::exit(1); // エラー終了
}

int main() {
    signal(SIGSEGV, errorHandler); // セグメンテーション違反のハンドリング
    int *p = nullptr;
    *p = 10;  // ここでセグメンテーション違反が発生する
    return 0;
}

このコードは、セグメンテーション違反(SIGSEGV)が発生した際に、エラーハンドラが呼び出され、プログラムがエラー終了する様子を表しています。

これにより、プログラムが予期せぬ状態に陥るのを防ぐことができます。

●よくあるエラーと対処法

C++でのsignal関数の使用においては、多くの一般的なエラーが発生する可能性があります。

これらのエラーは、プログラムの予期せぬ動作を引き起こす原因となるため、それぞれの問題に対処する適切な方法を理解することが重要です。

○エラーケース1:シグナルがキャッチできない

一つの典型的な問題は、シグナルが適切にキャッチされないことです。

この問題は通常、シグナルハンドラの設定ミスや、プログラムの他の部分がシグナルをブロックしている場合に発生します。

たとえば、下記のコードではSIGINTシグナルをキャッチしようとしていますが、設定に誤りがあるためにシグナルがキャッチされません。

#include <csignal>
#include <iostream>
#include <unistd.h>

void signalHandler(int signum) {
    std::cout << "Signal " << signum << " caught." << std::endl;
}

int main() {
    signal(SIGINT, SIG_IGN);  // SIGINTを無視
    signal(SIGINT, signalHandler);  // ハンドラを設定

    while (true) {
        std::cout << "Running..." << std::endl;
        sleep(1);
    }
    return 0;
}

このコードの問題点は、最初にSIGINTを無視するよう設定してからハンドラを設定していることです。

この順序では、最初の設定が後の設定によって上書きされず、シグナルはキャッチされません。

正しいアプローチは、ハンドラを最初に設定し、必要に応じて変更することです。

○エラーケース2:シグナルハンドラの誤用

シグナルハンドラの誤用もまた一般的な問題です。

シグナルハンドラ内で非再入可能な関数(例えば、mallocやprintfなど)を使用すると、プログラムが不安定になる可能性があります。

下記のコード例では、シグナルハンドラ内でstd::coutを使用していますが、これはシグナルセーフではありません。

#include <csignal>
#include <iostream>
#include <unistd.h>

void unsafeHandler(int signum) {
    std::cout << "Unsafe signal handler called with signal " << signum << std::endl;
}

int main() {
    signal(SIGINT, unsafeHandler);
    while (true) {
        sleep(1);
    }
    return 0;
}

このハンドラは、シグナルが発生した際に予期せぬ動作を引き起こすかもしれません。

シグナルハンドラで安全に使用できる関数のみを使用し、シグナルセーフなコードを書くことが重要です。

また、シグナルハンドラのロジックをシンプルに保ち、複雑な操作は避けるべきです。

●signal関数の応用例

C++のsignal関数は、基本的なエラーハンドリングやプロセス管理を超えた応用が可能です。

ここでは、マルチスレッド環境での利用やプログラムの動的な管理に役立つ応用例を見ていきます。

○サンプルコード6:マルチスレッド環境でのsignal関数利用

マルチスレッドプログラムでは、シグナルを特定のスレッドに送信して、特定の処理を行うことがしばしば求められます。

下記のサンプルコードでは、メインスレッドがシグナルを受け取り、ワーカースレッドに特定のタスクを割り当てる方法を表しています。

#include <csignal>
#include <iostream>
#include <thread>
#include <atomic>
#include <unistd.h>

std::atomic<bool> shutdownFlag(false);

void handleSignal(int signum) {
    std::cout << "Signal " << signum << " received, initiating shutdown..." << std::endl;
    shutdownFlag.store(true);
}

void workerThread() {
    while (!shutdownFlag.load()) {
        std::cout << "Worker thread is processing..." << std::endl;
        sleep(1);
    }
    std::cout << "Worker thread is stopping..." << std::endl;
}

int main() {
    signal(SIGINT, handleSignal);
    std::thread worker(workerThread);

    std::cout << "Main thread is running. Press Ctrl+C to stop." << std::endl;
    worker.join();
    return 0;
}

このコードでは、SIGINTシグナルによってshutdownFlagがtrueに設定され、ワーカースレッドが安全に停止します。

○サンプルコード7:動的にシグナルハンドラを変更する

システムの状態に応じてシグナルハンドラを動的に変更することで、柔軟にプログラムの挙動を制御することができます。

下記の例では、プログラム実行中にハンドラを変更する方法を表しています。

#include <csignal>
#include <iostream>
#include <unistd.h>

void initialHandler(int signum) {
    std::cout << "Initial handler for signal " << signum << std::endl;
    // 初期ハンドラから別のハンドラへ切り替え
    signal(signum, SIG_DFL);
}

int main() {
    signal(SIGINT, initialHandler);

    int count = 0;
    while (count < 5) {
        std::cout << "Running..." << std::endl;
        sleep(1);
        count++;
    }

    // デフォルトのシグナルハンドラに戻す
    signal(SIGINT, SIG_DFL);
    std::cout << "Default handler restored. Press Ctrl+C again to exit." << std::endl;

    while (true) {
        sleep(1);
    }

    return 0;
}

このコードでは、初めにカスタムハンドラを設定し、プログラムの状況に応じてデフォルトのハンドラに戻します。

○サンプルコード8:プログラムのモニタリングと自動再起動

プログラムが予期せぬシグナルを受けて終了した場合に自動的に再起動するように設定することができます。

下記の例では、SIGSEGVを検出し、プログラムを再起動する方法を表しています。

#include <csignal>
#include <unistd.h>
#include <cstdlib>
#include <iostream>

void restartHandler(int signum) {
    std::cerr << "Unhandled signal " << signum << " received, restarting..." << std::endl;
    // プログラムを再起動する前のクリーンアップ
    execl("/usr/bin/myProgram", "myProgram", (char *)NULL);
}

int main() {
    signal(SIGSEGV, restartHandler);
    int *p = nullptr;
    std::cout << "Program is running... Press Ctrl+C to stop or cause a segmentation fault." << std::endl;
    *p = 10;  // セグメンテーション違反を故意に引き起こす
    return 0;
}

このコードは非常に危际な使用例であり、通常は推奨されませんが、デモンプロセスなど、一部の背景で動作するアプリケーションでは有効な場合があります。

再起動の前に適切なクリーンアップとログ記録を行うことが重要です。

●エンジニアなら知っておくべき豆知識

エンジニアとして知っておくべき重要な情報のひとつに、シグナル処理のポータブルな考え方や、シグナルセーフ関数についての理解があります。

これらは特に、異なるプラットフォーム間でのプログラムの移植性や安全性を高めるために役立ちます。

○豆知識1:ポータブルなシグナル処理の考え方

プログラムが異なるオペレーティングシステム上で一貫して動作するようにするためには、ポータブルなコーディング技法が必要です。

シグナル処理においても、ポータブルな設計を意識することが重要です。

例えば、POSIX標準を準拠しているシステムでは、シグナルの種類やハンドラの設定方法が一定ですが、システムによっては独自の拡張が存在することがあります。

下記のコードスニペットは、ポータブルなシグナルハンドラの設定方法を表しています。

#include <csignal>
#include <iostream>
#include <unistd.h>

void portableSignalHandler(int signum) {
    std::cout << "Portable handler for signal " << signum << std::endl;
    // ここにクリーンアップコードを書く
}

int main() {
    struct sigaction sa;
    sa.sa_handler = portableSignalHandler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;  // または SA_RESTART などのフラグを設定

    sigaction(SIGINT, &sa, NULL);

    while (true) {
        std::cout << "Program is running. Press Ctrl+C to test." << std::endl;
        sleep(1);
    }

    return 0;
}

この例では、sigactionを使用してシグナルハンドラを設定し、より詳細なシグナル制御を可能にしています。

○豆知識2:シグナルセーフ関数とは

プログラムのシグナルハンドラ内で安全に呼び出せる関数は、シグナルセーフ関数と呼ばれます。

多くの標準ライブラリ関数はシグナルハンドラ内での使用が安全ではないため、これらを使用する際には注意が必要です。

シグナルセーフな関数の例としては、_Exitwriteがあります。

#include <csignal>
#include <unistd.h>

void safeSignalHandler(int signum) {
    const char msg[] = "Signal received, safely handling it.\n";
    write(STDOUT_FILENO, msg, sizeof(msg) - 1);
    _Exit(0);
}

int main() {
    signal(SIGINT, safeSignalHandler);

    while (true) {
        pause();  // プログラムをシグナルを待機させる
    }

    return 0;
}

このコードでは、write_Exitを使用して、シグナルハンドラ内での安全な操作を行っています。

これにより、シグナルを受け取った際の挙動が安定し、予期せぬ動作から保護することが可能です。

まとめ

この記事では、C++でのsignal関数の基本的な使い方から応用テクニックまでを詳しく解説しました。

初学者から中級者まで、プログラム中でのシグナル処理の理解を深めるために役立つ内容となっています。

特に、シグナルセーフ関数やポータブルなシグナル処理の考え方を学ぶことは、多様な環境でのプログラム開発において重要です。

これらの知識を活用して、より堅牢で信頼性の高いアプリケーション開発を目指しましょう。