How to use boost::asio with Linux GPIOs
Asked Answered
J

1

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;
}
Jyoti answered 26/5, 2015 at 12:44 Comment(4)
"boost uses edge-triggered manner of epoll"Receiptor
Well, thanks for the link, but as one can see in the grep output EPOLLET is not used in any case.Jyoti
how is it "not used in any case"? EPOLLET is used in various places.Receiptor
Sorry, any -> every ;)Jyoti
C
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.

Cherub answered 26/5, 2015 at 19:8 Comment(4)
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.Jyoti
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.Jyoti
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.Jyoti
@Jyoti 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.Cherub

© 2022 - 2024 — McMap. All rights reserved.