[PHP]pcntl_signal関数とは?シグナルハンドラの設定方法を徹底解説

PHP

こんにちは!今回はPHPのプロセス制御で重要な役割を果たすpcntl_signal関数について、実践的なコード例を交えながら詳しく解説していきます。プロセスの終了処理やタイムアウト制御を実装したい方は必見です!

pcntl_signal関数とは?

pcntl_signal()は、指定したシグナルに対するハンドラ(処理関数)を設定する関数です。Unixシグナルは、プロセス間の通信やイベント通知に使われる仕組みで、これを活用することで、Ctrl+Cによる中断処理やタイムアウト制御などを実装できます。

基本構文

pcntl_signal(int $signal, callable|int $handler, bool $restart_syscalls = true): bool
  • $signal: シグナル番号(SIGTERM、SIGINTなどの定数)
  • $handler: シグナルを受け取った時に実行する関数、またはSIG_IGN/SIG_DFL
  • $restart_syscalls: システムコールの自動再開フラグ(デフォルト: true)
  • 戻り値: 成功時true、失敗時false

主要なシグナルの種類

シグナル定数名意味
2SIGINTCtrl+Cによる割り込み
15SIGTERM終了要求(デフォルトのkillコマンド)
9SIGKILL強制終了(ハンドラ設定不可)
14SIGALRMアラームタイマー
17SIGCHLD子プロセスの状態変化
1SIGHUPハングアップ(再起動要求)
30SIGUSR1ユーザー定義シグナル1
31SIGUSR2ユーザー定義シグナル2

実践的なコード例

基本的な使い方:Ctrl+Cのハンドリング

<?php
// シグナルハンドラ関数
function signal_handler($signal) {
    switch($signal) {
        case SIGINT:
            echo "\nSIGINT(Ctrl+C)を受信しました。終了処理を実行します...\n";
            // クリーンアップ処理
            exit(0);
        case SIGTERM:
            echo "\nSIGTERMを受信しました。正常終了します...\n";
            exit(0);
    }
}

// シグナルハンドラを設定
pcntl_signal(SIGINT, 'signal_handler');
pcntl_signal(SIGTERM, 'signal_handler');

echo "プログラム実行中... (Ctrl+Cで終了できます)\n";

// メインループ
while (true) {
    echo "処理中...\n";
    sleep(1);
    
    // シグナルディスパッチ(重要!)
    pcntl_signal_dispatch();
}
?>

グレースフルシャットダウンの実装

<?php
class Worker {
    private $running = true;
    private $tasks = [];
    
    public function __construct() {
        // シグナルハンドラを設定
        pcntl_signal(SIGTERM, [$this, 'handleShutdown']);
        pcntl_signal(SIGINT, [$this, 'handleShutdown']);
        pcntl_signal(SIGHUP, [$this, 'handleReload']);
    }
    
    public function handleShutdown($signal) {
        echo "\n終了シグナルを受信しました。処理を完了してから終了します...\n";
        $this->running = false;
    }
    
    public function handleReload($signal) {
        echo "\nリロードシグナルを受信しました。設定を再読み込みします...\n";
        // 設定ファイルの再読み込みなど
    }
    
    public function run() {
        echo "ワーカー起動: PID=" . getmypid() . "\n";
        
        while ($this->running) {
            // 処理実行
            echo "タスク実行中...\n";
            sleep(2);
            
            // シグナルをチェック
            pcntl_signal_dispatch();
        }
        
        echo "全てのタスクが完了しました。終了します。\n";
    }
}

$worker = new Worker();
$worker->run();
?>

タイムアウト制御の実装

<?php
class TimeoutException extends Exception {}

function timeout_handler($signal) {
    throw new TimeoutException("処理がタイムアウトしました");
}

function execute_with_timeout($callback, $timeout = 5) {
    // タイムアウトハンドラを設定
    pcntl_signal(SIGALRM, 'timeout_handler');
    pcntl_alarm($timeout);
    
    try {
        $result = $callback();
        pcntl_alarm(0); // アラームをキャンセル
        return $result;
    } catch (TimeoutException $e) {
        echo "エラー: " . $e->getMessage() . "\n";
        return null;
    }
}

// 使用例
execute_with_timeout(function() {
    echo "重い処理を実行中...\n";
    sleep(3); // 3秒かかる処理
    echo "処理完了!\n";
    
    // シグナルディスパッチを呼ぶ
    pcntl_signal_dispatch();
    
    return "成功";
}, 5);

echo "\n";

// タイムアウトする例
execute_with_timeout(function() {
    echo "とても重い処理を実行中...\n";
    sleep(10); // 10秒かかる処理
    pcntl_signal_dispatch();
    return "成功";
}, 3);
?>

子プロセスの管理

<?php
$child_processes = [];

// 子プロセス終了時のハンドラ
function child_handler($signal) {
    global $child_processes;
    
    while (($pid = pcntl_wait($status, WNOHANG)) > 0) {
        echo "子プロセス(PID:{$pid})が終了しました\n";
        unset($child_processes[$pid]);
    }
}

pcntl_signal(SIGCHLD, 'child_handler');

// 複数の子プロセスを起動
for ($i = 0; $i < 3; $i++) {
    $pid = pcntl_fork();
    
    if ($pid === -1) {
        die("フォーク失敗\n");
    } elseif ($pid) {
        // 親プロセス
        $child_processes[$pid] = true;
        echo "子プロセス起動: PID={$pid}\n";
    } else {
        // 子プロセス
        echo "子プロセス{$i}実行中: PID=" . getmypid() . "\n";
        sleep(rand(1, 5));
        exit(0);
    }
}

// 親プロセスは全ての子プロセスを待機
while (!empty($child_processes)) {
    sleep(1);
    pcntl_signal_dispatch();
}

echo "全ての子プロセスが終了しました\n";
?>

非同期シグナル処理の有効化

<?php
// PHP 7.1以降で推奨される方法
pcntl_async_signals(true);

pcntl_signal(SIGINT, function($signal) {
    echo "\nCtrl+Cが押されました。終了します。\n";
    exit(0);
});

echo "非同期シグナル処理が有効です\n";
echo "pcntl_signal_dispatch()を呼ばなくても自動的に処理されます\n";

// pcntl_signal_dispatch()を明示的に呼ぶ必要がない
while (true) {
    echo "処理中...\n";
    sleep(1);
    // pcntl_signal_dispatch()は不要!
}
?>

シグナルハンドラの種類

1. カスタム関数

pcntl_signal(SIGINT, 'my_handler');

2. 無名関数(クロージャ)

pcntl_signal(SIGTERM, function($signal) {
    echo "SIGTERMを受信\n";
});

3. クラスメソッド

pcntl_signal(SIGHUP, [$object, 'method']);

4. 特殊な値

// シグナルを無視
pcntl_signal(SIGINT, SIG_IGN);

// デフォルトの動作に戻す
pcntl_signal(SIGINT, SIG_DFL);

重要なポイントと注意事項

⚠️ pcntl_signal_dispatch()を忘れずに!

PHP 7.1より前では、シグナルハンドラを実行するためにpcntl_signal_dispatch()を定期的に呼ぶ必要があります:

while (true) {
    // 処理
    pcntl_signal_dispatch(); // これが必要!
}

PHP 7.1以降では、pcntl_async_signals(true)を使うことで自動化できます。

💡 ベストプラクティス

  1. 必ず終了処理を実装する pcntl_signal(SIGTERM, function() { // データベース接続を閉じる // 一時ファイルを削除する // ログを記録する exit(0); });
  2. SIGKILL(9)とSIGSTOP(19)はハンドラ設定不可
    • これらのシグナルは捕捉できません
  3. 長時間の処理はハンドラ内で避ける
    • ハンドラは素早く完了すべきです
    • フラグを設定してメインループで処理する
  4. エラーハンドリングを適切に if (!pcntl_signal(SIGTERM, $handler)) { error_log("シグナルハンドラの設定に失敗しました"); }

🔧 動作環境の確認

// PCNTL拡張が利用可能かチェック
if (!function_exists('pcntl_signal')) {
    die('PCNTL拡張が利用できません');
}

// CLIモードかチェック
if (php_sapi_name() !== 'cli') {
    die('この機能はCLIモードでのみ動作します');
}

トラブルシューティング

シグナルが処理されない場合

  1. pcntl_signal_dispatch()を呼んでいますか?
  2. pcntl_async_signals(true)を設定していますか?
  3. シグナルハンドラ関数は正しく定義されていますか?
  4. プロセスがブロッキング状態になっていませんか?

よくあるエラー

// ❌ 間違い:存在しない関数を指定
pcntl_signal(SIGINT, 'non_existent_function');

// ✅ 正しい:関数の存在を確認
if (function_exists('my_handler')) {
    pcntl_signal(SIGINT, 'my_handler');
}

まとめ

pcntl_signal()は、PHPで堅牢なデーモンプロセスやワーカープロセスを実装するための必須関数です。適切なシグナルハンドリングを実装することで:

  • ✅ グレースフルシャットダウンが可能に
  • ✅ Ctrl+Cによる中断処理を制御できる
  • ✅ タイムアウト処理を実装できる
  • ✅ プロセス間通信が簡単に

長時間動作するCLIアプリケーションやバッチ処理を作成する際は、ぜひpcntl_signal()を活用してください!


関連記事

  • pcntl_signal_dispatch()の使い方
  • pcntl_async_signals()で非同期処理を実装
  • PHPでデーモンプロセスを作成する方法
  • pcntl_alarm()でタイムアウト制御
タイトルとURLをコピーしました