Skip to content

进程控制

概述

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

常见信号

信号说明
SIGTERM15终止信号(优雅关闭)
SIGINT2中断信号(Ctrl+C)
SIGHUP1挂断信号
SIGUSR110用户自定义信号 1
SIGUSR212用户自定义信号 2
SIGCHLD17子进程状态变化
SIGKILL9强制终止(不可捕获)

平台限制

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 修正
性能下降索引缺失/数据量大添加索引,优化查询
数据不一致并发冲突/事务残留使用锁机制和事务
内存溢出大数据集/未释放资源增大内存限制,分批处理

故障排除步骤

  1. 检查错误日志和异常信息
  2. 确认配置和环境是否正确
  3. 使用调试工具逐步排查
  4. 参考官方文档查找已知问题

版本兼容性说明

功能最低版本说明
基础功能PHP 8.1本文档基准版本
只读属性PHP 8.1public readonly 修饰符
枚举类型PHP 8.1enum 类型和 match 表达式
FiberPHP 8.1协程/轻量级并发
命名参数PHP 8.0foo(arg_name: value)
联合类型PHP 8.0`int
Null 安全运算符PHP 8.0$obj?->method()
析构器 promotionPHP 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');

参考链接