How to Write Advanced Signal Handlers in UNIX

by Darryl Gove
Published September 2014

This article demonstrates how to use signal handlers for various scenarios, such as profiling an application and handling illegal instructions in a UNIX environment.

Introduction

Signals are used to interrupt a running process and get it to do something else. They can come from within the process, or they can be sent externally using the kill command or some other mechanism. Once a process receives a signal, it either handles it with a custom handler or leaves it for the default handler. In many cases, the default handler causes the process to exit.

Signals can be used to handle events such as timers expiring, and they can even handle hardware problems such as misaligned memory accesses or illegal/unsupported instructions.

Installing a Signal Handler

The first step is to declare a signal handling routine. There are two kinds of signal handling routines: one kind that does not take any parameters and another kind that takes a set of three parameters. The parameters are necessary if the objective is to extract information from the current state of the application.

Listing 1 shows an empty handler that receives information about the signal number, information about the signal delivery, and information about the thread context.




void  handle_prof_signal(int signal, siginfo_t * info, void * context)
{
}

Listing 1: Example signal handler

The next step, which is shown in Listing 2, is to install the signal handler using the sigaction() system call. This identifies the signal that we want to handle and the action that we want to take when the signal is received, and it takes an optional parameter that will receive information about any previous signal handler that was installed for that signal. Information about the old signal handler can be useful when you need to preserve the existing behavior.

To pass information about the handler to sigaction(), we need to use a sigaction structure, which takes the address of the routine that will handle the signal, as well as certain other settings.

The sa_mask field indicates any signals that should be blocked while the signal handler is being executed. The sa_flags field determines a number of different things, but the important ones are whether we get the extended information (SA_SIGINFO ) and whether system calls that were interrupted by the signal are automatically restarted (SA_RESTART ), The alternative is that the interrupted system calls will fail, so restarting them is obviously a better approach.



// install signal handler
struct sigaction sig_action;
struct sigaction old_action;

memset(&sig_action, 0, sizeof(sig_action));

sig_action.sa_sigaction = handle_prof_signal;
sig_action.sa_flags = SA_RESTART | SA_SIGINFO;
sigemptyset(&sig_action.sa_mask);

sigaction(SIGPROF, &sig_action, &old_action);

Listing 2: Installing a signal handler using sigaction()

There are two ways that an application can receive a signal: from an external source or from an internal source. In this case, we are going to set a timer to send a signal to the application after one second. The function setitimer() takes the signal to be sent, a value for the timeout, and an optional pointer to a structure that records any previous value for the timer. Code for setting up the timer is shown in Listing 3.



struct itimerval timeout={0};
timeout.it_value.tv_sec=1;
setitimer(ITIMER_PROF, &timeout, 0);

Listing 3: Setting up a timer for a one-second timeout.

Listing 4 shows a complete application that waits in a loop until a timer expires.



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

static void handle_prof_signal(int sig_no, siginfo_t* info, void *context)
{
  printf("Done\n");
  exit(1);
}

void main()
{
  struct sigaction sig_action;
  memset(&sig_action, 0, sizeof(sig_action));
  sig_action.sa_sigaction = handle_prof_signal;
  sig_action.sa_flags = SA_RESTART | SA_SIGINFO;
  sigemptyset(&sig_action.sa_mask);
  sigaction(SIGPROF, &sig_action, 0);

  struct itimerval timeout={0};
  timeout.it_value.tv_sec=1;
  setitimer(ITIMER_PROF, &timeout, 0);

  do { } while(1);
}

Listing 4: Application that terminates after a one-second timeout

Async-Safe Functions

Many system calls are not safe to make in a signal handler. A good example of this is the function printf(), which is used in Listing 4. We can modify the do-while loop to demonstrate this; the modified loop is shown in Listing 5. In the code, the timeout is also reduced to 10 microseconds so that the code demonstrates the problem quickly.




 struct itimerval timeout={0};
  timeout.it_value.tv_usec=10;
  setitimer(ITIMER_PROF, &timeout, 0);  
  int i=0;
  do
  {
    printf("Fish%i%i%i%i%i%i",i,i,i,i,i,i); i++;
  } while(1);

Listing 5: Code to demonstrate a problem with non-async-safe routines

The tail end of the output from Listing 5 is shown in Listing 6. In the output, the last few characters are bolded to show that the output from the signal handler was mixed in with the output from the printf() being performed in the loop.




Fish390390390390390390Fish391391391391391391Fish392392392392392392Fish3933933933933933
93Fish394394394394394394Fish395395395395395395Fish396396396396396396Fish39739739739739
7397Fish398398398398398398Fish399399399399399399Fish400400400400400400Fish401401401401
401401Fish4024Done

Listing 6: Tail end of output from non-async-safe example

This characteristic is referred to as whether a function is "async-safe." Suppose the main thread is calling printf() when it is interrupted by the signal. The signal handler then calls printf(). If printf() in the main thread has acquired a mutex M in order to print something, and then the printf() in the signal handler attempts to acquire the same mutexM, the thread will deadlock—it's waiting for a mutex it has already acquired.

There are some functions that are async-safe. These are either listed in the documentation as being safe by POSIX definition or the functions are described as being async-safe on their man pages.

One way of making a function async-safe is for it to stop signals from arriving while it is active, and enable them only after it has left the critical code section. This can be achieved using sigprocmask().

So rather than calling printf(), which is not async-safe, we can use write(), which is safe to call from a signal handler.

Using Extended Information

If SA_SIGINFO is set, then the handler gets information about the machine context. Parts of the information will be platform-dependent, so the resulting code might need to be ported between platforms. The code in Listing 7 is for SPARC machines.

The context information contains details about the machine state: the values of current registers, pointers to the stack, and so on. So it is possible to produce a rudimentary profiling infrastructure using the timers and the machine context information. The code to do this is shown in Listing 7.




#include <signal.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/ucontext.h>

struct itimerval timeout={0};


static void handle_prof_signal(int sig_no, siginfo_t* info, void *vcontext)
{
  char output[100];
  ucontext_t *context = (ucontext_t*)vcontext;
  unsigned long pc = context->uc_mcontext.gregs[REG_PC];
  
  snprintf(output,100,"Sample at %lx\n",pc);
  write(1,output,strlen(output)+1);
  setitimer(ITIMER_PROF, &timeout, 0);
}


void main()
{
  struct sigaction sig_action;
  memset(&sig_action, 0, sizeof(sig_action));
  sig_action.sa_sigaction = handle_prof_signal;
  sig_action.sa_flags = SA_RESTART | SA_SIGINFO;
  sigemptyset(&sig_action.sa_mask);
  sigaction(SIGPROF, &sig_action, 0);

  timeout.it_value.tv_sec=1;
  setitimer(ITIMER_PROF, &timeout, 0);
  volatile int i=0;
  do { i++; } while(1);
}

Listing 7: Rudimentary profiling tool

There are a couple of interesting points here. The context information is platform-dependent, and the structure is documented in the header files. On SPARC machines, the uc_mcontext structure contains an array that holds the values of the general-purpose registers. One of these registers contains the Program Counter (PC), which is what we need for the profiling code.

The second point to notice is that once the handler has been triggered by the timer signal, the timer signal needs to be reset.

The infinite loop at the end of the code has been modified so that it contains some work, which ensures that the resulting profile does contain multiple PCs. The output from this code is shown in Listing 8.



$ cc -g -O  profile.c
$ ./a.out
Sample at 109d0
Sample at 109d8
Sample at 109d8
...

Listing 8: Output from profiling example

Handling Illegal Instructions

Using timers is not the only reason to install a signal handler. An application can generate signals during its execution; for example, a SIGBUS indicates an attempted misaligned memory access. And SPARC processors send a SIGILL instruction if they encounter an illegal instruction.

The code in Listing 9 shows a handler for SIGILL. This handler reports the address and value of any illegal instruction and then continues execution at the instruction following the illegal one.

The code contains a sequence of four illegal instructions (of value zero), and when executed, it will print out their addresses and values. Notice that the code needs to directly manipulate the PC and nPC (next PC) fields in the context. If these fields were not modified, the instruction would be retried and the resulting code would loop calling the trap handler.



#include <signal.h>
#include <string.h>
#include <unistd.h>
#include <sys/ucontext.h>

static void handle_prof_signal(int sig_no, siginfo_t* info, void *vcontext)
{
  char output[100];
  ucontext_t *context = (ucontext_t*)vcontext;
  unsigned long pc = context->uc_mcontext.gregs[REG_PC];
  
  snprintf(output,100,"Illegal instruction at %lx value %lx\n",pc,*(int*)pc);
  write(1,output,strlen(output)+1);
  context->uc_mcontext.gregs[REG_PC] = context->uc_mcontext.gregs[REG_nPC];
  context->uc_mcontext.gregs[REG_nPC] = context->uc_mcontext.gregs[REG_nPC]+4;
}


void main()
{
  struct sigaction sig_action;
  memset(&sig_action, 0, sizeof(sig_action));
  sig_action.sa_sigaction = handle_prof_signal;
  sig_action.sa_flags = SA_RESTART | SA_SIGINFO;
  sigemptyset(&sig_action.sa_mask);
  sigaction(SIGILL, &sig_action, 0);

  timeout.it_value.tv_sec=1;
  setitimer(ITIMER_PROF, &timeout, 0);
  asm(".word 0x00000000");
  asm(".word 0x00000000");
  asm(".word 0x00000000");
  asm(".word 0x00000000");
}

Listing 9: Handling illegal instructions

Conclusion

Signals are a very power feature of UNIX systems. The listings in this article demonstrate that they can be used to solve various problems, such as the relatively straightforward scenario of profiling an application to the more esoteric scenario of emulating support for missing instructions.

See Also

"Extending Traditional Signals" in the Multithreaded Programming Guide

Oracle Solaris Studio

About the Authors

Darryl Gove is a senior principal software engineer in the Oracle Solaris Studio team, working on optimizing applications and benchmarks for current and future processors. He is also the author of the books Multicore Application Programming, Solaris Application Programming , and The Developer's Edge. Find his blog at http://blogs.oracle.com/d.