Home > front end >  OS Signal handling loop - blocking or non-blocking read?
OS Signal handling loop - blocking or non-blocking read?

Time:12-22

My application has a thread for handling OS signals, so to not block the programLoop(). This thread, processOSSignals, basically keeps on reading the file descriptor for signals SIGINT, SIGTERM, SIGQUIT. On their reception, loopOver being initially true, is set to false.

int mSigDesc = -1;

void init()
{
// creates file descriptor for reading SIGINT, SIGTERM, SIGQUIT
// blocks signals with sigprocmask(SIG_BLOCK, &mask, nullptr)
    ...
    mSigDesc = signalfd(mSigDesc, &mask, SFD_NONBLOCK); // OR 3rd param = 0?
}

void processOSSignals()
{
    while (loopOver)
    {
        struct signalfd_siginfo fdsi;

        auto readedBytes = read(mSigDesc, &fdsi, sizeof(fdsi));
        ...
    }
}

int main()
{
    init();
    std::thread ossThread(processOSSignals);
    programLoop();
    ossThread.join();
}

My question is - should mSigDesc be set to blocking or non-blocking (asynchronous) mode?

In non-blocking mode, this thread is always busy, but inefficiently reading and returning EAGAIN over and over again.

In blocking mode, it waits until one of the signals is received, but if it is never sent, the ossThread will never join.

How should it be handled? Use sleep() in the non-blocking mode, to attempt reading only occasionally? Or maybe use select() in the blocking mode, to monitor mSigDesc and read only when sth. is available there?

CodePudding user response:

Whether you use blocking or non-blocking I/O depends on how you want to handle your I/O.

Typically, if you have a single thread which is dedicated to reading from the signal file descriptor and you simply want it to wait until it gets a signal, then you should use blocking I/O.

However, in many contexts, spawning a single thread for each I/O operation is inefficient. A thread requires a stack, which may consume a couple megabytes, and it's often more efficient to process many file descriptors (which may be of many different types) by putting them all in non-blocking mode and waiting until one of them is ready.

Typically, this is done portably using poll(2). select(2) is possible, but on many systems, it is limited to a certain number of file descriptors (on Linux, 1024), and many programs will exceed that number. On Linux, the epoll(7) family of functions can also be used, and you may prefer that if you're already using such non-portable constructions as signalfd(2).

For example, you might want to handle signal FDs as part of your main loop, in which case including that FD as one the FDs that your main loop processes using poll(2) or one of the other functions might be more desirable.

What you should avoid doing is spinning in a loop or sleeping with a non-blocking socket. If you use poll(2), you can specify a timeout after which the operation returns 0 if no file descriptor was ready, so you can already control a timeout without needing to resort to sleep.

CodePudding user response:

Same advise as bk2204 outlined: Just use poll. If you want to have a separate thread, a simple way to signal that thread is to add the read side of a pipe (or socket) to the set of polled file descriptors. The main thread then closes the write side when it wants the thread to stop. poll will then return and signal that reading from the pipe is possible (since it will signal EOF).

Here is the outline of an implementation:

We start by defining an RAII class for file descriptors.

#include <unistd.h>
// using pipe, close

#include <utility>
// using std::swap, std::exchange


struct FileHandle
{
    int fd;
    constexpr FileHandle(int fd=-1) noexcept
    : fd(fd)
    {}
    FileHandle(FileHandle&& o) noexcept
    : fd(std::exchange(o.fd, -1))
    {}
    ~FileHandle()
    {
        if(fd >= 0)
            ::close(fd);
    }
    void swap(FileHandle& o) noexcept
    {
        using std::swap;
        swap(fd, o.fd);
    }
    FileHandle& operator=(FileHandle&& o) noexcept
    {
        FileHandle tmp = std::move(o);
        swap(tmp);
        return *this;
    }
    operator bool() const noexcept
    { return fd >= 0; }

    void reset(int fd=-1) noexcept
    { *this = FileHandle(fd); }

    void close() noexcept
    { reset(); }
};

Then we use that to construct our pipe or socket pair.

#include <cerrno>
#include <system_error>


struct Pipe
{
    FileHandle receive, send;
    Pipe()
    {
        int fds[2];
        if(pipe(fds))
            throw std::system_error(errno, std::generic_category(), "pipe");
        receive.reset(fds[0]);
        send.reset(fds[1]);
    }
};

The thread then uses poll on the receive end and its signalfd.

#include <poll.h>
#include <signal.h>
#include <sys/signalfd.h>
#include <cassert>


void processOSSignals(const FileHandle& stop)
{
    sigset_t mask;
    sigemptyset(&mask);
    FileHandle sighandle{ signalfd(-1, &mask, 0) };
    if(! sighandle)
        throw std::system_error(errno, std::generic_category(), "signalfd");
    struct pollfd fds[2];
    fds[0].fd = sighandle.fd;
    fds[1].fd = stop.fd;
    fds[0].events = fds[1].events = POLLIN;
    while(true) {
        if(poll(fds, 2, -1) < 0)
            throw std::system_error(errno, std::generic_category(), "poll");
        if(fds[1].revents & POLLIN) // stop signalled
            break;
        struct signalfd_siginfo fdsi;
        // will not block
        assert(fds[0].revents != 0);
        auto readedBytes = read(sighandle.fd, &fdsi, sizeof(fdsi));
    }
}

All that remains to be done is create our various RAII classes in such an order that the write side of the pipe is closed before the thread is joined.

#include <thread>


int main()
{
    std::thread ossThread;
    Pipe stop; // declare after thread so it is destroyed first
    ossThread = std::thread(processOSSignals, std::move(stop.receive));
    programLoop();
    stop.send.close(); // also handled by destructor
    ossThread.join();
}

Other things to note:

  1. Consider switching to std::jthread so that it joins automatically even if the program loop throws an exception
  2. Depending on what your background thread does, you can also simply abandon it on program end by calling std::thread::detach
  3. If the thread may stay busy (not calling poll) for long loops, you can pair the pipe up with an std::atomic<bool> or jthread's std::stop_token to signal the stop event. That way the thread can check the flag in between loop iterations. Incidentally, your use of a plain global int was invalid as you read and write from different threads at the same time
  4. You could also use the signalfd and send a specific signal to the thread for it to quit
  • Related