Signals

Signals are messages sent to processes to notify them of certain events. These could be sent between processes, but the most common signals are sent by the kernel. The kernel will send a signal to a process if there is a hardware issue, like an attempt to divide by zero, or to access a non-existent address in memory. The kernel will also send signals to let processes know of any condition that affects them, like the reading of a file being completed, or a terminal being closed; more importantly, if the allotted CPU time for a process has concluded and needs to be terminated or sent to a waiting state. The kernel may also notify the process with a signal if the user pressed specific combinations of keys, like ctrl-C or ctrl-\, that have a special meaning for processes as we will see below.

Signals are codified with integer numbers greater than 1. The kernel uses the first 31 signals for specific well known messages. They are known as the standard signals. Each of these standard signals has a name that begins with the prefix SIG followed by three or four other letters. All of these macros are defined in the signal.h header.

When an event generates a signal, it is delivered to the process, if the process is currently running in the CPU, it receives the signal the next time it returns from kernel mode–e.g. returning from a system call, generating a page fault, being scheduled to run, etc. In the time between the generation and the actual delivery of the signal we said that the signal is pending. Once the signal is delivered, it normally elicits a default action from the process. These default actions could be one of following five:

Ignore

Ignore the signal.

Terminate

Abnormal termination of the process

Abort

Abnormal termination of the process with additional actions (core dump).

Stop

Stop the process.

Continue

Continue the process, if it is stopped; otherwise ignore the signal.

The list of which signal corresponds to which default action can be found in signal.h.

The listing below presents a program that simply busy-waits. We can use this program to review the four signals above. When the program runs in the foreground, we can terminate it by pressing either ctrl-c or ctrl-\. In the first case the keystrokes makes the terminal send a SIGINT signal. When the program receives it, the process terminates. The second case is similar, but the signal sent is SIGQUIT, and its default action is to terminate, but creating a core dump. We see the same process running, but this time we use the ampersand (&) to send it to the background. To terminate this process, we need to use the system call kill. Despite its name, the main purpose of this system call is to send signals to processes. We can use the shell utility kill

run_forever.c
int main() { for (;;); }
$ ./run_forever
^C

$ ./run_forever
^\Quit (core dumped)

$ run_forever &
[1] 4823
$ ps
   PID TTY         TIME CMD
   2605 pts/0    00:00:00 bash
   4823 pts/0    00:00:03 run_forever
   4828 pts/0    00:00:00 ps
$ kill -15 4823
[1]+  Terminated                    run_forever
$ ps
   PID TTY         TIME CMD
   2605 pts/0    00:00:00 bash
   4837 pts/0    00:00:00 ps

Notice that when a shell process has background processes, it assigns them a job control number or JobID. In the figure above, that number is 1 surrounded by square brackets [1]. Use that number to refer to this process. We can review all jobIDs with the bash command jobs. The fg and bg commands can be used to move jobs to/from foreground or background respectively. These jobs also receive signals with the kill command. The following output demonstrates run_forever.c run three times on the shell and being handled by the these various job control commands:

Running run_forever in the background three times
$ run_forever &
[1] 3300

$ run_forever &
[2] 3394

$ run_forever &
[3] 3417

$ ps
 PID TTY TIME CMD
 2647 pts/0 00:00:00 bash
 3300 pts/0 00:00:04 run_forever
 3394 pts/0 00:00:02 run_forever
 3417 pts/0 00:00:01 run_forever
 3399 pts/0 00:00:00 ps
Running run_forever in the background three times
$ run_forever &
[1] 3300

$ run_forever &
[2] 3394

$ run_forever &
[3] 3417

$ ps
 PID TTY TIME CMD
 2647 pts/0 00:00:00 bash
 3300 pts/0 00:00:04 run_forever
 3394 pts/0 00:00:02 run_forever
 3417 pts/0 00:00:01 run_forever
 3399 pts/0 00:00:00 ps
Sending job [2] to the foreground & Stopping it with ctrl-Z.
$ fg %2
run_forever
^Z
[2]+ Stopped run_forever

$ jobs
[1] Running run_forever &
[2]+ Stopped run_forever
[3]- Running run_forever &
Sending job [2] to the background again
$ bg %2
[2]+ run_forever &

$ jobs
[1] Running run_forever &
[2]- Running run_forever &
[3]+ Running run_forever &
Stopping job [3] with the SIGSTOP signal
$ kill -STOP %3

[3]+ Stopped run_forever

$ jobs
[1] Running run_forever &
[2]- Running run_forever &
[3]+ Stopped run_forever
Stopping job [3] with the SIGSTOP signal
$ kill -STOP %3

[3]+ Stopped run_forever

$ jobs
[1] Running run_forever &
[2]- Running run_forever &
[3]+ Stopped run_forever
Terminating job [3] with the SIGTERM signal
$ kill -TERM %3
[3]+ Terminated run_forever
Terminating job [2] with the SIGINT signal
$ kill -INT %2
[2]+ Interrupt run_forever
Terminating job [1] with the SIGKILL signal
$ kill -KILL %1
[1]+ Killed run_forever

$ jobs

$

In general, a process may decide not to take the default action requested by a signal, instead it may allow for the signal to be ignored, or it may decide to do something else when it receives it. A process changing its default action under a signal is said to be changing its signal disposition. This can be done if the process invokes the signal system call from signal.h that has the following prototype:

sighandler_t signal(int sig, void (*func)(int)))(int);

This prototype can be a bit confusing to read, made clearer by first defining a sighandler_t type,

typedef sighandler_t void (*)(int);
sighandler_t signal(int sig, sighandler_t handler);

The parameters for the signal system call are the signal number to which the process wants to change its disposition and a pointer to a handler function. The process must have access to the handler function and the handler function must receive an integer and return void. The handler function will contain the actions the process wants to be executed when the signal arrives. A common use of the handler function is to tidy up before the process is terminated, by returning resources, like memory previously requested with malloc, or close files, before calling the exit system call itself.

The following listing shows the program signal.c that changes the signal disposition for the SIGINT, SIGQUIT and SIGTERM signals using the signal system call to the handler function named sig_handler that stores the signal number that was received. We will discuss the purpose of sigprocmask and the sigset_t variables in short order.

signal.c
#define _POSIX_C_SOURCE 200809L
#include <err.h>
#include <errno.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

/* sig_atomic_t is an async-signal safe data type */
static sig_atomic_t volatile signo = 0;

/* Signal sets for blocking/unblocking signals. */
sigset_t sigset, oldsigset;

/** Record received signal handler */
static void
sig_handler(int sig)
{
  int errno_sav = errno; /* Cache errno */
  sigprocmask(SIG_BLOCK, &sigset, 0);
  signo = sig;
  signal(sig, sig_handler); /* Reinstall signal handler */
  errno = errno_sav;        /* Restore errno */
}

int
main(int argc, char *argv[])
{
  /* Hold pending signals until their handlers are installed */
  sigfillset(&sigset);
  sigprocmask(SIG_BLOCK, &sigset, &oldsigset);

  /* Install signal handlers SIGINT, SIGQUIT and SIGTERM */
  signal(SIGINT, sig_handler);
  signal(SIGQUIT, sig_handler);
  signal(SIGTSTP, sig_handler);

  int last_signo = signo;
  for (;;) {
    /* Unblock signals and sleep until a signal is caught */
    sigsuspend(&oldsigset);
    errno = 0;
    printf("\tSignal Received: %d (\"%s\")\n", signo, strsignal(signo));
    switch (signo) {
      case SIGINT:
        printf("I was, in fact, interrupted!\n");
        break;
      case SIGQUIT:
        printf("Ok, time to exit!\n");
        exit(0);
      case SIGTSTP:
        printf("Understood--powering down!");
        fflush(stdout);
        raise(SIGSTOP);
        break;
    }
    last_signo = signo;
  }
}
$ ./signal
^C	Signal Received: 2 ("Interrupt")
I was, in fact, interrupted!
^Z	Signal Received: 20 ("Stopped")
Understood--powering down!
[2]+  Stopped                 ./signal

$ fg
./signal
^\	Signal Received: 3 ("Quit")
Ok, time to exit!

$

Although by the description of the signal system call, it appears to be returning void, it is in fact returning a pointer to the previous disposition the signal had. This is handy when we want to preserve the previous handling conditions to be restored at a later point in time. The following listing demonstrates a program that alternates between the sig_handler handler function we previously saw, and the default handling for the SIGINT signal (ctrl-C). When the program invokes the signal system call to set the disposition to sig_handler for the SIGINT signal, it returns the default handling of this signal that is stored in a pointer named old_sig_handler, with the same signature as the sig_handler function. The old_sig_handler function is later restored with another call to signal. In the output, we see that while sig_handler is in use, the process cannot be terminated with ctrl-C, but this behavior stops when the old_sig_handler is in charge.

swapping_handlers.c
#define _POSIX_C_SOURCE 200809L
#include <err.h>
#include <errno.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

/* sig_atomic_t is an async-signal safe data type */
static sig_atomic_t volatile signo = 0;

/* Signal sets for blocking/unblocking signals. */
sigset_t sigset, oldsigset;

/* Pointer to a signal handler function */
static void (*old_sig_handler)(int);

/** Record received signal handler */
static void
sig_handler(int sig)
{
  int errno_sav = errno; /* Cache errno */
  sigprocmask(SIG_BLOCK, &sigset, 0);
  signo = sig;
  signal(sig, old_sig_handler); /* Revert signal handler */
  errno = errno_sav;            /* Restore errno */
}

int
main(int argc, char *argv[])
{
  /* Hold pending signals until their handlers are installed */
  sigfillset(&sigset);
  sigprocmask(SIG_BLOCK, &sigset, &oldsigset);

  /* Install signal handler for SIGINT */
  old_sig_handler = signal(SIGINT, sig_handler);
  printf("signal handler for SIGINT is active\n");
  fflush(stdout);

  int last_signo = signo;
  for (;;) {
    /* Unblock signals and sleep until a signal is caught */
    sigsuspend(&oldsigset);
    printf("\tSignal Received: %d (\"%s\")\n", signo, strsignal(signo));
    fflush(stdout);
  }
}
swapping_handlers output
$ ./swapping_handlers
signal handler for SIGINT is active
^C	Signal Received: 2 ("Interrupt")
^C

$

A process may also block the delivery of a signal. This will do nothing to the signal, but it may allow the process to complete some activity that must be done uninterrupted by the disposition of the signal. To block a signal, the process keeps track of a blocked set, this a set of signals with data type sigset_t that collects the numbers of all signals to be blocked by the process. A signal will remain blocked until it is released, at which point is delivered.

The blocked set must be initialized as empty with the sigemptyset system call. To which we must add all the signals we wish to block, one by one, with the sigaddset system call. We will also need the sigprocmask system call to manage the blocked set. The prototypes for these system calls are:

int sigemptyset(sigset_t *set);
int sigaddset(sigset_t *set, int sig);
int sigprocmask(int how, sigset_t const *set, sigset_t *oldset);

The following listing shows a program that alternates between blocking and unblocking signals,

blocking_signals.c
#include <errno.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <unistd.h>

char const *sig_names[] = {
    [1] = "SIGHUP",       [2] = "SIGINT",       [3] = "SIGTSTP",
    [4] = "SIGILL",       [5] = "SIGTRAP",      [6] = "SIGABRT",
    [7] = "SIGBUS",       [8] = "SIGFPE",       [9] = "SIGKILL",
    [10] = "SIGUSR1",     [11] = "SIGSEGV",     [12] = "SIGUSR2",
    [13] = "SIGPIPE",     [14] = "SIGALRM",     [15] = "SIGTERM",
    [16] = "SIGSTKFLT",   [17] = "SIGCHLD",     [18] = "SIGCONT",
    [19] = "SIGSTOP",     [20] = "SIGTSTP",     [21] = "SIGTTIN",
    [22] = "SIGTTOU",     [23] = "SIGURG",      [24] = "SIGXCPU",
    [25] = "SIGXFSZ",     [26] = "SIGVTALRM",   [27] = "SIGPROF",
    [28] = "SIGWINCH",    [29] = "SIGIO",       [30] = "SIGPWR",
    [31] = "SIGSYS",      [34] = "SIGRTMIN",    [35] = "SIGRTMIN+1",
    [36] = "SIGRTMIN+2",  [37] = "SIGRTMIN+3",  [38] = "SIGRTMIN+4",
    [39] = "SIGRTMIN+5",  [40] = "SIGRTMIN+6",  [41] = "SIGRTMIN+7",
    [42] = "SIGRTMIN+8",  [43] = "SIGRTMIN+9",  [44] = "SIGRTMIN+10",
    [45] = "SIGRTMIN+11", [46] = "SIGRTMIN+12", [47] = "SIGRTMIN+13",
    [48] = "SIGRTMIN+14", [49] = "SIGRTMIN+15", [50] = "SIGRTMAX-14",
    [51] = "SIGRTMAX-13", [52] = "SIGRTMAX-12", [53] = "SIGRTMAX-11",
    [54] = "SIGRTMAX-10", [55] = "SIGRTMAX-9",  [56] = "SIGRTMAX-8",
    [57] = "SIGRTMAX-7",  [58] = "SIGRTMAX-6",  [59] = "SIGRTMAX-5",
    [60] = "SIGRTMAX-4",  [61] = "SIGRTMAX-3",  [62] = "SIGRTMAX-2",
    [63] = "SIGRTMAX-1",  [64] = "SIGRTMAX",
};

/* sig_atomic_t is an async-signal safe data type */
static sig_atomic_t volatile signo = 0;

/* Signal sets for blocking/unblocking signals. */
sigset_t sigset, old_sigset;

/** Record received signal handler */
static void
sig_handler(int sig)
{
  int errno_sav = errno; /* Cache errno */
  sigprocmask(SIG_BLOCK, &sigset, 0);
  signo = sig;
  signal(sig, sig_handler); /* Reinstall signal handler */
  errno = errno_sav;        /* Restore errno */
}

int
main(int argc, char *argv[])
{
  /* Initializing Blocked set of signals */
  sigemptyset(&sigset);        
  sigaddset(&sigset, SIGINT);  
  sigaddset(&sigset, SIGTSTP); 
  sigaddset(&sigset, SIGALRM); 

  /* Changing the disposition of SIGINT and SIGTSTP */
  signal(SIGINT, sig_handler);
  signal(SIGTSTP, sig_handler);

  /* Store old signal mask */
  sigprocmask(0, 0, &old_sigset);

  for (;;) { // Loop oscillates between two states
    printf("Blocking signals (5 second sleep)\n");
    sigprocmask(SIG_BLOCK, &sigset, 0);

    /* Wait for a signal to become pending */
    struct timespec timeout = {.tv_sec = 5};
    signo = sigtimedwait(&sigset, 0, &timeout);
    if (signo < 0) errno = 0; /* Reset errno on timeout */

    printf("\tSignal %d (%s) was pending!\n", signo, sig_names[signo]);

    printf("Unblocking signals (5 second sleep)\n");
    alarm(5);

    /* Wait for a signal to be delivered */
    sigsuspend(&old_sigset);
    errno = 0; /* sigsuspend always sets errno to EINTR */
    printf("\tSignal %d (%s) was caught!\n", signo, sig_names[signo]);
  }
}
blocking_signals output
$ ./blocking_signals 
Blocking signals (5 second sleep)
^C	Signal 2 (SIGINT) was pending!
Unblocking signals (5 second sleep)
^Z	Signal 20 (SIGTSTP) was caught!
Blocking signals (5 second sleep)
^C	Signal 2 (SIGINT) was pending!
Unblocking signals (5 second sleep)
^Z	Signal 20 (SIGTSTP) was caught!
Blocking signals (5 second sleep)
^C	Signal 2 (SIGINT) was pending!
Unblocking signals (5 second sleep)
^Z	Signal 20 (SIGTSTP) was caught!
Blocking signals (5 second sleep)
^\Quit (core dumped)

$ 

The sigprocmask system call is used to enable and disable a blocking set. It takes a first parameter (named how in the prototype) to indicate the action to be taken. This parameter could be one of the following values:

  • SIG_BLOCK to add a set of signals to be blocked

  • SIG_UNBLOCK to remove a set of signals from the blocked set

  • SIG_SETMASK to replace an entire set of blocked signals with another set

In state 1 of the loop inside the program, we first use sigprogmask to add the signals in the blocked_set to the set of signals currently blocked. The old set of signals is returned in the default_set set of signals. In our case, this defaul_set has no signals blocked. In state 2 of the loop, sigprogmask is used to replace the set of blocked signals with the default_set that was saved with the SIG_SETMASK parameter. Alternatively, it could just unblock the same signals with the SIG_UNBLOCK parameter. The general effect of this loop is that initially it blocks the SIGINT and SIGQUIT signals, as can be seen in the bottom cell of the picture. Once the old set of blocked signals (default_set) is restored in state 2 of the loop, SIGINT and SIGQUIT are no longer blocked and they are delivered. The program uses a handler function that displays the signal that is received first, and stops the program naturally with exit(0).

Before entering into state 2, the program reports which signals were pending. To do so, it uses the sigpending and sigismember system calls that have the following prototypes:

int sigpending(sigset_t *set);

The sigpending system call retrieves a set of signals that are pending. As it can be seen in the bottom cell of the previous figure, the user pressed ctrl-C and ctrl-\ twice each, these produce the SIGINT and SIGQUIT signals that are part of the pending signals set retrieved by the sigpending system call (pending_set). To verify that a signal is part of a set we use the sigismember system call. In our example, it is separately checking if the SIGINT and SIGQUIT signals are members of the pending_set set of signals, and reporting on it. Notice here that the pending set only reports once per every different signal received. In other words, it does not matter how many times a signal is sent, the pending signal set will only contain one acknowledgment per each kind of signal.

It is important to emphasize that the SIGKILL signal cannot be blocked, for the same reason that it cannot be ignored or handled. This signal guarantees termination of a process, and, therefore the process cannot do anything about it. On the other hand, it should only be used as a last resort, because if a process uses a handler to have a clean exit, without resources left open or engaged, when the SIGKILL signal is received, the process will not be able to use the handler at all.