The Basics of Signal Handling

The-Great-Gatsby-Green-Light

Signals are one of the most basic ways programs can receive messages from the outside world. I’ve found limited tutorial-type documentation on them, so this post covers how to set them up and some debugging techniques.

The easiest way to get a feel for signal handling is to play with a simple C program, like this:

#include 
#include 
#include 

void my_handler(int signum) {
  const char msg[] = "Signal handler got signaln";
  write(STDOUT_FILENO, msg, sizeof msg);
}

int main(int argc, char *argv[]) {
  printf("PID: %dn", getpid());

  // Set up signal handler
  struct sigaction action = {};
  action.sa_handler = &my_handler;
  sigaction(SIGINT, &action, NULL);

  while (1) {
    pause();
  }
  return 0;
}

Compile and run and try hitting Ctrl-C a few times:

$ gcc signals.c -o signals
$ ./signals 
PID: 11152
^CSignal handler got signal 2
^CSignal handler got signal 2
^CSignal handler got signal 2

Each signal calls the signal handler we set up.

If you attach strace (system call tracer) and then hit Ctrl-C in the terminal running ./signals again, you can see each signal coming in:

$ $ strace -p 11152 -e trace=none -e signal=all
Process 11152 attached - interrupt to quit
--- SIGINT (Interrupt) @ 0 (0) ---
--- SIGINT (Interrupt) @ 0 (0) ---
--- SIGINT (Interrupt) @ 0 (0) ---

As we can’t kill it with Ctrl-C, we can use kill to shutdown ./signals:

$ kill 11152

kill defaults to sending a SIGTERM, which we’re not handling (yet). You could add a handler for it by adding the line sigaction(SIGTERM, &action, NULL); but then we’d have to kill -9 the process to kill it (which is two extra characters of typing) so I’m leaving SIGTERM unhandled.

Ignoring Signals

There are also ways to make your program not even receive signals: ignoring and blocking them (which are subtly different). To ignore a signal, change sa_action to SIG_IGN:

#include 
#include 
#include 

void my_handler(int signum) {
  const char msg[] = "Signal handler got signaln";
  write(STDOUT_FILENO, msg, sizeof msg);
}

int main(int argc, char *argv[]) {
  printf("PID: %dn", getpid());

  // Set up signal handler
  struct sigaction action = {};
  action.sa_handler = SIG_IGN;
  sigaction(SIGINT, &action, NULL);

  while (1) {
    pause();
  }
  return 0;
}

Now recompile and run and hit Ctrl-C. You’ll get something like this:

$ ./signals
PID: 86579
^C^C^C^C^C

If you attach strace, you’ll see that ./signals isn’t even receiving the SIGINTs.

You can see the signals a program is ignoring by looking at /proc/PID/status:

$ cat /proc/86579/status
Name:   signals
State:  S (sleeping)
Tgid:   86579
Pid:    86579
PPid:   30493
TracerPid:      0
Uid:    197420  197420  197420  197420
Gid:    5000    5000    5000    5000
FDSize: 256
Groups: 4 20 24 25 44 46 104 128 499 5000 5001 5762 74990 75209 77056 78700 79910 79982 
VmPeak:     4280 kB
VmSize:     4160 kB
VmLck:         0 kB
VmPin:         0 kB
VmHWM:       352 kB
VmRSS:       352 kB
VmData:       48 kB
VmStk:       136 kB
VmExe:         4 kB
VmLib:      1884 kB
VmPTE:        28 kB
VmSwap:        0 kB
Threads:        1
SigQ:   0/192723
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: 0000000000000002
SigCgt: 0000000000000000
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: ffffffffffffffff
Cpus_allowed:   ffffffff
Cpus_allowed_list:      0-31
Mems_allowed:   00000000,00000001
Mems_allowed_list:      0
voluntary_ctxt_switches:        2
nonvoluntary_ctxt_switches:     3

SigIgn is a hexidecimal number and it has kind of a weird format: the ignored signal number’s bit is set. So, for 2, the second bit is set. For SIGTERM (signal 15), the 15th bit is set: 0100_0000_0000_0000 in binary or 0x4000 in hexadecimal. So, if you were ignoring both SIGINT and SIGTERM, SigIgn would look like: 0000000000004002.

SigCgt is for signals that are being caught by the program and SigBlk is for signals that are being blocked.

Blocking Signals

What if you want your program to handle any signals that come in, just do it later? You might have a critical section where you don’t want to be interrupted, but afterwards you want to know what came in. That’s where blocking signals comes in handy.

You can block signals using sigprocmask:

#include 
#include 
#include 

void my_handler(int signum) {
  const char msg[] = "Signal handler got signaln";
  write(STDOUT_FILENO, msg, sizeof msg);
}

int main(int argc, char *argv[]) {
  printf("PID: %dn", getpid());

  // Set up signal handler                                                                                                                                                                                
  struct sigaction action = {};
  action.sa_handler = &my_handler;
  sigaction(SIGINT, &action, NULL);

  printf("Blocking signals...n");
  sigset_t sigset;
  sigemptyset(&sigset);
  sigaddset(&sigset, SIGINT);
  sigprocmask(SIG_BLOCK, &sigset, NULL);

  // Critical section
  sleep(5);

  printf("Unblocking signals...n");
  sigprocmask(SIG_UNBLOCK, &sigset, NULL);

  while (1) {
    pause();
  }
  return 0;
}

First we create a sigset_t which can hold a set of signals. We empty out the set with a call to sigemptyset and add a signal element: SIGINT. (There are a bunch of other set ops you can use to modify sigset_t, if necessary.)

If you compile and run this and try Ctrl-C-ing while signals are blocked, one signal will be “let through” when the signals are unblocked:

$ ./signals
PID: 86791
Blocking signals...
^C^C^C^C^C^CUnblocking signals...
Signal handler got signal 2

One common use for this is blocking signals while the signal handler is running. That way you can have your signal handler modify some non-atomic state (say, a counter of how many signals have come in) in a safe way.

However, suppose we called sigprocmask in the signal handler. There will always be a race condition: another signal could come in before we’ve called sigprocmask! So sigaction takes a mask of signals it should block while the handler is executing:

#include 
#include 
#include 

void my_handler(int signum) {
  const char msg[] = "Signal handler got signaln";
  write(STDOUT_FILENO, msg, sizeof msg);
}

int main(int argc, char *argv[]) {
  printf("PID: %dn", getpid());

  // Set up signal handler                                                                                                                                                                          
  struct sigaction action = {};
  action.sa_handler = &my_handler;
  sigset_t mask;
  sigemptyset(&mask);
  sigaddset(&mask, SIGINT);
  sigaddset(&mask, SIGTERM);
  action.sa_mask = mask;
  sigaction(SIGINT, &action, NULL);

  while (1) {
    pause();
  }
  return 0;
}

Here, we’re masking both SIGINT and SIGTERM: if either of these signals comes in while my_handler is running, they’ll be blocked until it completes.

Inheritance

Ignored and blocked signals are inherited when you fork a program. Thus, if a program isn’t responding to signals the way that you expect, it might be the fault of whoever forked it. Also, if you want your program to handle signals in certain ways you should explicitly set that rather than depending on the default.

If you want to use a signal’s default behavior (which is usually “terminate the program”), you can use SIG_DFL as the sa_handler.

What you can do in a signal handler

You might notice that I’m using write in the signal handlers above, instead of the somewhat more friendly printf. This is because there are only a small set of “async safe” functions you can call in a signal handler and printf isn’t one of them. There is a list of functions you can call on the signal(7) man page. A few examples that often come up: you cannot heap-allocate memory, buffer output, or mess with locks.

If you call any unsafe functions in a signal handler, the behavior is undefined (meaning it might work fine, or it might make your car blow up).

Edit: thanks to Vincent Bernat, who mentioned this in the comments.

References:

2 thoughts on “The Basics of Signal Handling

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: