Alex Pennace
May 19, 2008

Safe UNIX Signal Handling Tips

Suppose you have a program with a main loop like the following pseudo-code:

void gotsignal() {
  caught_signal=1;
}

void main() {
  signal(SIGwhatever, gotsignal);

  while(1) {
    /* do stuff */
    if(caught_signal) {
      /* do stuff */
    } else {
      /* do something else */
    }
    select();
    /* do more stuff */
  }

At first glance, everything is okay. We know that a signal caught in select() will cause select() to return immediately. caught_signal is evaluated just before select() is called. It would appear that signals occuring between select() are also handled properly.

But there exists an interval, after caught_signal is evaluated but before select() is called, that a signal can come in. caught_signal is then set, but select() doesn't return because select() wasn't called when the signal came in.

The now slightly less inexperienced programmer may "solve" this problem like so:

  ...
  while(1) {
    /* do stuff */
    if(caught_signal) {
      /* do stuff */
    } else {
      /* do something else */
    }
    sigprocmask(SIG_UNBLOCK, ...);
    select();
    sigprocmask(SIG_BLOCK, ...);
    /* do more stuff */
  }

By keeping the signal blocked until the last possible moment, then reblocking it at the first opportunity, the programmer hopes to hold signals at bay until the process is in select(). Sadly, there remains an interval between sigprocmask(SIG_UNBLOCK, ...) and select() where a signal can occur.

We are on the right track. It is clear that our strategy for handling signals should center on making sure that select() cannot be called after a signal that occured after the test to determine if a signal arrived!

Present solutions

  1. pselect()

    pselect() is much like select(), except it takes a list of signals as an additional argument. Functionally, it is like sigprocmask(SIG_UNBLOCK); select(); sigprocmask(SIG_BLOCK);, except signals have no opportunity to occure between sigprocmask(SIG_UNBLOCK) and select(). This is a guarantee made by the kernel.

    Sadly, pselect() is not available on many systems. Until recently, most Linux kernel/GNU libc stacks did not support pselect(), or used an imperfect emulation.

  2. The self-pipe trick

    The self-pipe trick involves an anonymous pipe, created with pipe(), a signal handler, and select(). The signal handler writes a byte to the writing end of the pipe, and select() waits for the reading end of the pipe to become readable.

    The self-pipe trick is reliable, but uses additional system resources (ofiles, memory, and so forth). Bernstein, D. J. "The self-pipe trick." van Bergen, Emile. "Avoiding races with Unix signals and select()."

  3. sigsetjmp/siglongjmp

    Our third strategy is using sigsetjmp() and siglongjmp() to jump to a specific spot in our main loop:

    void gotsignal() {
      caught_signal=1;
      if(oktojmp) siglongjmp();
    }
    
    void main() {
      signal(SIGwhatever, gotsignal);
    
      while(1) {
        /* do stuff */
        sigsetjmp(); oktojmp=1;
        if(caught_signal) {
          /* do stuff */
        } else {
          /* do something else */
        }
        select();
        /* do more stuff */
      }
    

    Regardless of where the process is, even if it is in select(), the siglongjmp() from the signal handler will cause program flow to be redirected to sigsetjmp(). This is the strategy I use in my code.