Brain Dump

Signal

Tags
comp-arch

Is a software level interrupt, providing a rudimentary level of inter-process communication and allowing the kernel to signal events to a process.

When a process receives a signal it stops the execution of the current program and makes it respond to the signal. You can define custom signal handlers for some signals using the signal system call.

Signals may be initialised by the OS kernel (example: SIGFPE, SIGSEGV, SIGIO) or by a process such as kill. They are eventually delivered by the kernel to the target process/thread which invoke either a generic action (ignore, terminate, dump core, etc) or a process provided signal handler.

Table 1: List of some common UNIX signals.
NamePortable NumberDefault ActionUsual Use
SIGINT2Terminate (Can be caught)Stop a process nicely
SIGQUIT3Terminate (Can be caught)Stop a process harshly
SIGTERM15Terminate ProcessStop a process even more harshly
SIGSTOPN/AStop Process (Cannot be caught)Suspends a process
SIGCONTN/AContinues a processStarts after a stop
SIGKILL9Terminate Process (Cannot be caught)You want the process gone

Sending Signals

You can generate a signal using the morbidly named kill syscall, or the kernel may generate one in response to a hardware interrupt, or you may trigger it interactively such as by entering CTRL-C in the terminal which sends SIGINT to the current process. You can also send a signal to your own process by using the raise syscall.

Table 2: The 4 states of a signals life-cycle.
StateDescription
GeneratedWhen a process generates a signal
PendingThe process blocked delivery of the signal
BlockedWhen the kernel is about to deliver the signal
ReceivedThe signal has been delivered to the process

Handling Signals

Signal handlers are user-defined functions that can be executed when a signal is delivered instead of the default signal handlers. You can set the handler for a given signal using the signal sys-call. Note: You don't have to handle signals synchronously, you can declare an asynchronous signal handler with sigaction.

There are strict limitations on executable code inside a signal handler. Most library and syscalls are async-signal-unsafe meaning they can't be used inside a signal handler because they aren't re-entrant. A safe way to deal with signal handlers is to set a variable in the handler body and let the program resume operating. The program should later check the variable and respond at a more appropriate time.

// Prevent multi-threading shenanigans and compiler
// optimisation optimising this variable out.
volatile sig_atomic_t please_stop;

void handle_sigint(int signal) {
    please_stop = 1;
}

int main() {
    signal(SIGINT, handle_sigint);
    pleast_stop = 0;
    while (!please_stop) {
	// Application logic here.
    }
    // Clean up code here.
}
Code Snippet 1: Example of cleanly handling a SIGINT.
struct sigaction sa;
sa.sa_handler = myhandler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGALRM, &sa, NULL)
Code Snippet 2: Example of setting a signal handler through sigaction and the struct sigaction structure.

Waiting for Pending Signals

You can wait for a pending signal using the sigwait sys-call. This blocks the current thread until a signal arrives and lets you handle it directly rather than through a callback.

One possible use case for this is to create a separate thread for handling incoming signals that can doesn't have the strict requirements of an ordinary signal handler. With such an approach you can use async-signal-unsafe functions.

static sigset_t signal_mask;

int main(int argc, char *argv[]) {
    pthread_t sig_thr_id;
    sigemptyset(&signal_mask);
    sigaddset(&signal_mask, SIGINT);
    sigaddset(&signal_mask, SIGTERM);
    // Sets the sigmask for the current thread and any threads
    // made after this point.
    pthread_sigmask(SIG_BLOCK, &signal_mask, NULL);

    // New threads will inherit the current threads sigmask.
    pthread_create(&sig_thr_id, NULL, signal_thread, NULL);

    // ... Application Code.
}

void *signal_thread(void *arg) {
    int sig_caught;

    // Use the same mask as the set of signals that we'd like to know about!
    sigwait(&signal_mask, &sig_caught);
    switch (sig_caught) {
    case SIGINT:
	// ...
	break;
    case SIGTERM:
	// ...
	break;
    default:
	fprintf(stderr, "Unexpected signal %d\n", sig_caught);
	break;
    }
}
Code Snippet 3: Demonsration of the usage of a dedicated separate thread for signal handling.

Ignoring Signal with a Signal Mask

The signal mask is the set of signals whose delivery is currently blocked for the caller. You can set a new mask or modify the existing masks with sigprocmask.

Never Use SIGKILL

Here's a friendly message from the UNIX useless awards:

No, no, no. Don’t use kill -9.

It doesn’t give the process a chance to cleanly:

  1. shut down socket connections
  2. clean up temp files
  3. inform its children that it is going away
  4. reset its terminal characteristics

and so on and so on and so on.

Generally, send 15, and wait a second or two, and if that doesn’t work, send 2, and if that doesn’t work, send 1. If that doesn’t, REMOVE THE BINARY because the program is badly behaved!

Don’t use kill -9. Don’t bring out the combine harvester just to tidy up the flower pot.

Links to this note