Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use boost::asio with Linux GPIOs

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;
}
like image 848
Florian Avatar asked May 26 '15 12:05

Florian


1 Answers

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.

like image 173
Tanner Sansbury Avatar answered Oct 21 '22 01:10

Tanner Sansbury