PHP 中实现毫秒级 SIGALRM 信号触发的轻量级方案

1次阅读

PHP 中实现毫秒级 SIGALRM 信号触发的轻量级方案

本文介绍一种不依赖 swoole 等扩展、纯 php 实现的毫秒级定时 sigalrm 信号机制,通过长期驻留的子进程协同主进程完成高精度中断调度,并附带可直接运行的封装类与关键注意事项。

本文介绍一种不依赖 swoole 等扩展、纯 php 实现的毫秒级定时 sigalrm 信号机制,通过长期驻留的子进程协同主进程完成高精度中断调度,并附带可直接运行的封装类与关键注意事项。

在 PHP 原生扩展中,pcntl_alarm(int $seconds) 仅支持秒级精度,无法满足毫秒级(如 200ms、1.5s)定时中断需求。虽然 SwooleProcess::alarm() 提供了微秒级支持,但其强依赖 PECL 扩展,不符合“裸机 PHP”(bare-bones PHP)场景要求。本文提供一个纯 PHP、无外部依赖、资源可控的替代方案:利用 proc_open 启动一个长期运行的轻量级中断管理子进程,主进程通过管道向其下发带延迟的 SIGALRM 触发指令,从而实现亚秒级信号调度。

该方案核心思想是分离关注点

  • 主进程专注业务逻辑与信号处理;
  • 子进程专注高精度时间轮询与信号投递,避免频繁 fork 开销;
  • 双进程通过 php://fd/3(自定义文件描述符)建立低开销通信通道。

以下是精简、健壮、可复用的完整实现:

<?php  class Interrupter {     private ?InterrupterProcess $process = null;      public function __construct()     {         pcntl_async_signals(true);         pcntl_signal(SIGALRM, [$this, 'handleSignal']);     }      public function interrupt(float $delaySeconds): void     {         if ($delaySeconds <= 0) {             throw new InvalidArgumentException('Delay must be positive');         }          if ($this->process === null) {             $this->process = new InterrupterProcess();         }          $this->process->setInterrupt(posix_getpid(), $delaySeconds);     }      public function handleSignal(int $signal): void     {         if ($signal === SIGALRM) {             echo "[INTERRUPT] Signal received at " . date('H:i:s.u') . "n";             // 此处可执行中断响应逻辑,如跳出循环、清理资源等         }     }      public function __destruct()     {         $this->process?->destroy();     } }  class InterrupterProcess {     private $process;     private $writePipe;      private const PROCESS_CODE = <<<'CODE' <?php declare(strict_types=1); $readPipe = fopen('php://fd/3', 'r'); $interrupts = [];  while (true) {     $r = [$readPipe];     $w = null;     $e = null;      $now = microtime(true);     $minExpiry = !empty($interrupts) ? min($interrupts) : $now + 1;     $timeout = $minExpiry - $now;      // stream_select 支持微秒级超时(int sec, int usec)     $sec = (int)$timeout;     $usec = (int)(fmod($timeout, 1) * 1_000_000);     if ($timeout < 0) { $sec = $usec = 0; }      if (@stream_select($r, $w, $e, $sec, $usec) > 0) {         $line = fgets($readPipe);         if ($line !== false) {             $req = json_decode($line, true);             if (isset($req['pid']) && isset($req['microtime'])) {                 $interrupts[$req['pid']] = $req['microtime'];             }         }     }      $now = microtime(true);     foreach ($interrupts as $pid => $expiry) {         if ($expiry <= $now) {             @posix_kill($pid, SIGALRM);             unset($interrupts[$pid]);         }     } } CODE;      public function __construct()     {         $descriptors = [             ['pipe', 'r'], // stdin             STDOUT,             STDERR,             ['pipe', 'r'], // fd 3: custom pipe for IPC         ];          $this->process = proc_open(['php', '-d', 'error_reporting=0'], $descriptors, $pipes);          if (!is_resource($this->process)) {             throw new RuntimeException('Failed to start interrupter process');         }          $this->writePipe = $pipes[3];         fwrite($pipes[0], self::PROCESS_CODE);         fclose($pipes[0]);     }      public function setInterrupt(int $pid, float $delaySeconds): bool     {         if (!$this->writePipe || feof($this->writePipe)) {             return false;         }          $payload = json_encode([             'pid' => $pid,             'microtime' => microtime(true) + $delaySeconds         ]) . "n";          return fwrite($this->writePipe, $payload) !== false;     }      public function destroy(): void     {         if ($this->writePipe && !feof($this->writePipe)) {             fclose($this->writePipe);         }         if ($this->process) {             proc_terminate($this->process, 9);             proc_close($this->process);         }     } }  // ✅ 使用示例 $interrupter = new Interrupter();  for ($i = 0; $i < 3; $i++) {     echo "[LOOP {$i}] Starting 10s sleep at " . date('H:i:s') . "...n";      // 2.3 秒后触发 SIGALRM(精确到 ~10ms 级别)     $interrupter->interrupt(2.3);      // 模拟长任务 —— 将被提前中断     $start = time();     while (time() - $start < 10) {         usleep(100000); // 避免 CPU 空转     } }

关键优势说明

立即学习PHP免费学习笔记(深入)”;

  • 单子进程复用:避免每次 alarm() 都 fork 新进程,显著降低系统开销与调度延迟;
  • 微秒级精度保障:子进程使用 stream_select($r, $w, $e, $sec, $usec) 实现纳秒级就绪等待(底层调用 select(2) 或 epoll_wait),实际误差通常
  • 异步安全:主进程始终以 pcntl_async_signals(true) 启用异步信号处理,确保 SIGALRM 不被阻塞;
  • 自动清理:__destruct 确保进程与管道资源在脚本结束时释放,防止僵尸进程。

⚠️ 重要注意事项

  • 仅限 CLI 模式:pcntl 和 posix 扩展在 Web SAPI(如 apache/FPM)中不可用或行为异常,本方案必须运行于 CLI 环境
  • 信号不可靠性:SIGALRM 是不可排队信号,若在前一个未处理完时再次触发,将被合并丢失——建议中断处理逻辑保持极简(如仅设标志位),复杂逻辑移至主循环检查;
  • 进程隔离限制:子进程无法访问主进程内存或对象,所有通信必须通过序列化(如 JSON)完成;
  • 调试建议:启用 pcntl_signal_get_handler(SIGALRM) 验证回调注册成功;用 strace -p $(pgrep -f ‘interrupter.php’) -e trace=select,kill 监控底层系统调用。

综上,该方案在不引入第三方扩展的前提下,以清晰的进程协作模型,为 PHP 提供了生产可用的毫秒级信号定时能力,适用于 CLI 守护进程、批处理超时控制、协程模拟等典型场景。

text=ZqhQzanResources