系统程序执行
概述
PHP 提供了多个函数用于执行系统命令和外部程序:exec()、system()、passthru()、shell_exec() 和 proc_open()。每个函数有不同的输出处理方式和返回值。使用这些函数时必须注意安全性,防止命令注入攻击。
适用场景
- 调用系统工具(如 git、rsync)
- 执行编译/构建命令
- 图片处理(ImageMagick、FFmpeg)
- 系统管理脚本
基础概念
执行函数对比
| 函数 | 输出处理 | 返回值 | 终止符 |
|---|---|---|---|
exec() | 可选输出数组 | 最后一行 | 无 |
system() | 直接输出 | 最后一行 | 无 |
passthru() | 直接输出(二进制安全) | 状态码 | 无 |
shell_exec() | 返回完整输出 | string|null | 反引号 |
proc_open() | 流式读写 | resource | 无 |
popen() | 流式读写 | resource | 有 |
安全警告
永远不要将用户输入直接传递给执行函数。使用 escapeshellarg() 和 escapeshellcmd() 进行转义。
语法与代码示例
exec()
php
<?php
// exec - 执行命令,返回最后一行
$output = exec('ls -la /tmp');
echo "最后一行: {$output}\n";
// exec 获取完整输出
exec('ls -la /tmp', $outputLines, $returnCode);
foreach ($outputLines as $line) {
echo $line . PHP_EOL;
}
echo "返回码: {$returnCode}\n";
// 使用附加输出参数
exec('git status --short', $files, $code);
echo "变更文件: " . count($files) . "\n";system()
php
<?php
// system - 执行命令并直接输出到 STDOUT
$lastLine = system('echo "Hello from system()"');
echo "最后一行: {$lastLine}\n";
// 常用于交互式命令
system('clear'); // 清屏passthru()
php
<?php
// passthru - 适合输出二进制数据(如图片)
header('Content-Type: image/png');
passthru('cat /tmp/screenshot.png');
// 带返回值
passthru('ffmpeg -i /tmp/input.mp4 /tmp/output.mp3', $code);
echo "FFmpeg 返回码: {$code}\n";shell_exec()
php
<?php
// shell_exec - 返回完整输出
$output = shell_exec('ls -la /tmp | wc -l');
echo "文件数: " . trim($output) . "\n";
// 反引号是 shell_exec 的简写
$output = `ls -la /tmp | wc -l`;
echo "文件数: " . trim($output) . "\n";
// 注意:shell_exec 在命令失败时返回 null
$result = shell_exec('ls /nonexistent');
var_dump($result); // null 或空字符串proc_open()
php
<?php
// proc_open - 完全控制输入输出流
$descriptors = [
0 => ['pipe', 'r'], // stdin
1 => ['pipe', 'w'], // stdout
2 => ['pipe', 'w'], // stderr
];
$process = proc_open('php -r "while($line=fgets(STDIN)){echo strtoupper($line);}"', $descriptors, $pipes);
if (is_resource($process)) {
// 写入 stdin
fwrite($pipes[0], "hello\n");
fwrite($pipes[0], "world\n");
fclose($pipes[0]);
// 读取 stdout
echo "输出: " . stream_get_contents($pipes[1]);
fclose($pipes[1]);
// 读取 stderr
$error = stream_get_contents($pipes[2]);
fclose($pipes[2]);
// 等待进程结束
$returnCode = proc_close($process);
echo "返回码: {$returnCode}\n";
}实战示例
安全的命令执行封装
php
<?php
declare(strict_types=1);
class CommandExecutor
{
/**
* 安全执行命令
*/
public static function exec(string $command, array $args = []): CommandResult
{
// 转义参数
$escapedArgs = array_map('escapeshellarg', $args);
$fullCommand = vsprintf($command, $escapedArgs);
$output = [];
$returnCode = 0;
$startTime = microtime(true);
exec($fullCommand . ' 2>&1', $output, $returnCode);
$elapsed = round(microtime(true) - $startTime, 3);
return new CommandResult(
command: $fullCommand,
output: $output,
returnCode: $returnCode,
elapsed: $elapsed
);
}
/**
* 交互式执行
*/
public static function execInteractive(string $command, string $stdin = ''): CommandResult
{
$descriptors = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
];
$process = proc_open($command, $descriptors, $pipes);
if (!is_resource($process)) {
return new CommandResult($command, [], 255, 0);
}
fwrite($pipes[0], $stdin);
fclose($pipes[0]);
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$returnCode = proc_close($process);
return new CommandResult($command, explode("\n", trim($stdout)), $returnCode, 0);
}
}
class CommandResult
{
public function __construct(
public readonly string $command,
public readonly array $output,
public readonly int $returnCode,
public readonly float $elapsed
) {}
public function isSuccessful(): bool
{
return $this->returnCode === 0;
}
public function getOutput(): string
{
return implode("\n", $this->output);
}
}
// 使用
$result = CommandExecutor::exec('git status --short', []);
if ($result->isSuccessful()) {
echo "变更文件: " . trim($result->getOutput()) . "\n";
} else {
fwrite(STDERR, "命令失败: {$result->command}\n");
}注意事项
命令注入防护
php
<?php
// 危险:直接拼接用户输入
$file = $_GET['file'];
exec("cat {$file}"); // 用户可以传入 "file; rm -rf /"
// 安全:使用 escapeshellarg
$file = escapeshellarg($_GET['file']);
exec("cat {$file}");
// 更安全:白名单验证
$allowedFiles = ['config.json', 'README.md'];
$requestedFile = basename($_GET['file']);
if (!in_array($requestedFile, $allowedFiles, true)) {
die("不允许的文件");
}
exec("cat " . escapeshellarg($requestedFile));禁用函数
bash
# php.ini 安全配置:禁用危险的执行函数
```ini
disable_functions = exec,passthru,shell_exec,system,proc_open,popen最佳实践
1. 记录命令执行日志
php
<?php
function safeExec(string $command, array $args = []): array
{
$escapedArgs = array_map('escapeshellarg', $args);
$fullCommand = vsprintf($command, $escapedArgs);
error_log("Executing: {$fullCommand}");
$output = [];
$code = 0;
exec($fullCommand . ' 2>&1', $output, $code);
if ($code !== 0) {
error_log("Command failed ({$code}): {$fullCommand}");
}
return ['output' => $output, 'code' => $code];
}2. 使用 proc_open 处理大输出
php
<?php
// 对于输出量大的命令,使用 proc_open 流式处理
function streamExec(string $command): Generator
{
$descriptors = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
];
$process = proc_open($command, $descriptors, $pipes);
if (!is_resource($process)) return;
fclose($pipes[0]);
while (($line = fgets($pipes[1])) !== false) {
yield trim($line);
}
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
}进阶用法
调试与测试技巧
php
<?php
declare(strict_types=1);
// 单元测试辅助函数
function createTestResource(): mixed
{
return match (true) {
default => new stdClass(),
};
}
// 调试输出函数
function debugOutput(mixed , string = ''): void
{
= ? ": " : '';
.= print_r(, true);
fwrite(STDERR, . "\n");
}
// 性能基准测试
function benchmark(callable , int = 1000): float
{
= hrtime(true);
for ($i = 0; $i < $iterations; $i++) {
$fn();
}
return (hrtime(true) - $start) / 1e9;
}日志记录实践
php
<?php
declare(strict_types=1);
/**
* 简易日志记录器
*/
class SimpleLogger
{
private string $logFile;
private string $level = 'INFO';
public function __construct(string $logFile)
{
$this->logFile = $logFile;
}
public function info(string $message, array $context = []): void
{
$this->log('INFO', $message, $context);
}
public function warning(string $message, array $context = []): void
{
$this->log('WARNING', $message, $context);
}
public function error(string $message, array $context = []): void
{
$this->log('ERROR', $message, $context);
}
private function log(string $level, string $message, array $context): void
{
$timestamp = date('Y-m-d H:i:s');
$contextStr = $context ? ' ' . json_encode($context, JSON_UNESCAPED_UNICODE) : '';
$line = "[{$timestamp}] [{$level}] {$message}{$contextStr}\n";
file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX);
}
}配置与环境检测
php
<?php
declare(strict_types=1);
// 环境检测工具
class EnvironmentChecker
{
public static function checkRequirements(array $requirements): array
{
$results = [];
foreach ($requirements as $name => $check) {
$results[$name] = is_callable($check) ? $check() : false;
}
return $results;
}
public static function getSystemInfo(): array
{
return [
'php_version' => PHP_VERSION,
'os' => PHP_OS,
'sapi' => PHP_SAPI,
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'loaded_extensions' => get_loaded_extensions(),
];
}
}常见问题排查
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 连接超时 | 网络问题/配置错误 | 检查配置,增加超时时间 |
| 权限不足 | 文件/目录权限 | 使用 chmod/chown 修正 |
| 性能下降 | 索引缺失/数据量大 | 添加索引,优化查询 |
| 数据不一致 | 并发冲突/事务残留 | 使用锁机制和事务 |
| 内存溢出 | 大数据集/未释放资源 | 增大内存限制,分批处理 |
故障排除步骤
- 检查错误日志和异常信息
- 确认配置和环境是否正确
- 使用调试工具逐步排查
- 参考官方文档查找已知问题
版本兼容性说明
| 功能 | 最低版本 | 说明 |
|---|---|---|
| 基础功能 | PHP 8.1 | 本文档基准版本 |
| 只读属性 | PHP 8.1 | public readonly 修饰符 |
| 枚举类型 | PHP 8.1 | enum 类型和 match 表达式 |
| Fiber | PHP 8.1 | 协程/轻量级并发 |
| 命名参数 | PHP 8.0 | foo(arg_name: value) |
| 联合类型 | PHP 8.0 | `int |
| Null 安全运算符 | PHP 8.0 | $obj?->method() |
| 析构器 promotion | PHP 8.0 | __construct(public $x) |
php
<?php
declare(strict_types=1);
// 版本兼容性检测
function ensureVersion(string $minVersion): void
{
if (version_compare(PHP_VERSION, $minVersion, '<')) {
throw new RuntimeException(
sprintf('需要 PHP %s+, 当前版本: %s', $minVersion, PHP_VERSION)
);
}
}
ensureVersion('8.1.0');