SourceXR

C/C++ Cross-Reference Tool

Synchronous Signal Handling

The traditional way of dealing with signals has always been to handle them asynchronously. However there are other ways of handling signals, which can be considered as safer or more convenient to use depending on the application architecture.

This small article will show how to handle signals synchronously. Of course the goal is not to poll for signals nor be waiting for them blocked without being able to perform useful work!

We'll start by a small example of the usual asynchronous handling.

Asynchronous Signal Handling

A signal handler is setup, taking care of actions to undertake when the registered signal is fired.

When a signal is delivered to a process, it interrupts the current execution, calls the signal handler, and then resumes the execution where it was interrupted. This is the basic way to handle signals.

For example, the following code sets up a signal handler for the SIGUSR1 signal. When the signal occurs, the signal handler sets a flag and returns. It is now the responsibility of the main loop to check the flag and proceed accordingly:

#include <signal.h>
#include <iostream>

bool interrupted = false;

void sigHandler (int) {
    interrupted = true;
}

int main () {
    // register signal handler
    struct sigaction sa;
    sa.sa_handler = sigHandler;
    sigemptyset (&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction (SIGUSR1, &sa, 0);

    // the process is performing work within a loop:
    while (true) {
        run ();
        if (interrupted) {
            // handle_signal();
            std::cout << "Received USR1\n";
            interrupted = false;
        }
    }
    return 0;
}

We assume that the run() call returns when a signal is caught to be able to check the flag.

We could have performed some work within the signal handler but it is not recommended because of the so-called async-signal-unsafe calls: for example, think of a signal triggered in the middle of a memory allocation for which a lock is held (if the allocation code does not block signals). The signal handler will be locked as well if it tries allocate memory. The application will result in a deadlock.

Asynchronous signal handling is simple but as the previous example suggests, care has to be taken when handling signals (sometimes blocking signals is necessary, etc.). Fortunately, there are ways of handling signals synchronously.

Synchronous Signal Handling

In that case, the execution of the signal handler code is not performed in the middle of the execution of the process code but rather when the process chooses so. We describe here two ways to synchronously handle signals.

sigwait

In a multithreaded process, signal handlers are shared between threads. Signal masks are set for each thread individually, but if some of the threads are using the same mask, any of them will receive the signal.

A convenient way of handling signals is to dedicate one thread to synchronously handle the signals the process is watching. That is, this thread is only doing one thing: receiving signals and handling them (possibly delegating their processing to other threads). This is done through the sigwait() call, which, as its name suggests, waits for signals.

Once a signal is received, sigwait returns with the received signal (we omitted here the code related to the work performed by the main thread, as well as the termination of the signal handling thread):

#include <signal.h>
#include <pthread.h> // link with -lpthread
#include <iostream>
#include <string.h>

sigset_t mask;

void *signalHandlerThread(void *) {
    while (true) {
        int signo = 0;
        // sigwait() wait for USR1 only (as set in the signal mask)
        sigwait (&mask, &signo);
        switch (signo) {
        case SIGUSR1:
            // handle_signal();
            std::cout << "Received USR1\n";
            break;
        }
    }
    return 0;
}

int main () {

    // set up signal mask
    sigemptyset (&mask);
    sigaddset (&mask, SIGUSR1);
    // USR1 is now blocked for all threads in the process
    pthread_sigmask (SIG_BLOCK, &mask, 0);

    // create signal handler thread
    pthread_t id;
    int r = pthread_create (&id, 0, signalHandlerThread, 0);
    if (r != 0) {
        std::cerr << strerror (r) << "\n";
        return 1;
    }

    // the process is performing work within a loop:
    while (true) {
        run ();
    }

    pthread_join (id, 0);
    return 0;
}

The disadvantage of sigwait is that it uses a thread to handle signals (it can be an advantage too!). In order to simplify things a little bit, we would have liked to be notified of a signal like we are notified when there is an event on a socket for example. This is where another way of handling signals comes to play.

signalfd

This is a system call introduced in the Linux kernel as of 2.6.25. It is not strictly speaking reserved for multithreaded processes and can be used in singlethreaded processes as well. The idea is here as well to replace the asynchronous delivery of signals by a synchronous one like sigwait.

Here, a file descriptor is obtained to be notified on a given set of signals. Then the file descriptor is registered into a reactor (select, poll, or epoll). Whenever a signal is to be delivered to the process, the file descriptor is readable and the reactor returns its availability. It can then be handled like any other event.

Here is an example of the signalfd use, demultiplexed with epoll:

#include <sys/signalfd.h>
#include <sys/epoll.h>
#include <iostream>
#include <signal.h>
#include <errno.h>
#include <string.h>

int main () {
    sigset_t mask;
    sigemptyset (&mask);
    sigaddset (&mask, SIGUSR1);

    // block watched signals: otherwise the default handler is called
    int i = sigprocmask (SIG_BLOCK, &mask, 0);
    if (i == -1) {
        std::cerr << strerror (errno) << "\n";
        return 1;
    }

    int fd = signalfd (-1, &mask, SFD_NONBLOCK);

    const int size = 10;
    int epollfd = epoll_create (size);
    if (epollfd == -1) {
        std::cerr << strerror (errno) << "\n";
        return 1;
    }

    epoll_event event;
    epoll_event events[size];

    // add signalfd to reactor
    event.events = EPOLLIN; // notification is a read event
    event.data.fd = fd; // user data
    int r = epoll_ctl (epollfd, EPOLL_CTL_ADD, fd, &event);
    if (r == -1) {
        std::cerr << strerror (errno) << "\n";
        close (epollfd);
        close (fd);
        return 1;
    }

    // watch possibly other fds
    // ...

    while (true) {

        const int infinity = -1;
        // and wait for events
        int r = epoll_wait (epollfd, events, size, infinity);
        if (r == -1) {
            close (epollfd);
            close (fd);
            return 1;
        }

        // demultiplex events
        int i = 0;
        while (i < r) {
            if (events[i].data.fd == fd) {
                // signal is pending
                const size_t s = 32;
                signalfd_siginfo siginfo[s];
                int i = read (fd, siginfo, sizeof (signalfd_siginfo) * s);
                if (i != -1) {
                    int k = 0;
                    const int j = i / sizeof (signalfd_siginfo);
                    while (k < j) {
                        // additional fields are available in the structure if needed
                        if (siginfo[k].ssi_signo == SIGUSR1) {
                            std::cout << "Received USR1\n";
                            // handle_signal ();
                        }
                        ++k;
                    }
                }
            }
            ++i;
        }
    }
    close (epollfd);
    close (fd);
    return 0;
}

signalfd can be used to register one or more signals. If the signal mask is to be changed, signalfd can be called with the first argument corresponding to a previous signalfd call.

signalfd provides a very convenient way to handle signals, especially for application built around an event loop (such as network applications), since handling is performed the same way as other events.

Comments !