PHP exec() 执行 mono 命令时为何延迟约一分钟?
结论
exec() 和 system() 通常不会在执行 mono 前固定等待一分钟。同一个 PHP 脚本在命令行中运行正常,却只在浏览器访问时出现延迟,问题更可能出在 Web 请求环境中:当前 PHP 请求占用了某项资源,而 PayterPay.exe 也在等待这项资源,只能等到网络请求或锁在约 60 秒后超时,才继续执行。
常见原因有两种:
$url指向同一台 Web 服务器,但服务器没有空闲 worker 处理回调请求。- 当前页面开启了 PHP session,回调地址又使用相同的 session,因而一直等待 session 文件锁。
所以,单纯替换 exec() 或 system() 解决不了问题。需要解除 Web 请求和支付程序之间的循环等待。
为什么命令行执行正常
从终端执行命令时,没有正在运行的 HTTP 请求占用 PHP session、PHP-FPM worker 或单线程开发服务器。
从浏览器执行时,流程可能是:
浏览器请求 PHP
↓
PHP 占用 session 或 Web worker
↓
PHP 使用 exec() 启动 PayterPay.exe
↓
PayterPay.exe 访问 -u 指定的 URL
↓
该 URL 等待 session 锁或可用 worker
↓
exec() 又在等待 PayterPay.exe 结束
这样就形成了循环等待。大约一分钟后,其中一层超时,流程才能继续。
切换到 www-data 后再从命令行测试,即使运行正常,也不能排除上述问题。用户身份相同,不代表运行环境相同。Web 请求还会受到 PHP SAPI、session 锁、Web 服务器 worker、超时配置、环境变量和输出缓冲等因素影响。
首先释放 PHP session 锁
如果页面调用过 session_start(),应在执行外部程序前保存并关闭 session:
<?php
session_start();
// 读取或修改 session 数据
$userId = $_SESSION['user_id'] ?? null;
// 立即保存数据并释放 session 文件锁
session_write_close();
$command = sprintf(
"printf 'c' | mono %s -u %s -a %s -t %s",
escapeshellarg('/home/pi/Desktop/PayterPay/PayterPay.exe'),
escapeshellarg($url),
escapeshellarg((string) $price),
escapeshellarg((string) $timeout)
);
$output = [];
$returnCode = 0;
exec($command . ' 2>&1', $output, $returnCode);
if ($returnCode !== 0) {
throw new RuntimeException(
"Payment command failed with exit code {$returnCode}:\n" .
implode("\n", $output)
);
}
调用 session_write_close() 后,当前请求仍然可以读取已经加载到 $_SESSION 中的数据,但此后对 $_SESSION 的修改不会自动保存。如果还需要写入,可以重新调用 session_start(),但不要在支付命令运行期间再次持有 session 锁。
检查 Web 服务器是否能并发处理回调
如果 $url 指向当前应用,可以直接在服务器上测试:
curl -v --max-time 10 'https://example.com/payment/callback'
需要重点检查这些情况:
- 是否使用了 PHP 内置开发服务器
php -S。在某些配置下,它无法处理这类嵌套请求,生产环境不应依靠它完成支付流程。 - PHP-FPM 是否只有一个可用 worker,例如
pm.max_children = 1。 - Apache 或其他 Web 服务器的 worker 数量是否过少。
- 回调 URL 是否指向当前正在执行支付请求的同一路由。
- 回调接口是否也在等待当前支付流程完成。
应让另一个 worker 能够独立处理回调接口,或者把支付任务交给后台 worker,避免单个 HTTP 请求同步占用整个流程。
不要把参数直接拼入 Shell 命令
如果原示例中的 $url、$price 和 $timeout 来自请求参数,直接拼接会带来命令注入风险。至少要使用 escapeshellarg(),同时验证数值范围:
<?php
$url = filter_input(INPUT_POST, 'url', FILTER_VALIDATE_URL);
$price = filter_input(INPUT_POST, 'price', FILTER_VALIDATE_INT);
$timeout = filter_input(INPUT_POST, 'timeout', FILTER_VALIDATE_INT);
if (
$url === false ||
$price === false ||
$timeout === false ||
$price < 1 ||
$timeout < 1
) {
throw new InvalidArgumentException('Invalid payment parameters');
}
session_write_close();
$executable = '/home/pi/Desktop/PayterPay/PayterPay.exe';
$command = sprintf(
"printf 'c' | mono %s -u %s -a %d -t %d",
escapeshellarg($executable),
escapeshellarg($url),
$price,
$timeout
);
exec($command . ' 2>&1', $output, $returnCode);
即使 $url 看起来只是普通网址,也可能包含 &、;、空格或引号,不能在未转义的情况下直接插入命令。
记录标准错误,找出真正等待的位置
exec() 默认只收集标准输出,而 Mono 程序、Shell 或网络库可能把错误写入标准错误。排查时应加入:
exec($command . ' 2>&1', $output, $returnCode);
error_log('Command: ' . $command);
error_log('Exit code: ' . $returnCode);
error_log("Output:\n" . implode("\n", $output));
还可以记录执行前后的时间:
$startedAt = microtime(true);
exec($command . ' 2>&1', $output, $returnCode);
$elapsed = microtime(true) - $startedAt;
error_log(sprintf('Payment command took %.3f seconds', $elapsed));
如果 Mono 程序支持分阶段日志,可以分别记录这些时间点:
- 进程启动;
- 开始访问
$url; - URL 请求返回;
- 向支付终端发送请求;
- 收到支付结果;
- 等待确认字符;
- 程序退出。
这些日志可以帮助确认一分钟究竟耗在 URL 请求、支付终端通信,还是标准输入处理上。
exec()、system() 与终端执行的区别
exec() 和 system() 都是同步调用,PHP 会一直等待外部进程退出。
它们的主要区别在于输出处理:
exec()会在命令结束后返回最后一行输出,也可以通过数组取得完整输出。system()会将输出直接发送到 PHP 输出流。- 两者都不会自动提供交互式终端。
- 浏览器无法直接与子进程的标准输入交互。
- Web 请求通常还会受到 PHP、PHP-FPM、反向代理和浏览器连接超时的限制。
下面这条命令:
printf 'c' | mono ...
会在程序启动时立即将字符 c 写入标准输入,然后关闭管道,并不会等终端显示 APPROVED 后再发送。如果程序稍后才读取标准输入,字符会留在管道缓冲区中,所以这种写法可能看起来能够正常工作。但如果协议要求收到批准后才能确认,它就无法严格保证时序。
还要确认程序读取的是单个字符还是一整行。如果它通过类似 Console.ReadLine() 的方式读取,应发送换行:
printf 'c\n' | mono ...
更稳妥的结构
支付流程通常不适合完整阻塞在一次浏览器请求中。更可靠的处理方式是:
- HTTP 请求创建支付任务并立即返回任务 ID。
- 独立后台 worker 执行 Mono 程序。
- 支付结果写入数据库或队列。
- 浏览器通过轮询、SSE 或 WebSocket 获取状态。
- 支付回调由独立接口处理,不依赖发起请求所持有的 session。
如果暂时仍需同步执行,至少要做到:
- 执行命令前调用
session_write_close(); - 保证 Web 服务器有多个可用 worker;
- 确认
$url在 Web 运行环境中可以快速访问; - 捕获
stderr和退出码; - 验证所有参数并进行 Shell 转义;
- 协调 PHP、Web 服务器和支付程序的 timeout 设置。
结合现有现象,建议先检查 session 锁和回调 URL 的并发处理能力。这两项最符合“命令行正常、浏览器延迟约一分钟、切换为 www-data 后命令行仍然正常”这一组表现。
备注:内容仅供参考。
