Self-Pipe Trick

by Ruslan Spivak on June 24, 2012

Imagine that your server process needs to simultaneously wait for I/O on multiple descriptors and also needs to wait for the delivery of a signal.
The problem is that you can’t safely mix select() with signals because there is a chance of race condition.

Consider the following code excerpt:

GOT_SIGNAL = False

def handler(signum, frame):
    global GOT_SIGNAL
    GOT_SIGNAL = True

...

signal.signal(signal.SIGUSR1, handler)

# What if the signal arrives at this point?

try:
    reads, writes, excs = select.select(rlist, wlist, elist)
except select.error as e:
    code, msg = e.args
    if code == errno.EINTR:
        if GOT_SIGNAL:
            print 'Got signal'
    else:
        raise

The problem here is that if the SIGUSR1 is delivered after setting the signal handler but before call to select then the select call will block and we won’t execute our application logic in response to the event thus effectively “missing” the signal (our application logic in this case is printing the message: Got signal).

That’s an example of possible nasty racing. Let’s simulate that with selsigrace.py

Start the program

$ python selsigrace.py
PID: 32324
Sleep for 10 secs

and send the USR1 signal to the PID(it’s different on every run) within the 10 second interval while the process is still sleeping:

$ kill -USR1 32324

You should see the program produce additional line of output ‘Wake up and block in “select”‘ and block without exiting, no message “Got signal”:

$ python selsigrace.py
PID: 32324
Sleep for 10 secs
Wake up and block in "select"

If you send yet another USR1 signal at this point then the select will be interrupted and the program will terminate with the message:

$ kill -USR1 32324
$ python selsigrace.py
PID: 32324
Sleep for 10 secs
Wake up and block in "select"
Got signal

Self-Pipe Trick is used to avoid race conditions when waiting for signals and calling select on a set of descriptors.

The following steps describe how to implement it:

  1. Create a pipe and change its read and write ends to be nonblocking
  2. Add the read end of the pipe to the read list of descriptors given to select
  3. Install a signal handler for the signal we’re concerned with. When the signal arrives the signal handler writes a byte of data to the pipe. Because the write end of the pipe is nonblocking we prevent the situation when signals flood the process, the pipe becomes full and the process blocks itself in the signal handler.
  4. When select successfully returns check if the read end of the pipe is in the readables list and if it is then our signal has arrived.
  5. When the signal arrives read all bytes that are in the pipe and execute any actions that have to be done in response to the signal delivery.

You can check out the implementation on GitHub and try it.

Start the selfpipe.py and send a USR1 signal to it. You should see it output a message Got signal and exit.

{ 9 comments }