PHP 通過 exec()、system()、passthru()、shell_exec()、proc_open() 等函數可調用操作系統 Shell 命令,實現啟動外部程式、執行批處理、視頻轉碼、圖片處理、系統維護等功能。但這也是安全風險最高的操作之一。本文系統梳理各函數差異、最佳實踐、常見坑點與防範措施。
| 函數 | 返回值 | 輸出行為 | 適用場景 | 獲取完整輸出 | 獲取退出碼 |
|---|---|---|---|---|---|
exec() | 最後一行輸出 (string) | 不直接輸出 | 需程式化處理輸出、獲取退出碼 | 第 2 參數 &$output (array) | 第 3 參數 &$return_var |
system() | 最後一行輸出 (string) | 直接輸出到標準輸出 | 簡單命令、調試、需即時顯示進度 | 不可直接獲取(需輸出緩衝捕獲) | 第 2 參數 &$return_var |
passthru() | void | 直接輸出二進制數據 | 圖片/PDF/視頻流、二進制輸出 | 不可 | 第 2 參數 &$return_var |
shell_exec() / ` ` | 完整輸出 (string) | 不直接輸出 | 需完整文本輸出、簡單腳本 | 返回值即完整輸出 | 不可直接獲取 |
proc_open() | resource (process handle) | 完全可控(管道讀寫) | 雙向通訊、長時間運行、非阻塞、多進程協作 | 通過管道流讀取 | proc_get_status()['exitcode'] |
<?php
$cmd = 'ps -ef | grep php-fpm';
$output = [];
$returnVar = 0;
$lastLine = exec($cmd, $output, $returnVar);
echo "退出碼: $returnVar\n"; // 0=成功,非 0=失敗
echo "最後一行: $lastLine\n";
echo "完整輸出:\n";
print_r($output); // Array ( [0]=>'root 123 ...', [1]=>'www-data 456 ...' )
<?php
// 模擬長時間任務,即時輸出進度
system('for i in {1..5}; do echo "Step $i"; sleep 1; done', $ret);
echo "返回碼: $ret";
注意: 若在 Web 環境需即時輸出,必須關閉輸出緩衝:@ob_end_flush(); ob_implicit_flush(true);。
核心原則: 將命令輸出重定向到文件/設備,並放入後台運行(& 或 start /B)。
<?php
$cmd = '/usr/bin/php /var/www/scripts/long_task.php > /var/log/long_task.log 2>&1 &';
exec($cmd); // 立即返回,不等待
// 或使用 nohup 免疫掛起信號
exec('nohup /usr/bin/php /var/www/scripts/long_task.php >> /var/log/long_task.log 2>&1 &');
<?php
// 方法 1:start /B (無新窗口)
exec('start /B "" "C:\php\php.exe" "C:\scripts\long_task.php" > "C:\logs\long_task.log" 2>&1');
// 方法 2:COM 組件 (真正獨立進程)
$WshShell = new COM("WScript.Shell");
$WshShell->Run('cmd /c "C:\php\php.exe C:\scripts\long_task.php"', 0, false);
// 參數 0=隱藏窗口, false=不等待返回
<?php
$descriptorspec = array(
0 => array("pipe", "r"), // stdin
1 => array("pipe", "w"), // stdout
2 => array("pipe", "w") // stderr
);
$cmd = 'php -r "fwrite(STDOUT, \"hello\\n\"); fwrite(STDERR, \"error\\n\"); exit(42);"';
$proc = proc_open($cmd, $descriptorspec, $pipes);
if (is_resource($proc)) {
// 寫入 stdin
fwrite($pipes[0], "input data\n");
fclose($pipes[0]);
// 讀取 stdout/stderr
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$status = proc_get_status($proc);
$exitCode = $status['exitcode'];
proc_close($proc);
echo "Exit: $exitCode\nOut: $stdout\nErr: $stderr";
}
php.ini 設置 disable_functions = exec,system,passthru,shell_exec,proc_open,popen,pcntl_exec。若業務必須使用,僅在特定 CLI 腳本中通過 ini_set() 臨時開啟(需 disable_functions 為空)。// ❌ 極度危險
exec("ping " . $_GET['ip']);
// ✅ 正確:escapeshellarg() 轉義單個參數
$ip = escapeshellarg($_GET['ip']);
exec("ping -c 4 $ip", $out, $ret);
// ✅ 更好:白名單驗證
if (filter_var($_GET['ip'], FILTER_VALIDATE_IP)) {
exec("ping -c 4 " . escapeshellarg($_GET['ip']), $out, $ret);
}
escapeshellcmd() 轉義整個命令字串(較少用,參數級用 escapeshellarg())。/usr/sbin/nologin),無 sudo 權限,僅能讀寫項目目錄。cmd, user, ip, time, exit_code 至不可篡改日誌系統。| 現象 | 原因 | 解決 |
|---|---|---|
| PHP 腳本卡住、超時 | 命令輸出未重定向、前台運行、等待輸入 | 重定向 stdout/stderr 到文件/ /dev/null,加 & 或 start /B 後台運行 |
Windows 下 exec('start ...') 仍阻塞 | 路徑含空格未加引號、未用 start /B | exec('start /B "" "C:\Program Files\app.exe"') 雙引號處理 |
| 權限被拒絕 | Web 用戶無執行權限、目標文件不可執行 | chmod +x script.sh;確認目錄可讀寫;SELinux/AppArmor 策略 |
| 環境變量丟失(PATH、LD_LIBRARY_PATH) | Web 環境非登入 Shell,環境變量極少 | 命令中使用絕對路徑 /usr/bin/php;或 putenv('PATH=...') 設置 |
| 中文亂碼 | 編碼不一致(Windows GBK vs PHP UTF-8) | iconv('GBK', 'UTF-8', $output);或 chcp 65001 設置控制台編碼 |
new Process(['php', 'script.php']),物件化、跨平台、支援超時、回調、輸出流。GD/Imagick、視頻用 FFmpeg-PHP、壓縮用 ZipArchive、郵件用 SwiftMailer/Symfony Mailer。PHP 執行系統命令是一把雙刃劍。首選 exec() 獲取結構化輸出,proc_open() 處理複雜交互,背景任務務必重定向輸出並後台運行。 生產環境嚴格遵循「最小權限、參數轉義、白名單驗證、審計日誌」四大原則。隨著生態成熟,越來越多場景可由純 PHP 庫或異步隊列替代,應優先評估替代方案,將 exec 類函數作為最後手段。