进程控制
概述
PHP 的 PCNTL 扩展(Process Control)提供了 Unix 风格的进程控制功能。主要包括进程创建(fork)、信号处理(signal)、进程等待(wait)和定时器(alarm)。PCNTL 仅在 CLI/CGI 模式下可用,不支持 Web 环境。
适用场景
- 守护进程开发
- 多进程并行处理
- 信号驱动的任务
- 后台任务管理
基础概念
核心函数
| 函数 | 功能 |
|---|---|
pcntl_fork() | 创建子进程 |
pcntl_waitpid() | 等待子进程结束 |
pcntl_signal() | 注册信号处理器 |
pcntl_signal_dispatch() | 调度信号处理器 |
pcntl_alarm() | 设置定时器 |
pcntl_async_signals() | 启用异步信号 |
pcntl_sigprocmask() | 阻塞/解除信号 |
pcntl_siginfo() | 获取信号信息 |
pcntl_getpid() | 获取当前进程 ID |
pcntl_getppid() | 获取父进程 ID |
常见信号
| 信号 | 值 | 说明 |
|---|---|---|
SIGTERM | 15 | 终止信号(优雅关闭) |
SIGINT | 2 | 中断信号(Ctrl+C) |
SIGHUP | 1 | 挂断信号 |
SIGUSR1 | 10 | 用户自定义信号 1 |
SIGUSR2 | 12 | 用户自定义信号 2 |
SIGCHLD | 17 | 子进程状态变化 |
SIGKILL | 9 | 强制终止(不可捕获) |
平台限制
PCNTL 扩展仅在 Unix/Linux 系统上可用,不支持 Windows。
语法与代码示例
pcntl_fork 创建子进程
php
<?php
declare(strict_types=1);
$pid = pcntl_fork();
if ($pid === -1) {
die("fork 失败\n");
} elseif ($pid === 0) {
// 子进程
echo "子进程 PID: " . pcntl_getpid() . " (父进程: " . pcntl_getppid() . ")\n";
sleep(2);
echo "子进程完成\n";
exit(0);
} else {
// 父进程
echo "父进程 PID: " . pcntl_getpid() . " (子进程: {$pid})\n";
pcntl_waitpid($pid, $status); // 等待子进程
echo "子进程已退出,状态: {$status}\n";
}信号处理
php
<?php
declare(strict_types=1);
// PHP 7.1+ 异步信号
pcntl_async_signals(true);
$keepRunning = true;
// 注册 SIGINT 处理器(Ctrl+C)
pcntl_signal(SIGINT, function (int $signo) use (&$keepRunning) {
fwrite(STDOUT, "\n收到 SIGINT,正在优雅关闭...\n");
$keepRunning = false;
});
// 注册 SIGTERM 处理器
pcntl_signal(SIGTERM, function (int $signo) use (&$keepRunning) {
fwrite(STDOUT, "收到 SIGTERM,正在关闭...\n");
$keepRunning = false;
});
fwrite(STDOUT, "运行中... 按 Ctrl+C 停止\n");
$counter = 0;
while ($keepRunning) {
$counter++;
sleep(1);
fwrite(STDOUT, "计数: {$counter}\r");
}
fwrite(STDOUT, "\n已清理资源,安全退出\n");pcntl_alarm 定时器
php
<?php
declare(strict_types=1);
pcntl_async_signals(true);
// 设置 3 秒定时器
pcntl_alarm(3);
pcntl_signal(SIGALRM, function (int $signo) {
fwrite(STDOUT, "定时器到期!\n");
pcntl_alarm(3); // 重新设置定时器
});
fwrite(STDOUT, "每 3 秒触发一次定时器\n");
while (true) {
sleep(1);
}多进程并行处理
php
<?php
declare(strict_types=1);
class ProcessPool
{
private int $maxWorkers;
private int $completed = 0;
private int $totalTasks;
public function __construct(int $maxWorkers = 4)
{
$this->maxWorkers = $maxWorkers;
}
public function run(array $tasks): void
{
$this->totalTasks = count($tasks);
$queue = array_values($tasks);
$activeWorkers = 0;
pcntl_async_signals(true);
while (count($queue) > 0 || $activeWorkers > 0) {
// 创建新进程处理任务
while ($activeWorkers < $this->maxWorkers && count($queue) > 0) {
$task = array_shift($queue);
$activeWorkers++;
$this->spawnWorker($task);
}
// 等待子进程
$status = 0;
$pid = pcntl_waitpid(-1, $status, WNOHANG);
if ($pid > 0) {
$activeWorkers--;
$this->completed++;
$percent = (int)(($this->completed / $this->totalTasks) * 100);
fwrite(STDOUT, "进度: {$percent}% ({$this->completed}/{$this->totalTasks})\n");
}
usleep(10000); // 10ms
}
fwrite(STDOUT, "全部完成\n");
}
private function spawnWorker(mixed $task): void
{
$pid = pcntl_fork();
if ($pid === -1) {
fwrite(STDERR, "fork 失败\n");
return;
}
if ($pid === 0) {
// 子进程
try {
$this->processTask($task);
} catch (Throwable $e) {
fwrite(STDERR, "任务错误: {$e->getMessage()}\n");
}
exit(0);
}
}
private function processTask(mixed $task): void
{
fwrite(STDOUT, "进程 " . pcntl_getpid() . " 处理: " . json_encode($task) . "\n");
sleep(rand(1, 3)); // 模拟工作
}
}
// 使用
$tasks = range(1, 10);
$pool = new ProcessPool(4);
$pool->run($tasks);实战示例
守护进程
php
<?php
declare(strict_types=1);
class Daemon
{
public static function run(string $name, callable $worker): void
{
// 第一次 fork
$pid = pcntl_fork();
if ($pid < 0) die("Cannot fork\n");
if ($pid > 0) exit(0); // 父进程退出
// 创建新会话
posix_setsid();
// 第二次 fork
$pid = pcntl_fork();
if ($pid < 0) die("Cannot fork\n");
if ($pid > 0) exit(0);
// 切换工作目录
chdir('/');
// 重设文件权限掩码
umask(0);
// 关闭标准文件描述符
fclose(STDIN);
fclose(STDOUT);
fclose(STDERR);
// 重定向标准 I/O 到 /dev/null
$stdin = fopen('/dev/null', 'r');
$stdout = fopen('/dev/null', 'a');
$stderr = fopen('/var/log/' . $name . '.log', 'a');
// 信号处理
pcntl_async_signals(true);
$running = true;
pcntl_signal(SIGTERM, function () use (&$running) {
$running = false;
});
pcntl_signal(SIGINT, function () use (&$running) {
$running = false;
});
// 主循环
while ($running) {
pcntl_signal_dispatch();
try {
$worker();
} catch (Throwable $e) {
fwrite($stderr, "[ERROR] {$e->getMessage()}\n");
}
sleep(1);
}
// 清理
fclose($stdout);
fclose($stderr);
exit(0);
}
}
// 使用
Daemon::run('mydaemon', function () {
static $count = 0;
$count++;
// 定时任务逻辑
});注意事项
PCNTL 不可用于 Web 环境
php
<?php
// pcntl_fork() 在 Web 模式下虽然可以调用,但极其危险
// 可能导致服务器崩溃或安全漏洞
// 仅在 CLI 模式下使用
if (PHP_SAPI !== 'cli') {
die("此脚本只能在 CLI 模式下运行\n");
}僵尸进程
php
<?php
// 子进程退出后如果父进程不调用 wait,子进程变为僵尸进程
// 解决方案 1:父进程调用 pcntl_waitpid
pcntl_waitpid($pid, $status);
// 解决方案 2:忽略 SIGCHLD 信号
pcntl_signal(SIGCHLD, SIG_IGN);
// 解决方案 3:双 fork(孙子进程由 init 接管)最佳实践
1. 使用 Swoole 替代原生 PCNTL
php
<?php
// 对于生产环境的进程管理,推荐使用 Swoole
// Swoole 提供了协程、进程池、异步 I/O 等高级功能
// Swoole 进程池示例
// $pool = new Swoole\Process\Pool(4);
// $pool->on('workerStart', function ($pool, $workerId) {
// // 处理任务
// });
// $pool->start();2. 优雅关闭
php
<?php
pcntl_async_signals(true);
$shutdown = false;
pcntl_signal(SIGTERM, function () use (&$shutdown) { $shutdown = true; });
pcntl_signal(SIGINT, function () use (&$shutdown) { $shutdown = true; });
while (!$shutdown) {
pcntl_signal_dispatch();
// 处理任务
}
// 清理资源进阶用法
调试与测试技巧
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');