PHP 事件中的循环行为

Looping behaviour in PHP Event

我正在使用 libevent 的 PHP 事件 class 包装器来读取串口。我正在使用它来避免缓冲区溢出 - 我的想法是使用 Event 定期检查端口,这样就不会丢失数据。

我原本希望事件会在设置时触发,但得出的结论是事件只会在调用 EventBase::loop() 后触发。在这种情况下,当我调用 loop() 时,控制流从我的代码转到 libevent 中的调度程序。最终控制流 returns 到循环调用后位置的我的代码。

我从这个行为中假设我基本上是在安排事件的调度并且应该定期调用 loop() 以避免我的事件被饿死 CPU。

但是在这种情况下,我永远无法调用 loop() 而之前的 loop() 调用是 运行 因为根据上面的解释,控制流要么在我的代码中或在 libevent 中,不能同时在两者中。

所以我通过我的代码调用了 loop()(总共四个 - 我感觉自己的方式),其中两个产生 libevent 重入警告。

我显然不明白这一点。有人可以帮忙吗?

干杯保罗

<?php

// serial comms defines
define("PORT", "/dev/serial0");
const PORTSETTINGS = array(
  'baud' => 9600,
  'bits' => 8,
  'stop'  => 1,
  'parity' => 0
);
define("SYN", 170);
define("MSB", 127);
const POLL = 0.1;



/*
** Class Scanner
**
** Manages low level serial comms with the vbus master
**
*/
class Scanner {
private $fd;
private $pkt;
private $state;
private $base;
private $timer;

   /*
   ** __construct()
   **
   ** setup the serial port for reading using dio
   ** setup a timer to read anything on the serial port frequently
   **
   */
   function __construct() {
       // set up port and state machine
       $this->fd = dio_open(PORT, O_RDONLY | O_NOCTTY | O_NONBLOCK);
       dio_tcsetattr($this->fd, PORTSETTINGS); 
       $this->pkt = array();
       $this->state = "discard";

       // set up timer handler
       $this->base = new EventBase();
       $this->timer = new Event($this->base, -1, Event::TIMEOUT |        Event::PERSIST, array($this, "Tickle"));
       $this->timer->addTimer(POLL);
       $this->base->loop(EventBase::LOOP_NONBLOCK);
   }

   function PrintPkt($pkt) {
     echo "\n\n".date("H:i:s");
     foreach($pkt as $i) 
     echo " ".dechex($i);
  }

  /*
  ** Tickle()
  **
  ** read the serial port, if MSB set discard the packet, else save    the packet and then pass for processing
  ** called by the event timer on a regular basis ie POLL seconds
  */
  function Tickle() {

     do {
        // read the next one and convert to int
        $ch = dio_read($this->fd, 1);
        $i = ord($ch);

        // check for MSB, if set discard to the next packet
        if (($i > MSB) && ($i != SYN)) 
           $state="discard";

        // if there is nothing on the port it returns 0x0 ie null/false
        if ($i) {
           if ($i == SYN) {
              // we are at the start of a new packet
              if (count($this->pkt) > 0) {
                 if ($this->state === "save")
                   // this is where we would save the packet but for now we are printing it.
                   $this->PrintPkt($this->pkt);
                 // reset for the next packet
                 $this->pkt = array();
                 $this->state = "save";
              }
          }
          // save this number
          $this->pkt[] = $i; 
       }        
     } while ($ch);
     // restart the timer
     $this->timer->addTimer(POLL);
  }

  /*
  ** spin()
  **
  ** call the base loop so that the timer event is serviced
  */
  function spin() {
    $this->base->loop(EventBase::LOOP_NONBLOCK);
  }

}




$c    = new Scanner();

echo "setup";

while(1);
 // $c->spin();




?>

I was hoping that the events would just fire when it was set, but came to the conclusion that events only fire after a call to EventBase::loop().

Event::__construct() 注册一个事件并将其与 EventBase 相关联。此时 Event 对象表示一组特定事件的条件和回调。在此状态下,事件 触发。

Event::add() 使事件 待处理 。当事件处于pending状态时,准备在满足相应条件时触发。

EventBase::loop() 运行s the EventBase 直到其中没有更多待处理的事件。只有当对应的基数为运行ning.

时才能触发事件

当一个事件被触发时,它变成active,它的回调是运行。如果事件配置为persistent,则回调为运行后仍保持挂起状态。否则,它将停止挂起。考虑一下:

$base = new EventBase();
$e = new Event($base, -1, Event::TIMEOUT, function() {
  // The event is not pending, since it is not persistent:
  printf("1 sec elapsed\n");
});
printf("1) Event is pending: %d\n", $e->pending);
// Make $e pending
$e->add(1);
printf("2) Event is pending: %d\n", $e->pending);
// Dispatch all pending events
$base->loop();
printf("3) Event is pending: %d\n", $e->pending);

输出

1) Event is pending: 0
2) Event is pending: 1
1 sec elapsed
3) Event is pending: 0

带有 Event::PERSIST 标志:

$e = new Event($base, -1, Event::TIMEOUT | Event::PERSIST, function() {

回调将每秒调用一次,因为事件仍未决。

Eventually flow of control returns to my code at the position after the call to loop.

在循环完成之前,进程将被阻塞。我们需要等待事件被处理。否则,流程可能会在所有事件都得到处理之前到达程序的末尾。这就是所有异步程序实际工作的方式。

I assumed from this behavior that I was essentially scheduling the dispatch of events and should call loop() regularly to avoid having my events being starved of CPU.

是的,您正在 运行 基地之前安排活动。不,你不应该定期调用 EventBase::loop(),也不需要考虑 CPU 是 "starved",因为底层实现是基于有效的特定于平台的后端,例如 epoll, poll, kqueue等。在空闲状态下(当运行ning base只是在等待事件发生时),进程消耗的系统资源可以忽略不计。

您可以通过计时器事件、adding/deleting 事件或修改事件回调中的事件属性来控制流程。

DIO

事件扩展当前无法识别 DIO 流。没有干净的方法来获取封装到 DIO 资源中的文件描述符。但有一个解决方法:

  • fopen() 端口打开流;
  • 使用 [stream_set_blocking()][3];
  • 使流成为非阻塞的
  • 使用 [EventUtil::getSocketFd()][3];
  • 从流中获取数字文件描述符
  • 将数字文件描述符传递给dio_fdopen()(当前未记录)并获取 DIO 资源;
  • 添加一个 Event 回调,用于侦听文件描述符上的读取事件;
  • 在回调中排出可用数据并根据您的应用程序逻辑对其进行处理。

备选方案:patching/contributing 到 DIO

当然,您可以添加一个将底层文件描述符导出为整数的函数。这很简单。检查项目:

svn checkout https://svn.php.net/repository/pecl/dio/trunk dio
cd dio

php7/dio.c 添加新功能:

/* {{{ proto int dio_get_fd(resource fd)
   Returns numeric file descriptor for the given DIO resource */
PHP_FUNCTION(dio_get_fd)
{
  zval     *r_fd;
  php_fd_t *f;

  if (zend_parse_parameters(ZEND_NUM_ARGS(), "r", &r_fd) == FAILURE) {
    return;
  }

  if ((f = (php_fd_t *) zend_fetch_resource(Z_RES_P(r_fd), le_fd_name, le_fd)) == NULL) {
    RETURN_FALSE;
  }

  RETURN_LONG(f->fd);
}
/* }}} */
/* ... */
  PHP_FE(dio_get_fd, dio_close_args)

其原型为php7/php_dio.h

PHP_FUNCTION(dio_get_fd);

重建扩展,你就可以使用了dio_get_fd():

$this->dio = dio_open($this->port, O_RDONLY | O_NOCTTY | O_NONBLOCK);
$this->fd = dio_get_fd($this->dio);

$this->e_read = new Event($this->base, $this->fd, Event::READ | Event::PERSIST,
  [$this, '_onRead']);
$this->e_read->add();
$this->base->dispatch();