Signals

Processes running in the system require a way to be told about events that influence them. On UNIX there is infrastructure between the kernel and processes called signals which allows a process to receive notification about events important to it.

When a signal is sent to a process, the kernel invokes a handler which the process must register with the kernel to deal with that signal. A handler is simply a designed function in the code that has been written to specifically deal with interrupt. Often the signal will be sent from inside the kernel itself, however it is also common for one process to send a signal to another process (one form of interprocess communication). The signal handler gets called asynchronously; that is the currently running program is interrupted from what it is doing to process the signal event.

For example, one type of signal is an interrupt (defined in system headers as SIGINT) is delivered to the process when the ctrl-c combination is pressed.

As a process uses the read system call to read input from the keyboard, the kernel will be watching the input stream looking for special characters. Should it see a ctrl-c it will jump into signal handling mode. The kernel will look to see if the process has registered a handler for this interrupt. If it has, then execution will be passed to that function where the function will handle it. Should the process have not registered a handler for this particular signal, then the kernel will take some default action. With ctrl-c the default action is to terminate the process.

A process can choose to ignore some signals, but other signals are not allowed to be ignored. For example, SIGKILL is the signal sent when a process should be terminated. The kernel will see that the process has been sent this signal and terminate the process from running, no questions asked. The process can not ask the kernel to ignore this signal, and the kernel is very careful about which process is allowed to send this signal to another process; you may only send it to processes owned by you unless you are the root user. You may have seen the command kill -9; this comes from the implementation SIGKILL signal. It is commonly known that SIGKILL is actually defined to be 0x9, and so when specified as an argument to the kill program means that the process specified is going to be stopped immediately. Since the process can not choose to ignore or handle this signal, it is seen as an avenue of last resort, since the program will have no chance to clean up or exit cleanly. It is considered better to first send a SIGTERM (for terminate) to the process first, and if it has crashed or otherwise will not exit then resort to the SIGKILL. As a matter of convention, most programs will install a handler for SIGHUP (hangup -- a left over from days of serial terminals and modems) which will reload the program, perhaps to pick up changes in a configuration file or similar.

If you have programmed on a Unix system you would be familiar with segmentation faults when you try to read or write to memory that has not been allocated to you. When the kernel notices that you are touching memory outside your allocation, it will send you the segmentation fault signal. Usually the process will not have a handler installed for this, and so the default action to terminate the program ensues (hence your program "crashes"). In some cases a program may install a handler for segmentation faults, although reasons for doing this are limited.

This raises the question of what happens after the signal is received. Once the signal handler has finished running, control is returned to the process which continues on from where it left off.

Example

The following simple program introduces a lot of signals to run!

Example 5.4. Signals Example
  1 
              $ cat signal.c
    #include <stdio.h>
    #include <unistd.h>
  5 #include <signal.h>
    
    void sigint_handler(int signum)
    {
            printf("got SIGINT\n");
 10 }
    
    int main(void)
    {
            signal(SIGINT, sigint_handler);
 15         printf("pid is %d\n", getpid());
            while (1)
                    sleep(1);
    }
    $ gcc -Wall -o signal signal.c
 20 $ ./signal
    pid is 2859
    got SIGINT # press ctrl-c 
               # press ctrl-z
    [1]+  Stopped                 ./signal
 25 
    $ kill -SIGINT 2859
    $ fg
    ./signal
    got SIGINT
 30 Quit # press ctrl-\
    
    $
            

We have simple program that simply defines a handler for the SIGINT signal, which is sent when the user presses ctrl-c. All the signals for the system are defined in signal.h, including the signal function which allows us to register the handling function.

The program simply sits in a tight loop doing nothing until it quits. When we start the program, we try pressing ctrl-c to make it quit. Rather than taking the default action, or handler is invoked and we get the output as expected.

We then press ctrl-z which sends a SIGSTOP which by default puts the process to sleep. This means it is not put in the queue for the scheduler to run and is thus dormant in the system.

As an illustration, we use the kill program to send the same signal from another terminal window. This is actually implemented with the kill system call, which takes a signal and PID to send to (this function is a little misnamed because not all signals do actually kill the process, as we are seeing, but the signal function was already taken to register the handler). As the process is stopped, the signal gets queued for the process. This means the kernel takes note of the signal and will deliver it when appropriate.

At this point we wake the process up by using the command fg. This actually sends a SIGCONT signal to the process, which by default will wake the process back up. The kernel knows to put the process on the run queue and give it CPU time again. We see at this point the queued signal is delivered.

In desperation to get rid of the program, we finally try ctrl-\ which sends a SIGQUIT (abort) to the process. But if the process has aborted, where did the Quit output come from?

You guessed it, more signals! When a parent child has a process that dies, it gets a SIGCHLD signal back. In this case the shell was the parent process and so it got the signal. Remember how we have the zombie process that needs to be reaped with the wait call to get the return code from the child process? Well another thing it also gives the parent is the signal number that the child may have died from. Thus the shell knows that child process died from a SIGABRT and as an informational service prints as much for the user (the same process happens to print out "Segmentation Fault" when the child process dies from a SIGSEGV).

You can see how in even a simple program, around 5 different signals were used to communicate between processes and the kernel and keep things running. There are many other signals, but these are certainly amongst the most common. Most have system functions defined by the kernel, but there are a few signals reserved for users to use for their own purposes within their programs (SIGUSR).