10

I have a single-threaded Linux application using boost::asio for asynchronous input/output. Now I need to extend this application to read in GPIO inputs on /sys/class/gpio/gpioXX/value.

It is possible to do that with boost::asio::posix::stream_descriptor on edge-triggered GPIO inputs?

I configured the GPIO input like follows:

echo XX >/sys/class/gpio/export
echo in >/sys/class/gpio/gpioXX/direction
echo both >/sys/class/gpio/gpioXX/edge

I managed to write a epoll based test application that blocks on the GPIO file descriptor until the GPIO signal changes but boost::asio does not seem to be able to block properly. A call to boost::asio::async_read always immediately invokes the handler (of course only within io_service.run()) with either EOF or - in case the file pointer was set back - 2 bytes data.

I'm not an expert in boost::asio internals but could the reason be that the boost::asio epoll reactor is level triggered instead of edge triggered in case of posix::stream_descriptor?

Here is my code:

#include <fcntl.h>

#include <algorithm>
#include <iterator>
#include <stdexcept>

#include <boost/asio.hpp>

boost::asio::io_service io_service;
boost::asio::posix::stream_descriptor sd(io_service);
boost::asio::streambuf streambuf;

void read_handler(const boost::system::error_code& error, std::size_t bytes_transferred)
{
    if (error.value() == boost::asio::error::eof) {
        // If we don't reset the file pointer we only get EOFs
        lseek(sd.native_handle(), 0, SEEK_SET);
    } else if (error)
        throw std::runtime_error(std::string("Error ") + std::to_string(error.value()) + " occurred (" + error.message() + ")");

    std::copy_n(std::istreambuf_iterator<char>(&streambuf), bytes_transferred, std::ostreambuf_iterator<char>(std::cout));
    streambuf.consume(bytes_transferred);
    boost::asio::async_read(sd, streambuf, &read_handler);
}

int main(int argc, char *argv[])
{
    if (argc != 2)
        return 1;

    int fd = open(argv[1], O_RDONLY);
    if (fd < 1)
        return 1;

    try {
        sd.assign(fd);
        boost::asio::async_read(sd, streambuf, &read_handler);
        io_service.run();
    } catch (...) {
        close(fd);
        return 1;
    }

    close(fd);
    return 0;
}
Florian
  • 1,036
  • 10
  • 15

1 Answers1

7

As far as I know, it is not possible to get this particular behavior with Boost.Asio. While the kernel flags some files on the procfs and sysfs as pollable, they do not provide the stream-like behavior that is expected from boost::asio::posix::stream_descriptor and its operations.

Boost.Asio's epoll reactor is edge-triggered (see Boost.Asio 1.43 revision history notes). Under certain conditions1, Boost.Asio will attempt the I/O operation within the context of the initiating function (e.g. async_read()). If the I/O operation completes (success or failure), then the completion handler is posted into the io_service as-if by io_service.post(). Otherwise, the file descriptor will be added to the event demultiplexer for monitoring. The documentation alludes to this behavior:

Regardless of whether the asynchronous operation completes immediately or not, the handler will not be invoked from within this function. Invocation of the handler will be performed in a manner equivalent to using boost::asio::io_service::post().

For composed operations, such as async_read(), EOF is treated as an error, as it indicates a violation in the operation's contract (i.e. completion condition will never be satisfied because no more data will be available). In this particular case, the I/O system call will occur within the async_read() initiating function, reading from the start of the file (offset 0) to the end of file, causing the operation to fail with boost::asio::error::eof. As the operation has completed, it is never added to the event demultiplexer for edge-triggered monitoring:

boost::asio::io_service io_service;
boost::asio::posix::stream_descriptor stream_descriptor(io_service);

void read_handler(const boost::system::error_code& error, ...)
{
  if (error.value() == boost::asio::error::eof)
  {
    // Reset to start of file.
    lseek(sd.native_handle(), 0, SEEK_SET);
  }

  // Same as below.  ::readv() will occur within this context, reading
  // from the start of file to end-of-file, causing the operation to
  // complete with failure.
  boost::asio::async_read(stream_descriptor, ..., &read_handler);
}

int main()
{
  int fd = open( /* sysfs file */, O_RDONLY);

  // This would throw an exception for normal files, as they are not
  // poll-able.  However, the kernel flags some files on procfs and
  // sysfs as pollable.
  stream_descriptor.assign(fd);

  // The underlying ::readv() system call will occur within the
  // following function (not deferred until edge-triggered notification
  // by the reactor).  The operation will read from start of file to
  // end-of-file, causing the operation to complete with failure.
  boost::asio::async_read(stream_descriptor, ..., &read_handler);

  // Run will invoke the ready-to-run completion handler from the above
  // operation.
  io_service.run();
}

1. Internally, Boost.Asio refers to this behavior as speculative operations. It is an implementation detail, but the I/O operation will be attempted within the initiating function if the operation may not need event notification (e.g. it can immediately attempt to a non-blocking I/O call), and and there are neither pending operations of the same type nor pending out-of-band operations on the I/O object. There are no customization hooks to prevent this behavior.

Tanner Sansbury
  • 51,153
  • 9
  • 112
  • 169
  • Thanks for your answer, it helped me a lot to better understand some of the concepts of boost::asio. I figured out that the EOF case can be avoided by using [null_buffers](http://www.boost.org/doc/libs/1_58_0/doc/html/boost_asio/overview/core/reactor.html). – Florian May 28 '15 at 08:30
  • However, digging a bit deeper in the sources of asios epoll reactor I found out that the reason for the described behavior is not a _speculative operation_ but rather the case that the epoll reactor calls `epoll_ctl` for every asynchronous operation which makes `epoll_wait` return immediately. – Florian May 28 '15 at 08:31
  • I proved in an `epoll` based implementation that using `epoll_ctl` only once in the very beginning makes subsequent `epoll_wait` invocations (except the immediately following one) block until a GPIO event occurs. Unfortunately, as you said, it seems not to be possible to get this particular behavior from boost::asio. – Florian May 28 '15 at 08:31
  • @Florian Glad it helped. Boost.Asio's exact usage of epoll is not specified, and the current implementation would certainly not provide the behavior you desire. However, even if the implementation did not issue `epoll_ctl` per operation, the speculative operation behavior that is permitted by the documentation would still prevent obtaining the desired behavior with async-read operations. – Tanner Sansbury May 29 '15 at 05:04