こんにちは!今回は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 {
    // 正常処理
}
トラブルシューティング
シグナルが受信できない
- シグナルをブロックしていますか? pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1]); // これが必要!
- 正しいシグナル番号を指定していますか? // ✓ 正しい pcntl_sigtimedwait([SIGUSR1], $info, 5, 0); // ✗ 間違い pcntl_sigtimedwait([30], $info, 5, 0); // 定数を使う
- シグナルハンドラと競合していませんか?
- ハンドラとsigtimedwaitは併用できません
 
まとめ
pcntl_sigtimedwait()は、シグナルを同期的に受信し、タイムアウト制御ができる強力な関数です。重要なポイント:
- ✅ シグナルを同期的に受信(順次処理)
- ✅ タイムアウトを設定して無限待機を防ぐ
- ✅ 使用前に必ずシグナルをブロック
- ✅ ナノ秒単位の高精度タイムアウト
- ✅ シグナル情報の詳細取得が可能
イベント駆動型ではなく、明示的にシグナルを待ち受けたい場合に最適な関数です。プロセス間通信やワーカープロセスの実装に活用しましょう!
関連記事
- pcntl_sigwaitinfo()でシグナルを無限待機
- pcntl_sigprocmask()でシグナルをブロック
- PHPでプロセス間通信を実装する方法
 
  
  
  
  