脚本在其无限while循环无所事事时对信号无响应

Dec*_*ler 5 php posix signals while-loop

我正在尝试创建一个可重用的通用cli服务器,该服务器可以在终端会话中进行控制(启动/暂停/恢复/停止)。

到目前为止,我的方法是让我有一个脚本独立地充当控制台(父循环)和服务器(子循环),而不是通过pcntl_fork()-ing,而是通过将proc_open()自身本身作为子进程来实现。

然后,控制台循环通过使用发出信号来作用于服务器循环posix_kill()

现在,不管这是否是一个明智的方法,我偶然发现了一个奇怪的东西-即,当控制台循环用SIGTSTP信号暂停服务器循环时,服务器循环将不会响应该SIGCONT信号,除非它的while-loop实际上在做一些有用的东西。

这可能是怎么回事?


编辑:

根据注释中的请求,我简化了代码示例。但是,正如我已经担心的那样,代码可以正常工作。

也许我用类忽略了代码中的某些内容,但是我只是看不到两个示例在例程中的不同之处-对我来说,这两个示例都遵循相同的例程。

还有一个重要的注意事项:在我更复杂的示例中,我尝试不断地写入中的文件loop(),即使暂停了该文件也确实有效。因此,这告诉我循环继续正常运行。在我将其暂停之后,服务器只是不想再响应信号了。

无论如何,这是我之前显示的示例的简化版本,如下所示:

$lockPath = '.lock';
if( file_exists( $lockPath ) ) {
  echo 'Process already running; exiting...' . PHP_EOL;
  exit( 1 );
}
else if( $argc == 2 && 'child' == $argv[ 1 ] ) {
  /* child process */

  if( false === ( $lock = fopen( $lockPath, 'x' ) ) ) {
    echo 'Unable to acquire lock; exiting...' . PHP_EOL;
    exit( 1 );
  }
  else if( false !== flock( $lock, LOCK_EX ) ) {
    echo 'Process started...' . PHP_EOL;

    $state = 1;

    declare( ticks = 1 );
    pcntl_signal( SIGTSTP, function( $signo ) use ( &$state ) {
      echo 'pcntl_signal SIGTSTP' . PHP_EOL;
      $state = 0;
    } );
    pcntl_signal( SIGCONT, function( $signo ) use ( &$state ) {
      echo 'pcntl_signal SIGCONT' . PHP_EOL;
      $state = 1;
    } );
    pcntl_signal( SIGTERM, function( $signo ) use ( &$state ) {
      echo 'pcntl_signal SIGTERM' . PHP_EOL;
      $state = -1;
    } );

    while( $state !== -1 ) {
      /**
       * It doesn't matter whether I leave the first echo out
       * and/or whether I put either echo's in functions,
       * Any combination simply works as expected here
       */
      echo 'Server state: ' . $state . PHP_EOL;
      if( $state !== 0 ) {
        echo 'Server tick.' . PHP_EOL;
      }
      usleep( 1000000 );
    }

    flock( $lock, LOCK_UN ) && fclose( $lock ) && unlink( $lockPath );
    echo 'Process ended; unlocked, closed and deleted lock file; exiting...' . PHP_EOL;
    exit( 0 );
  }
}
else {
  /* parent process */

  function consoleRead() {
    $fd = STDIN;
    $read = array( $fd );
    $write = array();
    $except = array();

    $result = stream_select( $read, $write, $except, 0 );
    if( $result === false ) {
      throw new RuntimeException( 'stream_select() failed' );
    }
    if( $result === 0 ) {
      return false;
    }

    return stream_get_line( $fd, 1024, PHP_EOL );
  }

  $decriptors = array(
    0 => STDIN,
    1 => STDOUT,
    2 => STDERR
  );
  $childProcess = proc_open( sprintf( 'exec %s child', __FILE__ ), $decriptors, $pipes );

  while( 1 ) {

    $childStatus = proc_get_status( $childProcess );
    $childPid    = $childStatus[ 'pid' ];
    if( false !== ( $command = consoleRead() ) ) {
      switch( $command ) {
        case 'status':
          var_export( $childStatus );
        break;
        case 'run':
        case 'start':
          // nothing?
        break;
        case 'pause':
        case 'suspend':
          // SIGTSTP
          if( false !== $childPid ) {
            posix_kill( $childPid, SIGTSTP );
          }
        break;
        case 'resume':
        case 'continue':
          // SIGCONT
          if( false !== $childPid ) {
            posix_kill( $childPid, SIGCONT );
          }
        break;
        case 'halt':
        case 'quit':
        case 'stop':
          // SIGTERM
          if( false !== $childPid ) {
            posix_kill( $childPid, SIGTERM );
          }
        break;
      }
    }
    usleep( 1000000 );
  }

  exit( 0 );
}
Run Code Online (Sandbox Code Playgroud)

在控制台中运行任一示例(上下)时,请输入pause<enter>,然后输入resume<enter>。预期的行为是,在恢复后,您将再次看到(除了其他内容之外)此流:

Server tick.
Server tick.
Server tick.
Run Code Online (Sandbox Code Playgroud)

/编辑


这是我使用的:

控制台和服务器都是我的抽象LoopedProcess类的实例:

Server tick.
Server tick.
Server tick.
Run Code Online (Sandbox Code Playgroud)

这是一个非常基本的抽象控制台类,基于LoopedProcess

abstract class LoopedProcess
{

  const STOPPED = -1;
  const PAUSED  =  0;
  const RUNNING =  1;

  private $state    = self::STOPPED;
  private $throttle = 50;

  final protected function getState() {
    return $this->state;
  }

  final public function isStopped() {
    return self::STOPPED === $this->getState();
  }

  final public function isPaused() {
    return self::PAUSED === $this->getState();
  }

  final public function isRunning() {
    return self::RUNNING === $this->getState();
  }

  protected function onBeforeRun() {}

  protected function onRun() {}

  final public function run() {
    if( $this->isStopped() && false !== $this->onBeforeRun() ) {
      $this->state = self::RUNNING;
      $this->onRun();
      $this->loop();
    }
  }

  protected function onBeforePause() {}

  protected function onPause() {}

  final public function pause() {
    if( $this->isRunning() && false !== $this->onBeforePause() ) {
      $this->state = self::PAUSED;
      $this->onPause();
    }
  }

  protected function onBeforeResume() {}

  protected function onResume() {}

  final public function resume() {
    if( $this->isPaused() && false !== $this->onBeforeResume() ) {
      $this->state = self::RUNNING;
      $this->onResume();
    }
  }

  protected function onBeforeStop() {}

  protected function onStop() {}

  final public function stop() {
    if( !$this->isStopped() && false !== $this->onBeforeStop() ) {
      $this->state = self::STOPPED;
      $this->onStop();
    }
  }

  final protected function setThrottle( $throttle ) {
    $this->throttle = (int) $throttle;
  }

  protected function onLoopStart() {}

  protected function onLoopEnd() {}

  final private function loop() {
    while( !$this->isStopped() ) {
      $this->onLoopStart();
      if( !$this->isPaused() ) {
        $this->tick();
      }
      $this->onLoopEnd();
      usleep( $this->throttle );
    }
  }

  abstract protected function tick();
}
Run Code Online (Sandbox Code Playgroud)

下面的实际服务器控制台扩展了上面的抽象控制台类。在内部,ServerConsole::tick()您会发现它响应命令,从终端键入命令,然后将信号发送到子进程(实际服务器)。

abstract class Console
  extends LoopedProcess
{

  public function __construct() {
    $this->setThrottle( 1000000 ); // 1 sec
  }

  public function consoleRead() {
    $fd = STDIN;
    $read = array( $fd );
    $write = array();
    $except = array();

    $result = stream_select( $read, $write, $except, 0 );
    if( $result === false ) {
      throw new RuntimeException( 'stream_select() failed' );
    }
    if( $result === 0 ) {
      return false;
    }

    return stream_get_line( $fd, 1024, PHP_EOL );
  }

  public function consoleWrite( $data ) {
    echo "\r$data\n";
  }
}
Run Code Online (Sandbox Code Playgroud)

这是服务器的实现。这是发生奇怪行为的地方。如果不覆盖该LoopedProcess::onLoopStart()挂钩,则一旦暂停,它将不再响应信号。因此,如果我摘下钩子,LoopedProcess::loop()实际上不再有任何重要意义。

class ServerConsole
  extends Console
{

  private $childProcess;
  private $childProcessId;

  public function __construct() {
    declare( ticks = 1 );
    $self = $this;
    pcntl_signal( SIGINT, function( $signo ) use ( $self ) {
      $self->consoleWrite( 'Console received SIGINT' );
      $self->stop();
    } );
    parent::__construct();
  }

  protected function onBeforeRun() {
    $decriptors = array( /*
      0 => STDIN,
      1 => STDOUT,
      2 => STDERR
    */ );
    $this->childProcess = proc_open( sprintf( 'exec %s child', __FILE__ ), $decriptors, $pipes );

    if( !is_resource( $this->childProcess ) ) {
      $this->consoleWrite( 'Unable to create child process; exiting...' );
      return false;
    }
    else {
      $this->consoleWrite( 'Child process created...' );
    }
  }

  protected function onStop() {
    $this->consoleWrite( 'Parent process ended; exiting...' );
    $childPid = proc_get_status( $this->childProcess )[ 'pid' ];
    if( false !== $childPid ) {
      posix_kill( $childPid, SIGTERM );
    }
  }

  protected function tick() {    
    $childStatus = proc_get_status( $this->childProcess );
    $childPid = $childStatus[ 'pid' ];
    if( false !== ( $command = $this->consoleRead() ) ) {
      var_dump( $childPid, $command );
      switch( $command ) {
        case 'run':
        case 'start':
          // nothing, for now
        break;
        case 'pause':
        case 'suspend':
          // SIGTSTP
          if( false !== $childPid ) {
            posix_kill( $childPid, SIGTSTP );
          }
        break;
        case 'resume':
        case 'continue':
          // SIGCONT
          if( false !== $childPid ) {
            posix_kill( $childPid, SIGCONT );
          }
        break;
        case 'halt':
        case 'quit':
        case 'stop':
          // SIGTERM
          if( false !== $childPid ) {
            posix_kill( $childPid, SIGTERM );
          }
        break;
      }
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

这是将它们联系在一起的脚本:

class Server
  extends LoopedProcess
{

  public function __construct() {
    declare( ticks = 1 );
    $self = $this;

    // install the signal handlers
    pcntl_signal( SIGTSTP, function( $signo ) use ( $self ) {
      echo 'pcntl_signal SIGTSTP' . PHP_EOL;
      $self->pause();
    } );
    pcntl_signal( SIGCONT, function( $signo ) use ( $self ) {
      echo 'pcntl_signal SIGCONT' . PHP_EOL;
      $self->resume();
    } );
    pcntl_signal( SIGTERM, function( $signo ) use ( $self ) {
      echo 'pcntl_signal SIGTERM' . PHP_EOL;
      $self->stop();
    } );
    $this->setThrottle( 2000000 ); // 2 sec
  }

  protected function tick() {
    echo 'Server tick.' . PHP_EOL;
  }

  protected function onBeforePause() {
    echo 'Server pausing.' . PHP_EOL;
  }

  protected function onPause() {
    echo 'Server paused.' . PHP_EOL;
  }

  protected function onBeforeResume() {
    echo 'Server resuming.' . PHP_EOL;
  }

  protected function onResume() {
    echo 'Server resumed.' . PHP_EOL;
  }

  /**
   * if I remove this hook, Server becomes unresponsive
   * to signals, after it has been paused
   */
  protected function onLoopStart() {
    echo 'Server state: ' . ( $this->getState() ) . PHP_EOL;
  }
}
Run Code Online (Sandbox Code Playgroud)

因此,总结一下:

Server被暂停并且loop()由于没有实现任何输出的挂钩而实际上在内部没有做任何重要的事情时,它将变得对新信号无响应。然而,当钩执行,按预期响应信号。

这可能是怎么回事?

Dec*_*ler 2

pcntl_signal_dispatch()我通过添加对inside 的调用来使其工作loop(),按照PHP 文档网站上的评论1 ,如下所示:

final private function loop() {
  while( !$this->isStopped() ) {
    $this->onLoopStart();
    if( !$this->isPaused() ) {
      $this->tick();
    }
    $this->onLoopEnd();
    pcntl_signal_dispatch(); // adding this worked
    // (I actually need to put it in onLoopEnd() though, this was just a temporary hack)
    usleep( $this->throttle );
  }
}
Run Code Online (Sandbox Code Playgroud)

然而,我的简化示例脚本不需要这个。因此,我仍然有兴趣知道在什么情况下需要打电话pcntl_signal_dispatch()以及背后的原因,如果有人对此有任何见解。


1) 该评论目前隐藏在网站标题后面,因此您可能需要向上滚动一点。