[PHP]pcntl_sigwaitinfo関数とは?シグナルの同期受信を徹底解説

PHP

こんにちは!今回はPHPのシグナル処理における同期受信の基本となるpcntl_sigwaitinfo関数について詳しく解説します。シグナルハンドラを使わずにシグナルを受信したい、より制御しやすいシグナル処理を実装したい方は必見です!

pcntl_sigwaitinfo関数とは?

pcntl_sigwaitinfo()は、指定したシグナルが到着するまで待機し、同期的にシグナルを受信する関数です。通常のシグナルハンドラ(非同期)とは異なり、プログラムの流れの中で明示的にシグナルを待ち受けることができます。

基本構文

pcntl_sigwaitinfo(array $signals, array &$info = []): int|false
  • $signals: 待機するシグナルの配列
  • $info: シグナル情報を格納する配列(参照渡し)
  • 戻り値: 受信したシグナル番号、エラー時false

pcntl_sigtimedwaitとの違い

関数タイムアウト動作
pcntl_sigwaitinfo()なしシグナルが来るまで無限に待機
pcntl_sigtimedwait()あり指定時間でタイムアウト

pcntl_sigwaitinfo()は、pcntl_sigtimedwait()のタイムアウトなし版と考えると分かりやすいです。

なぜ同期的なシグナル受信が必要なのか?

非同期ハンドラの課題

// 非同期ハンドラの例
pcntl_signal(SIGUSR1, function() {
    // いつ呼ばれるか予測できない
    // 処理の途中で突然割り込まれる可能性
});

同期受信の利点

// 同期受信の例
$signal = pcntl_sigwaitinfo([SIGUSR1], $info);
// この行より前でシグナルを待つことが保証される
// 処理の流れを完全に制御できる

主な使用シーン

  1. 順次処理が必要な場合
  2. プロセス間通信の実装
  3. ワーカープールの制御
  4. シグナルベースのイベントループ

実践的なコード例

基本的な使い方

<?php
echo "プロセスID: " . getmypid() . "\n";
echo "SIGUSR1を送信してください: kill -USR1 " . getmypid() . "\n";

// SIGUSR1をブロック(同期的に受信するため必須)
pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1]);

echo "シグナルを待機中...\n";

$info = [];
$signal = pcntl_sigwaitinfo([SIGUSR1], $info);

if ($signal !== false) {
    echo "シグナルを受信しました: {$signal}\n";
    echo "シグナル情報:\n";
    print_r($info);
} else {
    echo "エラーが発生しました\n";
}
?>

シンプルなワーカープロセス

<?php
class SimpleWorker {
    private $running = true;
    
    public function run() {
        echo "ワーカー起動: PID=" . getmypid() . "\n";
        echo "使用方法:\n";
        echo "  kill -USR1 " . getmypid() . " → タスク実行\n";
        echo "  kill -TERM " . getmypid() . " → 終了\n\n";
        
        // シグナルをブロック
        pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1, SIGTERM, SIGINT]);
        
        while ($this->running) {
            echo "シグナル待機中...\n";
            
            $info = [];
            $signal = pcntl_sigwaitinfo([SIGUSR1, SIGTERM, SIGINT], $info);
            
            if ($signal === false) {
                echo "エラーが発生しました\n";
                break;
            }
            
            echo "\n";
            
            switch ($signal) {
                case SIGUSR1:
                    echo "▶ SIGUSR1受信: タスクを実行します\n";
                    $this->executeTask();
                    break;
                    
                case SIGTERM:
                case SIGINT:
                    echo "▶ 終了シグナル受信: シャットダウンします\n";
                    $this->running = false;
                    break;
                    
                default:
                    echo "▶ 未知のシグナル: {$signal}\n";
            }
            
            echo "\n";
        }
        
        echo "ワーカー終了\n";
    }
    
    private function executeTask() {
        echo "  → タスク処理開始\n";
        sleep(2);
        echo "  → タスク処理完了\n";
    }
}

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

プロセス間通信の実装

<?php
// 親プロセスと子プロセス間で通信
$pid = pcntl_fork();

if ($pid === -1) {
    die("フォーク失敗\n");
    
} elseif ($pid > 0) {
    // 親プロセス
    echo "親プロセス: PID=" . getmypid() . "\n";
    echo "子プロセス: PID={$pid}\n";
    
    sleep(1);
    
    // 子プロセスにシグナルを送信
    echo "\n親: 子プロセスにSIGUSR1を送信\n";
    posix_kill($pid, SIGUSR1);
    
    sleep(2);
    
    echo "親: 子プロセスにSIGUSR2を送信\n";
    posix_kill($pid, SIGUSR2);
    
    sleep(2);
    
    echo "親: 子プロセスにSIGTERMを送信\n";
    posix_kill($pid, SIGTERM);
    
    // 子プロセスの終了を待つ
    pcntl_wait($status);
    echo "\n親: 子プロセスが終了しました\n";
    
} else {
    // 子プロセス
    echo "子プロセス起動: PID=" . getmypid() . "\n";
    
    // シグナルをブロック
    pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1, SIGUSR2, SIGTERM]);
    
    $running = true;
    while ($running) {
        echo "\n子: シグナル待機中...\n";
        
        $info = [];
        $signal = pcntl_sigwaitinfo([SIGUSR1, SIGUSR2, SIGTERM], $info);
        
        switch ($signal) {
            case SIGUSR1:
                echo "子: SIGUSR1受信 - タスクAを実行\n";
                sleep(1);
                echo "子: タスクA完了\n";
                break;
                
            case SIGUSR2:
                echo "子: SIGUSR2受信 - タスクBを実行\n";
                sleep(1);
                echo "子: タスクB完了\n";
                break;
                
            case SIGTERM:
                echo "子: SIGTERM受信 - 終了します\n";
                $running = false;
                break;
        }
    }
    
    exit(0);
}
?>

マルチシグナル処理とキューイング

<?php
class SignalQueue {
    private $running = true;
    private $task_queue = [];
    
    public function run() {
        echo "シグナルキュー処理: PID=" . getmypid() . "\n";
        echo "コマンド:\n";
        echo "  kill -USR1 " . getmypid() . " (複数回送信可)\n";
        echo "  kill -TERM " . getmypid() . " (終了)\n\n";
        
        // シグナルをブロック
        pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1, SIGTERM]);
        
        while ($this->running) {
            echo "シグナル待機中... (キュー:" . count($this->task_queue) . "件)\n";
            
            $info = [];
            $signal = pcntl_sigwaitinfo([SIGUSR1, SIGTERM], $info);
            
            if ($signal === SIGUSR1) {
                // タスクをキューに追加
                $task_id = uniqid('task_');
                $this->task_queue[] = [
                    'id' => $task_id,
                    'time' => time(),
                    'sender_pid' => $info['pid'] ?? 'unknown'
                ];
                echo "✓ タスク追加: {$task_id} (キュー:" . count($this->task_queue) . "件)\n";
                
                // キュー内のタスクを処理
                $this->processQueue();
                
            } elseif ($signal === SIGTERM) {
                echo "\n終了シグナル受信\n";
                echo "残タスク: " . count($this->task_queue) . "件\n";
                
                if (!empty($this->task_queue)) {
                    echo "残タスクを処理してから終了します\n";
                    $this->processQueue();
                }
                
                $this->running = false;
            }
            
            echo "\n";
        }
        
        echo "終了\n";
    }
    
    private function processQueue() {
        while (!empty($this->task_queue)) {
            $task = array_shift($this->task_queue);
            echo "  → 処理中: {$task['id']} (送信元PID:{$task['sender_pid']})\n";
            sleep(1);
            echo "  ✓ 完了: {$task['id']}\n";
        }
    }
}

$queue = new SignalQueue();
$queue->run();
?>

シグナル情報の詳細取得

<?php
echo "PID: " . getmypid() . "\n";
echo "シグナルを送信してください:\n";
echo "  kill -USR1 " . getmypid() . "\n";
echo "  kill -s SIGUSR1 " . getmypid() . "\n\n";

// シグナルをブロック
pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1, SIGUSR2]);

for ($i = 1; $i <= 3; $i++) {
    echo "=== 待機 {$i}/3 ===\n";
    
    $info = [];
    $signal = pcntl_sigwaitinfo([SIGUSR1, SIGUSR2], $info);
    
    if ($signal !== false) {
        echo "シグナル受信!\n";
        echo "━━━━━━━━━━━━━━━━━━\n";
        echo "シグナル番号: {$signal}\n";
        
        // $info配列の内容を表示
        $info_fields = [
            'signo' => 'シグナル番号',
            'errno' => 'エラー番号',
            'code' => 'シグナルコード',
            'pid' => '送信元プロセスID',
            'uid' => '送信元ユーザーID',
            'status' => 'ステータス',
            'utime' => 'ユーザーCPU時間',
            'stime' => 'システムCPU時間',
            'band' => 'バンドイベント',
            'fd' => 'ファイルディスクリプタ'
        ];
        
        foreach ($info_fields as $key => $label) {
            if (isset($info[$key])) {
                echo "{$label}: {$info[$key]}\n";
            }
        }
        
        echo "━━━━━━━━━━━━━━━━━━\n\n";
    } else {
        echo "エラーが発生しました\n";
        break;
    }
}

echo "プログラム終了\n";
?>

エラーハンドリング付き実装

<?php
class RobustSignalWaiter {
    private $running = true;
    
    public function run() {
        echo "PID: " . getmypid() . "\n";
        
        // シグナルをブロック
        if (!pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1, SIGTERM])) {
            error_log("シグナルマスクの設定に失敗");
            return false;
        }
        
        while ($this->running) {
            try {
                $signal = $this->waitForSignal([SIGUSR1, SIGTERM]);
                
                if ($signal !== null) {
                    $this->handleSignal($signal);
                }
                
            } catch (Exception $e) {
                error_log("エラー: " . $e->getMessage());
                break;
            }
        }
        
        echo "終了\n";
        return true;
    }
    
    private function waitForSignal($signals) {
        $info = [];
        $signal = pcntl_sigwaitinfo($signals, $info);
        
        if ($signal === false) {
            // エラー発生
            $errno = pcntl_get_last_error();
            $errmsg = pcntl_strerror($errno);
            
            throw new RuntimeException(
                "pcntl_sigwaitinfo failed: [{$errno}] {$errmsg}"
            );
        }
        
        // シグナル情報を含めて返す
        return [
            'signal' => $signal,
            'info' => $info
        ];
    }
    
    private function handleSignal($data) {
        $signal = $data['signal'];
        $info = $data['info'];
        
        echo "\nシグナル受信: {$signal}\n";
        
        if (isset($info['pid'])) {
            echo "送信元PID: {$info['pid']}\n";
        }
        
        switch ($signal) {
            case SIGUSR1:
                echo "処理を実行します\n";
                sleep(1);
                echo "処理完了\n";
                break;
                
            case SIGTERM:
                echo "終了します\n";
                $this->running = false;
                break;
        }
    }
}

$waiter = new RobustSignalWaiter();
$waiter->run();
?>

複数の子プロセスを管理

<?php
class ProcessManager {
    private $children = [];
    
    public function run($num_workers = 3) {
        echo "プロセスマネージャー起動: PID=" . getmypid() . "\n";
        
        // 子プロセスを起動
        for ($i = 1; $i <= $num_workers; $i++) {
            $pid = $this->spawnWorker($i);
            if ($pid > 0) {
                $this->children[$pid] = $i;
                echo "ワーカー{$i}起動: PID={$pid}\n";
            }
        }
        
        echo "\n子プロセス数: " . count($this->children) . "\n";
        echo "子プロセスの終了を待機中...\n\n";
        
        // SIGCHLDをブロック
        pcntl_sigprocmask(SIG_BLOCK, [SIGCHLD]);
        
        // 全ての子プロセスが終了するまで待機
        while (!empty($this->children)) {
            $info = [];
            $signal = pcntl_sigwaitinfo([SIGCHLD], $info);
            
            if ($signal === SIGCHLD) {
                $pid = $info['pid'];
                $worker_id = $this->children[$pid] ?? '不明';
                
                echo "ワーカー{$worker_id} (PID:{$pid}) が終了しました\n";
                
                // ゾンビプロセスを回収
                pcntl_waitpid($pid, $status, WNOHANG);
                
                unset($this->children[$pid]);
                echo "残り子プロセス: " . count($this->children) . "\n\n";
            }
        }
        
        echo "全ての子プロセスが終了しました\n";
    }
    
    private function spawnWorker($id) {
        $pid = pcntl_fork();
        
        if ($pid === 0) {
            // 子プロセス
            echo "  [ワーカー{$id}] 起動: PID=" . getmypid() . "\n";
            
            // ランダムな時間処理
            $sleep_time = rand(2, 5);
            echo "  [ワーカー{$id}] {$sleep_time}秒処理します\n";
            sleep($sleep_time);
            
            echo "  [ワーカー{$id}] 完了\n";
            exit(0);
        }
        
        return $pid;
    }
}

$manager = new ProcessManager();
$manager->run(5);
?>

シグナルベースのイベントループ

<?php
class EventLoop {
    private $running = true;
    private $handlers = [];
    
    public function on($signal, $callback) {
        $this->handlers[$signal] = $callback;
    }
    
    public function run() {
        echo "イベントループ起動: PID=" . getmypid() . "\n";
        
        // 登録されたシグナルをブロック
        $signals = array_keys($this->handlers);
        if (empty($signals)) {
            echo "ハンドラが登録されていません\n";
            return;
        }
        
        pcntl_sigprocmask(SIG_BLOCK, $signals);
        
        echo "待機中のシグナル: " . implode(', ', $signals) . "\n\n";
        
        while ($this->running) {
            $info = [];
            $signal = pcntl_sigwaitinfo($signals, $info);
            
            if ($signal !== false && isset($this->handlers[$signal])) {
                echo "イベント発火: シグナル{$signal}\n";
                
                $callback = $this->handlers[$signal];
                $callback($signal, $info, $this);
                
                echo "\n";
            }
        }
        
        echo "イベントループ終了\n";
    }
    
    public function stop() {
        $this->running = false;
    }
}

// 使用例
$loop = new EventLoop();

$loop->on(SIGUSR1, function($signal, $info, $loop) {
    echo "  → USR1ハンドラ実行\n";
    echo "  → 送信元PID: " . ($info['pid'] ?? 'N/A') . "\n";
});

$loop->on(SIGUSR2, function($signal, $info, $loop) {
    echo "  → USR2ハンドラ実行\n";
});

$loop->on(SIGTERM, function($signal, $info, $loop) {
    echo "  → 終了ハンドラ実行\n";
    $loop->stop();
});

echo "シグナルを送信してください:\n";
echo "  kill -USR1 " . getmypid() . "\n";
echo "  kill -USR2 " . getmypid() . "\n";
echo "  kill -TERM " . getmypid() . "\n\n";

$loop->run();
?>

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

⚠️ シグナルを必ずブロックする

pcntl_sigwaitinfo()を使う前に、対象シグナルを必ずブロックする必要があります:

// ✅ 正しい使い方
pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1]);
$signal = pcntl_sigwaitinfo([SIGUSR1], $info);

// ❌ 間違い(ブロックしない)
$signal = pcntl_sigwaitinfo([SIGUSR1], $info);
// シグナルハンドラが実行される可能性がある

💡 無限待機に注意

pcntl_sigwaitinfo()はタイムアウトがありません:

// この行でプログラムが止まる可能性
$signal = pcntl_sigwaitinfo([SIGUSR1], $info);

// タイムアウトが必要な場合はpcntl_sigtimedwait()を使う
$signal = pcntl_sigtimedwait([SIGUSR1], $info, 5, 0);

🔍 シグナルハンドラとの併用不可

同じシグナルに対して、ハンドラとsigwaitinfoは併用できません:

// ❌ 間違い:両方を設定
pcntl_signal(SIGUSR1, function() { /* ... */ });
pcntl_sigwaitinfo([SIGUSR1], $info); // どちらが実行される?

// ✅ 正しい:どちらか一方を使う

ベストプラクティス

1. エラーチェックを必ず行う

$signal = pcntl_sigwaitinfo([SIGUSR1], $info);

if ($signal === false) {
    $errno = pcntl_get_last_error();
    error_log("Error: " . pcntl_strerror($errno));
}

2. シグナルマスクを適切に管理

// シグナルをブロック
pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1], $old_mask);

try {
    $signal = pcntl_sigwaitinfo([SIGUSR1], $info);
    // 処理
} finally {
    // 元に戻す
    pcntl_sigprocmask(SIG_SETMASK, $old_mask);
}

3. 複数シグナルの優先度を考慮

// 優先度の高いシグナルを先に処理
while ($running) {
    $signal = pcntl_sigwaitinfo([SIGTERM, SIGINT, SIGUSR1], $info);
    
    if ($signal === SIGTERM || $signal === SIGINT) {
        // 即座に終了
        break;
    }
    // その他の処理
}

まとめ

pcntl_sigwaitinfo()は、シグナルを同期的に受信するための基本関数です。重要なポイント:

  • ✅ シグナルを明示的に待機(プログラムフローを制御)
  • ✅ 非同期ハンドラより予測可能な動作
  • ✅ プロセス間通信に最適
  • ✅ 使用前に必ずシグナルをブロック
  • ✅ タイムアウトなし(無限待機)
  • ✅ シグナル情報の詳細取得が可能

順次処理が必要な場合や、プロセス間通信を実装する際に、非常に有用な関数です。タイムアウトが必要な場合はpcntl_sigtimedwait()を使いましょう!


関連記事

  • pcntl_sigtimedwait()でタイムアウト付き待機
  • pcntl_sigprocmask()でシグナルをブロック
  • pcntl_signal()との使い分け方
タイトルとURLをコピーしました