首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 命令行編程實戰:shell_exec、proc_open 與異步並發執行

     編輯:PHP 相關知識

前言:PHP 不只是 Web,也是強大的 Shell 腳本語言

自 PHP 4.3 起,CLI SAPI 正式支持,使 PHP 能像 Perl、Python、Bash 一樣編寫系統管理腳本、數據處理管道、守護進程。本文深度講解 shell_exec、exec、passthru、system 四大執行函數差異、proc_open 雙向通信、流選擇器實現進程內並發、安全防護與最佳實踐。

一、四大執行函數對比

函數返回值輸出處理適用場景阻塞性
shell_exec完整輸出字符串捕獲 stdout簡單命令、需獲取完整結果阻塞
exec最後一行 / 數組可選捕獲每行到數組需逐行處理、獲取退出碼阻塞
passthru退出碼直接輸出到 stdout二進制輸出(圖像、PDF)、流式傳輸阻塞
system最後一行直接輸出到 stdout交互式命令、實時顯示進度阻塞

代碼示例

<?php
// 1. shell_exec - 獲取完整輸出
$files = shell_exec('ls -la /tmp');
echo "
$files
"; // 2. exec - 逐行捕獲 + 退出碼 $output = []; $exitCode = 0; exec('wc -l *.log', $output, $exitCode); print_r($output); // 每行一個元素 echo "Exit: $exitCode"; // 3. passthru - 直接輸出(適合圖像流) header('Content-Type: image/png'); passthru('cat /path/to/image.png'); // 4. system - 實時顯示(適合長時間任務) system('rsync -avz /src/ /dest/'); // 用戶實時看到進度 ?>

二、反撇號操作符:shell_exec 別名

$output = `ls -la`; // 等同於 shell_exec('ls -la')
// 注意:反撇號內不支持變量插值,需用雙引號包裹
$cmd = "ls -la $dir";
$output = `$cmd`; // 正確

三、proc_open:雙向通信與精細控制

當需要寫入 stdin、讀取 stdout/stderr 獨立流、非阻塞輪詢、超時控制時,proc_open 是唯一選擇。

<?php
$descriptors = [
    0 => ['pipe', 'r'],  // stdin
    1 => ['pipe', 'w'],  // stdout
    2 => ['pipe', 'w'],  // stderr
];

$proc = proc_open('php -r "fwrite(STDERR, \"err\\n\"); echo \"out\\n\";"', $descriptors, $pipes);

if (is_resource($proc)) {
    // 寫入 stdin
    fwrite($pipes[0], "input data\n");
    fclose($pipes[0]);
    
    // 讀取 stdout
    $stdout = stream_get_contents($pipes[1]);
    fclose($pipes[1]);
    
    // 讀取 stderr
    $stderr = stream_get_contents($pipes[2]);
    fclose($pipes[2]);
    
    $exitCode = proc_close($proc);
    
    echo "STDOUT: $stdout";
    echo "STDERR: $stderr";
    echo "EXIT: $exitCode";
}
?>

四、進程內異步並發:stream_select 實現多任務

PHP 無線程,但可用非阻塞流 + stream_select實現單進程並發等待多個外部命令/HTTP 請求。

<?php
// 並發請求多個 HTTP 端點(模擬)
$urls = [
    'https://httpbin.org/delay/1',
    'https://httpbin.org/delay/2',
    'https://httpbin.org/delay/3',
];

$streams = [];
$results = [];

foreach ($urls as $i => $url) {
    // 非阻塞異步連接
    $fp = stream_socket_client(
        parse_url($url, PHP_URL_HOST) . ':80',
        $errno, $errstr, 5,
        STREAM_CLIENT_ASYNC_CONNECT | STREAM_CLIENT_CONNECT
    );
    if ($fp) {
        stream_set_blocking($fp, 0); // 關鍵:非阻塞
        fwrite($fp, "GET " . parse_url($url, PHP_URL_PATH) . " HTTP/1.0\r\nHost: " . parse_url($url, PHP_URL_HOST) . "\r\n\r\n");
        $streams[(int)$fp] = ['fp' => $fp, 'id' => $i, 'buf' => ''];
    }
}

$start = microtime(true);
while ($streams) {
    $read = $write = $except = [];
    foreach ($streams as $fd => $s) {
        $read[] = $s['fp'];
    }
    
    // 等待任意流可讀/可寫/異常,超時 10 秒
    $n = stream_select($read, $write, $except, 10);
    
    if ($n === false) break; // 錯誤
    if ($n === 0) { echo "Timeout\n"; break; }
    
    foreach ($read as $fp) {
        $fd = (int)$fp;
        $s = &$streams[$fd];
        $data = fread($fp, 8192);
        if ($data === '' || $data === false) {
            // EOF 或錯誤
            fclose($fp);
            $results[$s['id']] = $s['buf'];
            unset($streams[$fd]);
        } else {
            $s['buf'] .= $data;
        }
    }
}

echo "Total time: " . (microtime(true) - $start) . "s\n"; // ~3s (並發) vs 6s (串行)
print_r($results);
?>

五、安全防護鐵律

風險防護措施代碼體現
命令注入絕不直接拼接用戶輸入escapeshellarg($arg)、escapeshellcmd($cmd)
參數注入使用 proc_open 繞過 Shellproc_open(['php', 'script.php', $arg], ...)
權限提升禁用 sudo、以低權限用戶運行php-fpm 用戶、容器 USER 指令
資源耗盡限制並發數、設置超時proc_terminate、stream_set_timeout
信息洩露不輸出 stderr 給用戶、記錄審計日誌獨立 stderr 管道、syslog 記錄

安全調用模式

function safe_exec(string $cmd, array $args = [], int $timeout = 30): array {
    // 1. 參數獨立傳遞,繞過 Shell
    $process = proc_open(
        array_merge([$cmd], array_map('escapeshellarg', $args)),
        [
            0 => ['pipe', 'r'],
            1 => ['pipe', 'w'],
            2 => ['pipe', 'w'],
        ],
        $pipes
    );
    
    if (!is_resource($process)) {
        throw new RuntimeException("Failed to start: $cmd");
    }
    
    // 2. 非阻塞讀取 + 超時控制
    stream_set_blocking($pipes[1], 0);
    stream_set_blocking($pipes[2], 0);
    stream_set_timeout($pipes[1], $timeout);
    stream_set_timeout($pipes[2], $timeout);
    
    $stdout = $stderr = '';
    $start = time();
    
    while (true) {
        $read = [$pipes[1], $pipes[2]];
        $n = stream_select($read, $write = null, $except = null, 1);
        
        if ($n > 0) {
            foreach ($read as $fp) {
                $data = fread($fp, 8192);
                if ($fp === $pipes[1]) $stdout .= $data ?: '';
                else $stderr .= $data ?: '';
            }
        }
        
        $status = proc_get_status($process);
        if (!$status['running']) break;
        if (time() - $start > $timeout) {
            proc_terminate($process);
            throw new RuntimeException("Timeout: $cmd");
        }
        usleep(10000); // 10ms 降低 CPU
    }
    
    fclose($pipes[0]); fclose($pipes[1]); fclose($pipes[2]);
    $exitCode = proc_close($process);
    
    return ['exit' => $exitCode, 'stdout' => $stdout, 'stderr' => $stderr];
}

// 使用
$result = safe_exec('convert', ['input.jpg', '-resize', '800x600', 'output.jpg']);

六、現代替代方案

  • Symfony Process:new Process(['ls', '-la']),物件化、超時、回調、輸出流
  • ReactPHP ChildProcess:異步非阻塞、Promise API、事件循環集成
  • Swoole Process:多進程、通信管道、共享內存、協程調度
  • PHP 8.1+ Fiber:用戶態協程,配合 proc_open 實現優雅異步

總結

PHP 命令行編程的核心是流抽象。掌握 shell_exec 簡單場景、exec 逐行處理、passthru 二進制流、system 實時輸出、proc_open 雙向控制、stream_select 並發等待,並嚴守繞過 Shell、參數化傳遞、超時熔斷、權限最小化安全原則。現代項目請優先引入 Symfony Process 或 ReactPHP,將底層細節封裝,專注業務邏輯。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved