A Process can be interrupted with various types of signals. This interruption can occur in the middle of most code.

Purely implemented in software, supported by the hardware mentioned above. The operating system forwards the signal to the correct process.

  • Signals apply to a process. The kernel (aka the OS) or some other process delivers that signal.
  • Each signal has a different meaning, number, and way of handling it.

Every signal has a current disposition (aka handler), or the “natural” way a process responds to the arrival of a signal. We can also define custom signal dispositions by writing our own signal handler.

Signals can interrupt other signals.

Changing how a signal is handled

int sigaction(int signum, struct sigaction* act, struct sigaction* old);

Specify the signal number, gives the signal a new handler, and contains the previous behavior as an output parameter in old.

SIGKILL and SIGSTOP cannot have their handler changed and therefore always perform their default behaviors, which is… to kill and stop the process respectively.

Signal handlers

A function that takes in a parameter, the signal number that raised the handler, return type is void.

  • Automatically called when process is interrupted by signal
  • Can manipulate global state
  • Signal handlers set by a process will be retained by children

Making changes to handlers and signal masks within a signal handler

Note that the OS implicitly resets the signal disposition and signal block mask to what it was before the handler was invoked. So after we exit the handler, any changes are not saved!

For more special behavior related to triggering handlers, see below.

Sending signals

int kill(pid_t pid, int sig);

Despite its name, this call allows us to send specific signals to a specific process (not just SIGKILL).

Sending signals between processes

Signals are sent from process to process via the operating system. It would maybe be more efficient for a process to directly send the signal, but this poses major security concerns.

The OS enforces whether a process has the permission required to send a signal. Operating systems are a protection system for processes.

Signal blocking and signal masks

A process maintains a set of signals called a signal mask. Signals in that set/mask are “blocked” meaning they’re delayed in being delivered to the process. Once unblocked, the process responds to the signals accordingly.

A signal mask is implemented as a C bitset. It is literally a set; sending multiple of the same blocked signal only results in the handler being called once after it’s unblocked.

By default, the signal that triggers the handler is also blocked until we finish executing the handler and exit. This can be changed with the flag SA_NODEFER.

sigset_t

Fortunately, someone has already implemented this signal mask for us! sigset_t is a typedef type that represents a signal bitset that we can manipulate.

Important

A sigset_t variable must first be initialized by calling sigemptyset() before being used.

We manipulate signal sets (adding signals, removing them, etc.) using sigprocmask().

  • To see the current signal mask, we can pass in NULL for the new set, and call it like so: sigprocmask(0, NULL, &previous_set). This gives us the current mask in previous_set.

Masking a function

In critical sections we don’t want signals to interrupt functions in the middle of execution. We can update the signal mask so signals are blocked before we start running function code.

  1. Set up appropriate mask
  2. Block those signals (in addition to those perhaps previously blocked), and store the previous signal mask somewhere
  3. Set the mask using sigprocmask()
  4. After we finish executing function body, we restore the previous signal mask.