[PHP]proc_terminate関数の使い方を徹底解説!プロセス強制終了の完全ガイド

PHP

こんにちは!今日はPHPで実行中のプロセスを安全に終了させるための関数「proc_terminate」について、実践的な使い方を詳しく解説していきます。

proc_terminateとは?

proc_terminateは、proc_openで起動したプロセスを強制的に終了させるための関数です。暴走したプロセスやタイムアウトしたプロセスを適切に停止させる際に不可欠な機能です。

基本構文

bool proc_terminate(resource $process, int $signal = SIGTERM)
  • $process: proc_openで取得したプロセスリソース
  • $signal: 送信するシグナル(デフォルトはSIGTERM)

なぜproc_terminateが必要なのか?

プロセスを適切に終了させないと、以下のような問題が発生します:

  • リソースリーク: メモリやファイルハンドルが解放されない
  • ゾンビプロセス: 終了しても残り続けるプロセスが発生
  • システム負荷: 不要なプロセスがCPUやメモリを消費し続ける
  • デッドロック: パイプがブロックされたまま残る

基本的な使い方

例1: シンプルなプロセス終了

<?php
$descriptorspec = [
    0 => ["pipe", "r"],
    1 => ["pipe", "w"],
    2 => ["pipe", "w"]
];

// 長時間実行されるプロセスを起動
$process = proc_open('sleep 100', $descriptorspec, $pipes);

if (is_resource($process)) {
    echo "プロセスを起動しました\n";
    
    // 3秒待機
    sleep(3);
    
    // プロセスを終了
    $result = proc_terminate($process);
    
    if ($result) {
        echo "プロセスを正常に終了させました\n";
    } else {
        echo "プロセスの終了に失敗しました\n";
    }
    
    // リソースをクリーンアップ
    fclose($pipes[0]);
    fclose($pipes[1]);
    fclose($pipes[2]);
    proc_close($process);
}
?>

シグナルの種類と使い分け

Unix系システムでは、異なるシグナルを送ることで、プロセスの終了方法を制御できます。

主要なシグナル

<?php
// SIGTERM (15) - 通常の終了要求(デフォルト)
// プロセスはクリーンアップ処理を実行できる
proc_terminate($process, 15);

// SIGKILL (9) - 強制終了
// プロセスは即座に終了、クリーンアップ不可
proc_terminate($process, 9);

// SIGINT (2) - 割り込み(Ctrl+Cと同等)
proc_terminate($process, 2);

// SIGHUP (1) - ハングアップ
proc_terminate($process, 1);
?>

シグナルの選択基準

<?php
/**
 * 段階的な終了処理の実装
 */
function graceful_terminate($process, $timeout = 5) {
    // ステップ1: 優しく終了を要求(SIGTERM)
    echo "SIGTERM を送信...\n";
    proc_terminate($process, 15); // SIGTERM
    
    $start = time();
    while (time() - $start < $timeout) {
        $status = proc_get_status($process);
        if (!$status['running']) {
            echo "プロセスは正常に終了しました\n";
            return true;
        }
        usleep(100000); // 0.1秒待機
    }
    
    // ステップ2: まだ動いている場合は強制終了(SIGKILL)
    echo "タイムアウト。SIGKILL を送信...\n";
    proc_terminate($process, 9); // SIGKILL
    
    sleep(1);
    $status = proc_get_status($process);
    return !$status['running'];
}

// 使用例
$descriptorspec = [
    0 => ["pipe", "r"],
    1 => ["pipe", "w"],
    2 => ["pipe", "w"]
];

$process = proc_open('sleep 100', $descriptorspec, $pipes);

if (is_resource($process)) {
    sleep(2);
    
    if (graceful_terminate($process)) {
        echo "プロセスを安全に終了できました\n";
    } else {
        echo "プロセスの終了に問題がありました\n";
    }
    
    fclose($pipes[0]);
    fclose($pipes[1]);
    fclose($pipes[2]);
    proc_close($process);
}
?>

実践例1: タイムアウト付きコマンド実行

最も一般的な使用例です。

<?php
/**
 * タイムアウト機能付きでコマンドを実行
 */
function execute_with_timeout($command, $timeout = 10) {
    $descriptorspec = [
        0 => ["pipe", "r"],
        1 => ["pipe", "w"],
        2 => ["pipe", "w"]
    ];
    
    $process = proc_open($command, $descriptorspec, $pipes);
    
    if (!is_resource($process)) {
        return ['success' => false, 'error' => 'プロセスの起動に失敗'];
    }
    
    // 非ブロッキングモードに設定
    stream_set_blocking($pipes[1], false);
    stream_set_blocking($pipes[2], false);
    
    $start_time = time();
    $output = '';
    $errors = '';
    
    while (true) {
        // プロセスの状態を確認
        $status = proc_get_status($process);
        
        // 出力を読み取る
        $output .= stream_get_contents($pipes[1]);
        $errors .= stream_get_contents($pipes[2]);
        
        // プロセスが終了していればループを抜ける
        if (!$status['running']) {
            break;
        }
        
        // タイムアウトチェック
        if (time() - $start_time > $timeout) {
            echo "タイムアウト!プロセスを終了します\n";
            proc_terminate($process, 15); // SIGTERM
            sleep(1);
            
            // まだ動いていれば強制終了
            $status = proc_get_status($process);
            if ($status['running']) {
                proc_terminate($process, 9); // SIGKILL
            }
            
            fclose($pipes[0]);
            fclose($pipes[1]);
            fclose($pipes[2]);
            proc_close($process);
            
            return [
                'success' => false,
                'error' => 'タイムアウト',
                'output' => $output,
                'errors' => $errors
            ];
        }
        
        usleep(100000); // 0.1秒待機
    }
    
    fclose($pipes[0]);
    fclose($pipes[1]);
    fclose($pipes[2]);
    
    $exit_code = proc_close($process);
    
    return [
        'success' => $exit_code === 0,
        'exit_code' => $exit_code,
        'output' => $output,
        'errors' => $errors
    ];
}

// 使用例
$result = execute_with_timeout('ping -c 5 google.com', 3);

if ($result['success']) {
    echo "成功:\n" . $result['output'];
} else {
    echo "失敗: " . $result['error'] . "\n";
    if (!empty($result['errors'])) {
        echo "エラー出力: " . $result['errors'] . "\n";
    }
}
?>

実践例2: 並列プロセス管理

複数のプロセスを同時に管理し、タイムアウトで一括終了する例です。

<?php
/**
 * 複数プロセスのマネージャークラス
 */
class ProcessManager {
    private $processes = [];
    
    public function add($id, $command) {
        $descriptorspec = [
            0 => ["pipe", "r"],
            1 => ["pipe", "w"],
            2 => ["pipe", "w"]
        ];
        
        $process = proc_open($command, $descriptorspec, $pipes);
        
        if (is_resource($process)) {
            stream_set_blocking($pipes[1], false);
            stream_set_blocking($pipes[2], false);
            
            $this->processes[$id] = [
                'process' => $process,
                'pipes' => $pipes,
                'start_time' => time(),
                'output' => '',
                'errors' => ''
            ];
            
            return true;
        }
        
        return false;
    }
    
    public function monitor($timeout = 30) {
        while (!empty($this->processes)) {
            foreach ($this->processes as $id => $data) {
                $status = proc_get_status($data['process']);
                
                // 出力を収集
                $data['output'] .= stream_get_contents($data['pipes'][1]);
                $data['errors'] .= stream_get_contents($data['pipes'][2]);
                $this->processes[$id] = $data;
                
                // プロセスが終了していれば削除
                if (!$status['running']) {
                    echo "プロセス {$id} が正常終了しました\n";
                    $this->cleanup($id);
                    continue;
                }
                
                // タイムアウトチェック
                if (time() - $data['start_time'] > $timeout) {
                    echo "プロセス {$id} がタイムアウトしました\n";
                    $this->terminate($id);
                }
            }
            
            usleep(100000); // 0.1秒待機
        }
    }
    
    public function terminate($id, $force = false) {
        if (!isset($this->processes[$id])) {
            return false;
        }
        
        $process = $this->processes[$id]['process'];
        
        if ($force) {
            proc_terminate($process, 9); // SIGKILL
        } else {
            proc_terminate($process, 15); // SIGTERM
            sleep(1);
            
            $status = proc_get_status($process);
            if ($status['running']) {
                proc_terminate($process, 9); // SIGKILL
            }
        }
        
        $this->cleanup($id);
        return true;
    }
    
    public function terminateAll() {
        foreach (array_keys($this->processes) as $id) {
            $this->terminate($id);
        }
    }
    
    private function cleanup($id) {
        if (!isset($this->processes[$id])) {
            return;
        }
        
        $pipes = $this->processes[$id]['pipes'];
        fclose($pipes[0]);
        fclose($pipes[1]);
        fclose($pipes[2]);
        proc_close($this->processes[$id]['process']);
        
        unset($this->processes[$id]);
    }
    
    public function getOutput($id) {
        return $this->processes[$id]['output'] ?? null;
    }
}

// 使用例
$manager = new ProcessManager();

// 複数のプロセスを起動
$manager->add('job1', 'sleep 5 && echo "Job 1 done"');
$manager->add('job2', 'sleep 10 && echo "Job 2 done"');
$manager->add('job3', 'sleep 50 && echo "Job 3 done"'); // タイムアウトする

// タイムアウト30秒で監視
$manager->monitor(30);

echo "すべてのジョブが完了または終了しました\n";
?>

実践例3: メモリ制限の監視と終了

プロセスのメモリ使用量を監視し、制限を超えたら終了する例です。

<?php
/**
 * メモリ使用量を監視してプロセスを管理
 */
function execute_with_memory_limit($command, $max_memory_mb = 100, $timeout = 60) {
    $descriptorspec = [
        0 => ["pipe", "r"],
        1 => ["pipe", "w"],
        2 => ["pipe", "w"]
    ];
    
    $process = proc_open($command, $descriptorspec, $pipes);
    
    if (!is_resource($process)) {
        return ['success' => false, 'error' => 'プロセス起動失敗'];
    }
    
    stream_set_blocking($pipes[1], false);
    stream_set_blocking($pipes[2], false);
    
    $start_time = time();
    $output = '';
    $errors = '';
    
    while (true) {
        $status = proc_get_status($process);
        
        if (!$status['running']) {
            break;
        }
        
        // メモリ使用量をチェック(Linux)
        $pid = $status['pid'];
        $mem_info = shell_exec("ps -o rss= -p {$pid}");
        $memory_kb = intval(trim($mem_info));
        $memory_mb = $memory_kb / 1024;
        
        echo "プロセス {$pid} のメモリ使用量: {$memory_mb} MB\n";
        
        // メモリ制限チェック
        if ($memory_mb > $max_memory_mb) {
            echo "メモリ制限超過!プロセスを終了します\n";
            proc_terminate($process, 9);
            
            fclose($pipes[0]);
            fclose($pipes[1]);
            fclose($pipes[2]);
            proc_close($process);
            
            return [
                'success' => false,
                'error' => 'メモリ制限超過',
                'memory_used' => $memory_mb
            ];
        }
        
        // タイムアウトチェック
        if (time() - $start_time > $timeout) {
            proc_terminate($process, 15);
            sleep(1);
            proc_terminate($process, 9);
            
            fclose($pipes[0]);
            fclose($pipes[1]);
            fclose($pipes[2]);
            proc_close($process);
            
            return ['success' => false, 'error' => 'タイムアウト'];
        }
        
        $output .= stream_get_contents($pipes[1]);
        $errors .= stream_get_contents($pipes[2]);
        
        sleep(1);
    }
    
    fclose($pipes[0]);
    fclose($pipes[1]);
    fclose($pipes[2]);
    $exit_code = proc_close($process);
    
    return [
        'success' => $exit_code === 0,
        'exit_code' => $exit_code,
        'output' => $output,
        'errors' => $errors
    ];
}

// 使用例
$result = execute_with_memory_limit('php memory_intensive_script.php', 50, 120);

if ($result['success']) {
    echo "実行成功\n";
} else {
    echo "実行失敗: " . $result['error'] . "\n";
}
?>

Windowsでの注意点

Windowsでは、Unix系のシグナルが完全にサポートされていません。

<?php
// Windows環境での対応
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
    // Windowsではシグナル番号は無視される
    // proc_terminate()は常にプロセスを強制終了
    proc_terminate($process);
} else {
    // Unix系では段階的終了が可能
    proc_terminate($process, 15); // SIGTERM
    sleep(1);
    
    $status = proc_get_status($process);
    if ($status['running']) {
        proc_terminate($process, 9); // SIGKILL
    }
}
?>

よくあるエラーと対処法

1. プロセスが終了しない

<?php
// 問題: SIGTERMで終了しないプロセス
// 解決: SIGKILLを使用

function force_terminate($process) {
    proc_terminate($process, 15); // まずSIGTERM
    
    for ($i = 0; $i < 10; $i++) {
        usleep(100000);
        $status = proc_get_status($process);
        if (!$status['running']) {
            return true;
        }
    }
    
    // それでも終了しない場合はSIGKILL
    proc_terminate($process, 9);
    sleep(1);
    
    $status = proc_get_status($process);
    return !$status['running'];
}
?>

2. ゾンビプロセスが残る

<?php
// 問題: proc_close()を呼び忘れ
// 解決: 必ずproc_close()を呼ぶ

function proper_cleanup($process, $pipes) {
    // プロセスを終了
    proc_terminate($process);
    
    // パイプを閉じる
    foreach ($pipes as $pipe) {
        if (is_resource($pipe)) {
            fclose($pipe);
        }
    }
    
    // プロセスリソースを解放(重要!)
    proc_close($process);
}
?>

3. パーミッションエラー

<?php
// 問題: プロセスを終了する権限がない
// 解決: エラーハンドリングを追加

function safe_terminate($process) {
    $status = proc_get_status($process);
    
    if (!$status['running']) {
        return true; // すでに終了している
    }
    
    $result = @proc_terminate($process, 15);
    
    if (!$result) {
        error_log("プロセス終了に失敗: PID " . $status['pid']);
        return false;
    }
    
    return true;
}
?>

ベストプラクティス

1. 段階的な終了

<?php
function terminate_gracefully($process, $gentle_timeout = 5) {
    // フェーズ1: 優しい終了要求
    proc_terminate($process, 15);
    
    $start = time();
    while (time() - $start < $gentle_timeout) {
        $status = proc_get_status($process);
        if (!$status['running']) {
            return 'gentle';
        }
        usleep(100000);
    }
    
    // フェーズ2: 強制終了
    proc_terminate($process, 9);
    sleep(1);
    
    return 'forced';
}
?>

2. リソース管理

<?php
class ManagedProcess {
    private $process;
    private $pipes;
    
    public function __construct($command) {
        $descriptorspec = [
            0 => ["pipe", "r"],
            1 => ["pipe", "w"],
            2 => ["pipe", "w"]
        ];
        
        $this->process = proc_open($command, $descriptorspec, $this->pipes);
    }
    
    public function terminate() {
        if (is_resource($this->process)) {
            proc_terminate($this->process, 15);
            sleep(1);
            
            $status = proc_get_status($this->process);
            if ($status['running']) {
                proc_terminate($this->process, 9);
            }
        }
    }
    
    public function __destruct() {
        // デストラクタで確実にクリーンアップ
        $this->terminate();
        
        foreach ($this->pipes as $pipe) {
            if (is_resource($pipe)) {
                fclose($pipe);
            }
        }
        
        if (is_resource($this->process)) {
            proc_close($this->process);
        }
    }
}

// 使用例
$proc = new ManagedProcess('sleep 100');
sleep(3);
$proc->terminate();
// デストラクタで自動的にクリーンアップされる
?>

3. ログ記録

<?php
function terminate_with_logging($process, $reason = 'unknown') {
    $status = proc_get_status($process);
    
    error_log(sprintf(
        "プロセス終了: PID=%d, Reason=%s, Time=%s",
        $status['pid'],
        $reason,
        date('Y-m-d H:i:s')
    ));
    
    $result = proc_terminate($process, 15);
    
    if (!$result) {
        error_log("プロセス終了失敗: PID=" . $status['pid']);
        return false;
    }
    
    sleep(1);
    $status = proc_get_status($process);
    
    if ($status['running']) {
        error_log("強制終了を実行: PID=" . $status['pid']);
        proc_terminate($process, 9);
    }
    
    return true;
}
?>

まとめ

proc_terminateを正しく使うためのポイント:

段階的な終了: まずSIGTERM、それでも終了しなければSIGKILL ✅ タイムアウト設定: 長時間実行プロセスには必須 ✅ リソース管理: 必ずproc_close()を呼ぶ ✅ エラーハンドリング: 終了失敗のケースを考慮 ✅ ログ記録: トラブルシューティングのために記録を残す

プロセス管理は、PHPアプリケーションの安定性とパフォーマンスに直結する重要な要素です。proc_terminateを適切に使用して、堅牢なシステムを構築しましょう!

参考リンク

質問やフィードバックがあれば、コメント欄でお気軽にどうぞ!

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