Receiving Signals

When a signal is sent to a process, it is added to a set of pending signals for that process. What happens next is determined by whether the process has blocked that signal by adding it to its signal mask using sigprocmask(). Before returning from kernel mode, such as when returning from a system call or the scheduler, the kernel first checks the set of pending signals against the signal mask for that process. Below is an example of blocking and unblocking a signal with sigprocmask():

sigset_t s;
sigemptyset(&s);
sigaddset(&s, SIGINT);
sigprocmask(SIG_BLOCK, &s, 0);
/* SIGINT is now blocked */

sigprocmask(SIG_UNBLOCK, &s, 0);
/* SIGINT is now unblocked */

If an unblocked signal is pending, the kernel selects one and delivers it to the process. A process can configure what occurs when a signal is delivered to it, by setting that signal’s disposition. These dispositions include ignoring the signal; applying a default action associated with that particular signal, such as terminating the program, stopping it, or aborting it; or calling a custom signal handler function that the process previously associated with that signal. This process is called asynchronous signal delivery, because signal delivery interrupts normal program flow to execute special signal handling behaviors. Processes can use the simple C library method signal() to set the disposition of a signal, or use the more powerful POSIX method sigaction() which gives fine-grained control over how a signal is handled, and allows a process to save and restore signal dispositions.

Signal handlers are functions with the signature void f(int signum), passed as a function-pointer argument to signal(), or as the .sa_handler member of the struct sigaction object passed to sigaction(). For example, the following code snippet demonstrates how a signal handler would be registered both signal() and sigaction().

void sig_handler(int signum);
signal(SIGINT, sig_handler);

struct sigaction sa = {.sa_handler = sig_handler};
sigaction(SIGINT, &sa, 0);

Processes can also choose to accept signals, by waiting for any of a subset of its blocked signals to become pending with the sigwait() method. When a process accepts a blocked signal in this way, the pending signal is removed from the pending signal set, and the process is able to handle the signal without interrupting normal program flow, thus this process is called synchronous signal handling. A process may also first poll if any signals are pending, using sigpending() before waiting on them, so that it can continue with some other task and check back later on its pending signals without stopping to wait for one to arrive. This method of signal handling is called synchronous signal handling, because the process is in control of when and how it responds to a signal.

Synchronous signal handling is generally less prone to programming mistakes since the flow of the program is easier to reason about, versus asynchronous signal handling where a signal could be delivered at any machine instruction boundary which can be between sequence points. This means the program may not be in a settled state with well-defined object values when a signal handler is triggered, and accessing or modifying the program state can cause serious errors–actually, doing so is generally considered undefined behavior!

For this reason, synchronous signal handling is usually preferred. However, there are common situations where asynchronous signal handling is necessary or desirable. In particular, this is the case when a process needs to respond to signals that are received while the process is in an interruptible sleep state. Processes enter interruptible sleep when they execute certain system calls that block while waiting for an event to occur. For example, the read() system call can place the process in an interruptible sleep while waiting for additional data from a slow device. If a deliverable signal is received during an interruptible sleep, the process immediately wakes back up and executes the signal handler or default signal action. The interrupted system call will either be resumed, or return an error code EINTR (“Interrupted System Call”). This allows a process to respond to signals as quickly as possible.

The basic set of signal handling related functions are listed below,

kill()

Send a signal to a process or process group. Processes can send signals to themselves and to their own process groups as well.

raise()

Raise a signal in the caller, synchronously performing the signal’s action.

signal() & sigaction()

Modify a signal’s disposition. sigaction() is a POSIX extension that gives processes more fine-grained control compared to signal().

sigsuspend()

Wait for a signal in a given signal set to be delivered.

sigprocmask()

Modify the signal mask of a process. Masked signals are blocked from being delivered, and remain pending.

sigpending()

Retrieve the pending signal set.

sigwait()

Wait for a signal in a given signal set to be pending. Returns the pending signal, which is removed from this set of pending signals.

sigemptyset(), sigfillset(), sigaddset(), sigdelset() & sigismember()

Manipulate and query signal set data type sigset_t.