[PHP]pcntl_wifstopped関数とは?子プロセスの停止状態を検出する方法を徹底解説

PHP

はじめに

PHPでマルチプロセスプログラミングを行う際、子プロセスが終了したのか、それとも一時停止しているだけなのかを区別する必要があります。今回は、子プロセスが停止状態にあるかを判定するpcntl_wifstopped関数について、実践的な例を交えて詳しく解説します。

pcntl_wifstopped関数とは?

pcntl_wifstopped()は、子プロセスが現在停止(stopped)状態にあるかどうかを判定するPHP関数です。プロセスが終了したのではなく、シグナルによって一時停止されている状態を検出します。

基本構文

pcntl_wifstopped(int $status): bool

パラメータ:

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

戻り値:

  • true: 子プロセスが停止状態にある
  • false: 子プロセスが停止していない(実行中または終了済み)

プロセスの停止状態とは?

プロセスには以下の主な状態があります:

  1. 実行中(Running): 通常の実行状態
  2. 停止中(Stopped): SIGSTOPやSIGTSTPシグナルで一時停止
  3. 終了(Terminated): プロセスが完全に終了

停止状態のプロセスは終了していないため、SIGCONTシグナルで再開できます。デバッガやジョブ制御で頻繁に使用される機能です。

WUNTRACEDオプションの重要性

pcntl_wifstopped()を使用するには、pcntl_waitpid()の第3引数にWUNTRACEDオプションを指定する必要があります。このオプションがないと、停止中のプロセスの情報は取得できません。

// 正しい使用方法
pcntl_waitpid($pid, $status, WUNTRACED);

if (pcntl_wifstopped($status)) {
    echo "プロセスは停止中です\n";
}

実践的なコード例

基本的な使用例

<?php

$pid = pcntl_fork();

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

停止シグナルの種類を判定する例

<?php

function checkProcessStatus($pid)
{
    $result = pcntl_waitpid($pid, $status, WUNTRACED | WNOHANG);
    
    if ($result == 0) {
        return "実行中";
    } elseif ($result == $pid) {
        if (pcntl_wifstopped($status)) {
            $signal = pcntl_wstopsig($status);
            
            $signalNames = [
                SIGSTOP => 'SIGSTOP(強制停止)',
                SIGTSTP => 'SIGTSTP(端末停止)',
                SIGTTIN => 'SIGTTIN(バックグラウンド読み込み)',
                SIGTTOU => 'SIGTTOU(バックグラウンド書き込み)'
            ];
            
            $signalName = $signalNames[$signal] ?? "シグナル $signal";
            return "停止中: $signalName";
        } elseif (pcntl_wifexited($status)) {
            $exitcode = pcntl_wexitstatus($status);
            return "終了済み(終了コード: $exitcode)";
        } elseif (pcntl_wifsignaled($status)) {
            $signal = pcntl_wtermsig($status);
            return "シグナルで終了(シグナル: $signal)";
        }
    }
    
    return "不明";
}

// 使用例
$pid = pcntl_fork();

if ($pid == 0) {
    // 子プロセス
    while (true) {
        sleep(1);
    }
} else {
    // 親プロセス
    sleep(1);
    echo "初期状態: " . checkProcessStatus($pid) . "\n";
    
    // 停止
    posix_kill($pid, SIGSTOP);
    sleep(1);
    echo "停止後: " . checkProcessStatus($pid) . "\n";
    
    // 再開
    posix_kill($pid, SIGCONT);
    sleep(1);
    echo "再開後: " . checkProcessStatus($pid) . "\n";
    
    // 終了
    posix_kill($pid, SIGTERM);
    pcntl_waitpid($pid, $status);
    echo "終了後: 終了しました\n";
}

関連する重要な関数

1. pcntl_wstopsig()

停止中のプロセスが、どのシグナルで停止したかを取得します。

if (pcntl_wifstopped($status)) {
    $signal = pcntl_wstopsig($status);
    echo "停止シグナル: $signal\n";
}

2. posix_kill() – SIGCONTで再開

停止中のプロセスを再開するには、SIGCONTシグナルを送信します。

// プロセスを停止
posix_kill($pid, SIGSTOP);

// プロセスを再開
posix_kill($pid, SIGCONT);

3. WUNTRACEDオプション

停止状態の検出には必須のオプションです。

// WUNTRACEDを使用しないと停止状態を検出できない
pcntl_waitpid($pid, $status, WUNTRACED);

実用的な応用例: プロセス監視システム

<?php

class ProcessMonitor
{
    private $processes = [];
    
    public function spawn($name, callable $task)
    {
        $pid = pcntl_fork();
        
        if ($pid == -1) {
            throw new Exception("フォークに失敗しました");
        } elseif ($pid == 0) {
            // 子プロセス
            $task();
            exit(0);
        } else {
            // 親プロセス
            $this->processes[$pid] = [
                'name' => $name,
                'started' => time(),
                'status' => 'running'
            ];
            echo "プロセス '$name' (PID: $pid) を起動しました\n";
            return $pid;
        }
    }
    
    public function pause($pid)
    {
        if (!isset($this->processes[$pid])) {
            echo "エラー: プロセス $pid は存在しません\n";
            return false;
        }
        
        posix_kill($pid, SIGSTOP);
        $this->processes[$pid]['status'] = 'paused';
        echo "プロセス {$this->processes[$pid]['name']} を一時停止しました\n";
        return true;
    }
    
    public function resume($pid)
    {
        if (!isset($this->processes[$pid])) {
            echo "エラー: プロセス $pid は存在しません\n";
            return false;
        }
        
        posix_kill($pid, SIGCONT);
        $this->processes[$pid]['status'] = 'running';
        echo "プロセス {$this->processes[$pid]['name']} を再開しました\n";
        return true;
    }
    
    public function checkStatus($pid)
    {
        $result = pcntl_waitpid($pid, $status, WUNTRACED | WNOHANG);
        
        if ($result == 0) {
            // まだ実行中
            return $this->processes[$pid]['status'];
        } elseif ($result == $pid) {
            if (pcntl_wifstopped($status)) {
                $signal = pcntl_wstopsig($status);
                $this->processes[$pid]['status'] = 'stopped';
                return "停止中(シグナル: $signal)";
            } elseif (pcntl_wifexited($status)) {
                $exitcode = pcntl_wexitstatus($status);
                unset($this->processes[$pid]);
                return "終了(終了コード: $exitcode)";
            } elseif (pcntl_wifsignaled($status)) {
                $signal = pcntl_wtermsig($status);
                unset($this->processes[$pid]);
                return "シグナルで終了(シグナル: $signal)";
            }
        }
        
        return "不明";
    }
    
    public function listProcesses()
    {
        echo "\n=== プロセス一覧 ===\n";
        foreach ($this->processes as $pid => $info) {
            $status = $this->checkStatus($pid);
            $runtime = time() - $info['started'];
            echo "PID: $pid | 名前: {$info['name']} | 状態: $status | 実行時間: {$runtime}秒\n";
        }
        echo "==================\n\n";
    }
    
    public function waitAll()
    {
        while (count($this->processes) > 0) {
            foreach (array_keys($this->processes) as $pid) {
                $result = pcntl_waitpid($pid, $status, WUNTRACED | WNOHANG);
                
                if ($result == $pid) {
                    if (pcntl_wifexited($status) || pcntl_wifsignaled($status)) {
                        echo "プロセス {$this->processes[$pid]['name']} が終了しました\n";
                        unset($this->processes[$pid]);
                    }
                }
            }
            sleep(1);
        }
    }
}

// 使用例
$monitor = new ProcessMonitor();

// タスク1: カウンター
$pid1 = $monitor->spawn('counter', function() {
    for ($i = 1; $i <= 30; $i++) {
        echo "カウンター: $i\n";
        sleep(1);
    }
});

// タスク2: タイマー
$pid2 = $monitor->spawn('timer', function() {
    $start = time();
    while (time() - $start < 30) {
        echo "経過時間: " . (time() - $start) . "秒\n";
        sleep(2);
    }
});

sleep(3);
$monitor->listProcesses();

// プロセス1を一時停止
$monitor->pause($pid1);
sleep(5);
$monitor->listProcesses();

// プロセス1を再開
$monitor->resume($pid1);
sleep(5);
$monitor->listProcesses();

// すべてのプロセスの終了を待機
$monitor->waitAll();

よくある使用シーン

1. デバッガの実装

デバッガは、対象プロセスを停止・再開しながらデバッグ情報を取得します。

function debugProcess($pid, $breakpoints)
{
    // ブレークポイントに到達したらプロセスを停止
    posix_kill($pid, SIGSTOP);
    
    $result = pcntl_waitpid($pid, $status, WUNTRACED);
    
    if (pcntl_wifstopped($status)) {
        echo "ブレークポイントで停止しました\n";
        
        // デバッグ情報を表示
        // ...
        
        // ユーザーの入力を待つ
        readline("続行するにはEnterを押してください...");
        
        // プロセスを再開
        posix_kill($pid, SIGCONT);
    }
}

2. ジョブコントロール

バックグラウンドジョブの一時停止・再開機能の実装。

class JobController
{
    private $jobs = [];
    
    public function addJob($pid, $name)
    {
        $this->jobs[$pid] = ['name' => $name, 'state' => 'running'];
    }
    
    public function stopJob($pid)
    {
        posix_kill($pid, SIGSTOP);
        pcntl_waitpid($pid, $status, WUNTRACED | WNOHANG);
        
        if (pcntl_wifstopped($status)) {
            $this->jobs[$pid]['state'] = 'stopped';
            echo "ジョブ {$this->jobs[$pid]['name']} を停止しました\n";
        }
    }
    
    public function resumeJob($pid)
    {
        posix_kill($pid, SIGCONT);
        $this->jobs[$pid]['state'] = 'running';
        echo "ジョブ {$this->jobs[$pid]['name']} を再開しました\n";
    }
    
    public function listJobs()
    {
        foreach ($this->jobs as $pid => $job) {
            pcntl_waitpid($pid, $status, WUNTRACED | WNOHANG);
            
            if (pcntl_wifstopped($status)) {
                $job['state'] = 'stopped';
            }
            
            echo "[$pid] {$job['name']} - {$job['state']}\n";
        }
    }
}

3. リソース制御

プロセスが多くのリソースを消費している場合、一時停止して他のプロセスに優先権を与える。

function monitorResourceUsage($pid)
{
    while (true) {
        // CPU使用率を確認(擬似コード)
        $cpuUsage = getCpuUsage($pid);
        
        if ($cpuUsage > 80) {
            echo "CPU使用率が高いため、プロセスを一時停止します\n";
            posix_kill($pid, SIGSTOP);
            
            sleep(5); // 5秒待機
            
            echo "プロセスを再開します\n";
            posix_kill($pid, SIGCONT);
        }
        
        sleep(1);
    }
}

SIGSTOPとSIGTSTPの違い

停止に関連する主なシグナルは2つあります:

SIGSTOP

  • キャッチ不可: プロセスがハンドラを設定できない
  • 必ず停止: 確実にプロセスを停止させる
  • 用途: システムレベルの強制停止
posix_kill($pid, SIGSTOP); // 確実に停止

SIGTSTP

  • キャッチ可能: プロセスがハンドラを設定できる
  • Ctrl+Z: 端末から送信される
  • 用途: ユーザーレベルの一時停止
posix_kill($pid, SIGTSTP); // Ctrl+Zと同等

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

1. WUNTRACEDオプションは必須

停止状態を検出するには、必ずWUNTRACEDオプションを使用します。

// ✗ 間違い: 停止状態を検出できない
pcntl_waitpid($pid, $status);

// ✓ 正しい: 停止状態を検出できる
pcntl_waitpid($pid, $status, WUNTRACED);

2. WNOHANGと組み合わせる

ノンブロッキングで状態をチェックする場合は、WNOHANGも指定します。

pcntl_waitpid($pid, $status, WUNTRACED | WNOHANG);

3. 停止後は必ず再開を検討

停止したプロセスを放置すると、リソースリークの原因になります。

if (pcntl_wifstopped($status)) {
    // 必要な処理を実行
    doSomething();
    
    // プロセスを再開または終了
    posix_kill($pid, SIGCONT); // 再開
    // または
    posix_kill($pid, SIGKILL); // 終了
}

4. 状態判定の順序

プロセスの状態を正しく判定するには、適切な順序でチェックします。

if (pcntl_wifstopped($status)) {
    // 停止中
} elseif (pcntl_wifexited($status)) {
    // 正常終了
} elseif (pcntl_wifsignaled($status)) {
    // シグナルで終了
}

トラブルシューティング

問題1: 停止状態が検出されない

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

解決策:

// WUNTRACEDを追加
pcntl_waitpid($pid, $status, WUNTRACED);

問題2: プロセスが再開しない

原因: SIGCONTシグナルを送信していない

解決策:

if (pcntl_wifstopped($status)) {
    // SIGCONTで再開
    posix_kill($pid, SIGCONT);
}

問題3: ゾンビプロセスが発生

原因: 停止後に適切にwaitしていない

解決策:

// 最終的には必ずwaitする
posix_kill($pid, SIGKILL);
pcntl_waitpid($pid, $status);

まとめ

pcntl_wifstopped()は、プロセスの一時停止を検出するための重要な関数です。以下のポイントを押さえておきましょう:

  • 目的: 子プロセスが停止状態にあるかを判定
  • 必須: WUNTRACEDオプションを使用
  • 組み合わせ: pcntl_wstopsig()で停止シグナルを取得
  • 再開: SIGCONTシグナルでプロセスを再開
  • 用途: デバッガ、ジョブコントロール、リソース管理

プロセスの停止・再開機能を理解することで、より柔軟なプロセス管理が可能になります。ぜひ実際のプロジェクトで活用してみてください!

参考リンク

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