草庐IT

php - 当无限 while 循环无所事事时,脚本对信号无响应

coder 2024-04-07 原文

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

到目前为止,我的方法是让一个脚本独立充当控制台(父循环)和服务器(子循环),而不是 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 );
}

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

Server tick.
Server tick.
Server tick.

/编辑


这是我使用的:

控制台和服务器都是我的抽象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();
}

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

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";
  }
}

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

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;
      }
    }
  }
}

这是服务器实现。这就是奇怪行为发生的地方。如果不覆盖 LoopedProcess::onLoopStart() Hook ,一旦暂停,它将不再响应信号。所以,如果我去掉钩子(Hook),LoopedProcess::loop()有效地不再做任何重要的事情。

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;
  }
}

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

$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;
    $server = new Server();
    $server->run();
    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 */

  $console = new ServerConsole();
  $console->run();
  exit( 0 );
}

总结一下:

Server暂停并且在 loop() 中实际上没有做任何重要的事情因为我没有实现输出任何东西的钩子(Hook),所以它对新信号没有反应。但是,当 Hook 实现时,它会按预期响应信号。

这里可能发生了什么?

最佳答案

我已经通过添加对 pcntl_signal_dispatch() 的调用让它工作了在 loop() 中,根据 this comment 1 在 PHP 的文档网站上,像这样:

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 );
  }
}

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


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

关于php - 当无限 while 循环无所事事时,脚本对信号无响应,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/31114583/

有关php - 当无限 while 循环无所事事时,脚本对信号无响应的更多相关文章

  1. ruby - 树顶语法无限循环 - 2

    我脑子里浮现出一些关于一种新编程语言的想法,所以我想我会尝试实现它。一位friend建议我尝试使用Treetop(Rubygem)来创建一个解析器。Treetop的文档很少,我以前从未做过这种事情。我的解析器表现得好像有一个无限循环,但没有堆栈跟踪;事实证明很难追踪到。有人可以指出入门级解析/AST指南的方向吗?我真的需要一些列出规则、常见用法等的东西来使用像Treetop这样的工具。我的语法分析器在GitHub上,以防有人希望帮助我改进它。class{initialize=lambda(name){receiver.name=name}greet=lambda{IO.puts("He

  2. ruby-on-rails - 在 Ruby 中循环遍历多个数组 - 2

    我有多个ActiveRecord子类Item的实例数组,我需要根据最早的事件循环打印。在这种情况下,我需要打印付款和维护日期,如下所示:ItemAmaintenancerequiredin5daysItemBpaymentrequiredin6daysItemApaymentrequiredin7daysItemBmaintenancerequiredin8days我目前有两个查询,用于查找maintenance和payment项目(非排他性查询),并输出如下内容:paymentrequiredin...maintenancerequiredin...有什么方法可以改善上述(丑陋的)代

  3. ruby - RuntimeError(自动加载常量 Apps 多线程时检测到循环依赖 - 2

    我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("

  4. ruby-on-rails - 每次我尝试部署时,我都会得到 - (gcloud.preview.app.deploy) 错误响应 : [4] DEADLINE_EXCEEDED - 2

    我是Google云的新手,我正在尝试对其进行首次部署。我的第一个部署是RubyonRails项目。我基本上是在关注thisguideinthegoogleclouddocumentation.唯一的区别是我使用的是我自己的项目,而不是他们提供的“helloworld”项目。这是我的app.yaml文件runtime:customvm:trueentrypoint:bundleexecrackup-p8080-Eproductionconfig.ruresources:cpu:0.5memory_gb:1.3disk_size_gb:10当我转到我的项目目录并运行gcloudprevie

  5. Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting - 2

    1.错误信息:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:requestcanceledwhilewaitingforconnection(Client.Timeoutexceededwhileawaitingheaders)或者:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:TLShandshaketimeout2.报错原因:docker使用的镜像网址默认为国外,下载容易超时,需要修改成国内镜像地址(首先阿里

  6. ruby-on-rails - 在 Ruby on Rails 中发送响应之前如何等待多个异步操作完成? - 2

    在我做的一些网络开发中,我有多个操作开始,比如对外部API的GET请求,我希望它们同时开始,因为一个不依赖另一个的结果。我希望事情能够在后台运行。我找到了concurrent-rubylibrary这似乎运作良好。通过将其混合到您创建的类中,该类的方法具有在后台线程上运行的异步版本。这导致我编写如下代码,其中FirstAsyncWorker和SecondAsyncWorker是我编写的类,我在其中混合了Concurrent::Async模块,并编写了一个名为“work”的方法来发送HTTP请求:defindexop1_result=FirstAsyncWorker.new.async.

  7. ruby - Ruby 中的闭包和 for 循环 - 2

    我是Ruby的新手,有些闭包逻辑让我感到困惑。考虑这段代码:array=[]foriin(1..5)array[5,5,5,5,5]这对我来说很有意义,因为i被绑定(bind)在循环之外,所以每次循环都会捕获相同的变量。使用每个block可以解决这个问题对我来说也很有意义:array=[](1..5).each{|i|array[1,2,3,4,5]...因为现在每次通过时都单独声明i。但现在我迷路了:为什么我不能通过引入一个中间变量来修复它?array=[]foriin1..5j=iarray[5,5,5,5,5]因为j每次循环都是新的,我认为每次循环都会捕获不同的变量。例如,这绝对

  8. ruby - 从 Ruby : capturing the output while displaying the output? 运行 shell 命令 - 2

    我有一个问题。我想从另一个ruby​​脚本运行一个ruby​​脚本并捕获它的输出信息,同时让它也输出到屏幕。亚军#!/usr/bin/envrubyprint"Enteryourpassword:"password=gets.chompputs"Hereisyourpassword:#{password}"我运行的脚本文件:开始.rboutput=`runner`putsoutput.match(/Hereisyour(password:.*)/).captures[0].to_s正如您在此处看到的那样,存在问题。在start.rb的第一行,屏幕是空的。我在运行程序中看不到“输入您的密

  9. Ruby:数组中的下一个/上一个值,循环数组,数组位置 - 2

    假设我有一个没有特定顺序的随机数数组。假设这些是参加马拉松比赛的人的ID#,他们按照完成的顺序添加到数组中,例如:race1=[8,102,67,58,91,16,27]race2=[51,31,7,15,99,58,22]这是一个简化且有些做作的示例,但我认为它传达了基本思想。现在有几个问题:首先,我如何获得特定条目之前和之后的ID?假设我正在查看运行者58,我想知道谁在他之前和之后完成了比赛。race1,runner58:previousfinisher=67,nextfinisher=91race2,runner58:previousfinisher=99,nextfinishe

  10. ruby - 奇怪的 ruby​​ for 循环行为(为什么这样做有效) - 2

    defreverse(ary)result=[]forresult[0,0]inaryendresultendassert_equal["baz","bar","foo"],reverse(["foo","bar","baz"])这行得通,我想了解原因。有什么解释吗? 最佳答案 如果我使用each而不是for/in重写它,它看起来像这样:defreverse(ary)result=[]#forresult[0,0]inaryary.eachdo|item|result[0,0]=itemendresultendforainb基本上就

随机推荐