One signal, two signal(s), red signal, blue signal — xkcd

Journey into the machine: Signals

What is a signal

A signal is a very short message delivered to a process to notify an event occurred in the system. They were introduced by the first Unix systems, to allow an inter process communication.

Signals are actually numbers, each type of signal is assigned a small integer starting from 1. In Linux, signals are identified by macros all starting with SIG. There are 31 Linux signals, called regular signals (the number is defined by NSIG in signal.h). Users can also define their own signals, so called realtime signals. Their behavior is slightly different, and they will not be considered here. man 7 signal gives a list of signals, the default action and corresponding event.

Who sends signals

  • A process (with suitable permissions) can send a signal to another process (e.g. function kill).
  • A process can send a signal to itself (e.g. functions raise, abort).
  • The kernel can send signals. For example, it can relay exceptions coming from the hardware, such as an illegal instruction (SIGILL), an invalid memory reference (SIGSEGV), or a floating point exception (like division by 0 SIGFPE). It can also relay keyboard inputs such as ctrl + c (SIGINT), or notify a child process has terminated (SIGCHLD).

Signal Delivery

Once a signal is sent, it becomes pending. It is delivered to a process as soon as the process resumes execution. However, a process may block a signal (see man sigprogmask, man sigsetops and sigset_t struct). In that case the signal is delivered as soon as it is unblocked. Blocked signals are stored in the process’ signal mask. At any time, at most one signal of a given type can be pending for a process. Additional signals will be discarded.

Signal Reception

The kernel forces a process to receive and react to a signal. The process can use either the signal’s default action, or change the disposition for that signal through a signal handler.

The default actions are:

  • the signal is ignored,
  • the process is terminated,
  • a core dump is generated and the process is terminated,
  • the process is suspended (stopped) until a signal resumes it,
  • the process is resumed.

The signals SIGKILL and SIGSTOP cannot be caught, blocked or handled.

Signal Handlers

Here is the general anatomy:

Let’s look at one example, using google breakpad. Breakpad contains a library that will record crashes as minidumps.

A default action for some signals is to generate a core dump and terminate the process. A core dump is a file containing a memory image of the process at the time it terminated [1]. For a (long) time it never occurred to me reading core dumped on my terminal meant a file was supposed to be written. It was not created because the ulimit -c on my machine was set to 0. For more information: man core .

The problem with core dumps is they can get really large, so not very practical to send across network. The minidump format, devised by Windows only captures the stack of the crashing process, which makes it much smaller. That means information on the heap is lost, so this a trade off.

Here is an overview of how the signal handler is created, and the system calls used.

There is a linux startup guide. I will focus on this line:

google_breakpad::ExceptionHandler eh(descriptor, NULL, dumpCallback, NULL, true, -1);

It instantiates an ExceptionHandler object. Looking into the source file, it sets an array of signals:

const int kExceptionSignals[] = {SIGSEGV, SIGABRT, SIGFPE, SIGILL, SIGBUS, SIGTRAP};

These are signals which default action is to create a core file.

Creating an ExceptionHandler object will set a signal handler for those signals following those steps:

  • It creates an alternative stack: sys_sigaltstack(&new_stack, NULL) . The default behavior, when the kernel passes control to a signal handler is to create a frame for it on the user stack. The problem is in case of stack overflow, the kernel generates a SIGSEGV, but then cannot create a frame for the handler, and the signal handler does not run. This solves this problem by creating an alternative signal stack.
  • It stores the old handlers:sigaction(kExceptionSignals[i], NULL, &old_handlers[i])
  • It creates a signal mask, to block all above signals when the signal handler is running: sigaddset(&sa.sa_mask, kExceptionSignals[i]) . When control is transferred to a signal handler, the signal that was raised is blocked, but not the others. That means if a different signal is raised, control switches to the signal handler set for that other signal. Blocking allows to modify that behaviour, in that case the blocked signal will be delivered when control returns to the process (if the process is not blocking it as well).
  • It sets a new signal handler: sigaction(kExceptionSignals[i], &sa, NULL)

The signal handler is defined by void ExceptionHandler::SignalHandler(int sig, siginfo_t* info, void* uc). Arguments to this function are defined in man sigaction . The comments give a description of the signal handler flow. I copied it below for convenience.

Copy of the flow

After successfully handling the signal, the signal handler will restore the default handler, which means a core dump can be generated as well and the process will be terminated. Otherwise it will restore the previous handler.


There is much more in this library about signals and how to read the memory, different architectures and the code is abundantly commented, I find it great for learning.

References

Books

[1] The Linux Programming Interface

[2] Understanding the Linux Kernel

[3] Computer Systems: a Programmer’s Perspective

Internet

Wikipedia: signals

gnu documentation