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
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:
$ 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
$ 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
$ fg %2
run_forever
^Z
[2]+ Stopped run_forever
$ jobs
[1] Running run_forever &
[2]+ Stopped run_forever
[3]- Running run_forever &
$ bg %2
[2]+ run_forever &
$ jobs
[1] Running run_forever &
[2]- Running run_forever &
[3]+ Running run_forever &
$ kill -STOP %3
[3]+ Stopped run_forever
$ jobs
[1] Running run_forever &
[2]- Running run_forever &
[3]+ Stopped run_forever
$ kill -STOP %3
[3]+ Stopped run_forever
$ jobs
[1] Running run_forever &
[2]- Running run_forever &
[3]+ Stopped run_forever
$ kill -TERM %3
[3]+ Terminated run_forever
$ kill -INT %2
[2]+ Interrupt run_forever
$ 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.
#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.
#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
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,
#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
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 blockedSIG_UNBLOCK
to remove a set of signals from the blocked setSIG_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.