Sunday, August 17, 2008

PyMOTW: signal

Receive notification of asynchronous system events with the signal module.

Module: signal
Purpose: Handle asynchronous events.
Python Version: 1.4 and later

Description:

Programming with Unix signal handlers is a non-trivial endeavor. This is an introduction, and does not include all of the details you may need to use signals successfully on every platform. There is some degree of standardization across versions of Unix, but there is also some variation, so consult documentation for your OS if you run into trouble.

Signals are a means of notifying your program of an event, and having it handled asynchronously. They can be generated by the system itself, or sent from one process to another. Since signals interrupt the regular flow of your program, it is possible that some operations (especially I/O) may produce error if a signal is received in the middle.

Signals are identified by integers and are defined in the operating system C headers. Python defines the signals appropriate for the platform as symbols in the signal module. For the examples below, I will use SIGINT and SIGUSR1. Both are typically defined for all Unix and Unix-like systems.

Receiving Signals:

As with other forms of event-based programming, signals are received by establishing a callback function, called a signal handler, that is invoked when the signal occurs. The arguments to your signal handler are the signal number and the stack frame from the point in your program that was interrupted by the signal.

import signal
import os
import time

def receive_signal(signum, stack):
print 'Received:', signum

signal.signal(signal.SIGUSR1, receive_signal)
signal.signal(signal.SIGUSR2, receive_signal)

print 'My PID is:', os.getpid()

while True:
print 'Waiting...'
time.sleep(3)


This relatively simple example script loops indefinitely, pausing for a few seconds each time. When a signal comes in, the sleep call is interrupted and the signal handler receive_signal() prints the signal number. When the signal handler returns, the loop continues.

To send signals to the running program, I use the command line program kill. To produce the output below, I ran signal_signal.py in one window, then kill -USR1 $pid, kill -USR2 $pid, and kill -INT $pid in another.

$ python signal_signal.py 
My PID is: 71387
Waiting...
Waiting...
Waiting...
Received: 30
Waiting...
Waiting...
Received: 31
Waiting...
Waiting...
Traceback (most recent call last):
File "signal_signal.py", line 25, in
time.sleep(3)
KeyboardInterrupt


getsignal():

To see what signal handlers are registered for a signal, use getsignal(). Pass the signal number as argument. The return value is the registered handler, or one of the special values signal.SIG_IGN (if the signal is being ignored), signal.SIG_DFL (if the default behavior is being used), or None (if the existing signal handler was registered from C, rather than Python).

import signal
import pprint

def alarm_received(n, stack):
return

signal.signal(signal.SIGALRM, alarm_received)

signals_to_names = {}
for n in dir(signal):
if n.startswith('SIG') and not n.startswith('SIG_'):
signals_to_names[getattr(signal, n)] = n

for s in xrange(1, signal.NSIG):
name = signals_to_names[s]
handler = signal.getsignal(s)
if handler is signal.SIG_DFL:
handler = 'SIG_DFL'
elif handler is signal.SIG_IGN:
handler = 'SIG_IGN'
print '%-10s (%2d):' % (name, s), handler


Again, since each OS may have different signals defined, the output you see from running this on other systems may vary. This is from OS X:


$ python signal_getsignal.py
SIGHUP ( 1): SIG_DFL
SIGINT ( 2): <built-in function default_int_handler>
SIGQUIT ( 3): SIG_DFL
SIGILL ( 4): SIG_DFL
SIGTRAP ( 5): SIG_DFL
SIGIOT ( 6): SIG_DFL
SIGEMT ( 7): SIG_DFL
SIGFPE ( 8): SIG_DFL
SIGKILL ( 9): None
SIGBUS (10): SIG_DFL
SIGSEGV (11): SIG_DFL
SIGSYS (12): SIG_DFL
SIGPIPE (13): SIG_IGN
SIGALRM (14): <function alarm_received at 0x7c3f0>
SIGTERM (15): SIG_DFL
SIGURG (16): SIG_DFL
SIGSTOP (17): None
SIGTSTP (18): SIG_DFL
SIGCONT (19): SIG_DFL
SIGCHLD (20): SIG_DFL
SIGTTIN (21): SIG_DFL
SIGTTOU (22): SIG_DFL
SIGIO (23): SIG_DFL
SIGXCPU (24): SIG_DFL
SIGXFSZ (25): SIG_IGN
SIGVTALRM (26): SIG_DFL
SIGPROF (27): SIG_DFL
SIGWINCH (28): SIG_DFL
SIGINFO (29): SIG_DFL
SIGUSR1 (30): SIG_DFL
SIGUSR2 (31): SIG_DFL


Sending Signals:

The function for sending signals is os.kill(). Its use is covered in the PyMOTW article covering the os module.

Alarms:

Alarms are a somewhat special sort of signal, where your program asks the OS to notify it after some period of time has elapsed. As the standard module documentation points out, this is useful for avoiding blocking indefinitely on an I/O operation or other system call.

import signal
import time

def receive_alarm(signum, stack):
print 'Alarm :', time.ctime()

# Call receive_alarm in 2 seconds
signal.signal(signal.SIGALRM, receive_alarm)
signal.alarm(2)

print 'Before:', time.ctime()
time.sleep(4)
print 'After :', time.ctime()


In this example, the call to sleep() does not last the full 4 seconds.


$ python signal_alarm.py
Before: Sun Aug 17 10:51:09 2008
Alarm : Sun Aug 17 10:51:11 2008
After : Sun Aug 17 10:51:11 2008


Ignoring Signals:

To ignore a signal, register SIG_IGN as the handler. This script replaces the default handler for SIGINT with SIG_IGN, and registers a handler for SIGUSR1. Then it uses signal.pause() to wait for a signal to be received.

import signal
import os
import time

def do_exit(sig, stack):
raise SystemExit('Exiting')

signal.signal(signal.SIGINT, signal.SIG_IGN)
signal.signal(signal.SIGUSR1, do_exit)

print 'My PID:', os.getpid()

signal.pause()


Normally SIGINT (the signal sent by the shell to your program when you hit Ctrl-C) raises a KeyboardInterrupt. In this case, we ignore SIGINT and raise SystemExit when we see SIGUSR1. Each ^C represents an attempt to use Ctrl-C to kill the script from the terminal. Using kill -USR1 72531 from another terminal eventually causes the script to exit.

$ python signal_ignore.py 
My PID: 72598
^C^C^C^CExiting


Signals and Threads:

Signals and threads don't generally mix well. Only the main thread of a process will receive signals, so it is not generally useful to try to use them in threads. The following example sets up a signal handler, waits for the signal in one thread, and sends the signal from another.

import signal
import threading
import os
import time

def signal_handler(num, stack):
print 'Received signal %d in %s' % (num, threading.currentThread())

signal.signal(signal.SIGUSR1, signal_handler)

def wait_for_signal():
print 'Waiting for signal in', threading.currentThread()
signal.pause()
print 'Done waiting'

# Start a thread that will not receive the signal
receiver = threading.Thread(target=wait_for_signal, name='receiver')
receiver.start()
time.sleep(0.1)

def send_signal():
print 'Sending signal in', threading.currentThread()
os.kill(os.getpid(), signal.SIGUSR1)

sender = threading.Thread(target=send_signal, name='sender')
sender.start()
sender.join()

# Wait for the thread to see the signal (not going to happen!)
print 'Waiting for', receiver
signal.alarm(2)
receiver.join()


Notice that the signal handlers were all registered in the main thread. This is a requirement of the signal module implementation for Python, regardless of underlying platform support for mixing threads and signals. Although the receiver thread calls signal.pause(), it does not receive the signal. The signal.alarm(2) call near the end of the example prevents an infinite block, since the receiver thread will never exit.

$ python signal_threads.py 
Waiting for signal in <Thread(receiver, started)>
Sending signal in <Thread(sender, started)>
Received signal 30 in <_MainThread(MainThread, started)>
Waiting for <Thread(receiver, started)>
Alarm clock


Although alarms can be set in threads, they are also received by the main thread.

import signal
import time
import threading

def signal_handler(num, stack):
print time.ctime(), 'Alarm in', threading.currentThread()

signal.signal(signal.SIGALRM, signal_handler)

def use_alarm():
print time.ctime(), 'Setting alarm in', threading.currentThread()
signal.alarm(1)
print time.ctime(), 'Sleeping in', threading.currentThread()
time.sleep(3)
print time.ctime(), 'Done with sleep'

# Start a thread that will not receive the signal
alarm_thread = threading.Thread(target=use_alarm, name='alarm_thread')
alarm_thread.start()
time.sleep(0.1)

# Wait for the thread to see the signal (not going to happen!)
print time.ctime(), 'Waiting for', alarm_thread
alarm_thread.join()

print time.ctime(), 'Exiting normally'


Notice that the alarm does not abort the sleep() call in use_alarm().


$ python signal_threads_alarm.py
Sun Aug 17 12:06:00 2008 Setting alarm in <Thread(alarm_thread, started)>
Sun Aug 17 12:06:00 2008 Sleeping in <Thread(alarm_thread, started)>
Sun Aug 17 12:06:00 2008 Waiting for <Thread(alarm_thread, started)>
Sun Aug 17 12:06:03 2008 Done with sleep
Sun Aug 17 12:06:03 2008 Alarm in <_MainThread(MainThread, started)>
Sun Aug 17 12:06:03 2008 Exiting normally


References:

Python Module of the Week Home
Download Sample Code


Technorati Tags:
,




[Updated 19 Aug to point to release 1.66.1, which includes a fix for the logic bug pointed out by Ernesto in comments.]

8 comments:

Ernesto said...

I've found a bug in your code for getsignal. Using signal_getsignal.py on Linux (Ubuntu 7.10) will raise a KeyError exception. signal.NSIG is 65 but there are some "gaps" between 1 and 64. The correct way would be to iterate the signals_to_names dictionary with signals_to_names.keys() instead of using a xrange(1, signal.NSIG).

Doug Hellmann said...

Thanks, that's somewhat subtle. I didn't realize there could be gaps like that.

I've updated the post to point to release 1.66.1, which includes a fix.

Gerhard Prochaska said...

I m working on SuSE 11 with Kernel 2.6.25.16-0.1-pae running Python 2.5.2.

Had same Problem as ernesto and changed your code to:

for s in xrange(1, signal.NSIG):

try:
name = signals_to_names[s]
handler = signal.getsignal(s)
except KeyError:
name = 'undefined'
handler = ''

So i see that:

SIGUSR1 (10): SIG_DFL
SIGALRM (14): SIG_DFL

point at their defaults.

But no i have troubles understanding your example for 'Signals and Threads'.

your signal.alarm(2) is breaking the block on the main thread which is done by receiver.join() but when i try to detect an alarm signal by having an additional alarm handler it wont work:

import signal
import threading
import os
import time

def signal_handler(num, stack):
print 'Received signal %d in %s' % (num, threading.currentThread())

def alarm_handler(num, stack):
print 'Received alarm %d in %s' % (num, threading.currentThread())

signal.signal(signal.SIGUSR1, signal_handler)
signal.signal(signal.SIGALRM, alarm_handler)

def wait_for_signal():
print 'Waiting for signal in', threading.currentThread()
signal.pause()
print 'Done waiting'

# Start a thread that will not receive the signal
receiver = threading.Thread(target=wait_for_signal, name='receiver')
receiver.start()
time.sleep(0.1)

def send_signal():
print 'Sending signal in', threading.currentThread()
os.kill(os.getpid(), signal.SIGUSR1)

sender = threading.Thread(target=send_signal, name='sender')
sender.start()
sender.join()

# Wait for the thread to see the signal (not going to happen!)
print 'Waiting for', receiver
signal.alarm(2)
receiver.join()

I'd now expect to see my message 'Received alarm ..." after 2 Seconds but i dont get the message and the program wont stop now ...

any hints what i did wrong ?

greetings Gerhard

Doug Hellmann said...

@gerhard - It's likely that joining the thread is preventing you from receiving or processing the alarm signal.

Gerhard Prochaska said...

Hi Dough !

What i really want to do is .. I want to have control with my python script over what happens if someone closes my konsole window which invoked the script by clicking on the x symbol (upper right corner symbol). I dont find the signal which i have to provide a handler for to be able to cleanup everything my script initiated.

For example i use the pexpect library to do an ssh session to a foreign host. When my script has troubles (network timeout, cable disconnect,..) i can handle this in the script with exceptions and timeouts. The only situation an ssh session will stay as a zombie is when someone kills the script by pressing the x symbol.

If i knew the signal i could track the spawned process id's and kill em with the signal handling routine ...

Do you know what signal i need to intercept or am i on the wrong track to achieve my goal ???

Help would be appreciated :-)
Greetings from Vienna.

Doug Hellmann said...

@Gerhard - It might depend on the type of terminal. Usually it would be SIGTERM, but it might be SIGKILL.

Gerhard Prochaska said...

Hi again !

Topic: Running Cleanup Code when KDS konsole which invoked a Python Script is closed manually by pressing Close Button (upper right X-Button)

This Code is doing the trick if cleanup procedure does not take too much time:

def print_callback():
print "print_callback"

def receive_signal(signum, stack):
print 'Received:', signum

if signum==1:
print "SIGHUP Received"
# this Signal is raised when Crtl-F4 is pressed
# or
# if the X Button in the upper right corner of the KDE konsole window is pressed
elif signum==2:
print "SIGINT Received"
# this Signal is raised when Crtl-C is pressed
else:
print "SIGNAL ",signum, " Received"

time.sleep(10)
print 'Sleep over'
#those print statements are queued somehow
# If Signal is triggered several times they appear all at the same time

I am able to trap the Signals now which are raised when someone presses Crtl-C, Crtl-F4 or if the X-Button in the KDE Konsole which invoked the Script is pressed. On the Close-Event (X-Button) or Crtl-F4 after approximatly 1 Second a window appears "Program does not respond" asking "do you want to terminate Konsole" yes/no.

Can i avoid this window somehow except ending my script within this 1 second timeframe.

I read in some article that SIGKILL can NEVER be handled by a script so the sequence Crtl-Alt-Escape which produces a grafical Cursor with an X-Symbol and a SIGKILL Signal when you click into the konsole window can never be caught to run cleanup code before the kill is executed.

Doug Hellmann said...

Unfortunately I can't tell what parts of your program are in functions or not because the whitespace was stripped from the commands. Can you post it on a pastebin site?