Signals

Signals are software interrupts. A signal can be generated at any time (asynchronously) or at specified times (synchronously). They are generated by the kernel as a result of an exceptional event (floating point error, hangup, etc.) or from the terminal/shell (when ^Z, ^C, or kill are typed) or from processes (kill(2)). Signals have almost no info content. You can't usually find out who sent the signal, so don't use them as a messaging system.

Signal handling has evolved on UNIX. Some UNIX's (including SVR4) let you block them, hold them, find out which PID and UID sent them, or determine why they were sent. Unreliable signal handling was the only kind available on older systems and some functions such as signal(3C) may provide only unreliable signals. Signals could not be queued, blocked, or if blockable then another signal could slip through. When the only purpose for handling a signal is to clean up before terminating this is not quite so bad. Many textbooks discuss only unreliable signals and have flawed code.

 
Types of signals
The types of signals vary with the version of Unix; see signal(5) or kill -l for a list. Interesting ones for us are: SIGHUP (hangup), SIGINT (^C), SIGQUIT (^), SIGFPE (floating point exceptions such as division by 0), SIGKILL (desperate kill: uncatchable or ignorable), SIGTERM (graceful kill: generate by the shell's kill command), SIGTSTP (^Z), SIGALRM (timer alarm), SIGWINCH (window change).
 
Signal delivery and actions
After a signal is generated by the kernel or a process, the kernel makes it pending to some process(es). The signal is said to be delivered to a process once the process acts on the signal. A process may block a signal, which leaves the signal pending until it is unblocked. A signal which is not blocked will be delivered immediately. The signal mask specifies which signals are blocked. A process can determine which signals are pending. Most UNIX's will not queue multiple instances of the same pending signal; only one instance of each signal can be pending.

There are three actions that a process can take on a signal. The signal can be default processed (SIG_DFL), ignored (SIG_IGN) or caught. The default action (SIG_DFL) on receiving most signals is termination of the process. Signals could instead be ignored (SIG_IGN), but this is reasonable for only certain signals and even then not always. A signal is caught if a signal handler function has been installed. The action in this case is to call the signal handler. This is fraught with problems, mostly because signals can arrive at awkward times and race conditions may occur.

 
What should a signal handler do?
There are basically three strategies used in signal handlers.
  1. Deal with the signal in the signal handler, then return or exit.
  2. Set a flag then return. In the main code, test for the flag at a convenient time. The flag must be declared volatile as it can change value at any time.
  3. Use longjmp(3C) or better siglongjmp(3C) to jump back to a previous execution point. siglongjmp is essentially a non-local go to; it unwinds the stack. At some point in the program call sigsetjmp, which marks the current point of execution and stores the signal mask. When siglongjmp is called, processing returns to the sigsetjmp point. There are several restrictions, including that you cannot siglongjmp to a sigsetjmp point once the function containing the sigsetjmp has returned, so change the signal handler before returning from the function. All register and non-volatile auto variables at the sigsetjmp point have undefined values when siglongjmp is taken.

Suppose a signal arrives in the middle of a malloc. This is very awkward because chaos will result if the signal handler also calls malloc. So it is unsafe to use malloc in a handler. The same applies to any other library functions that uses static data structures or that calls malloc. This includes all stdio functions and many other library functions. A further problem is that if processing is to be resumed, then the malloc must be resumed, so siglongjmp should not be used in the handler for this signal. Exiting or setting a flag is the only reasonable solution.

If the handler returns, processing resumes at the point of interruption. Depending on the signal this can be undesirable. For example, a return in connection with SIGFPE will lead to an immediate second SIGFPE unless you patch up the problem. Patching is very difficult but even if possible, will require some dirty machine dependent method. Rather than returning from the handler, a siglongjmp or exit is more reasonable in this case.

 
Generating Signals
Note the Bourne and Korn shells handle signals and have a command trap for altering how to handling them. Consider using it instead of writing C code. ^Z is used in interactive shells to suspend a process for possible later restart. A user generated ^Z is turned into a SIGTSTP signal by the terminal driver. Similarly for other keyboard generated signals. The shell kill raises a signal by making the system call kill(2).

A process can generate signals in several ways. Of course it can unintentional generate them by attempting illegal hardware operations, such as by dividing by zero, or by attempting to execute malformed machine instructions. A process can generate signals intentional with: kill(2), raise(3C) (sends the signal to itself), alarm(2) (which starts a timer). Related to these is pause(2) or better yet sigsuspend(2) which suspends a process until the next signal is delivered. Note: Making a process ``sleep'' on a multitasking system by using a time consuming loop is very poor manners. Instead use an alarm and sigsuspend or sleep(1) or sleep(3C) which in turn use alarm and pause.

 
Signal.h
To use signals always: #include <signal.h> which defines SIG_DFL, SIG_IGN, the signals SIGHUP, etc., and declares the various signal functions.

The basic but unreliable signal function is signal(3C). Suppose we define:

typedef void (*HANDLER) (int);
then signal( ) is a function that replaces the old handler with a new handler:
old_handler = HANDLER signal(int signo, HANDLER new_handler);
The handler is a pointer to a function, or SIG_DFL or SIG_IGN. signal returns the old handler, for possible reuse, or SIG_ERR if there was an error. One of the problems with signal is that it is not possible to determine what the signal handler is without replacing it.

Typically it is used as follows:

if (signal(signo,SIG_IGN) != SIG_IGN) oldhandler = signal(signo, handler);
where handler is a function defined elsewhere. The test for SIG_IGN is typical: if an application was initially ignoring a signal then usually it should continue to be ignored. This is especially true of SIGTERM and SIGINT signals, because the shell will set a background job to ignore these so that ^C will only kill the foreground job. If you un-ignore them, then when the process is backgrounded it will catch signals that are intended only for the foreground job.
 
Signals and Exec
After an exec, a process does not have access to previous code, so it cannot inherit the signal handlers. For this reason after an exec, all signals are reset to SIG_DFL, except SIG_IGN ones remain SIG_IGN. Of course, the new process can define its own signal handlers.

Signals are especially hard to handle in system calls. Calls that involve waits such as open(2), read(2), and write(2) on slow devices (terminal and pipes but not disks) that could block for long periods, if interrupted will return with the error EINTR and are not restarted by default (but modern UNIX's can transparently restart them). Your program needs to restart them in this case. A not completely satisfactory alternative is to block signals while doing these, however, your program becomes temporarily uninterruptable! This affects stdio functions like getchar( ) since EOF is returned even though a ``real'' EOF has not yet been seen; set a flag in the handler and then test this flag to see if the EOF was caused by a signal.

For a related example, consider:

system("ls > files_in_dir");
/* commands to process files_in_dir follow */
Note that system( ) is not an ordinary function. It involves running another program as a child process and a wait is involved. A ^C while the system( ) is running will error out system( ) and processing will resume with the lines below if ^C is caught. In this case files_in_dir is probably corrupt or incomplete, so the system( ) should be done over.
 
Sigaction
The replacement for unreliable and flawed signal(3C) is the reliable sigaction(2). The syntax and steps required are more involved.

As a first step, sigset_t sset declares a signal set (mask) called sset. Use sigemptyset(&sset) or sigfillset(&sset) to exclude all or include all signals in the set. Then include or exclude each desired signal, signo, one at a time with sigaddset(&sset, signo) or sigdelset(&sset, signo). To test if signo is in a set use: sigismember(&sset, signo).

The signal mask for a process is altered with sigprocmask(how, &sset, &oldsset) which can be used to change which signals are blocked. how determines the interpretation of sset. A SIG_BLOCK or SIG_UNBLOCK indicates that sset specifies additional signals to block or unblock. If how is SIG_SETMASK then sset is the new mask. If &oldsset is not NULL then the old mask is returned through it. If &sset is NULL then how is not used and no change is made to the current mask.

sigpending(&sset) tells which signals are pending. To test if SIGINT is pending:

sigpending(&sset);   if(sigismember(&sset,SIGINT)) printf("SIGINT pending");

In the next example, ^C is disabled in a critical region and ^C is tested to see if it is pending.

#include <signal.h>
void fatal(void) { /* signal error */ exit(21); }
int main() {   
    sigset_t sset,oldsset,pendingset;

    sigemptyset(&sset); /* better: sigprocmask(SIG_BLOCK,NULL,&sset); */
    sigaddset(&sset,SIGINT);
    if (-1 == sigprocmask(SIG_BLOCK,&sset,&oldsset)) fatal();
    /* signal now blocked */
    printf("Entering pretend critical region\n");
    sleep(5);
    sigpending(&pendingset);     /* test if signal is pending */
    if (sigismember(&pendingset,SIGINT)) printf("SIGINT pending\n");
    printf("Leaving critical region\n");
    /* restore previous set */
    if (-1 == sigprocmask(SIG_SETMASK,&oldsset,NULL)) fatal();  
    /* WRONG: sigprocmask(SIG_UNBLOCK,&sset,NULL)            */
    /*        [Should not unblock SIGINT if it was initially blocked]  */

    return  printf("Bye - you never pressed ^C\n") , 0;
}
sigaction(2) uses a three member structure sigaction.
struct sigaction act, oldact;
sigaction(signo, &act, &oldact);
where act.sa_handler is the name of the signal handler, SIG_DFL or SIG_IGN, act.sa_mask is a set of signals (in addition to previously blocked ones and signo) to block while in the signal handler and act.sa_flags are flags. Flag SA_RESTART indicates that interrupted system calls should be automatically restarted. For SIGCHILD, flags SA_NOCLDWAIT means this process will generate no zombies, while SA_NOCLDSTOP means do not generate SIGCHILD if child is stopped by a job control signal. If &oldact is not NULL then the old sigaction is stored there. If &act is not NULL then act specifies the new action.

Finally, sigsuspend(sset) makes sset the signal mask and then sleeps the process until a signal arrives. It is like a sigprocmask followed by a pause but avoids the race condition.

For more details and examples see the excellent book Advanced Programming in the UNIX Environment by W. Richard Stevens, Addison Wesley.

Last update: 2001 April 28