[PHP]pcntl_sigtimedwait関数とは?タイムアウト付きシグナル待機を徹底解説

PHP

こんにちは!今回はPHPのシグナル制御における高度な機能、pcntl_sigtimedwait関数について詳しく解説します。シグナルを同期的に待ちたい、タイムアウト処理を実装したい方は必見です!

pcntl_sigtimedwait関数とは?

pcntl_sigtimedwait()は、指定したシグナルが到着するまで待機し、タイムアウト時間を設定できる関数です。通常のシグナルハンドラとは異なり、シグナルを同期的に受信できるのが大きな特徴です。

基本構文

pcntl_sigtimedwait(array $signals, array &$info = [], int $seconds = 0, int $nanoseconds = 0): int|false
  • $signals: 待機するシグナルの配列
  • $info: シグナル情報を格納する配列(参照渡し)
  • $seconds: タイムアウト秒数
  • $nanoseconds: タイムアウトナノ秒数(0-999,999,999)
  • 戻り値: 受信したシグナル番号、タイムアウト時false、エラー時-1

pcntl_sigwaitinfoとの違い

関数タイムアウト用途
pcntl_sigwaitinfo()なし(無限待機)シグナルを確実に受信したい
pcntl_sigtimedwait()ありタイムアウト処理が必要

実践的なコード例

基本的な使い方

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

// SIGUSR1をブロック(ハンドラではなく同期的に受信するため)
pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1]);

echo "SIGUSR1を5秒間待機します...\n";

$info = [];
$signal = pcntl_sigtimedwait([SIGUSR1], $info, 5, 0);

if ($signal === false) {
    echo "タイムアウトしました(5秒以内にシグナルが来なかった)\n";
} elseif ($signal === -1) {
    echo "エラーが発生しました\n";
} else {
    echo "シグナルを受信しました: {$signal}\n";
    echo "シグナル情報:\n";
    print_r($info);
}
?>

タイムアウト処理を含むワーカー

<?php
class SignalWorker {
    private $running = true;
    
    public function run() {
        echo "ワーカー起動: PID=" . getmypid() . "\n";
        echo "コマンド例:\n";
        echo "  - 処理実行: kill -USR1 " . getmypid() . "\n";
        echo "  - 停止: kill -TERM " . getmypid() . "\n";
        
        // シグナルをブロック(同期的に受信するため)
        pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1, SIGTERM, SIGINT]);
        
        while ($this->running) {
            echo "\n待機中... (タイムアウト:10秒)\n";
            
            $info = [];
            $signal = pcntl_sigtimedwait(
                [SIGUSR1, SIGTERM, SIGINT],
                $info,
                10,  // 10秒
                0
            );
            
            if ($signal === false) {
                // タイムアウト
                echo "[タイムアウト] シグナルなし。ヘルスチェック実行\n";
                $this->healthCheck();
                
            } elseif ($signal === SIGUSR1) {
                // 処理実行シグナル
                echo "[SIGUSR1] 処理を実行します\n";
                $this->processTask();
                
            } elseif ($signal === SIGTERM || $signal === SIGINT) {
                // 終了シグナル
                echo "[終了シグナル] シャットダウンします\n";
                $this->running = false;
                
            } else {
                echo "[不明なシグナル] {$signal}\n";
            }
        }
        
        echo "ワーカー終了\n";
    }
    
    private function processTask() {
        echo "  タスク実行中...\n";
        sleep(2);
        echo "  タスク完了\n";
    }
    
    private function healthCheck() {
        $memory = memory_get_usage(true);
        echo "  メモリ使用量: " . round($memory / 1024 / 1024, 2) . " MB\n";
    }
}

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

高精度タイムアウト(ナノ秒指定)

<?php
echo "PID: " . getmypid() . "\n";

// SIGUSRをブロック
pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1]);

// ナノ秒単位の高精度タイムアウト
echo "待機開始(タイムアウト:1.5秒)\n";
$start = microtime(true);

$signal = pcntl_sigtimedwait(
    [SIGUSR1],
    $info,
    1,           // 1秒
    500000000    // 500ミリ秒(0.5秒)
);

$elapsed = microtime(true) - $start;

if ($signal === false) {
    echo "タイムアウト! 経過時間: " . round($elapsed, 3) . "秒\n";
} else {
    echo "シグナル受信: {$signal}, 経過時間: " . round($elapsed, 3) . "秒\n";
}
?>

複数シグナルの優先度制御

<?php
class PrioritySignalHandler {
    private $running = true;
    
    public function run() {
        echo "PID: " . getmypid() . "\n";
        echo "シグナル優先度:\n";
        echo "  1. SIGTERM (最優先) - 即座に終了\n";
        echo "  2. SIGUSR1 - 通常処理\n";
        echo "  3. SIGUSR2 - 低優先度処理\n";
        
        // 全てのシグナルをブロック
        pcntl_sigprocmask(SIG_BLOCK, [SIGTERM, SIGINT, SIGUSR1, SIGUSR2]);
        
        while ($this->running) {
            // まず高優先度シグナルをチェック(タイムアウト0)
            $signal = pcntl_sigtimedwait([SIGTERM, SIGINT], $info, 0, 0);
            
            if ($signal !== false) {
                echo "\n[緊急] 終了シグナル受信\n";
                $this->running = false;
                break;
            }
            
            // 次に通常優先度シグナルをチェック
            $signal = pcntl_sigtimedwait([SIGUSR1], $info, 2, 0);
            
            if ($signal === SIGUSR1) {
                echo "\n[通常] SIGUSR1を処理\n";
                $this->handleNormalTask();
                continue;
            }
            
            // 最後に低優先度シグナルをチェック
            $signal = pcntl_sigtimedwait([SIGUSR2], $info, 1, 0);
            
            if ($signal === SIGUSR2) {
                echo "\n[低優先度] SIGUSR2を処理\n";
                $this->handleLowPriorityTask();
                continue;
            }
            
            // 全てタイムアウトの場合
            echo "アイドル状態...\n";
            sleep(1);
        }
        
        echo "終了\n";
    }
    
    private function handleNormalTask() {
        echo "  通常タスク実行中\n";
        sleep(1);
    }
    
    private function handleLowPriorityTask() {
        echo "  低優先度タスク実行中\n";
        sleep(1);
    }
}

$handler = new PrioritySignalHandler();
$handler->run();
?>

シグナル情報の詳細取得

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

// SIGUSRをブロック
pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1]);

echo "待機中...\n";

$info = [];
$signal = pcntl_sigtimedwait([SIGUSR1], $info, 30, 0);

if ($signal !== false) {
    echo "\nシグナルを受信しました!\n";
    echo "━━━━━━━━━━━━━━━━━━━━━━\n";
    echo "シグナル番号: {$signal}\n";
    
    if (isset($info['signo'])) {
        echo "signo: {$info['signo']}\n";
    }
    if (isset($info['errno'])) {
        echo "errno: {$info['errno']}\n";
    }
    if (isset($info['code'])) {
        echo "code: {$info['code']}\n";
    }
    if (isset($info['pid'])) {
        echo "送信元PID: {$info['pid']}\n";
    }
    if (isset($info['uid'])) {
        echo "送信元UID: {$info['uid']}\n";
    }
    if (isset($info['status'])) {
        echo "status: {$info['status']}\n";
    }
    if (isset($info['utime'])) {
        echo "utime: {$info['utime']}\n";
    }
    if (isset($info['stime'])) {
        echo "stime: {$info['stime']}\n";
    }
    
    echo "━━━━━━━━━━━━━━━━━━━━━━\n";
} else {
    echo "タイムアウトまたはエラー\n";
}
?>

ポーリングパターン(タイムアウト0)

<?php
class NonBlockingSignalPoller {
    private $running = true;
    private $task_queue = [];
    
    public function run() {
        echo "ノンブロッキングポーラー起動: PID=" . getmypid() . "\n";
        
        // シグナルをブロック
        pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1, SIGTERM]);
        
        while ($this->running) {
            // シグナルをポーリング(即座に戻る)
            $signal = pcntl_sigtimedwait([SIGUSR1, SIGTERM], $info, 0, 0);
            
            if ($signal === SIGUSR1) {
                echo "[シグナル受信] タスクをキューに追加\n";
                $this->task_queue[] = time();
                
            } elseif ($signal === SIGTERM) {
                echo "[シグナル受信] 終了要求\n";
                $this->running = false;
                continue;
            }
            
            // メイン処理(シグナルとは独立)
            if (!empty($this->task_queue)) {
                $task = array_shift($this->task_queue);
                echo "タスク処理: {$task}\n";
                usleep(500000); // 0.5秒
            } else {
                echo "アイドル...\n";
                usleep(100000); // 0.1秒
            }
        }
        
        echo "残タスク: " . count($this->task_queue) . "個\n";
        echo "終了\n";
    }
}

$poller = new NonBlockingSignalPoller();
$poller->run();
?>

子プロセスとの通信

<?php
$pid = pcntl_fork();

if ($pid === -1) {
    die("フォーク失敗\n");
    
} elseif ($pid > 0) {
    // 親プロセス
    echo "親プロセス: PID=" . getmypid() . "\n";
    echo "子プロセス: PID={$pid}\n";
    
    // SIGCHLDをブロック
    pcntl_sigprocmask(SIG_BLOCK, [SIGCHLD]);
    
    echo "子プロセスの完了を待機中...\n";
    
    $info = [];
    $signal = pcntl_sigtimedwait([SIGCHLD], $info, 30, 0);
    
    if ($signal === SIGCHLD) {
        echo "子プロセスが終了しました\n";
        echo "子プロセスPID: {$info['pid']}\n";
        echo "ステータス: {$info['status']}\n";
        
        // ゾンビプロセスを回収
        pcntl_waitpid($pid, $status, WNOHANG);
        
    } else {
        echo "タイムアウト:子プロセスが応答しません\n";
    }
    
} else {
    // 子プロセス
    echo "子プロセス起動: PID=" . getmypid() . "\n";
    
    // 何か処理
    echo "子:処理実行中...\n";
    sleep(3);
    
    echo "子:処理完了\n";
    exit(0);
}
?>

リトライ付き待機

<?php
class RetryableSignalWaiter {
    private $max_retries;
    private $timeout_seconds;
    
    public function __construct($max_retries = 3, $timeout_seconds = 5) {
        $this->max_retries = $max_retries;
        $this->timeout_seconds = $timeout_seconds;
    }
    
    public function waitForSignal($signals) {
        echo "シグナル待機開始\n";
        
        // シグナルをブロック
        pcntl_sigprocmask(SIG_BLOCK, $signals);
        
        for ($retry = 1; $retry <= $this->max_retries; $retry++) {
            echo "\n試行 {$retry}/{$this->max_retries}\n";
            echo "待機中 (タイムアウト:{$this->timeout_seconds}秒)...\n";
            
            $info = [];
            $signal = pcntl_sigtimedwait(
                $signals,
                $info,
                $this->timeout_seconds,
                0
            );
            
            if ($signal !== false) {
                echo "✓ シグナル受信: {$signal}\n";
                return ['success' => true, 'signal' => $signal, 'info' => $info];
            }
            
            echo "✗ タイムアウト\n";
            
            if ($retry < $this->max_retries) {
                echo "リトライします...\n";
            }
        }
        
        echo "\n最大リトライ回数に達しました\n";
        return ['success' => false];
    }
}

echo "PID: " . getmypid() . "\n";
echo "SIGUSR1を送信: kill -USR1 " . getmypid() . "\n\n";

$waiter = new RetryableSignalWaiter(3, 5);
$result = $waiter->waitForSignal([SIGUSR1]);

if ($result['success']) {
    echo "\n処理を続行します\n";
} else {
    echo "\nシグナルを受信できませんでした\n";
}
?>

エラーハンドリング

<?php
function safe_sigtimedwait($signals, $timeout_sec = 5) {
    // シグナルをブロック
    $old_mask = [];
    if (!pcntl_sigprocmask(SIG_BLOCK, $signals, $old_mask)) {
        error_log("シグナルマスクの設定に失敗");
        return null;
    }
    
    try {
        $info = [];
        $signal = pcntl_sigtimedwait($signals, $info, $timeout_sec, 0);
        
        if ($signal === false) {
            // タイムアウト
            return ['status' => 'timeout'];
            
        } elseif ($signal === -1) {
            // エラー
            $errno = pcntl_get_last_error();
            $errmsg = pcntl_strerror($errno);
            error_log("pcntl_sigtimedwait エラー: {$errmsg}");
            return ['status' => 'error', 'errno' => $errno, 'errmsg' => $errmsg];
            
        } else {
            // 成功
            return ['status' => 'success', 'signal' => $signal, 'info' => $info];
        }
        
    } finally {
        // シグナルマスクを復元
        pcntl_sigprocmask(SIG_SETMASK, $old_mask);
    }
}

// 使用例
echo "PID: " . getmypid() . "\n";

$result = safe_sigtimedwait([SIGUSR1], 3);

switch ($result['status']) {
    case 'success':
        echo "シグナル受信: {$result['signal']}\n";
        break;
        
    case 'timeout':
        echo "タイムアウト\n";
        break;
        
    case 'error':
        echo "エラー: {$result['errmsg']}\n";
        break;
}
?>

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

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

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

// ❌ 間違い:ブロックせずに待機
$signal = pcntl_sigtimedwait([SIGUSR1], $info, 5, 0);
// シグナルハンドラが実行されてしまう可能性

// ✅ 正しい:ブロックしてから待機
pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1]);
$signal = pcntl_sigtimedwait([SIGUSR1], $info, 5, 0);

💡 タイムアウト0の活用

タイムアウトを0にすると、ノンブロッキングで動作します:

// シグナルが来ていればすぐ返る、なければfalse
$signal = pcntl_sigtimedwait([SIGUSR1], $info, 0, 0);

🔍 シグナルハンドラとの違い

方式処理タイミング用途
ハンドラ非同期(任意のタイミング)イベント駆動型
sigtimedwait同期(明示的に待機)順次処理型
// ハンドラ方式:非同期
pcntl_signal(SIGUSR1, function() {
    echo "いつ呼ばれるか分からない\n";
});

// sigtimedwait方式:同期
$signal = pcntl_sigtimedwait([SIGUSR1], $info, 5, 0);
echo "この行に到達する前に確実に待機する\n";

ベストプラクティス

1. タイムアウトは適切に設定

// ✅ 用途に応じた適切なタイムアウト
pcntl_sigtimedwait([SIGUSR1], $info, 30, 0);    // 長い処理
pcntl_sigtimedwait([SIGUSR1], $info, 1, 0);     // ポーリング
pcntl_sigtimedwait([SIGUSR1], $info, 0, 100000000); // 0.1秒

2. 複数シグナルの優先度管理

// 高優先度シグナルを先にチェック
$signal = pcntl_sigtimedwait([SIGTERM], $info, 0, 0);
if ($signal !== false) {
    // 即座に終了
}

// 通常シグナルをチェック
$signal = pcntl_sigtimedwait([SIGUSR1], $info, 5, 0);

3. エラーチェックを忘れずに

$signal = pcntl_sigtimedwait([SIGUSR1], $info, 5, 0);

if ($signal === -1) {
    // エラー処理
    $errno = pcntl_get_last_error();
    error_log("Error: " . pcntl_strerror($errno));
} elseif ($signal === false) {
    // タイムアウト処理
} else {
    // 正常処理
}

トラブルシューティング

シグナルが受信できない

  1. シグナルをブロックしていますか? pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1]); // これが必要!
  2. 正しいシグナル番号を指定していますか? // ✓ 正しい pcntl_sigtimedwait([SIGUSR1], $info, 5, 0); // ✗ 間違い pcntl_sigtimedwait([30], $info, 5, 0); // 定数を使う
  3. シグナルハンドラと競合していませんか?
    • ハンドラとsigtimedwaitは併用できません

まとめ

pcntl_sigtimedwait()は、シグナルを同期的に受信し、タイムアウト制御ができる強力な関数です。重要なポイント:

  • ✅ シグナルを同期的に受信(順次処理)
  • ✅ タイムアウトを設定して無限待機を防ぐ
  • ✅ 使用前に必ずシグナルをブロック
  • ✅ ナノ秒単位の高精度タイムアウト
  • ✅ シグナル情報の詳細取得が可能

イベント駆動型ではなく、明示的にシグナルを待ち受けたい場合に最適な関数です。プロセス間通信やワーカープロセスの実装に活用しましょう!


関連記事

  • pcntl_sigwaitinfo()でシグナルを無限待機
  • pcntl_sigprocmask()でシグナルをブロック
  • PHPでプロセス間通信を実装する方法
タイトルとURLをコピーしました