Skip to content
Advertisement

Is there a way to signal a SIGALRM with a finer-grained delay than pcntl_alarm(int $seconds)?

pcntl_alarm(int $seconds) only has a resolution of seconds. Is there a way in PHP to signal a SIGALRM with a delay of, say, milliseconds? Maybe a posix_kill() with a delay argument?

PS.: I’m aware of SwooleProcess::alarm() from the PECL extension Swoole, but I’m looking for a more bare-bones PHP solution.

Advertisement

Answer

I found one way to do it, but it is a bit convoluted:

<?php

// alarm uses proc_open() to signal this process from a child process
function alarm(int $msec): void {
  $desc = [
    ['pipe', 'r']
  ];
  $pid = posix_getpid();
  $process = proc_open('php', $desc, $pipes);
  fwrite(
    $pipes[0],
    "<?php
      usleep($msec * 1000);
      posix_kill($pid, SIGALRM);
    "
  );
  fclose($pipes[0]);
}

function handleSignal(int $signal): void {
  switch($signal) {
    case SIGALRM:
      echo "interrupted by ALARMn";
      break;
  }
}

pcntl_async_signals(true);
pcntl_signal(SIGALRM, 'handleSignal');

// set alarm 200ms from now
alarm(200);

while(true) {
  echo "going to sleep for 10 seconds...n";
  // first sleep(10) will be interrupted after 200ms
  sleep(10);
}

…and it’s way too resource intensive. And because it needs to spawn a new process each time probably not very time-accurate either.


Addendum:
I’ve managed to make it more efficient by creating only one long-running interrupter process, instead of creating short-running processes for each interruption request.

It’s still far from ideal, but it does the job for now:

<?php

// long-running interrupter process for the Interrupter class
// that accepts interruption requests with a delay
class InterrupterProcess
{
  private $process;
  private $writePipe;
  
  private const PROCESS_CODE = <<<'CODE'
<?php
  $readPipe = fopen('php://fd/3', 'r');
  $interrupts = [];
  
  while(true) {
    $r = [$readPipe];
    $w = null;
    $e = null;
    
    $time = microtime(true);
    $minExpiry = min($interrupts + [($time + 1)]);
    $timeout = $minExpiry - $time;
    if(stream_select($r, $w, $e, (int) $timeout, (int) (fmod($timeout, 1) * 1e6)) > 0) {
      $interrupt = json_decode(fread($readPipe, 1024), true);
      $interrupts[$interrupt['pid']] = $interrupt['microtime'];
    }
    
    $time = microtime(true);
    foreach($interrupts as $pid => $interrupt) {
      if($interrupt <= $time) {
        posix_kill($pid, SIGALRM);
        unset($interrupts[$pid]);
      }
    }
  }
CODE;
  
  public function __construct() {
    $desc = [
      ['pipe', 'r'],
      STDOUT,
      STDOUT,
      ['pipe', 'r']
    ];
    $this->process = proc_open(['php'], $desc, $pipes);
    $this->writePipe = $pipes[3];
    fwrite($pipes[0], self::PROCESS_CODE);
    fclose($pipes[0]);
  }
  
  public function __destruct() {
    $this->destroy();
  }
  
  public function setInterrupt(int $pid, float $delay): bool {
    if(!is_null($this->writePipe)) {
      fwrite($this->writePipe, json_encode(['pid' => $pid, 'microtime' => microtime(true) + $delay]));
      return true;
    }
    
    return false;
  }
  
  private function destroy(): void {
    if(!is_null($this->writePipe)) {
      fclose($this->writePipe);
      $this->writePipe = null;
    }
    if(!is_null($this->process)) {
      proc_terminate($this->process);
      proc_close($this->process);
      $this->process = null;
    }
  }
}

// main Interrupter class
class Interrupter
{
  private $process;
  
  public function __destruct() {
    $this->destroy();
  }
  
  public function interrupt(float $delay): void {
    if(is_null($this->process)) {
      pcntl_async_signals(true);
      pcntl_signal(SIGALRM, function(int $signal): void {
        $this->handleSignal($signal);
      });
      $this->process = $this->createInterrupterProcess();
    }
    $this->process->setInterrupt(posix_getpid(), $delay);
  }
  
  private function createInterrupterProcess(): InterrupterProcess {
    return new InterrupterProcess();
  }
  
  private function handleSignal(int $signal): void {
    switch($signal) {
      case SIGALRM:
        $time = time();
        echo "interrupted by ALARM @ $timen";
        break;
    }
  }
  
  private function destroy(): void {
    $this->process = null;
  }
}

$interrupter = new Interrupter();
while(true) {
  $time = time();
  echo "going to sleep for 10 seconds @ $time...n";
  $interrupter->interrupt(2);
  sleep(10);
}
User contributions licensed under: CC BY-SA
7 People found this is helpful
Advertisement