Basics of signals

From the earliest days of the Unix operating system the operating system has had the ability to send signals to running processes. When the operating system sends a signal to a process the operating system will suspend the normal operation of the process and give the process a chance to catch and handle the signal. Processes catch and handle signals by running special signal handler routines. After the signal handler exits the operating system will restart the process at the point where the process was originally interrupted.

There a number of different signals that the operating system can send. Each signal type is identified by a numeric identifier. You can find a complete table of signal types for the Linux operating system along with other important details about signals in Linux at the signals overview page at man7.org.

Signals differ in their level of severity, as well as whether or not the signal can be caught and handled. In the table on the page I linked to you will find information about the default action for each signal. The default action is what will happen if the signal is not caught and handled by the application. The default action for many signal types is to terminate the process immediately. In a few cases the default action is to simply ignore the signal. Also, a number of the more severe signal types, such as SIGKILL, can not be caught and handled. These signal types instead cause the application to immediately terminate.

In many cases the operating system will generate a signal in response to an error condition. The most obvious examples of these kinds of signals include SIGSEGV (illegal memory access) and SIGFPE (division by 0 or other illegal arithmetic operation).

Users can also generate signals by typing an interrupt sequence in a terminal session. Examples of signals that users can send include SIGINT (control-c), SIGQUIT (control-\), and SIGTSTP (control-z).

Another method used to send a signal to a running application is to run the kill command in a terminal. For example, to send the signal SIGINT to a process whose process id is 1706 you would run

kill -SIGINT 1706

The kill command also accepts numeric codes for signals in place of the signal name. An alternative method to send the SIGINT signal to process 1706 is to do

kill -2 1706

We have also seen that an application can send a signal to another application by using the kill() system call. For example, to send the SIGINT signal to process 1706 we would do

#include <signal.h>
kill(1706,SIGINT);

Installing a signal handler

To be able to catch and respond to a signal an application must install a signal handler for that signal. The code example below is an application that will catch and respond to the SIGTSTP signal.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

int count = 0;

void handler(int sig)
{
  if(count == 2) {
    printf("Too many interruptions! I quit!\n");
    exit(1);
  } else {
    count++;
    printf("I have been interrupted %d times.\n",count);
  }
}

int main(int argc, char *argv[])
{
    if (signal(SIGTSTP, handler) == SIG_ERR) {
      printf("Error: could not install handler.\n");
      exit(1);
    }

    while(1) {
        printf("Hello!\n");
        sleep(1);
    }
}

The signal handler function has to be a function that returns void and accepts a single integer parameter. That parameter is the id number for signal being handled.

To install a signal handler we call the signal() function. The first parameter is the signal number the function will handle and the second is the function we want to use as a signal handler.

Masking signals

One problem with signal handlers is that they can sometimes run at an inconvenient time. In the example above there is a tiny, but nonzero, probability that the signal will arrive when the application is in the middle of printing the "Hello!" message. Having the signal handler run and print its message to the console in the middle of printing the "Hello!" message is awkward, so we might want to avoid this.

The solution is to temporarily block signals. We do this by setting up a signal mask that lists a set of signals that we want to block. As long as the mask is set, any masked signals sent to the application will be held by the operating system. Once the mask is removed, any pending signals that were temporarily blocked will be immediately delivered to the application.

The next example demonstrates how to set a mask to block signals temporarily.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

int count = 0;

void handler(int sig)
{
  if(count == 2) {
    printf("Too many interruptions! I quit!\n");
    exit(1);
  }
    count++;
    printf("I have been interrupted %d times.\n",count);
}

int main(int argc, char *argv[])
{
    if (signal(SIGTSTP, handler) == SIG_ERR) {
      printf("Error: could not install handler.\n");
      exit(1);
    }

    sigset_t prevMask, blockMask;
    sigemptyset(&blockMask);
    sigaddset(&blockMask,SIGTSTP);

    while(1) {
        if (sigprocmask(SIG_SETMASK, &blockMask, &prevMask) == -1) {
            printf("Error setting mask.\n");
            exit(1);
        }
        printf("Hello!\n");
        sleep(3);
        if (sigprocmask(SIG_SETMASK, &prevMask, NULL) == -1) {
            printf("Error setting mask.\n");
            exit(1);
        }
        sleep(3);
    }
}

To mask one or more signals, we start by creating a signal set, which is represented by a structure of type sigset_t. To empty the set and then add a single signal type to it we use a combination of the sigemptyset() and sigaddset() functions.

To install our masking set temporarily we call sigprocmask(). The second and third parameters to sigprocmask() are pointers to two mask sets: the new set we want to install, and a second set in which to save the previous mask settings. The usual convention to follow is to install our custom mask set while saving the old set before doing anything critical that would require us to temporarily suspend the delivery of signals, and then reinstall the original mask set when we leave the critical section.

Threads and signals

Unix signals were originally designed at a time when all applications were single-threaded applications. Adding threads to the mix complicates the picture. Here are some things to know about the interaction of threads and signals:

Here is an example program that demonstrates a commonly used strategy for mixing threads and signals. The example program launches a group of threads that are set up to run forever. After launching the threads the main program enters a wait state that will get terminated when the application receives the SIGINT signal. When the program receives that signal the main routine will shut down all of the threads and exit.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <pthread.h>

static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void safe_print(char* message) {
    pthread_mutex_lock(&mutex);
    printf("%s",message);
    pthread_mutex_unlock(&mutex);
}

void* thread_print(void* arg) {
    int num = *((int*) arg);
    char msg[32];
    sprintf(msg,"Thread %d\n",num);
    while(1) {
        safe_print(msg);
        sleep(2);
    }
}

int main() {
    sigset_t set;
    pthread_t threads[3];
    int nums[3];
    int n;

    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    pthread_sigmask(SIG_BLOCK, &set, NULL);
    for(n = 0;n < 3;n++) {
        nums[n] = n;
        pthread_create(&(threads[n]), NULL, thread_print, (void *) &(nums[n]));
    }

    int sig;
    sigwait(&set,&sig);

    safe_print("Exiting!\n");
    pthread_mutex_lock(&mutex);
    for(n = 0;n < 3;n++)
        pthread_cancel(threads[n]);
    return 0;
}

To ensure that none of the threads will receive the SIGINT signal when it arrives, we mask out that signal for all of the threads by using pthread_sigmask(). We pass this function a signal set containing only the SIGINT signal type. After launching the threads the main program calls sigwait() with the same signal set. The sigwait() function will block until a signal in the mask set arrives. After receiving that signal the application will call pthread_cancel() on each of the running threads to shut them down, and then exit.