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()
は複合的な情報を含むステータス値を返します。このステータス値には以下の情報が含まれています:
- 終了コード: プロセスが正常終了した場合の値(0-255)
- シグナル番号: プロセスがシグナルで終了した場合
- コアダンプ情報: メモリダンプが発生したかどうか
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で本格的なマルチプロセッシングシステムを構築でき、システムのスケーラビリティと信頼性を大幅に向上させることができるでしょう。