CSE 120: Handling Exceptions and Interrupts

In lecture we discussed how there are a variety of events — exceptions and interrupts — that cause the CPU to immediately context switch to the operating system. The operating system will then handle these events, also in a variety of ways depending upon the nature of the event. The goal of this page is to make the concept of events more concrete by giving examples of how these events propagate to user-level applications.

Exceptions

Exceptions are caused by a program executing an instruction that results in the exception. The example we used a few times in lecture is a program dividing a value by zero, which results in the divide by zero exception. For this exception, the default behavior of most operating systems is to terminate the process since the program presumably has an unrecoverable bug. Here is a very simple program dbz.c that triggers a divide by zero exception on Unix:

#include <stdio.h>

int
main (int argc, char *argv[])
{
    int x = 0;
    return 1/x;
}

You can compile this program and run it on ieng6:

% cc dbz.c
% ./a.out
Floating point exception (core dumped)

Unix reports this exception under the broad category of floating point exceptions (which is a bit of a misnomer since it also includes fixed point arithmetic exceptions).

Here is the disassembly of the main procedure:

% objdump -d a.out | more
[...]      
00000000004004ed <main>:
  4004ed:       55                      push   %rbp
  4004ee:       48 89 e5                mov    %rsp,%rbp
  4004f1:       89 7d ec                mov    %edi,-0x14(%rbp)
  4004f4:       48 89 75 e0             mov    %rsi,-0x20(%rbp)
  4004f8:       c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)
  4004ff:       b8 01 00 00 00          mov    $0x1,%eax
  400504:       99                      cltd
  400505:       f7 7d fc                idivl  -0x4(%rbp)
  400508:       5d                      pop    %rbp
  400509:       c3                      retq
  40050a:       66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)

Specifically, the idivl instruction is the one that triggers the exception when the CPU tries to execute it.

Handling Exceptions

Programs can override the default behavior of the operating system when an exception occurs. On Unix, the OS notifies programs of events via signals. Programs register signal handlers with Unix, and when the corresponding exception occurs Unix will cause the signal handler to execute. Here is another program dbz-handle.c that shows how to register a signal handler to catch divide-by-zero exceptions:

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

void
signal_me (int signum, siginfo_t *si, void *context)
{
    /* we are looking for an integer divide-by-zero exception */
    if (si->si_signo == SIGFPE && si->si_code == FPE_INTDIV) {
        printf ("zoinks! divide by zero\n");
    } else {
        printf ("unexpected signal %d\n", si->si_signo);
    }
    exit (-1);
}

int
main (int argc, char *argv[])
{
    struct sigaction sigact;
    int x = 0;

    sigact.sa_sigaction = signal_me;
    sigact.sa_flags = SA_SIGINFO | SA_RESTART;
    if (sigaction (SIGFPE, &sigact, NULL) != 0) {
        printf ("sigaction returned error %d\n", errno);
    }
    // we have registered our handler for arithmetic exceptions,
    // now let's trigger it
    return 1/x;
}

When we compile and run it, the OS invokes our handler when the divide by zero exception occurs:

% cc dbz-handle.c
% ./a.out
zoinks! divide by zero

Language Support

With languages like Java, the language runtime (the Java virtual machine) will register handlers for many exceptions, and the Java VM will translate them into language-level exceptions that Java programs can handle in catch blocks. For example, the program DivideByZero.java catches ArithmeticException to detect when the program divides by zero.

public class DivideByZero {
    public static void main (String args[]) {
	try {
	    int x = 0;
	    System.out.println (1/x);
	} catch (ArithmeticException e) {
	    System.out.print (e.getMessage());
	    System.out.println (": zoinks! divide by zero");
	}
    }
}

When we compile and run it, the OS invokes the handler registered by the Java virtual machine, which then invokes the handler we implemented in our catch block for ArithmeticException:

% javac DivideByZero.java
% java DivideByZero
/ by zero: zoinks! divide by zero

Interrupts

Operating systems use the same mechanism to respond to some interrupts as well. Recall that exceptions occur as a result of the program executing a particular instruction. Interrupts, though, are external to the program.

Here is a program ctrlc.c that registers a handler for the SIGINT signal. This signal is the "program interrupt" signal, which is typically triggered when users type Ctrl+C. After registering the handler, the program goes to sleep so that it does not exit on its own (and is more friendly than executing an infinite loop):

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

void
signal_me (int signum, siginfo_t *si, void *context)
{
    /* we are looking for a program interrupt signal */
    if (si->si_signo == SIGINT) {
        printf ("zoinks! who interrupted our sleep?\n");
    } else {
        printf ("unexpected signal %d\n", si->si_signo);
    }
}

int
main (int argc, char *argv[])
{
    struct sigaction sigact;
    int x = 0;

    sigact.sa_sigaction = signal_me;
    sigact.sa_flags = SA_SIGINFO | SA_RESTART;
    if (sigaction (SIGINT, &sigact, NULL) != 0) {
        printf ("sigaction returned error %d\n", errno);
    }
    // we have registered our handler for SIGINT
    // now let's go to sleep waiting for someone to interrupt us
    sleep (1024 * 1024);
    return 0;
}

When we compile it, run it, and type Ctrl+C, Unix will execute our signal handler:

% cc ctrlc.c
% ./a.out
^Czoinks! who interrupted our sleep?

In slightly more detail, typing Ctrl+C will cause keyboard interrupts to the OS. The OS will collect the key presses and recognize Ctrl+C as a special combination to interrupt the program. If the program does not have a handler registered with the OS to handle SIGINT, Unix will terminate the program. In our case we do have a handler, so Unix executes it.

Users can trigger Unix to deliver arbitrary signals to another process using the kill program (which internally uses the kill() system call). For instance, we can compile our program that registers a handler for SIGINT, and then run it in the background (so that we can run other programs in the shell):

% cc ctrlc.c
% ./a.out &
[2] 17740
% kill -2 17740
zoinks! who interrupted our sleep?

The shell tells us that the PID of the program we run in the background is 17749. We then tell kill to deliver signal 2, which corresponds to SIGINT, to this process. Unix then delivers the signal and calls our handler as if we had typed Ctrl+C. (We could also have run kill -s INT 17740, which is equivalent.)

One common use of signals on Unix is to use the SIGHUP signal to tell a server or daemon process to clean up its state (e.g., flush all data to disk, close open files, free any locks) and shut down cleanly. We would also use kill to do this, e.g., with kill -1 PID (or equivalently kill -s HUP PID).

The infamous kill -9 PID command causes Unix to terminate a process, and the process cannot handle this signal.

Recursive Events

An interesting situation is when an event occurs while executing an exception handler. One approach to handling it is for the OS to just note that another event needs to be delivered to the process, wait for the current handler to return, and then invoke the handler for the pending event. (In other words, the OS masks events to the process while a handler is running.) Another approach is to recursively invoke the handler. Linux is typically described as masking, but running the program below suggests recursive.

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

int globalvar = 0;

void
signal_me (int signum, siginfo_t *si, void *context)
{
    if (si->si_signo == SIGFPE && si->si_code == FPE_INTDIV) {
	printf ("zoinks! divide by zero\n");
	// trigger another exception/signal
	kill (getpid (), SIGINT);
	// if recursive, then the following line does not execute
	globalvar = 1;
    } else if (si->si_signo == SIGINT) {
	printf ("zoinks! who interrupted us? (globalvar %d)\n", globalvar);
    } else {
	printf ("unexpected signal %d\n", si->si_signo);
    }
    exit (-1);
}

int
main (int argc, char *argv[])
{
    struct sigaction sigact;
    int x = 0;

    sigact.sa_sigaction = signal_me;
    sigact.sa_flags = SA_SIGINFO | SA_RESTART;
    if (sigaction (SIGFPE, &sigact, NULL) != 0) {
	printf ("sigaction returned error %d\n", errno);
    }
    if (sigaction (SIGINT, &sigact, NULL) != 0) {
	printf ("sigaction returned error %d\n", errno);
    }
    // we have registered our handler for arithmetic exceptions,
    // now let's trigger it
    return 1/x;
}

Compiling and running the program multi-handle.c generates:

% cc multi-handle.c
% ./a.out
zoinks! divide by zero
zoinks! who interrupted us? (globalvar 0)