[PHP]pcntl_wexitstatus関数の使い方|子プロセスの終了ステータスを正確に取得する方法

PHP

PHPでマルチプロセッシングを実装している際、子プロセスが正常に終了したのか、それともエラーで終了したのかを判定したいことはありませんか?そんな時に必要不可欠なのがpcntl_wexitstatus関数です。

この記事では、pcntl_wexitstatus関数の基本的な使い方から、実践的なプロセス管理システムの構築まで詳しく解説します。

pcntl_wexitstatus関数とは?

pcntl_wexitstatus()は、PHPのプロセス制御関数の一つで、pcntl_waitpid()やpcntl_wait()で得られた子プロセスの終了ステータスから、実際の終了コード(exit code)を抽出する関数です。

基本構文

int pcntl_wexitstatus(int $status)
  • パラメータ:
    • $status: pcntl_wait()またはpcntl_waitpid()から返された終了ステータス
  • 戻り値: 子プロセスの終了コード(0-255)
  • 前提条件: pcntl_wifexited()が真を返している必要があります

終了ステータスの仕組み

PHPで子プロセスの終了を待つ際、pcntl_wait()pcntl_waitpid()は複合的な情報を含むステータス値を返します。このステータス値には以下の情報が含まれています:

  1. 終了コード: プロセスが正常終了した場合の値(0-255)
  2. シグナル番号: プロセスがシグナルで終了した場合
  3. コアダンプ情報: メモリダンプが発生したかどうか

pcntl_wexitstatus()はこのステータス値から終了コードのみを抽出します。

基本的な使い方

シンプルな例

<?php
// 子プロセスの作成
$pid = pcntl_fork();

if ($pid === 0) {
    // 子プロセス
    echo "子プロセス(PID: " . getmypid() . ")が実行中\n";
    
    // 何か処理を実行
    sleep(2);
    
    // 終了コード 42 で終了
    exit(42);
    
} elseif ($pid > 0) {
    // 親プロセス
    echo "子プロセス(PID: {$pid})を待機中...\n";
    
    // 子プロセスの終了を待つ
    $status = 0;
    $waitedPid = pcntl_waitpid($pid, $status);
    
    // 正常終了したかチェック
    if (pcntl_wifexited($status)) {
        // 終了コードを取得
        $exitCode = pcntl_wexitstatus($status);
        echo "子プロセスが終了コード {$exitCode} で終了しました\n";
    } else {
        echo "子プロセスが異常終了しました\n";
    }
    
} else {
    // fork失敗
    echo "プロセスの作成に失敗しました\n";
}
?>

終了コードの種類と意味

<?php
// 異なる終了コードで複数の子プロセスを実行
$processes = [];

for ($i = 0; $i < 3; $i++) {
    $pid = pcntl_fork();
    
    if ($pid === 0) {
        // 子プロセス
        $exitCode = ($i + 1) * 10; // 10, 20, 30
        echo "子プロセス #{$i} (PID: " . getmypid() . ") は終了コード {$exitCode} で終了します\n";
        sleep(1);
        exit($exitCode);
        
    } elseif ($pid > 0) {
        // 親プロセス:子プロセスのPIDを記録
        $processes[$pid] = $i;
    }
}

// 全ての子プロセスを待つ
echo "\n全ての子プロセスを待機中...\n";

while (!empty($processes)) {
    $status = 0;
    $completedPid = pcntl_wait($status);
    
    if ($completedPid > 0) {
        $index = $processes[$completedPid];
        
        // 終了ステータスを詳細に分析
        echo "\n--- プロセス #{$index} (PID: {$completedPid}) の終了ステータス分析 ---\n";
        
        if (pcntl_wifexited($status)) {
            $exitCode = pcntl_wexitstatus($status);
            echo "✓ 正常終了 (終了コード: {$exitCode})\n";
            echo "  意味: " . $this->interpretExitCode($exitCode) . "\n";
            
        } elseif (pcntl_wifsignaled($status)) {
            $signal = pcntl_wtermsig($status);
            echo "✗ シグナルで終了 (シグナル番号: {$signal})\n";
            echo "  シグナル名: " . $this->getSignalName($signal) . "\n";
            
        } elseif (pcntl_wifstopped($status)) {
            $signal = pcntl_wstopsig($status);
            echo "⏸ 停止中 (シグナル番号: {$signal})\n";
        }
        
        // プロセスを削除
        unset($processes[$completedPid]);
    }
}

function interpretExitCode($code) {
    $interpretations = [
        0 => '成功/通常終了',
        1 => '一般的なエラー',
        2 => 'シェル引数のミスアプリケーション',
        126 => 'コマンドが実行不可',
        127 => 'コマンドが見つからない',
        128 => '無効な引数',
        130 => 'スクリプトが SIGINT(Ctrl+C)で中断',
        255 => '終了コード範囲外'
    ];
    
    return $interpretations[$code] ?? "カスタム終了コード #{$code}";
}

function getSignalName($signal) {
    $signalNames = [
        1 => 'SIGHUP',
        2 => 'SIGINT',
        3 => 'SIGQUIT',
        4 => 'SIGILL',
        5 => 'SIGTRAP',
        6 => 'SIGABRT',
        8 => 'SIGFPE',
        9 => 'SIGKILL',
        11 => 'SIGSEGV',
        13 => 'SIGPIPE',
        14 => 'SIGALRM',
        15 => 'SIGTERM'
    ];
    
    return $signalNames[$signal] ?? "SIGNAL #{$signal}";
}
?>

実践的な活用例

1. プロセス管理とリトライシステム

<?php
class ProcessManagerWithRetry {
    private $maxRetries = 3;
    private $processTimeout = 30;
    private $successThreshold = 0;
    private $processLog = [];
    
    public function __construct($config = []) {
        $this->maxRetries = $config['max_retries'] ?? 3;
        $this->processTimeout = $config['timeout'] ?? 30;
        $this->successThreshold = $config['success_threshold'] ?? 0;
    }
    
    public function executeWithRetry($callback, $processName = 'unnamed') {
        $attempts = 0;
        $lastExitCode = null;
        $successCount = 0;
        
        while ($attempts < $this->maxRetries) {
            $attempts++;
            
            echo "[{$processName}] 実行試行 {$attempts}/{$this->maxRetries}\n";
            
            $result = $this->executeProcess($callback, $processName, $attempts);
            $lastExitCode = $result['exit_code'];
            
            // 成功判定
            if ($result['exit_code'] === 0) {
                $successCount++;
                echo "[{$processName}] ✓ 実行成功\n";
                
                // 成功しきい値に達したか確認
                if ($successCount > $this->successThreshold) {
                    return [
                        'success' => true,
                        'exit_code' => 0,
                        'attempts' => $attempts,
                        'output' => $result['output'],
                        'duration' => $result['duration']
                    ];
                }
            } else {
                echo "[{$processName}] ✗ 失敗 (終了コード: {$lastExitCode})\n";
                
                // 最後の試行以外の場合は再試行
                if ($attempts < $this->maxRetries) {
                    $delay = min(pow(2, $attempts - 1), 30); // 指数バックオフ
                    echo "[{$processName}] {$delay}秒後に再試行します\n";
                    sleep($delay);
                }
            }
        }
        
        // 全ての試行が失敗した場合
        return [
            'success' => false,
            'exit_code' => $lastExitCode,
            'attempts' => $attempts,
            'message' => "最大試行回数({$this->maxRetries})に達しました",
            'last_output' => $result['output'] ?? null
        ];
    }
    
    private function executeProcess($callback, $processName, $attemptNumber) {
        $startTime = microtime(true);
        
        // 子プロセスを作成
        $pid = pcntl_fork();
        
        if ($pid === 0) {
            // 子プロセス
            try {
                $result = $callback();
                exit($result === true ? 0 : 1);
            } catch (Exception $e) {
                error_log("Process {$processName} (attempt {$attemptNumber}): " . $e->getMessage());
                exit(1);
            }
            
        } elseif ($pid > 0) {
            // 親プロセス
            $status = 0;
            $completedPid = pcntl_waitpid($pid, $status, 0);
            
            $duration = microtime(true) - $startTime;
            
            // 終了ステータスを分析
            $exitCode = 1; // デフォルトはエラー
            $output = '';
            
            if (pcntl_wifexited($status)) {
                $exitCode = pcntl_wexitstatus($status);
                $output = "プロセス終了コード: {$exitCode}";
                
            } elseif (pcntl_wifsignaled($status)) {
                $signal = pcntl_wtermsig($status);
                $output = "シグナル #{$signal} で強制終了";
                $exitCode = 128 + $signal;
                
            } elseif (pcntl_wifstopped($status)) {
                $output = "プロセスが停止中";
                $exitCode = 255;
            }
            
            // ログに記録
            $this->processLog[] = [
                'process_name' => $processName,
                'attempt' => $attemptNumber,
                'pid' => $pid,
                'exit_code' => $exitCode,
                'duration' => $duration,
                'timestamp' => date('Y-m-d H:i:s')
            ];
            
            return [
                'exit_code' => $exitCode,
                'output' => $output,
                'duration' => $duration,
                'pid' => $pid
            ];
            
        } else {
            return [
                'exit_code' => 127,
                'output' => 'プロセス作成失敗',
                'duration' => 0
            ];
        }
    }
    
    public function getProcessLog() {
        return $this->processLog;
    }
    
    public function generateReport() {
        $report = [
            'total_processes' => count($this->processLog),
            'successful_processes' => count(array_filter($this->processLog, function($log) {
                return $log['exit_code'] === 0;
            })),
            'failed_processes' => count(array_filter($this->processLog, function($log) {
                return $log['exit_code'] !== 0;
            })),
            'total_duration' => array_sum(array_column($this->processLog, 'duration')),
            'average_duration' => 0,
            'success_rate' => 0,
            'details' => $this->processLog
        ];
        
        if ($report['total_processes'] > 0) {
            $report['average_duration'] = $report['total_duration'] / $report['total_processes'];
            $report['success_rate'] = ($report['successful_processes'] / $report['total_processes']) * 100;
        }
        
        return $report;
    }
}

// 使用例:複数のタスクをリトライ付きで実行
$manager = new ProcessManagerWithRetry([
    'max_retries' => 3,
    'timeout' => 10,
    'success_threshold' => 0
]);

// 成功しやすいタスク
$task1 = function() {
    echo "タスク1を実行中...\n";
    sleep(1);
    return true; // 成功
};

// 不安定なタスク(時々失敗)
$task2 = function() {
    echo "タスク2を実行中...\n";
    sleep(1);
    $random = rand(1, 3);
    return $random !== 1; // 33%の確率で失敗
};

// 常に失敗するタスク
$task3 = function() {
    echo "タスク3を実行中...\n";
    sleep(1);
    return false; // 失敗
};

echo "=== タスク1実行 ===\n";
$result1 = $manager->executeWithRetry($task1, 'Task-1');
echo "結果: " . ($result1['success'] ? '成功' : '失敗') . "\n";

echo "\n=== タスク2実行 ===\n";
$result2 = $manager->executeWithRetry($task2, 'Task-2');
echo "結果: " . ($result2['success'] ? '成功' : '失敗') . "\n";

echo "\n=== タスク3実行 ===\n";
$result3 = $manager->executeWithRetry($task3, 'Task-3');
echo "結果: " . ($result3['success'] ? '成功' : '失敗') . "\n";

// レポート表示
echo "\n=== 実行レポート ===\n";
$report = $manager->generateReport();
echo "総プロセス数: " . $report['total_processes'] . "\n";
echo "成功: " . $report['successful_processes'] . "\n";
echo "失敗: " . $report['failed_processes'] . "\n";
echo "成功率: " . round($report['success_rate'], 1) . "%\n";
echo "平均実行時間: " . round($report['average_duration'], 2) . "秒\n";
?>

2. パラレル処理とワーカープール

<?php
class WorkerPool {
    private $workerCount = 4;
    private $workers = [];
    private $queue = [];
    private $results = [];
    private $maxQueueSize = 100;
    
    public function __construct($workerCount = 4) {
        $this->workerCount = $workerCount;
    }
    
    public function addTask($taskId, $taskData, $priority = 5) {
        if (count($this->queue) >= $this->maxQueueSize) {
            throw new Exception("タスクキューが満杯です");
        }
        
        $this->queue[] = [
            'id' => $taskId,
            'data' => $taskData,
            'priority' => $priority,
            'added_at' => time()
        ];
        
        // 優先度でソート
        usort($this->queue, function($a, $b) {
            return $b['priority'] - $a['priority'];
        });
    }
    
    public function execute() {
        // ワーカープロセスを起動
        $this->startWorkers();
        
        // タスクをワーカーに割り当て
        $this->distributeTasksToWorkers();
        
        // すべてのワーカーが完了するまで待つ
        $this->waitForWorkers();
        
        return [
            'results' => $this->results,
            'completed_tasks' => count($this->results),
            'total_tasks' => count(array_unique(array_column($this->results, 'task_id')))
        ];
    }
    
    private function startWorkers() {
        for ($i = 0; $i < $this->workerCount; $i++) {
            $pid = pcntl_fork();
            
            if ($pid === 0) {
                // ワーカープロセス
                $this->workerProcess($i);
                exit(0);
                
            } elseif ($pid > 0) {
                // 親プロセス:ワーカーPIDを記録
                $this->workers[$pid] = [
                    'id' => $i,
                    'status' => 'running',
                    'tasks_completed' => 0
                ];
            }
        }
    }
    
    private function workerProcess($workerId) {
        while (true) {
            if (empty($this->queue)) {
                break;
            }
            
            // キューからタスクを取得
            $task = array_pop($this->queue);
            
            // タスクを実行
            $result = $this->executeTask($workerId, $task);
            
            // 結果を記録
            $this->results[] = [
                'task_id' => $task['id'],
                'worker_id' => $workerId,
                'result' => $result,
                'executed_at' => time()
            ];
            
            echo "[Worker {$workerId}] タスク {$task['id']} を完了\n";
        }
    }
    
    private function executeTask($workerId, $task) {
        // 実際のタスク処理
        $startTime = microtime(true);
        
        // タスクの内容に応じた処理
        usleep(rand(100000, 500000)); // 0.1-0.5秒のランダム処理
        
        $duration = microtime(true) - $startTime;
        
        return [
            'status' => 'completed',
            'duration' => $duration,
            'data' => $task['data']
        ];
    }
    
    private function distributeTasksToWorkers() {
        // タスクはワーカーによって自動的に取得される
    }
    
    private function waitForWorkers() {
        while (!empty($this->workers)) {
            $status = 0;
            $completedPid = pcntl_wait($status);
            
            if ($completedPid > 0 && isset($this->workers[$completedPid])) {
                // ワーカーの終了ステータスを確認
                if (pcntl_wifexited($status)) {
                    $exitCode = pcntl_wexitstatus($status);
                    $workerId = $this->workers[$completedPid]['id'];
                    
                    echo "[Manager] ワーカー {$workerId} (PID: {$completedPid}) が終了コード {$exitCode} で終了\n";
                    
                    unset($this->workers[$completedPid]);
                }
            }
        }
    }
}

// 使用例
echo "=== ワーカープール実行 ===\n";
$pool = new WorkerPool(4);

// 複数のタスクを追加
for ($i = 1; $i <= 16; $i++) {
    $pool->addTask("task_{$i}", ['data' => $i], rand(1, 10));
}

// 実行
$result = $pool->execute();

echo "\n=== 実行結果 ===\n";
echo "完了したタスク: {$result['completed_tasks']}\n";
echo "総タスク数: {$result['total_tasks']}\n";
?>

3. 子プロセスの健全性チェックシステム

<?php
class ProcessHealthMonitor {
    private $childProcesses = [];
    private $healthMetrics = [];
    private $alertThresholds = [
        'cpu_usage' => 80,
        'memory_usage' => 512 * 1024 * 1024, // 512MB
        'execution_time' => 300 // 5分
    ];
    
    public function startMonitoringProcess($callback, $processName) {
        $pid = pcntl_fork();
        
        if ($pid === 0) {
            // 子プロセス
            try {
                $callback();
                exit(0);
            } catch (Exception $e) {
                error_log("Process error: " . $e->getMessage());
                exit(1);
            }
            
        } elseif ($pid > 0) {
            // 親プロセス
            $this->childProcesses[$pid] = [
                'name' => $processName,
                'start_time' => microtime(true),
                'start_memory' => memory_get_usage(),
                'status' => 'running',
                'health_checks' => []
            ];
            
            return [
                'pid' => $pid,
                'name' => $processName,
                'status' => 'started'
            ];
        }
        
        return ['status' => 'fork_failed'];
    }
    
    public function checkProcessHealth($pid) {
        if (!isset($this->childProcesses[$pid])) {
            return ['status' => 'not_found'];
        }
        
        $process = $this->childProcesses[$pid];
        $health = [
            'pid' => $pid,
            'name' => $process['name'],
            'uptime' => microtime(true) - $process['start_time'],
            'status' => 'healthy',
            'issues' => []
        ];
        
        // 実行時間チェック
        if ($health['uptime'] > $this->alertThresholds['execution_time']) {
            $health['issues'][] = [
                'type' => 'execution_timeout',
                'message' => "実行時間が {$this->alertThresholds['execution_time']} 秒を超えています",
                'severity' => 'high'
            ];
            $health['status'] = 'warning';
        }
        
        // /proc ファイルシステムからのメトリクス取得(Linux環境の場合)
        if (file_exists("/proc/{$pid}/stat")) {
            $metrics = $this->readProcessMetrics($pid);
            
            if ($metrics['memory'] > $this->alertThresholds['memory_usage']) {
                $health['issues'][] = [
                    'type' => 'high_memory',
                    'message' => "メモリ使用量が " . round($metrics['memory'] / 1024 / 1024, 1) . "MB です",
                    'severity' => 'high'
                ];
                $health['status'] = 'warning';
            }
            
            if ($metrics['cpu_percent'] > $this->alertThresholds['cpu_usage']) {
                $health['issues'][] = [
                    'type' => 'high_cpu',
                    'message' => "CPU使用率が {$metrics['cpu_percent']}% です",
                    'severity' => 'medium'
                ];
                if ($health['status'] !== 'warning') {
                    $health['status'] = 'caution';
                }
            }
            
            $health['metrics'] = $metrics;
        }
        
        // 健康チェック情報を記録
        $this->healthMetrics[$pid][] = [
            'timestamp' => time(),
            'health' => $health
        ];
        
        return $health;
    }
    
    private function readProcessMetrics($pid) {
        $statFile = "/proc/{$pid}/stat";
        $statusFile = "/proc/{$pid}/status";
        
        $metrics = [
            'cpu_percent' => 0,
            'memory' => 0
        ];
        
        // メモリ情報を取得
        if (file_exists($statusFile)) {
            $status = file_get_contents($statusFile);
            if (preg_match('/VmRSS:\s+(\d+)\s+kB/', $status, $matches)) {
                $metrics['memory'] = $matches[1] * 1024;
            }
        }
        
        return $metrics;
    }
    
    public function waitAndCheckProcesses() {
        $completedProcesses = [];
        
        while (!empty($this->childProcesses)) {
            $status = 0;
            $completedPid = pcntl_wait($status);
            
            if ($completedPid > 0 && isset($this->childProcesses[$completedPid])) {
                $process = $this->childProcesses[$completedPid];
                $duration = microtime(true) - $process['start_time'];
                
                $exitInfo = [
                    'pid' => $completedPid,
                    'name' => $process['name'],
                    'duration' => $duration,
                    'status' => 'completed'
                ];
                
                // 終了ステータスを分析
                if (pcntl_wifexited($status)) {
                    $exitCode = pcntl_wexitstatus($status);
                    $exitInfo['exit_code'] = $exitCode;
                    $exitInfo['exit_type'] = 'normal';
                    $exitInfo['message'] = $this->interpretExitCode($exitCode);
                    
                } elseif (pcntl_wifsignaled($status)) {
                    $signal = pcntl_wtermsig($status);
                    $exitInfo['signal'] = $signal;
                    $exitInfo['exit_type'] = 'signaled';
                    $exitInfo['message'] = "シグナル #{$signal} で強制終了";
                    
                } elseif (pcntl_wifstopped($status)) {
                    $signal = pcntl_wstopsig($status);
                    $exitInfo['signal'] = $signal;
                    $exitInfo['exit_type'] = 'stopped';
                    $exitInfo['message'] = "シグナル #{$signal} で停止";
                }
                
                // 最後の健康チェックを実行
                $finalHealth = $this->checkProcessHealth($completedPid);
                $exitInfo['final_health'] = $finalHealth;
                
                $completedProcesses[] = $exitInfo;
                unset($this->childProcesses[$completedPid]);
                
                echo "[Monitor] プロセス '{$process['name']}' (PID: {$completedPid}) が完了\n";
                echo "  実行時間: " . round($duration, 2) . "秒\n";
                echo "  終了情報: {$exitInfo['message']}\n";
            }
        }
        
        return $completedProcesses;
    }
    
    private function interpretExitCode($code) {
        $interpretations = [
            0 => '正常終了',
            1 => 'エラーで終了',
            127 => 'コマンドが見つかりません',
            255 => '予期しないエラー'
        ];
        
        return $interpretations[$code] ?? "終了コード {$code}";
    }
    
    public function generateHealthReport() {
        $report = [
            'total_processes' => count($this->healthMetrics),
            'snapshots' => $this->healthMetrics,
            'summary' => []
        ];
        
        // 各プロセスの要約を作成
        foreach ($this->healthMetrics as $pid => $history) {
            $lastCheck = end($history);
            $report['summary'][$pid] = [
                'name' => $lastCheck['health']['name'],
                'final_status' => $lastCheck['health']['status'],
                'uptime' => $lastCheck['health']['uptime'],
                'issue_count' => count($lastCheck['health']['issues']),
                'metrics' => $lastCheck['health']['metrics'] ?? null
            ];
        }
        
        return $report;
    }
}

// 使用例:複数プロセスの健全性監視
echo "=== プロセス健全性監視デモ ===\n";

$monitor = new ProcessHealthMonitor();

// 複数の子プロセスを開始
$process1 = $monitor->startMonitoringProcess(function() {
    echo "プロセス1が実行中\n";
    sleep(5);
    echo "プロセス1が完了\n";
}, 'LongRunningTask');

$process2 = $monitor->startMonitoringProcess(function() {
    echo "プロセス2が実行中\n";
    sleep(3);
    echo "プロセス2が完了\n";
}, 'QuickTask');

// プロセスが完了するまで待機
$results = $monitor->waitAndCheckProcesses();

// レポート表示
echo "\n=== 健全性レポート ===\n";
$report = $monitor->generateHealthReport();

echo "監視対象プロセス数: " . count($report['summary']) . "\n";
foreach ($report['summary'] as $pid => $summary) {
    echo "\nプロセス: {$summary['name']} (PID: {$pid})\n";
    echo "  状態: {$summary['final_status']}\n";
    echo "  実行時間: " . round($summary['uptime'], 2) . "秒\n";
    echo "  問題数: {$summary['issue_count']}\n";
    
    if (isset($summary['metrics'])) {
        echo "  メモリ: " . round($summary['metrics']['memory'] / 1024 / 1024, 1) . "MB\n";
        echo "  CPU: {$summary['metrics']['cpu_percent']}%\n";
    }
}
?>

エラーハンドリングとベストプラクティス

1. 安全なプロセス終了処理

<?php
class SafeProcessManager {
    private $processes = [];
    private $signalHandlers = [];
    
    public function __construct() {
        // シグナルハンドラーを設定
        pcntl_signal(SIGCHLD, [$this, 'handleChildSignal']);
        pcntl_signal(SIGTERM, [$this, 'handleTerminateSignal']);
        pcntl_signal(SIGINT, [$this, 'handleInterruptSignal']);
    }
    
    public function handleChildSignal($signo) {
        // 非ブロッキングで子プロセスの終了状態を確認
        while (true) {
            $pid = pcntl_waitpid(-1, $status, WNOHANG);
            
            if ($pid <= 0) {
                break;
            }
            
            // 終了ステータスを処理
            $this->handleProcessExit($pid, $status);
        }
    }
    
    private function handleProcessExit($pid, $status) {
        if (!isset($this->processes[$pid])) {
            return;
        }
        
        $processInfo = $this->processes[$pid];
        $exitData = [
            'pid' => $pid,
            'name' => $processInfo['name'],
            'start_time' => $processInfo['start_time'],
            'end_time' => microtime(true),
            'status' => $status
        ];
        
        // 終了タイプを判定
        if (pcntl_wifexited($status)) {
            $exitCode = pcntl_wexitstatus($status);
            $exitData['type'] = 'normal_exit';
            $exitData['exit_code'] = $exitCode;
            $exitData['success'] = ($exitCode === 0);
            
            error_log("プロセス '{$processInfo['name']}' (PID: {$pid}) が終了コード {$exitCode} で終了");
            
        } elseif (pcntl_wifsignaled($status)) {
            $signal = pcntl_wtermsig($status);
            $exitData['type'] = 'signaled_exit';
            $exitData['signal'] = $signal;
            $exitData['success'] = false;
            
            error_log("プロセス '{$processInfo['name']}' (PID: {$pid}) がシグナル {$signal} で強制終了");
            
        } elseif (pcntl_wifstopped($status)) {
            $signal = pcntl_wstopsig($status);
            $exitData['type'] = 'stopped';
            $exitData['signal'] = $signal;
            $exitData['success'] = false;
            
            error_log("プロセス '{$processInfo['name']}' (PID: {$pid}) がシグナル {$signal} で停止");
        }
        
        // コールバック関数を実行
        if (isset($processInfo['callback'])) {
            try {
                call_user_func($processInfo['callback'], $exitData);
            } catch (Exception $e) {
                error_log("コールバック実行エラー: " . $e->getMessage());
            }
        }
        
        unset($this->processes[$pid]);
    }
    
    public function handleTerminateSignal($signo) {
        error_log("終了シグナル (SIGTERM) を受信しました");
        $this->terminateAllProcesses();
        exit(0);
    }
    
    public function handleInterruptSignal($signo) {
        error_log("中断シグナル (SIGINT) を受信しました");
        $this->terminateAllProcesses();
        exit(130); // SIGINT の標準終了コード
    }
    
    public function startProcess($callback, $processName, $onExit = null) {
        $pid = pcntl_fork();
        
        if ($pid === 0) {
            // 子プロセス
            try {
                $result = $callback();
                exit($result === true ? 0 : 1);
            } catch (Exception $e) {
                error_log("子プロセスエラー: " . $e->getMessage());
                exit(1);
            }
            
        } elseif ($pid > 0) {
            // 親プロセス
            $this->processes[$pid] = [
                'name' => $processName,
                'start_time' => microtime(true),
                'callback' => $onExit
            ];
            
            return $pid;
            
        } else {
            throw new Exception("プロセスの作成に失敗しました");
        }
    }
    
    public function terminateAllProcesses($signal = SIGTERM) {
        foreach ($this->processes as $pid => $info) {
            if (posix_kill($pid, $signal)) {
                error_log("プロセス {$pid} に シグナル {$signal} を送信しました");
            }
        }
        
        // すべてのプロセスが終了するまで待機
        $timeout = 30; // 30秒
        $startTime = time();
        
        while (!empty($this->processes) && (time() - $startTime) < $timeout) {
            // シグナルを処理
            pcntl_signal_dispatch();
            usleep(100000); // 0.1秒待機
        }
        
        // タイムアウト後の強制終了
        if (!empty($this->processes)) {
            error_log("警告: タイムアウトにより残りのプロセスを強制終了します");
            foreach ($this->processes as $pid => $info) {
                posix_kill($pid, SIGKILL);
            }
        }
    }
    
    public function waitForAllProcesses() {
        while (!empty($this->processes)) {
            pcntl_signal_dispatch();
            usleep(100000);
        }
    }
    
    public function getActiveProcessCount() {
        return count($this->processes);
    }
}

// 使用例:安全なプロセス管理
echo "=== 安全なプロセス管理デモ ===\n";

$manager = new SafeProcessManager();

// 複数のプロセスを開始
$pids = [];

for ($i = 1; $i <= 3; $i++) {
    $pid = $manager->startProcess(
        function() use ($i) {
            echo "プロセス {$i} が実行中 (PID: " . getmypid() . ")\n";
            sleep(rand(2, 5));
            echo "プロセス {$i} が完了\n";
            return true;
        },
        "Task-{$i}",
        function($exitData) use ($i) {
            echo "プロセス {$i} の終了コールバック: " . 
                 ($exitData['success'] ? '成功' : '失敗') . "\n";
        }
    );
    
    $pids[] = $pid;
    echo "プロセス {$i} を開始 (PID: {$pid})\n";
}

// すべてのプロセスの完了を待機
$manager->waitForAllProcesses();

echo "\n=== すべてのプロセスが完了しました ===\n";
?>

2. プロセス終了ステータスの詳細分析

<?php
class ProcessExitStatusAnalyzer {
    
    public static function analyzeStatus($status) {
        $analysis = [
            'raw_status' => $status,
            'timestamp' => time(),
            'analysis' => []
        ];
        
        // すべての可能な終了条件をチェック
        if (pcntl_wifexited($status)) {
            $analysis['analysis']['exit_type'] = 'normal_termination';
            $exitCode = pcntl_wexitstatus($status);
            $analysis['analysis']['exit_code'] = $exitCode;
            $analysis['analysis']['interpretation'] = self::interpretExitCode($exitCode);
            $analysis['analysis']['is_error'] = ($exitCode !== 0);
            
        } elseif (pcntl_wifsignaled($status)) {
            $analysis['analysis']['exit_type'] = 'killed_by_signal';
            $signal = pcntl_wtermsig($status);
            $analysis['analysis']['signal_number'] = $signal;
            $analysis['analysis']['signal_name'] = self::getSignalName($signal);
            
            // コアダンプの確認
            if (function_exists('pcntl_wcoredump') && pcntl_wcoredump($status)) {
                $analysis['analysis']['core_dump'] = true;
                $analysis['analysis']['core_dump_info'] = 'コアダンプが生成されました';
            }
            
            $analysis['analysis']['is_error'] = true;
            
        } elseif (pcntl_wifstopped($status)) {
            $analysis['analysis']['exit_type'] = 'stopped';
            $signal = pcntl_wstopsig($status);
            $analysis['analysis']['stop_signal'] = $signal;
            $analysis['analysis']['signal_name'] = self::getSignalName($signal);
            $analysis['analysis']['is_error'] = false;
        }
        
        return $analysis;
    }
    
    public static function interpretExitCode($code) {
        $standardCodes = [
            0 => [
                'message' => '成功(正常終了)',
                'severity' => 'success',
                'description' => 'プロセスが正常に完了しました'
            ],
            1 => [
                'message' => '一般的なエラー',
                'severity' => 'error',
                'description' => 'プロセスがエラーで終了しました'
            ],
            2 => [
                'message' => 'シェル誤用による終了',
                'severity' => 'error',
                'description' => 'シェルコマンドの使用方法が誤っています'
            ],
            126 => [
                'message' => 'コマンド実行不可',
                'severity' => 'error',
                'description' => 'ファイルは存在しますが実行権限がありません'
            ],
            127 => [
                'message' => 'コマンドが見つかりません',
                'severity' => 'error',
                'description' => 'コマンドまたはプログラムが見つかりません'
            ],
            128 => [
                'message' => '無効な引数',
                'severity' => 'error',
                'description' => '無効な引数でプロセスが終了しました'
            ],
            130 => [
                'message' => 'SIGINT による中断',
                'severity' => 'warning',
                'description' => 'Ctrl+C で中断されました'
            ],
            255 => [
                'message' => '範囲外の終了コード',
                'severity' => 'error',
                'description' => '終了コードが0-255の範囲外です'
            ]
        ];
        
        if (isset($standardCodes[$code])) {
            return $standardCodes[$code];
        }
        
        // カスタム終了コード
        if ($code >= 1 && $code <= 125) {
            return [
                'message' => "カスタム終了コード #{$code}",
                'severity' => 'error',
                'description' => "アプリケーション定義のエラーコード #{$code}"
            ];
        }
        
        if ($code > 128) {
            $signal = $code - 128;
            return [
                'message' => "シグナルによる終了 (シグナル #{$signal})",
                'severity' => 'error',
                'description' => self::getSignalDescription($signal)
            ];
        }
        
        return [
            'message' => "不明な終了コード #{$code}",
            'severity' => 'error',
            'description' => '終了理由が特定できません'
        ];
    }
    
    public static function getSignalName($signal) {
        $signals = [
            1 => 'SIGHUP',
            2 => 'SIGINT',
            3 => 'SIGQUIT',
            4 => 'SIGILL',
            5 => 'SIGTRAP',
            6 => 'SIGABRT',
            8 => 'SIGFPE',
            9 => 'SIGKILL',
            11 => 'SIGSEGV',
            13 => 'SIGPIPE',
            14 => 'SIGALRM',
            15 => 'SIGTERM',
            19 => 'SIGSTOP',
            20 => 'SIGTSTP'
        ];
        
        return $signals[$signal] ?? "SIGNAL #{$signal}";
    }
    
    public static function getSignalDescription($signal) {
        $descriptions = [
            1 => 'ハングアップ検出',
            2 => 'キーボードからの中断',
            3 => 'キーボードからの終了',
            4 => '不正な命令',
            5 => 'トレース/ブレークポイントトラップ',
            6 => '中止シグナル',
            8 => '浮動小数点例外',
            9 => '強制終了',
            11 => 'セグメンテーション違反',
            13 => 'パイプ破損',
            14 => 'アラームクロック',
            15 => '終了シグナル',
            19 => 'ストップシグナル',
            20 => 'キーボードからの停止'
        ];
        
        return $descriptions[$signal] ?? 'シグナル ' . $signal . ' で強制終了';
    }
    
    public static function generateDetailedReport($status) {
        $analysis = self::analyzeStatus($status);
        
        $report = "=== プロセス終了ステータス詳細レポート ===\n";
        $report .= "生のステータス値: " . $analysis['raw_status'] . "\n";
        $report .= "タイムスタンプ: " . date('Y-m-d H:i:s', $analysis['timestamp']) . "\n\n";
        
        $details = $analysis['analysis'];
        $report .= "終了タイプ: " . $details['exit_type'] . "\n";
        
        if ($details['exit_type'] === 'normal_termination') {
            $report .= "終了コード: " . $details['exit_code'] . "\n";
            $report .= "メッセージ: " . $details['interpretation']['message'] . "\n";
            $report .= "説明: " . $details['interpretation']['description'] . "\n";
            
        } elseif ($details['exit_type'] === 'killed_by_signal') {
            $report .= "シグナル番号: " . $details['signal_number'] . "\n";
            $report .= "シグナル名: " . $details['signal_name'] . "\n";
            
            if (isset($details['core_dump']) && $details['core_dump']) {
                $report .= "⚠️ " . $details['core_dump_info'] . "\n";
            }
            
            $report .= "説明: " . self::getSignalDescription($details['signal_number']) . "\n";
            
        } elseif ($details['exit_type'] === 'stopped') {
            $report .= "停止シグナル: " . $details['stop_signal'] . "\n";
            $report .= "シグナル名: " . $details['signal_name'] . "\n";
        }
        
        return $report;
    }
}

// 使用例:終了ステータスの詳細分析
echo "=== 終了ステータス分析デモ ===\n";

// 異なる終了条件をシミュレート
$testCases = [];

// ケース1:正常終了
$pid = pcntl_fork();
if ($pid === 0) {
    exit(0);
} else {
    $status = 0;
    pcntl_waitpid($pid, $status);
    $testCases['正常終了 (exit 0)'] = $status;
}

// ケース2:エラー終了
$pid = pcntl_fork();
if ($pid === 0) {
    exit(42);
} else {
    $status = 0;
    pcntl_waitpid($pid, $status);
    $testCases['エラー終了 (exit 42)'] = $status;
}

// ケース3:シグナルでの強制終了
$pid = pcntl_fork();
if ($pid === 0) {
    sleep(10); // 強制終了されるまで待機
} else {
    usleep(100000);
    posix_kill($pid, SIGTERM);
    $status = 0;
    pcntl_waitpid($pid, $status);
    $testCases['シグナル終了 (SIGTERM)'] = $status;
}

// 各ケースを分析
foreach ($testCases as $caseName => $status) {
    echo "\n--- {$caseName} ---\n";
    echo ProcessExitStatusAnalyzer::generateDetailedReport($status);
}
?>

まとめ

pcntl_wexitstatus関数は、PHPのマルチプロセッシング機能において、子プロセスの終了状態を正確に把握するために必須の関数です。この記事で紹介した実装例を通じて、以下のような実践的な機能を実現できます:

✨ 主要なメリット

  • 精密な制御: 子プロセスの終了コードを正確に取得
  • エラーハンドリング: 異常終了の早期検出と対応
  • リトライ機能: 失敗時の自動再試行
  • プロセス監視: 健全性チェックと自動復旧

🚀 実践的応用分野

  • バッチ処理: 大量タスク並列処理
  • ワーカープール: マルチスレッド的な処理
  • システムモニタリング: プロセスの健全性監視
  • エラーリカバリー: 障害時の自動対応

💡 技術的価値

  • 完全なプロセス制御: fork/exec完全サポート
  • プロアクティブ管理: 潜在的問題の早期検出
  • 自動化: 手動介入を排除したシステム
  • スケーラビリティ: 大規模並列処理対応

pcntl_wexitstatusを活用することで、PHPで本格的なマルチプロセッシングシステムを構築でき、システムのスケーラビリティと信頼性を大幅に向上させることができるでしょう。

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