[PHP]pcntl_wstopsig関数とは?停止シグナルを取得する方法を徹底解説

PHP

はじめに

PHPでマルチプロセスプログラミングを行う際、子プロセスがどのシグナルで停止したのかを知ることは、適切なプロセス管理において非常に重要です。今回は、停止中のプロセスが受信したシグナル番号を取得するpcntl_wstopsig関数について、実践的な例を交えて詳しく解説します。

pcntl_wstopsig関数とは?

pcntl_wstopsig()は、停止中の子プロセスがどのシグナルによって停止したかを返すPHP関数です。デバッガやプロセス監視ツールを構築する際に欠かせない機能を提供します。

基本構文

pcntl_wstopsig(int $status): int|false

パラメータ:

  • $status: pcntl_waitpid()で取得したステータス値(WUNTRACEDオプション使用時)

戻り値:

  • int: 停止させたシグナル番号(SIGSTOP、SIGTSTPなど)
  • false: プロセスが停止状態でない場合

使用前の必須条件

pcntl_wstopsig()を使用するには、以下の条件を満たす必要があります:

  1. pcntl_waitpid()WUNTRACEDオプションを指定
  2. pcntl_wifstopped()trueを返すこと
// 正しい使用の流れ
pcntl_waitpid($pid, $status, WUNTRACED);

if (pcntl_wifstopped($status)) {
    $signal = pcntl_wstopsig($status); // ここで初めて使用可能
    echo "停止シグナル: $signal\n";
}

主な停止シグナルの種類

プロセスを停止させるシグナルには、いくつかの種類があります:

シグナル定数名説明キャッチ可能
19SIGSTOP強制停止不可
20SIGTSTP端末停止(Ctrl+Z)可能
21SIGTTINバックグラウンド読み込み可能
22SIGTTOUバックグラウンド書き込み可能
// シグナル番号から名前を取得する関数
function getSignalName($signal)
{
    $signals = [
        SIGSTOP => 'SIGSTOP',
        SIGTSTP => 'SIGTSTP',
        SIGTTIN => 'SIGTTIN',
        SIGTTOU => 'SIGTTOU',
    ];
    
    return $signals[$signal] ?? "Unknown($signal)";
}

実践的なコード例

基本的な使用例

<?php

$pid = pcntl_fork();

if ($pid == -1) {
    die("フォークに失敗しました\n");
} elseif ($pid == 0) {
    // 子プロセス
    echo "子プロセス(PID: " . getmypid() . ")を開始します\n";
    
    // 長時間実行される処理
    for ($i = 1; $i <= 30; $i++) {
        echo "処理中... $i/30\n";
        sleep(1);
    }
    
    exit(0);
} else {
    // 親プロセス
    echo "親プロセス: 子プロセス(PID: $pid)を生成しました\n";
    
    // 3秒後に子プロセスを停止
    sleep(3);
    echo "\n子プロセスにSIGSTOPを送信します\n";
    posix_kill($pid, SIGSTOP);
    
    // 停止状態を確認
    $result = pcntl_waitpid($pid, $status, WUNTRACED | WNOHANG);
    
    if ($result == $pid && pcntl_wifstopped($status)) {
        $signal = pcntl_wstopsig($status);
        echo "子プロセスが停止しました\n";
        echo "停止シグナル番号: $signal\n";
        echo "停止シグナル名: " . getSignalName($signal) . "\n";
        
        // 5秒待ってから再開
        sleep(5);
        echo "\n子プロセスを再開します(SIGCONT送信)\n\n";
        posix_kill($pid, SIGCONT);
    }
    
    // 最終的な終了を待機
    pcntl_waitpid($pid, $status);
    echo "\n子プロセスが終了しました\n";
}

function getSignalName($signal)
{
    $signals = [
        SIGSTOP => 'SIGSTOP',
        SIGTSTP => 'SIGTSTP',
        SIGTTIN => 'SIGTTIN',
        SIGTTOU => 'SIGTTOU',
    ];
    
    return $signals[$signal] ?? "Unknown($signal)";
}

複数のシグナルを試す例

<?php

function testSignal($signalToSend, $signalName)
{
    echo "\n=== $signalName のテスト ===\n";
    
    $pid = pcntl_fork();
    
    if ($pid == 0) {
        // 子プロセス
        // SIGTSTPのハンドラを設定(キャッチ可能なシグナルの場合)
        if ($signalToSend == SIGTSTP) {
            pcntl_signal(SIGTSTP, function($signo) {
                // 何もしない(デフォルトの停止動作を許可)
            });
        }
        
        while (true) {
            pcntl_signal_dispatch();
            sleep(1);
        }
    } else {
        // 親プロセス
        sleep(1);
        
        echo "子プロセスに $signalName を送信...\n";
        posix_kill($pid, $signalToSend);
        
        usleep(100000); // 0.1秒待機
        
        $result = pcntl_waitpid($pid, $status, WUNTRACED | WNOHANG);
        
        if ($result == $pid && pcntl_wifstopped($status)) {
            $receivedSignal = pcntl_wstopsig($status);
            echo "✓ プロセスが停止しました\n";
            echo "  受信シグナル番号: $receivedSignal\n";
            echo "  受信シグナル名: " . getSignalName($receivedSignal) . "\n";
            
            // 検証
            if ($receivedSignal == $signalToSend) {
                echo "  ✓ 送信シグナルと一致しています\n";
            }
            
            // クリーンアップ
            posix_kill($pid, SIGKILL);
            pcntl_waitpid($pid, $status);
        } else {
            echo "✗ プロセスが停止しませんでした\n";
            posix_kill($pid, SIGKILL);
            pcntl_waitpid($pid, $status);
        }
    }
}

function getSignalName($signal)
{
    $signals = [
        SIGSTOP => 'SIGSTOP',
        SIGTSTP => 'SIGTSTP',
        SIGTTIN => 'SIGTTIN',
        SIGTTOU => 'SIGTTOU',
    ];
    
    return $signals[$signal] ?? "Unknown($signal)";
}

// 各シグナルをテスト
testSignal(SIGSTOP, 'SIGSTOP');
testSignal(SIGTSTP, 'SIGTSTP');

echo "\n全てのテストが完了しました\n";

関連する重要な関数

1. pcntl_wifstopped()

pcntl_wstopsig()を使用する前に、必ずプロセスが停止状態かを確認します。

if (pcntl_wifstopped($status)) {
    // 停止状態の場合のみwstopsigを使用
    $signal = pcntl_wstopsig($status);
}

2. pcntl_wtermsig()

終了シグナルを取得する関数(停止とは異なる)。

if (pcntl_wifsignaled($status)) {
    $signal = pcntl_wtermsig($status); // 終了シグナル
} elseif (pcntl_wifstopped($status)) {
    $signal = pcntl_wstopsig($status); // 停止シグナル
}

3. posix_kill()

プロセスにシグナルを送信します。

// 停止
posix_kill($pid, SIGSTOP);

// 再開
posix_kill($pid, SIGCONT);

実用的な応用例: プロセスデバッガ

<?php

class SimpleDebugger
{
    private $targetPid;
    private $breakpoints = [];
    
    public function __construct($pid)
    {
        $this->targetPid = $pid;
    }
    
    public function attachToProcess()
    {
        echo "プロセス {$this->targetPid} にアタッチしています...\n";
        
        // プロセスを停止
        posix_kill($this->targetPid, SIGSTOP);
        
        $result = pcntl_waitpid($this->targetPid, $status, WUNTRACED);
        
        if (pcntl_wifstopped($status)) {
            $signal = pcntl_wstopsig($status);
            echo "✓ プロセスを停止しました(シグナル: " . $this->getSignalName($signal) . ")\n";
            return true;
        }
        
        echo "✗ プロセスの停止に失敗しました\n";
        return false;
    }
    
    public function stepExecution()
    {
        echo "\n--- ステップ実行 ---\n";
        
        // プロセスを一時的に再開
        posix_kill($this->targetPid, SIGCONT);
        
        // 短時間実行させる
        usleep(100000); // 0.1秒
        
        // 再度停止
        posix_kill($this->targetPid, SIGSTOP);
        
        $result = pcntl_waitpid($this->targetPid, $status, WUNTRACED);
        
        if (pcntl_wifstopped($status)) {
            $signal = pcntl_wstopsig($status);
            echo "ステップ実行完了(停止シグナル: " . $this->getSignalName($signal) . ")\n";
            $this->showProcessInfo();
        }
    }
    
    public function continueExecution()
    {
        echo "\n実行を継続します...\n";
        posix_kill($this->targetPid, SIGCONT);
    }
    
    public function showProcessInfo()
    {
        echo "プロセス情報:\n";
        echo "  PID: {$this->targetPid}\n";
        
        // プロセスの状態を確認
        $result = pcntl_waitpid($this->targetPid, $status, WUNTRACED | WNOHANG);
        
        if ($result == 0) {
            echo "  状態: 実行中\n";
        } elseif (pcntl_wifstopped($status)) {
            $signal = pcntl_wstopsig($status);
            echo "  状態: 停止中\n";
            echo "  停止シグナル: " . $this->getSignalName($signal) . " ($signal)\n";
        }
    }
    
    public function detach()
    {
        echo "\nプロセスからデタッチします...\n";
        posix_kill($this->targetPid, SIGCONT);
        echo "✓ デタッチ完了\n";
    }
    
    private function getSignalName($signal)
    {
        $signals = [
            SIGSTOP => 'SIGSTOP',
            SIGTSTP => 'SIGTSTP',
            SIGTTIN => 'SIGTTIN',
            SIGTTOU => 'SIGTTOU',
        ];
        
        return $signals[$signal] ?? "Unknown";
    }
}

// デモ用の対象プロセスを作成
$targetPid = pcntl_fork();

if ($targetPid == 0) {
    // デバッグ対象のプロセス
    for ($i = 1; $i <= 100; $i++) {
        file_put_contents('php://stdout', "作業中: $i/100\n");
        sleep(1);
    }
    exit(0);
} else {
    // デバッガプロセス
    sleep(2); // 対象プロセスが起動するのを待つ
    
    $debugger = new SimpleDebugger($targetPid);
    
    // プロセスにアタッチ
    if ($debugger->attachToProcess()) {
        sleep(2);
        
        // プロセス情報を表示
        $debugger->showProcessInfo();
        sleep(2);
        
        // ステップ実行
        $debugger->stepExecution();
        sleep(2);
        
        $debugger->stepExecution();
        sleep(2);
        
        // 実行を継続
        $debugger->continueExecution();
        sleep(3);
        
        // 再度停止して確認
        posix_kill($targetPid, SIGSTOP);
        pcntl_waitpid($targetPid, $status, WUNTRACED);
        
        if (pcntl_wifstopped($status)) {
            $signal = pcntl_wstopsig($status);
            echo "\n再停止時のシグナル: $signal\n";
        }
        
        // デタッチ
        $debugger->detach();
    }
    
    // クリーンアップ
    sleep(2);
    posix_kill($targetPid, SIGTERM);
    pcntl_waitpid($targetPid, $status);
    echo "\nデバッグセッション終了\n";
}

よくある使用シーン

1. シグナルログ記録

どのシグナルでプロセスが停止したかを記録します。

function logStopSignal($pid, $processName)
{
    $result = pcntl_waitpid($pid, $status, WUNTRACED | WNOHANG);
    
    if ($result == $pid && pcntl_wifstopped($status)) {
        $signal = pcntl_wstopsig($status);
        $signalName = getSignalName($signal);
        $timestamp = date('Y-m-d H:i:s');
        
        $logEntry = sprintf(
            "[%s] プロセス '%s' (PID: %d) が %s (番号: %d) で停止しました\n",
            $timestamp,
            $processName,
            $pid,
            $signalName,
            $signal
        );
        
        file_put_contents('process_log.txt', $logEntry, FILE_APPEND);
        echo $logEntry;
    }
}

2. 停止理由の分析

異なる停止シグナルに対して、異なる処理を実行します。

function handleStoppedProcess($pid)
{
    pcntl_waitpid($pid, $status, WUNTRACED);
    
    if (pcntl_wifstopped($status)) {
        $signal = pcntl_wstopsig($status);
        
        switch ($signal) {
            case SIGSTOP:
                echo "強制停止を検出。システムによる停止の可能性があります。\n";
                // 自動的に再開
                posix_kill($pid, SIGCONT);
                break;
                
            case SIGTSTP:
                echo "ユーザーによる停止(Ctrl+Z)を検出。\n";
                echo "再開するには 'fg' コマンドを使用してください。\n";
                break;
                
            case SIGTTIN:
                echo "バックグラウンドでの入力試行を検出。\n";
                echo "フォアグラウンドに移動してください。\n";
                break;
                
            case SIGTTOU:
                echo "バックグラウンドでの出力試行を検出。\n";
                echo "端末設定を確認してください。\n";
                break;
                
            default:
                echo "不明な停止シグナル: $signal\n";
        }
    }
}

3. デバッグポイントの実装

特定の条件でプロセスを停止し、シグナルを記録します。

class DebugPoint
{
    private $pid;
    private $stopHistory = [];
    
    public function __construct($pid)
    {
        $this->pid = $pid;
    }
    
    public function hitBreakpoint($location)
    {
        echo "ブレークポイント到達: $location\n";
        
        // プロセスを停止
        posix_kill($this->pid, SIGSTOP);
        
        pcntl_waitpid($this->pid, $status, WUNTRACED);
        
        if (pcntl_wifstopped($status)) {
            $signal = pcntl_wstopsig($status);
            
            $this->stopHistory[] = [
                'location' => $location,
                'signal' => $signal,
                'time' => microtime(true)
            ];
            
            echo "停止シグナル: $signal\n";
            echo "停止回数: " . count($this->stopHistory) . "\n";
        }
    }
    
    public function getStopHistory()
    {
        return $this->stopHistory;
    }
    
    public function resume()
    {
        posix_kill($this->pid, SIGCONT);
    }
}

SIGSTOPとSIGTSTPの判別

pcntl_wstopsig()を使用することで、強制停止とユーザー停止を区別できます。

function analyzeStopReason($pid)
{
    pcntl_waitpid($pid, $status, WUNTRACED);
    
    if (pcntl_wifstopped($status)) {
        $signal = pcntl_wstopsig($status);
        
        if ($signal == SIGSTOP) {
            return [
                'type' => 'forced',
                'reason' => 'システムまたは他のプロセスによる強制停止',
                'catchable' => false,
                'action' => '自動再開を推奨'
            ];
        } elseif ($signal == SIGTSTP) {
            return [
                'type' => 'user',
                'reason' => 'ユーザーによる停止(Ctrl+Z)',
                'catchable' => true,
                'action' => 'ユーザー操作を待つ'
            ];
        }
    }
    
    return null;
}

// 使用例
$info = analyzeStopReason($pid);
if ($info) {
    echo "停止タイプ: {$info['type']}\n";
    echo "理由: {$info['reason']}\n";
    echo "推奨アクション: {$info['action']}\n";
}

エラーハンドリング

停止状態でない場合の処理

function safeGetStopSignal($status)
{
    if (!pcntl_wifstopped($status)) {
        throw new RuntimeException(
            "プロセスは停止状態ではありません。" .
            "pcntl_wstopsig()は停止状態のプロセスにのみ使用できます。"
        );
    }
    
    $signal = pcntl_wstopsig($status);
    
    if ($signal === false) {
        throw new RuntimeException("停止シグナルの取得に失敗しました");
    }
    
    return $signal;
}

// 安全な使用例
try {
    $signal = safeGetStopSignal($status);
    echo "停止シグナル: $signal\n";
} catch (RuntimeException $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}

注意点とベストプラクティス

1. 必ずpcntl_wifstopped()と組み合わせる

// ✗ 間違い: 停止状態を確認していない
$signal = pcntl_wstopsig($status);

// ✓ 正しい: 停止状態を確認してから使用
if (pcntl_wifstopped($status)) {
    $signal = pcntl_wstopsig($status);
}

2. WUNTRACEDオプションの使用

// ✗ 間違い: WUNTRACEDなし
pcntl_waitpid($pid, $status);

// ✓ 正しい: WUNTRACEDを指定
pcntl_waitpid($pid, $status, WUNTRACED);

3. シグナル番号の解釈

// シグナル番号を人間が読める形式に変換
function interpretSignal($signal)
{
    $info = [
        SIGSTOP => [
            'name' => 'SIGSTOP',
            'description' => '強制停止(キャッチ不可)',
            'can_handle' => false
        ],
        SIGTSTP => [
            'name' => 'SIGTSTP',
            'description' => '端末停止(Ctrl+Z)',
            'can_handle' => true
        ],
        SIGTTIN => [
            'name' => 'SIGTTIN',
            'description' => 'バックグラウンド入力',
            'can_handle' => true
        ],
        SIGTTOU => [
            'name' => 'SIGTTOU',
            'description' => 'バックグラウンド出力',
            'can_handle' => true
        ]
    ];
    
    return $info[$signal] ?? [
        'name' => "Unknown($signal)",
        'description' => '不明なシグナル',
        'can_handle' => false
    ];
}

4. 停止後のクリーンアップ

function properCleanup($pid)
{
    // プロセスを停止
    posix_kill($pid, SIGSTOP);
    pcntl_waitpid($pid, $status, WUNTRACED);
    
    if (pcntl_wifstopped($status)) {
        $signal = pcntl_wstopsig($status);
        echo "シグナル $signal で停止しました\n";
        
        // 必要な処理を実行
        // ...
        
        // 最終的には必ず終了処理
        posix_kill($pid, SIGKILL);
        pcntl_waitpid($pid, $status);
    }
}

トラブルシューティング

問題1: falseが返される

原因: プロセスが停止状態でない

解決策:

if (pcntl_wifstopped($status)) {
    $signal = pcntl_wstopsig($status);
} else {
    echo "プロセスは停止状態ではありません\n";
}

問題2: シグナル番号が予期しない値

原因: 複数のシグナルが送信されている

解決策:

// 最新の停止シグナルのみ取得される
$signal = pcntl_wstopsig($status);
echo "最新の停止シグナル: $signal\n";

問題3: WUNTRACEDなしで停止が検出されない

原因: WUNTRACEDオプションを指定していない

解決策:

// WUNTRACEDを必ず指定
pcntl_waitpid($pid, $status, WUNTRACED | WNOHANG);

まとめ

pcntl_wstopsig()は、停止中のプロセスが受信したシグナルを取得するための重要な関数です。以下のポイントを押さえておきましょう:

  • 目的: 停止シグナルの番号を取得
  • 前提条件: pcntl_wifstopped()trueを返すこと
  • 必須オプション: WUNTRACEDを使用
  • 主な用途: デバッガ、プロセス監視、停止理由の分析
  • 関連関数: pcntl_wifstopped()pcntl_wtermsig()

停止シグナルを正確に把握することで、より高度なプロセス制御が可能になります。ぜひ実際のプロジェクトで活用してみてください!

参考リンク

タイトルとURLをコピーしました