自 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/'); // 用戶實時看到進度
?>
$output = `ls -la`; // 等同於 shell_exec('ls -la')
// 注意:反撇號內不支持變量插值,需用雙引號包裹
$cmd = "ls -la $dir";
$output = `$cmd`; // 正確
當需要寫入 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";
}
?>
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 繞過 Shell | proc_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']);
new Process(['ls', '-la']),物件化、超時、回調、輸出流proc_open 實現優雅異步PHP 命令行編程的核心是流抽象。掌握 shell_exec 簡單場景、exec 逐行處理、passthru 二進制流、system 實時輸出、proc_open 雙向控制、stream_select 並發等待,並嚴守繞過 Shell、參數化傳遞、超時熔斷、權限最小化安全原則。現代項目請優先引入 Symfony Process 或 ReactPHP,將底層細節封裝,專注業務邏輯。