Program execution

简单的命令执行函数

这些函数通常用于执行一个外部程序并获取其输出。

exec

$command   = 'ls -l'; // 在 Linux/macOS 上列出当前目录文件
$last_line = exec($command, $output, $return_var);
echo "最后一行输出: " . $last_line . PHP_EOL;
echo "--- 完整输出 (数组形式) ---" . PHP_EOL;
var_export($output);
echo "退出状态码: " . $return_var . PHP_EOL;
/*
最后一行输出: t3.php
--- 完整输出 (数组形式) ---
array (
  0 => 't1.php',
  1 => 't2.php',
  2 => 't3.php',
)
*/

system

// 运行一个简单的 ping 命令,并直接显示结果
echo "开始执行 ping..." . PHP_EOL;
$last_line = system('ping -c 3 127.0.0.1', $return_var);
echo PHP_EOL . "--- 命令结束 ---" . PHP_EOL;
echo "最后一行输出 (通常是 ping 的总结): " . $last_line . "" . PHP_EOL;
echo "退出状态码: " . $return_var . "" . PHP_EOL;
/*
开始执行 ping...
PING 127.0.0.1 (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.068 ms
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.083 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.119 ms
*/

passthru

快记忆(通过调和乳)

header('Content-Type: image/jpeg');  
header('Content-Disposition: inline; filename="generated_image.jpg"');  
// 直接将脚本生成的原始 JPEG 数据流输出到浏览器  
passthru('/usr/bin/generate_image.sh --format jpeg');  
// PHP 不会尝试将图像数据存储到变量中

shell_exec(或反引号 ` 操作符)

// 读取一个文件的内容,并将其存储在一个字符串变量中
// $file_content = shell_exec('cat /etc/hosts');
$file_content = `cat /etc/hosts`;
if ($file_content === null) {
    echo "命令执行失败或被禁用。" . PHP_EOL;
} else {
    echo "--- hosts 文件内容 ---" . PHP_EOL;
    echo $file_content;
}
/*
--- hosts 文件内容 ---
127.0.0.1 host.docker.internal
*/

高级/精细控制的进程管理

需要更复杂的控制,例如与外部程序进行双向通信(输入/输出)、控制其生命周期或获取更详细的进程信息,应该使用 proc_open()

proc_open

PHP: proc_open - Manual

参数

1. command (命令行)

2. descriptor_spec (描述符规格)

3. pipes (管道句柄)

4. cwd (工作目录)

5. env_vars (环境变量)

$descriptorSpec = [
    0 => ["pipe", "r"],  // 标准输入,子进程从此管道中读取数据
    1 => ["pipe", "w"],  // 标准输出,子进程向此管道中写入数据
    2 => ["file", "/tmp/error-output.txt", "a"], // 标准错误,写入到一个文件
];
// 若子进程需要拿到父进程的输入输出句柄,则通过以下设置
// $descriptor_spec = [
//     1 => STDOUT, // 继承父进程的 STDOUT 文件描述符
//     2 => STDERR  // 继承父进程的 STDERR 文件描述符
// ];
$cwd            = '/tmp';
$env            = ['some_option' => 'aeiou'];
$process        = proc_open('php', $descriptorSpec, $pipes, $cwd, $env);
if (is_resource($process)) {
    // $pipes 现在看起来是这样的:
    // 0 => 可以向子进程标准输入写入的句柄
    // 1 => 可以从子进程标准输出读取的句柄
    // 错误输出将被追加到文件 /tmp/error-output.txt
    fwrite($pipes[0], '<?php print_r($_ENV); ?>');
    echo stream_get_contents($pipes[1]);
    // 切记:在调用 proc_close 之前关闭所有的管道以避免死锁。
    foreach ($pipes as $pipe) {
	    fclose($pipe);
    }
    $return_value = proc_close($process);
    echo "command returned $return_value\n"; // command returned 127
}

关闭顺序建议:

  1. stdin 管道 - 通知进程没有更多输入
  2. 发送终止信号 - proc_terminate
  3. stdout/stderr 管道 - 读取可能的最后输出
  4. 进程句柄 - proc_close
$process = proc_open(...);
// ... 使用进程
proc_terminate($process); // 关闭子进程
foreach ($pipes as $pipe) fclose($pipe); // 管道
proc_close($process); // 关闭进程句柄

注意事项

概念 解释
问题核心 父进程退出导致子进程的管道被关闭,当子进程尝试向已关闭的管道写入时,会收到 SIGPIPE 信号而被终止
缓冲区大小 多数现代系统默认为 64 KB
后果 缓冲区满时,子进程的 echo (写入操作) 被 阻塞 (Block),导致子进程挂起 (Hang),无法继续执行代码(即使代码是无限循环)。
触发条件 使用 proc_open 并通过 ["pipe", "w"] 捕获子进程 STDOUT/STDERR,而父进程未读取相应管道。
解决方案 持续读取子进程输出: 父进程必须定期且以非阻塞模式 (stream_set_blocking) 从管道中读取数据。

详细解析

  1. 子进程向 stdout/stderr 写入数据
  2. 数据进入管道缓冲区
  3. 当缓冲区满时,子进程的 write() 系统调用会被阻塞
  4. 如果此时父进程关闭了管道读取端,子进程才会收到 SIGPIPE
    • 阻塞:父进程还在,只是不读取 → 子进程在缓冲区满时被挂起
    • SIGPIPE:父进程关闭了管道 → 子进程收到信号被终止

实例

主进程

$dir     = __DIR__;
$pid = posix_getpid();
$command = "php $dir/child.php --pid=$pid";
// 定义 I/O 规范,不需要 STDIN (0)
$spec = [
    0 => ['pipe', 'r'], // STDIN
    1 => ['pipe', 'w'], // STDOUT
    2 => ['pipe', 'w'], // STDERR
];
// 启动进程,exec 已被移除,减少不必要的 shell 替换
$resource = proc_open($command, $spec, $pipes);
if (!is_resource($resource)) {
    die("无法启动子进程\n");
}
// 管道句柄
$stdin = $pipes[0];
$stdout = $pipes[1];
$stderr = $pipes[2];
// 设置管道为非阻塞模式
stream_set_blocking($stdout, 0);
stream_set_blocking($stderr, 0);
// 循环直到子进程退出
while (true) {
    $read   = [$stdout, $stderr];
    $write  = [$stdin];
    $except = null;
    // 使用 stream_select 等待 I/O 事件,超时设为 1 秒 (更高效的等待)
    if (stream_select($read, $write, $except, 1) > 0) {
        // 读取 STDOUT
        if (in_array($stdout, $read)) {
            echo "子进程输出: " . stream_get_contents($stdout);
        }
        // 读取 STDERR
        if (in_array($stderr, $read)) {
            echo "子进程错误: " . stream_get_contents($stderr);
        }
    }
    // 检查子进程状态并判断退出
    $status = proc_get_status($resource);
    if (!$status['running']) {
        // 关闭所有资源
        fclose($stdout);
        fclose($stderr);
        proc_close($resource);
        echo "子进程退出,状态码: {$status['exitcode']}\n";
        break;
    }
}

子进程

// 解析 --pid 参数  
$pid = getopt("", ["pid:"])['pid'] ?? 0;  
$myPid = posix_getpid();  
// 循环运行并输出  
while (true) {  
    sleep(1);  
    // 简化输出内容  
    echo "Run P:{$myPid} PP:{$pid}\n";  
}

pcntl_fork()

PCNTL 扩展主要用于类 Unix 环境(Linux, macOS),提供了对进程创建和信号处理的更底层控制。

pcntl_exec()

Swoole\Process

// 必须启用 Swoole 扩展
$process = new Swoole\Process(function (Swoole\Process $worker) {
    // 【子进程】: 读取父进程发送的数据,并响应
    $data = $worker->pop();
    $worker->write("Pong: $data");
    exit(0);
}, true); // true 启用管道
$process->start();
// 【父进程】: 发送数据给子进程
$process->push("Ping");
// 【父进程】: 读取子进程的响应
$response = $process->read();
echo "父进程收到响应: " . $response . "\n";
// 回收子进程资源
Swoole\Process::wait(true);

parallel\Runtime

// 必须启用 parallel 扩展
use parallel\Runtime;
// 1. 启动运行时环境 (Worker)
$runtime = new Runtime();
// 2. 提交一个异步任务 (闭包)
$future = $runtime->run(function () {
    // 异步工作者执行的代码
    sleep(1); 
    return "异步任务完成!";
});
echo "主线程未被阻塞,继续执行...\n";
// 3. 阻塞并获取异步任务的结果
$result = $future->value();
echo "异步任务结果: " . $result . "\n";
$runtime->close();

总结

方法 主要用途 返回值/输出方式 适用场景
exec() 执行命令 返回最后一行输出,所有输出可选传入数组 简单的单行命令,需要获取特定输出
system() 执行命令并显示输出 直接输出到标准输出 (stdout) 需要实时显示简单命令的输出
passthru() 执行命令并输出原始数据 直接输出原始数据 处理二进制数据,避免 PHP 缓冲大数据,节省内存和时间。
shell_exec()、`(反引号) 通过 shell 执行命令 返回完整的输出字符串 将完整的命令执行结果集中在一个变量中,方便后续处理。
proc_open() 高级进程管理 返回文件指针 (pipes) 需要双向通信精细控制进程生命周期
pcntl_fork() 创建子进程 返回子进程 ID 专门用于类 Unix 系统,创建守护进程多任务处理
Swoole\Process 高性能多进程管理 返回 Swoole\Process 实例 Worker进程池、实现高效的进程间通信 (IPC)、后台任务处理
parallel\Runtime 异步并行计算 返回 parallel\Future 对象 执行CPU密集型任务、阻塞 I/O 任务并行、简单安全的并行计算