Notes 8: Signals

Corresponds to Chapter 10 in Advanced Programming in the Unix Environment.

Overview

Signals are asynchronous events which can cause a process to jump to a function called a signal handler. Signals can be raised because of an external event (like ^C was pressed, raising SIGINT) or because of hardware failure or invalid memory access, or even just because another process wanted to raise one.

There are three things a process can do with a signal:

  1. Ignore it. This does nothing. Two signals cannot be caught or ignored: SIGKILL and SIGSTOP.

  2. Handle ("Catch") it. A signal handler is called when the signal is raised so you can handle it as you see fit. Doesn't work with SIGKILL or SIGSTOP.

  3. Let the default action happen. Sometimes this is for the program to exit, exit and dump core, or do nothing.

Common Signals

Here is a partial list of signals. Ones of interest are marked in bold.

Signal Description
SIGABRT Process abort signal.
SIGALRM Alarm clock.
SIGFPE Erroneous arithmetic operation.
SIGHUP Hangup.
SIGILL Illegal instruction.
SIGINT Terminal interrupt signal.
SIGKILL Kill (cannot be caught or ignored).
SIGPIPE Write on a pipe with no one to read it.
SIGQUIT Terminal quit signal.
SIGSEGV Invalid memory reference.
SIGTERM Termination signal.
SIGUSR1 User-defined signal 1.
SIGUSR2 User-defined signal 2.
SIGCHLD Child process terminated or stopped.
SIGCONT Continue executing, if stopped.
SIGSTOP Stop executing (cannot be caught or ignored).
SIGTSTP Terminal stop signal.
SIGTTIN Background process attempting read.
SIGTTOU Background process attempting write.
SIGBUS Bus error.
SIGPOLL Pollable event.
SIGPROF Profiling timer expired.
SIGSYS Bad system call.
SIGTRAP Trace/breakpoint trap.
SIGURG High bandwidth data is available at a socket.
Table 1. Common signals.

The signal() call

In a nutshell, don't use it. It is the old unreliable method of delivering signals and it full of race conditions you can easily do without. The sigaction() method is far superior.

Better signals

In the new reliable system, signals are first generated. From that time until the signal is delivered to the process, it is said to be pending. A process can force a signal to remain pending by blocking that signal. The signal will be delivered once it is unblocked. Each process maintains a mask of signals which are currently blocked, called the signal mask.

Signal sets

Many of the signal operations require the use of a set of signals, which is stored in a sigset_t. This variable is operated on by a number of set functions:

int sigemptyset(sigset_t *set)
Clear the set. Returns -1 on error, just like the next three.

int sigfillset(sigset_t *set)
Mark every signal in the set.

int sigaddset(sigset_t *set, int signum)
Add the signal signum to the set.

int sigdelset(sigset_t *set, int signum)
Delete the signal signum from the set. error.

int sigismember(sigset_t *set, int signum)
Returns true if the signal signum is in the set.

Catching signals with sigaction()

For an example, lets write a program that catches SIGINT, which is what you get when you hit ^C from the terminal. Instead of the default action (which is to exit the program), we'll have it print "Ouch!"

When catching a signal, you have the ability to mark other signals as blocked while the signal is being handled. Note that the signal that is being handled is automatically added to the set of signals that are blocked while the program is executing the signal handler. This means that even if you hit ^C again while it's in the handler, it won't reenter it. In this example, we don't care if the SIGINT handler is interrupted by any other signals, so we'll just use sigemptyset() to clear the set.

Here is the prototype for sigaction():

    #include <signal.h>

    int sigaction(int signum, struct sigaction *act, struct sigaction *oldact); 

The first argument is the signal you want to handle. The second and third arguments are both pointers to struct sigactions, which contain info about how the signal is handled. The first, act, is the new action for the signal. The second, oldact, is what the action for the signal was before the call took place. (You can enter NULL for oldact if you don't care about the previous handler.)

What does the struct sigaction look like? It's:

    struct sigaction {
        void (*sa_handler)(int);  /* pointer to the handler function */
        sigset_t sa_mask;         /* other signals to be blocked */
        int sa_flags;             /* controls specific behavior */
    }

You put a pointer to the handler in sa_handler, a sigset_t in sa_mask that describes which other signals are to be blocked when the signal is being handled, and sa_flags, which is a bitwise-ORd set of flags.

The first argument, sa_handler, in addition to being set to point at the handler function, can be one of two special macros: SIG_IGN or SIG_DFL, to ignore the signal or restore the default handler, respectively. What is a pointer to a function? Well if you have a function called:

    void sigint_handler(int signo);

then a pointer to that function is simply referred to as "sigint_handler":

    sa.sa_handler = sigint_handler;

The second argument, sa_mask should be constructed with the signal set operations previously mentioned.

Finally, sa_flags can be one of a lot of things, but you should probably just set it to SA_RESTART now which tells the kernel to automatically restart interrupted slow system calls (like read() or other calls that can block forever).

Here is an example which catches SIGINT (^C) and prints "Ouch!" instead of exiting the program. Meanwhile, it's counting down from 10 to 1. Notice how the call to sleep can be interrupted and broken out of; if you hold down ^C, it finishes in well under 10 seconds.

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <signal.h>

    main()
    {
        void sigint_handler(int signo);
        int i;
        struct sigaction sa;
        char s[100];
        sigset_t mask;

        sigemptyset(&mask); /* we don't care to block any other signals */
        sa.sa_handler = sigint_handler;
        sa.sa_mask = mask;
        sa.sa_flags = SA_RESTART; /* restart slow system calls */
        sigaction(SIGINT, &sa, NULL); /* you should error-check this! */

        for(i = 10; i >= 1; i--) {
            printf("%d\n", i);
            sleep(1);
        }
    }

    void sigint_handler(int signo)
    {
        printf("Ouch!\n");
    }

Generating signals

There are a few ways to generate signals: kill(), raise(), abort(), and alarm(), are a few of these.

raise()--Signal yourself

If you want to send a signal to your own process, use the raise() call with the signal number as the argument. For instance, if you want to make it appear that ^C was pressed, you might:

    raise(SIGINT);

kill()--Signal other processes

If you want to be able to send a signal to a specific PID, use kill(). (Incidentally, the kill() system call is the basis for the Unix kill command.) You simply need to pass a PID and the signal number to generate:

    kill(SIGINT, 13245);

Feature: you can pass 0 as the signal number. This is known as the null signal. If you do kill() with the null-signal, no signal is actually generated for the target PID; it doesn't even know anything happened. What's the use? Doing this can allow you to check the existence of the target process--if it doesn't exist, kill() will return error. If it does exist, kill() will succeed, but there will be no effect.

abort()--Something is seriously hosed

In case of very grave and serious catastrophic failure of something or another, your program might want to call abort(). It takes no arguments.

abort() will raise the SIGABRT signal which has a default action of "terminate and dump core". Even if you write a signal handler for this signal, the process will still terminate once the handler returns. (You can siglongjump() out of there still, though...why you'd want to do this is a good question.)

alarm()--Wake me up in 8 hours

The alarm() call can be used to raise SIGALRM some number of seconds in the future. (The Unix ping command uses alarm() each second to send out another ping packet.) The default action for this signal is to exit the process--be sure to write a handler if you want something else to happen.

Raise SIGALRM in 10 seconds:

    alarm(10);

Note: alarm() returns the number of seconds remaining in the previous alarm. For instance if you call alarm(10), but only 6 seconds elapse before it is interrupted, the next call to alarm() will return 4. If you just want to check to see how much time remains on the previous alarm but don't want to schedule a new one, you can call alarm(0).

Interrupted system calls

I mentioned the SA_RESTART flag for sigaction(), earlier. This tells the kernel to restart interrupted slow system calls, which are calls that have the potential to block forever. For example, say I have a signal handler ("Ouch!") on SIGINT and I call read() on standard input then hit ^C. What happens? Well (on HPUX) read() will return -1 and set errno to EINTR, but it won't actually complete the read!

So that you don't have to always check to see if read() returned EINTR, you can specify the SA_RESTART flag in your struct sigaction. This will cause read() and the other slow system calls to automatically restart after an interrupt.

Linux seems to ignore this flag, restarting them all automatically.

sigprocmask()--Block these signals

You can command the kernel to block certain signals from occurring using this system call. The prototype is:

    int sigprocmask(int how, sigset_t *set, sigset_t *oldset);

The first argument, how, tells the kernel what to do with the set of signals to be blocked, set. It can be one of the following:

SIG_BLOCK
The new signal mask becomes the current signal mask union-ed with set. Use this to add signals to the mask.

SIG_UNBLOCK
The new signal mask becomes the current signal mask intersection-ed with the complement of set. Use this to remove signals from the mask.

SIG_SETMASK
The new signal mask becomes the signal mask in set.

The second argument, set, can also be NULL. If it is, the signal mask will not be modified, but the current signal mask will be returned in oldset.

The final argument, oldset, will contain the old signal mask before the modification. You can set this to NULL if you don't care what the signal mask used to be.

sigpending()--Which blocked signals are pending?

This system call will return a sigset_t containing all the signals that are pending. The prototype is:

    int sigpending(sigset_t *set);

The pending signals can be found by running set through sigismember(), described above.

sigsetjmp() and siglongjmp()

These work just like setjmp() and longjmp(), except they return the signal mask to normal if used within a signal handler. (Remember how the signal that's being currently handled is automatically added to the signal mask? Using these inside the handler will restore the proper mask settings.)

pause()--Wait for a signal

You can halt the execution of your program until any signal occurs by calling pause(). It takes no arguments and always returns -1 and sets errno to EINTR, so you don't have to error check it.

sigsuspend()--Set the mask and pause

There are times when you want to set the signal mask (which might be pending) and then wait for those signals to be handled before continuing. Often you'll want to do this in an atomic operation if you're blocking signals around a critical section of code.

This call, sigsuspend(), is like sigprocmask() (with SIG_SETMASK) and pause() bundled into one:

    int sigsuspend(sigset_t *mask);

Like pause(), it always returns -1 and sets errno to EINTR.