信号处理
概述
信号(Signal)是 Unix/Linux 操作系统提供的一种进程间通信机制,用于通知进程发生了特定事件。在 PHP CLI 守护进程或长运行脚本中,信号处理至关重要 -- 它允许程序优雅地响应终止信号、定时器信号等外部事件,确保资源的正确释放和状态的一致性。
PHP 版本要求
PHP 8.1+ 中 pcntl_async_signals(true) 使得信号处理更加可靠。PHP 8.1 引入了 Fiber,需要注意信号与 Fiber 的交互。本文基于 PHP 8.1+ 编写。
基础概念
什么是信号
信号是操作系统发送给进程的异步通知。每个信号都有一个编号和名称,常见信号包括:
| 信号 | 编号 | 说明 | 默认行为 |
|---|---|---|---|
SIGINT | 2 | 中断信号(Ctrl+C) | 终止进程 |
SIGTERM | 15 | 终止请求 | 终止进程 |
SIGHUP | 1 | 终端断开 | 终止进程 |
SIGUSR1 | 10 | 用户自定义信号 1 | 终止进程 |
SIGUSR2 | 12 | 用户自定义信号 2 | 终止进程 |
SIGALRM | 14 | 闹钟超时 | 终止进程 |
SIGCHLD | 17 | 子进程状态变化 | 忽略 |
PHP 信号处理模型
PHP 的信号处理有两种模式:
- 同步模式(PHP 7.1 之前):信号仅在执行引擎的特定检查点被处理
- 异步模式(PHP 7.1+):通过
pcntl_async_signals(true)启用,信号会中断当前的 PHP 代码执行
安装与配置
bash
# pcntl 扩展通常随 PHP 一起编译
php -m | grep pcntl
# 如果未安装(编译安装时添加)
./configure --enable-pcntl
# POSIX 扩展(信号相关常量)
php -m | grep posix详细说明
pcntl_signal 信号注册
php
<?php
declare(strict_types=1);
/**
* 基本信号处理
*/
class SignalHandler
{
private bool $shouldStop = false;
public function register(): void
{
// 启用异步信号处理
pcntl_async_signals(true);
// 注册 SIGINT 处理(Ctrl+C)
pcntl_signal(SIGINT, function (int $signo): void {
echo PHP_EOL . "收到 SIGINT ({$signo}),准备优雅退出..." . PHP_EOL;
$this->shouldStop = true;
});
// 注册 SIGTERM 处理
pcntl_signal(SIGTERM, function (int $signo): void {
echo "收到 SIGTERM ({$signo}),准备优雅退出..." . PHP_EOL;
$this->shouldStop = true;
});
// 注册 SIGHUP 处理(重新加载配置)
pcntl_signal(SIGHUP, function (int $signo): void {
echo "收到 SIGHUP ({$signo}),重新加载配置..." . PHP_EOL;
$this->reloadConfig();
});
// 注册 SIGUSR1 处理(自定义功能,如状态输出)
pcntl_signal(SIGUSR1, [$this, 'handleSigusr1']);
}
public function run(): void
{
$this->register();
echo "守护进程启动,PID: " . getmypid() . PHP_EOL;
echo "发送 SIGINT/SIGTERM 停止 | SIGHUP 重载配置 | SIGUSR1 状态输出" . PHP_EOL;
while (!$this->shouldStop) {
// 执行业务逻辑
$this->doWork();
// pcntl_signal_dispatch() 在异步模式下通常不需要
// 但在某些场景下仍有用
pcntl_signal_dispatch();
usleep(100000); // 100ms
}
$this->cleanup();
echo "进程优雅退出" . PHP_EOL;
}
private function doWork(): void
{
// 模拟工作
static $counter = 0;
$counter++;
if ($counter % 50 === 0) {
echo ".";
}
}
private function reloadConfig(): void
{
echo "配置已重新加载" . PHP_EOL;
}
private function cleanup(): void
{
echo "清理资源..." . PHP_EOL;
}
public function handleSigusr1(int $signo): void
{
echo PHP_EOL . "SIGUSR1: 状态报告" . PHP_EOL;
echo " 内存使用: " . round(memory_get_usage() / 1024 / 1024, 2) . "MB" . PHP_EOL;
echo " 运行时间: " . (time() - ($_SERVER['REQUEST_TIME'] ?? time())) . "秒" . PHP_EOL;
}
}
if (PHP_SAPI === 'cli') {
$handler = new SignalHandler();
$handler->run();
}pcntl_sigprocmask 信号屏蔽
php
<?php
declare(strict_types=1);
/**
* 信号屏蔽与批量处理
* 在关键操作期间屏蔽信号,操作完成后批量处理
*/
class SignalMaskManager
{
/**
* 在关键操作期间屏蔽信号
*/
public function criticalSection(): void
{
// 屏蔽 SIGINT 和 SIGTERM
pcntl_sigprocmask(SIG_BLOCK, [SIGINT, SIGTERM]);
echo "进入临界区..." . PHP_EOL;
try {
// 执行不能被中断的操作
$this->atomicOperation();
} finally {
// 解除屏蔽
pcntl_sigprocmask(SIG_UNBLOCK, [SIGINT, SIGTERM]);
}
echo "退出临界区" . PHP_EOL;
}
/**
* 收集被屏蔽期间的信号并处理
*/
public function processPendingSignals(): void
{
// 获取待处理的信号
$pending = [];
pcntl_sigpending($pending);
if (!empty($pending)) {
echo "待处理信号: " . implode(', ', $pending) . PHP_EOL;
}
}
/**
* 设置信号屏蔽集并等待
*/
public function waitForSignals(): void
{
// 仅等待 SIGUSR1
pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1]);
echo "等待 SIGUSR1 信号... (PID: " . getmypid() . ")" . PHP_EOL;
echo "发送: kill -USR1 " . getmypid() . PHP_EOL;
// 阻塞等待信号
pcntl_sigwaitinfo([SIGUSR1], $info);
echo "收到信号: {$info['signo']}" . PHP_EOL;
// 解除屏蔽
pcntl_sigprocmask(SIG_UNBLOCK, [SIGUSR1]);
}
private function atomicOperation(): void
{
echo "执行原子操作..." . PHP_EOL;
sleep(3);
echo "原子操作完成" . PHP_EOL;
}
}
// CLI 使用
if (PHP_SAPI === 'cli' && isset($argv[1]) && $argv[1] === 'wait') {
pcntl_async_signals(true);
$manager = new SignalMaskManager();
$manager->waitForSignals();
}pcntl_async_signals 异步模式
php
<?php
declare(strict_types=1);
/**
* 异步信号 vs 同步信号对比
*/
/**
* 同步模式(PHP 7.1 之前的行为)
* 信号仅在引擎检查点处理
*/
function syncModeExample(): void
{
pcntl_async_signals(false);
pcntl_signal(SIGUSR1, function (int $signo): void {
echo "同步模式: 收到 SIGUSR1" . PHP_EOL;
});
echo "同步模式: 等待 5 秒..." . PHP_EOL;
sleep(5); // 信号可能在 sleep 期间被处理
echo "同步模式: 结束" . PHP_EOL;
}
/**
* 异步模式(PHP 7.1+)
* 信号立即中断当前执行
*/
function asyncModeExample(): void
{
pcntl_async_signals(true);
pcntl_signal(SIGALRM, function (int $signo): void {
echo "异步模式: 闹钟触发!" . PHP_EOL;
});
echo "异步模式: 设置 2 秒闹钟..." . PHP_EOL;
pcntl_alarm(2);
// 执行耗时操作(会被信号中断)
echo "异步模式: 开始计数..." . PHP_EOL;
for ($i = 0; $i < 10; $i++) {
echo " {$i}" . PHP_EOL;
sleep(1);
}
echo "异步模式: 结束" . PHP_EOL;
}定时器信号 SIGALRM
php
<?php
declare(strict_types=1);
/**
* 基于 SIGALRM 的定时器
*/
class SignalTimer
{
private int $tickCount = 0;
private int $interval;
public function __construct(int $intervalSeconds = 1)
{
$this->interval = $intervalSeconds;
}
/**
* 启动定时器
*/
public function start(): void
{
pcntl_async_signals(true);
pcntl_signal(SIGALRM, function (): void {
$this->tickCount++;
echo "[定时器] tick #{$this->tickCount} | "
. date('Y-m-d H:i:s') . " | "
. "内存: " . round(memory_get_usage() / 1024, 2) . "KB" . PHP_EOL;
});
pcntl_alarm($this->interval);
}
/**
* 使用多闹钟实现不同间隔
*/
public function startMultiAlarm(): void
{
pcntl_async_signals(true);
// 每秒触发
pcntl_signal(SIGALRM, function (): void {
echo "[" . date('H:i:s') . "] 1秒定时器" . PHP_EOL;
});
pcntl_alarm(1);
// SIGUSR1 作为 5 秒定时器(需要外部进程发送或使用 pcntl_alarm 重置)
pcntl_signal(SIGUSR1, function (): void {
echo "[" . date('H:i:s') . "] 5秒定时器(模拟)" . PHP_EOL;
});
}
/**
* 心跳监控模式
*/
public function heartbeat(): void
{
$lastTick = time();
pcntl_async_signals(true);
pcntl_signal(SIGALRM, function () use (&$lastTick): void {
$now = time();
$gap = $now - $lastTick;
if ($gap > 2) {
echo "[警告] 心跳延迟 {$gap} 秒!" . PHP_EOL;
} else {
echo "[心跳] 正常 (间隔 {$gap}s)" . PHP_EOL;
}
$lastTick = $now;
pcntl_alarm(1); // 重新设置闹钟
});
pcntl_alarm(1);
}
}实战示例
优雅退出的守护进程
php
<?php
declare(strict_types=1);
/**
* 生产级守护进程 - 完整的信号处理
*/
class DaemonProcess
{
private bool $running = true;
private int $startTime;
private int $processedJobs = 0;
private \PDO $db;
public function __construct(private string $name = 'worker')
{
$this->startTime = time();
}
public function start(): void
{
$this->setupSignalHandlers();
$this->daemonize();
$this->runMainLoop();
}
private function setupSignalHandlers(): void
{
pcntl_async_signals(true);
// SIGTERM/SIGINT: 优雅退出
$stopHandler = function (int $signo): void {
$signalName = match ($signo) {
SIGTERM => 'SIGTERM',
SIGINT => 'SIGINT',
default => "信号{$signo}",
};
echo "[{$this->name}] 收到 {$signalName},开始优雅关闭..." . PHP_EOL;
$this->running = false;
};
pcntl_signal(SIGTERM, $stopHandler);
pcntl_signal(SIGINT, $stopHandler);
// SIGHUP: 重新加载配置
pcntl_signal(SIGHUP, function (): void {
echo "[{$this->name}] 收到 SIGHUP,重新加载配置" . PHP_EOL;
// 重新加载配置逻辑
});
// SIGUSR1: 输出状态信息
pcntl_signal(SIGUSR1, function (): void {
$uptime = time() - $this->startTime;
echo "[{$this->name}] 状态报告:" . PHP_EOL;
echo " PID: " . getmypid() . PHP_EOL;
echo " 运行时间: {$uptime}秒" . PHP_EOL;
echo " 处理任务: {$this->processedJobs}" . PHP_EOL;
echo " 内存: " . round(memory_get_usage() / 1024 / 1024, 2) . "MB" . PHP_EOL;
echo " 峰值内存: " . round(memory_get_peak_usage() / 1024 / 1024, 2) . "MB" . PHP_EOL;
});
// SIGUSR2: 输出堆栈
pcntl_signal(SIGUSR2, function (): void {
echo "[{$this->name}] 调用堆栈:" . PHP_EOL;
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
foreach (array_slice($trace, 1, 10) as $frame) {
echo " {$frame['file']}:{$frame['line']} {$frame['function']}()" . PHP_EOL;
}
});
}
private function daemonize(): void
{
$pid = pcntl_fork();
if ($pid < 0) {
throw new RuntimeException('Fork 失败');
}
if ($pid > 0) {
exit(0); // 父进程退出
}
// 子进程成为新会话的领导者
if (posix_setsid() < 0) {
throw new RuntimeException('setsid 失败');
}
// 重设文件创建掩码
umask(0);
// 关闭标准 I/O
fclose(STDIN);
fclose(STDOUT);
fclose(STDERR);
// 重新打开标准 I/O 到 /dev/null
$stdin = fopen('/dev/null', 'r');
$stdout = fopen('/dev/null', 'w');
$stderr = fopen('/var/log/' . $this->name . '.log', 'a');
}
private function runMainLoop(): void
{
while ($this->running) {
$this->processJob();
usleep(100000); // 100ms
}
$this->gracefulShutdown();
}
private function processJob(): void
{
// 模拟处理任务
$this->processedJobs++;
}
private function gracefulShutdown(): void
{
echo "[{$this->name}] 清理资源..." . PHP_EOL;
// 关闭数据库连接
// 释放锁
// 写入状态文件
echo "[{$this->name}] 优雅退出完成" . PHP_EOL;
}
}
// 使用示例
if (PHP_SAPI === 'cli' && ($argv[1] ?? '') === 'start') {
$daemon = new DaemonProcess('queue-worker');
$daemon->start();
}信号与中断处理
php
<?php
declare(strict_types=1);
/**
* 信号中断长操作
* 展示信号如何在长时间运行的操作中被处理
*/
class InterruptibleTask
{
private bool $interrupted = false;
/**
* 可中断的长循环
*/
public function longRunningTask(int $totalSteps): int
{
pcntl_async_signals(true);
pcntl_signal(SIGINT, function (): void {
$this->interrupted = true;
});
$completedSteps = 0;
for ($i = 0; $i < $totalSteps; $i++) {
if ($this->interrupted) {
echo "任务被中断(完成 {$completedSteps}/{$totalSteps})" . PHP_EOL;
break;
}
// 模拟计算
$result = 0;
for ($j = 0; $j < 10000; $j++) {
$result += $j;
}
$completedSteps++;
if ($completedSteps % 1000 === 0) {
echo "进度: {$completedSteps}/{$totalSteps} (" .
round($completedSteps / $totalSteps * 100, 1) . "%)" . PHP_EOL;
}
}
return $completedSteps;
}
}
// 使用示例
if (PHP_SAPI === 'cli') {
$task = new InterruptibleTask();
echo "开始长任务(Ctrl+C 中断)..." . PHP_EOL;
$completed = $task->longRunningTask(100000);
echo "已完成: {$completed} 步" . PHP_EOL;
}注意事项
信号安全的操作
并非所有操作在信号处理器中都是安全的。信号处理器中应避免:
- I/O 操作:不安全的(可能引起死锁)
- 内存分配:不安全的(可能触发内存管理器死锁)
- 调用非可重入函数:不安全的
- 抛出异常:PHP 7.1+ 中信号处理器抛出异常可能导致不可预期行为
php
<?php
declare(strict_types=1);
// 信号处理器中只应做最简单的操作
pcntl_signal(SIGTERM, function (int $signo): void {
// 好的做法:仅设置标志
global $shouldStop;
$shouldStop = true;
});
// 不好:在信号处理器中执行复杂操作
// pcntl_signal(SIGTERM, function (int $signo): void {
// $this->closeDatabase(); // 不安全
// $this->writeLog(); // 不安全
// echo "bye"; // 相对安全但不推荐
// });PHP-FPM 中的信号限制
Web 模式下的限制
在 PHP-FPM / Apache 模块中,信号处理受到严重限制。pcntl_* 函数仅在 CLI / CGI 模式下可用。Web 请求中不应使用信号处理。
最佳实践
1. 守护进程模板
php
<?php
declare(strict_types=1);
// 1. 启用异步信号
pcntl_async_signals(true);
// 2. 注册信号处理器
$shutdown = false;
$reload = false;
pcntl_signal(SIGTERM, fn() => $shutdown = true);
pcntl_signal(SIGINT, fn() => $shutdown = true);
pcntl_signal(SIGHUP, fn() => $reload = true);
// 3. 主循环
while (!$shutdown) {
if ($reload) {
// 重新加载配置
$reload = false;
}
// 执行任务
pcntl_signal_dispatch();
usleep(100000);
}
// 4. 优雅退出
// cleanup...2. 使用状态文件
守护进程应将其 PID 写入文件,方便管理和发送信号:
php
<?php
declare(strict_types=1);
// 写入 PID 文件
file_put_contents('/var/run/mydaemon.pid', (string) getmypid());
// 管理脚本
// kill -TERM $(cat /var/run/mydaemon.pid)
// kill -HUP $(cat /var/run/mydaemon.pid)
// kill -USR1 $(cat /var/run/mydaemon.pid)下一节
继续学习:共享内存与信号量