Search code examples
c++linuxmultithreadingsignals

OS Signal handling loop - blocking or non-blocking read?


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?


Solution

  • 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