Handling UNIX/POSIX signals is hard. If you don’t know that they’re hard, then you aren’t paying attention. In Linux there’s a system call signalfd() that makes handling them much less error-prone and fits right in to a poll()-based event-loop. This article demonstrates why signals are hard, and how to use signalfd(2) with poll(2).[1]

The Problem

Why are signals hard? Suppose that a thread has mutex `m‘ locked. When your process receives the signal, the thread is interrupted to handle the signal. In the signal handler, you try to lock `m‘. Your program just deadlocked. The signal handler waits until `m‘ unlocked. But `m‘ is never unlocked because the thread holding the lock was suspended to handle the signal. In contrast, a multi-threaded program wouldn’t usually deadlock because both threads are still active (running).

Thus, signals programming is harder than multi-threaded programming.

If you read the man page for signal(7), it contains a list of functions that POSIX requires be “async-safe.” In other words, reentrant functions that don’t lock mutexes or semaphores. Functions that are safe to call from from a signal handler. If a function is not on the list, it could lead to deadlocks and undefined behavior. Note that functions like fprintf(), snprintf(), syslog(), etc. are not on the list.

Also, since you can’t lock a mutex, manipulating any global variables in your program is hard. However, it’s common for programmers to ignore this and illicitly do dangerous things in signal handlers (lock mutexes, use an un-safe function, write to variables without regard for atomicity). Usually they get away with it because signals are usually for exceptional (rare) situations — so they usually don’t hit the error space.

But after a developer learns how dangerous it is to do anything useful in a signal handler,[2] it’s not uncommon to see something like this:

volatile sig_atomic_t shutdown_program;

void signal_handler(int sig)
{
        if (sig == SIGTERM)
                shutdown_program = 1;
}

…and then let your program’s main loop poll the variable regularly to catch the shutdown signal. (See glibc’s recommendation)

Using signalfd(2) as a solution

The man page for signal(7) lists some synchronous solutions to this problem:

  1. sigwaitinfo(2), sigtimedwait(2), sigwait(3) – These suspend execution until a signal is received.  This is useful (for instance) if you have real work being done on threads, and you use your main() function exclusively to handle signals.
  2. signalfd(2) – Provides a file descriptor that will provide information about the signals when they occur.

The reason why I think signalfd(2) is cool is that you can make the file descriptor generate events for your main event loop. For example, if you use poll(2), ppoll(2), select(2), pselect(2), epoll(7), or the GLib GMainLoop… it’s easy to add file-descriptor-bound events to those loops.

Example using poll(2)

The code that follows is a simple example of how to use signalfd(2) with poll(2).  (Note: the man page for signalfd usually includes an example where you use read(2) to block until you get a signal.)  The procedure is as follows:

  1. Block the signals that you’re interested in. (Yes, block them.)
  2. Create the signalfd for the signals that you’re interested in.  (If you don’t block them, then signalfd won’t receive them.)
  3. When the file descriptor is ready to be read, read a `signalfd_siginfo’ structure from the file descriptor to get info about the signal.

Here’s an example:

/* Simple program using signalfd to watch for SIGINT
 * Compile: gcc -o main main.c
 * Execute: ./main
 * Warning: You must use kill(1) to terminate the program.
 *     Ctrl+C won't terminate it.
 */
#include <assert.h>
#include <poll.h>
#include <signal.h>
#include <stdio.h>
#include <sys/signalfd.h>
#include <unistd.h>

#define NFDS 1

int main(int argc, char* argv[])
{
        int err;
        sigset_t sigset;
        int fd;

        /* Create a sigset of all the signals that we're interested in */
        err = sigemptyset(&sigset);
        assert(err == 0);
        err = sigaddset(&sigset, SIGINT);
        assert(err == 0);
        err = sigaddset(&sigset, SIGHUP);
        assert(err == 0);

        /* We must block the signals in order for signalfd to receive them */
        err = sigprocmask(SIG_BLOCK, &sigset, NULL);
        assert(err == 0);

        /* Create the signalfd */
        fd = signalfd(-1, &sigset, 0);
        assert(fd != -1);

        /* This is the main loop */
        struct pollfd pfd[NFDS];
        int ret;
        ssize_t bytes;

        pfd[0].fd = fd;
        pfd[0].events = POLLIN | POLLERR | POLLHUP;

        for (;;) {
                printf("Waiting.\n");
                ret = poll(pfd, NFDS, -1);
                printf("I'm awake!\n");

                /* Bail on errors (for simplicity) */
                assert(ret > 0);
                assert(pfd[0].revents & POLLIN);

                /* We have a valid signal, read the info from the fd */
                struct signalfd_siginfo info;
                bytes = read(fd, &info, sizeof(info));
                assert(bytes == sizeof(info));

                unsigned sig = info.ssi_signo;
                unsigned user = info.ssi_uid;

                if (sig == SIGINT)
                        printf("Got SIGINT from user %u\n", user);
                if (sig == SIGHUP)
                        printf("Got SIGHUP from user %u\n", user);
        }

        return 0;
}

Try it out by running it and hitting `Ctrl+C’ or sending it a signal using kill(1). Because all our handling is happening in the main thread’s context, we don’t have hidden gotchas with respect to handling the signal.


[1] – Whenever you see `foo(2)’, that’s a reference to a man page that you can view by typing `man 2 foo’. The number is like saying “I want to read about `foo’ from chapter 2.” This is important because some man pages appear in more than one chapter (e.g. signal(2), signal(7)).

[2] – More precisely: “…in a signal handler that returns.”

Advertisements