13.4.1. Signals

A signal is a software interrupt that is sent by one process to another process via the OS. When a process receives a signal, its current execution point is interrupted by the OS to run signal handler code. If the signal handler returns, the process’s execution continues from where it was interrupted to handle the signal. Sometimes the signal handler causes the process to exit, and thus it does not continue its execution from where it left off.

Signals are similar to hardware interrupts and traps but are different from both. Whereas a trap is a synchronous software interrupt that occurs when a process explicitly invokes a system call, signals are asynchronous — a process may be interrupted by the receipt of a signal at any point in its execution. Signals also differ from asynchronous hardware interrupts in that they are triggered by software rather than hardware devices.

A process can send another process a signal by executing the kill system call, which requests that the OS post a signal to another process. The OS handles posting the signal to the target process and setting its execution state to run the signal handler code associated with the particular posted signal.

The name of the kill system call is potentially misleading as well as unfortunately violent. While it can be (and often is) used to deliver a termination signal, it is also used to send any other type of signal to a process.

The OS itself also uses signals to notify processes of certain events. For example, the OS posts a SIGCHLD signal to a process when one of its child processes exits.

Systems define a fixed number of signals (e.g., Linux defines 32 different signals). As a result, signals provide a limited way in which processes can communicate with one another, as opposed to other interprocess communication methods such as messaging or shared memory.

Table 1 lists some of the defined signals. See the man page (man 7 signal) for additional examples.

Table 1. Example Signals Used for Interprocess Communication
Signal Name Description

SIGSEGV

Segmentation fault (e.g., dereferencing a null pointer)

SIGINT

Interrupt process (e.g., Ctrl-C in terminal window to kill process)

SIGCHLD

Child process has exited (e.g., a child is now a zombie after running exit)

SIGALRM

Notify a process a timer goes off (e.g., alarm(2) every 2 secs)

SIGKILL

Terminate a process (e.g., pkill -9 a.out)

SIGBUS

Bus error occurred (e.g., a misaligned memory address to access an int value)

SIGSTOP

Suspend a process, move to Blocked state (e.g., Ctrl-Z)

SIGCONT

Continue a blocked process (move it to the Ready state; e.g., bg or fg)

When a process receives a signal, one of several default actions can occur:

  • the process can terminate

  • the signal can be ignored

  • the process can be blocked

  • the process can be unblocked

The OS defines a default action and supplies the default signal handler code for every signal number. Application programmers, however, can change the default action of most signals and can write their own signal handler code. If an application program doesn’t register its own signal handler function for a particular signal, then the OS’s default handler executes when the process receives a signal. For some signals, the OS-defined default action cannot be overridden by application signal handler code. For example, if a process receives a SIGKILL signal, the OS will always force the process to exit, and receiving a SIGSTOP signal will always block the process until it receives a signal to continue (SIGCONT) or to exit (SIGKILL).

Linux supports two different system calls that can be used to change the default behavior of a signal or to register a signal handler on a particular signal: sigaction and signal. Because sigaction is POSIX compliant and more featureful, it should be used in production software. However, we use signal in our example code because it is easier to understand.

Following is an example program that registers signal handlers for SIGALRM, SIGINT, and SIGCONT signals using the signal system call (error handling is removed for readability):

/*
 * Example of signal handlers for SIGALRM, SIGINT, and SIGCONT
 *
 * A signal handler function prototype must match:
 *   void handler_function_name(int signum);
 *
 * Compile and run this program, then send this process signals by executing:
 *  kill -INT  pid  (or Ctrl-C) will send a SIGINT
 *  kill -CONT pid  (or Ctrl-Z fg) will send a SIGCONT
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

/* signal handler for SIGALRM */
void sigalarm_handler(int sig) {
    printf("BEEP, signal number %d\n.", sig);
    fflush(stdout);
    alarm(5);  /* sends another SIGALRM in 5 seconds */
}

/* signal handler for SIGCONT */
void sigcont_handler(int sig) {
    printf("in sigcont handler function, signal number %d\n.", sig);
    fflush(stdout);
}

/* signal handler for SIGINT */
void sigint_handler(int sig) {
    printf("in sigint handler function, signal number %d...exiting\n.", sig);
    fflush(stdout);
    exit(0);
}

/* main: register signal handlers and repeatedly block until receive signal */
int main(void) {

    /* Register signal handlers. */
    if (signal(SIGCONT, sigcont_handler) == SIG_ERR) {
        printf("Error call to signal, SIGCONT\n");
        exit(1);
    }

    if (signal(SIGINT, sigint_handler) == SIG_ERR) {
        printf("Error call to signal, SIGINT\n");
        exit(1);
    }

    if (signal(SIGALRM, sigalarm_handler) == SIG_ERR) {
        printf("Error call to signal, SIGALRM\n");
        exit(1);
    }

    printf("kill -CONT %d to send SIGCONT\n", getpid());

    alarm(5);  /* sends a SIGALRM in 5 seconds */

    while(1) {
        pause(); /* wait for a signal to happen */
    }
}

When run, the process receives a SIGALRM every 5 seconds (due to the call to alarm in main and sigalarm_handler). The SIGINT and SIGCONT signals can be triggered by running the kill or pkill commands in another shell. For example, if the process’s PID is 1234 and its executable is a.out, then the following shell command sends the process SIGINT and SIGCONT signals, triggering their signal handler functions to run:

pkill -INT a.out
kill  -INT 1234

pkill -CONT a.out
kill  -CONT 1234

Writing a SIGCHLD handler

Recall that when a process terminates, the OS delivers a SIGCHLD signal to its parent process. In programs that create child processes, the parent process does not always want to block on a call to wait until its child processes exit. For example, when a shell program runs a command in the background, it continues to run concurrently with its child process, handling other shell commands in the foreground as the child process runs in the background. A parent process, however, needs to call wait to reap its zombie child processes after they exit. If not, the zombie processes will never die and will continue to hold on to some system resources. In these cases, the parent process can register a signal handler on SIGCHLD signals. When the parent receives a SIGCHLD from an exited child process, its handler code runs and makes calls to wait to reap its zombie children.

Below is a code snippet showing the implementation of a signal handler function for SIGCHLD signals. This snippet also shows parts of a main function that register the signal handler function for the SIGCHLD signal (note that this should be done before any calls to fork):

/*
 * signal handler for SIGCHLD: reaps zombie children
 *  signum: the number of the signal (will be 20 for SIGCHLD)
 */
void sigchld_handler(int signum) {
    int status;
    pid_t pid;

    /*
     * reap any and all exited child processes
     * (loop because there could be more than one)
     */
    while( (pid = waitpid(-1, &status, WNOHANG)) > 0) {
        /* uncomment debug print stmt to see what is being handled
        printf("signal %d me:%d child: %d\n", signum, getpid(), pid);
         */
    }
}

int main(void) {

    /* register SIGCHLD handler: */
    if ( signal(SIGCHLD, sigchild_handler) == SIG_ERR) {
        printf("ERROR signal failed\n");
	exit(1);
    }

    ...

    /* create a child process */
    pid = fork();
    if(pid == 0) {
        /* child code...maybe call execvp */
        ...
    }
    /* the parent continues executing concurrently with child */
    ...

The example above passes -1 as the PID to waitpid, which means "reap any zombie child process." It also passes the WNOHANG flag, which means that the call to waitpid does not block if there are no zombie child processes to reap. Also note that waitpid is called inside a while loop that continues as long as it returns a valid PID value (as long as it reaps a zombie child process). It is important that the signal handler function calls waitpid in a loop because as it is running, the process could receive additional SIGCHLD signals from other exited child process. The OS doesn’t keep track of the number of SIGCHLD signals a process receives, it just notes that the process received a SIGCHLD and interrupts its execution to run the handler code. As a result, without the loop, the signal handler could miss reaping some zombie children.

The signal handler executes whenever the parent receives a SIGCHLD signal, regardless of whether the parent is blocked on a call to wait or waitpid. If the parent is blocked on a call to wait when it receives a SIGCHLD, it wakes up and runs the signal handler code to reap one or more of its zombie children. It then continues execution at the point in the program after the call to wait (it just reaped an exited child process). If, however, the parent is blocked on a call to waitpid for a specific child, then the parent may or may not continue to block after its signal handler code runs to reap an exited child. The parent process continues execution after its call to waitpid if the signal handler code reaped the child for which it was waiting. Otherwise, the parent continues to block on the call to waitpid to wait for the specified child to exit. A call to waitpid with a PID of a non existent child process (perhaps one that was previously reaped in the signal handler loop) does not block the caller.