Search code examples
phplibeventpecl-eventpecl-dio

Looping behaviour in PHP Event


I am using the PHP Event class wrapper for libevent to read a serial port. I am using this to avoid buffer over runs - the idea is to use Event to check the port regularly so no data is lost.

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(). In which case flow of control goes from my code to the dispatcher in libevent when I call loop(). 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.

However in this scenario I should never be able to call loop() while a previous loop() call was running because by the explanation above, flow of control is either in my code or in libevent and can't be in both.

So I placed calls to loop() through my code (four in total - I am feeling my way) and two of them produce libevent re-entrant warnings.

I am obviously not understanding this. Can anyone help?

Cheers Paul

<?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();




?>

Solution

  • 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() registers an event and associates it with EventBase. At this point the Event object represents a set of conditions and callbacks for specific event(s). In this state the event is not triggered.

    Event::add() makes the event pending. When the event is in pending state, it is ready to be triggered when the corresponding conditions are met.

    EventBase::loop() runs the EventBase until there are no more events pending in it. An event can be triggered only when the corresponding base is running.

    When an event is triggered, it becomes active, and its callback is run. If the event is configured as persistent, it remains pending after the callback is run. Otherwise, it stops being pending. Consider this:

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

    Output

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

    With Event::PERSIST flag:

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

    the callback will be invoked every second, since the event remains pending.

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

    The process is blocked until the loop finishes. We need to wait for the events to be handled. Otherwise, the flow may reach the end of the program before all of the events are handled. That's the way all asynchronous programs actually work.

    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.

    Yes, you are scheduling the events before running the base. No, you shouldn't call EventBase::loop() regularly, nor you need to think about CPU being "starved", since the underlying implementation is based on effective platform-specific backends such as epoll, poll, kqueue, etc. In the idle state (when a running base is only waiting for events to occur), the process consumes negligibly small amount of system resources.

    You can control the flow by means of the timer events, by adding/deleting events, or modifying the event properties in their callbacks, for instance.

    DIO

    DIO streams are currently not recognized by the Event extension. There is no clean way to obtain the file descriptor encapsulated into the DIO resource. But there is a workaround:

    • open stream for the port with fopen();
    • make the stream non-blocking with [stream_set_blocking()][3];
    • obtain numeric file descriptor from the stream with [EventUtil::getSocketFd()][3];
    • pass the numeric file descriptor to dio_fdopen() (currently undocumented) and get the DIO resource;
    • add an Event with a callback for listening to the read events on the file descriptor;
    • in the callback drain the available data and process it according to the logic of your application.

    Alternative: patching/contributing to DIO

    Of course, you can add a function that will export the underlying file descriptor as integer. That's easy. Checkout the project:

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

    Add new function to 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)
    

    And its prototype to php7/php_dio.h:

    PHP_FUNCTION(dio_get_fd);
    

    Rebuild the extension, and you are ready to use 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();