Signal -- Asynchronous System Events

Signals are an operating system feature that provide a means of notifying a 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 the program, it is possible that some operations (especially I/O) may produce errors if a signal is received in the middle.

Signals are identified by integers and are defined in the operating system C headers. Python exposes the signals appropriate for the platform as symbols in the signal module. The examples in this section 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 the signal handler are the signal number and the stack frame from the point in the program that was interrupted by the signal.


In [ ]:
# %load signal_signal.py
import signal
import os
import time


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


# Register signal handlers
signal.signal(signal.SIGUSR1, receive_signal)
signal.signal(signal.SIGUSR2, receive_signal)

# Print the process ID so it can be used with 'kill'
# to send this program signals.
print('My PID is:', os.getpid())

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

This 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. After the signal handler returns, the loop continues.

Send signals to the running program using os.kill() or the Unix command line program kill.


In [3]:
!python signal_signal.py


My PID is: 23775
Waiting...
Waiting...
Waiting...
Waiting...
Waiting...
Received: 10
Waiting...
Received: 10
Waiting...
Waiting...
Received: 12
Waiting...
Waiting...
Waiting...
Terminated

Retrieving Registered Handlers

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 SIG_IGN (if the signal is being ignored), SIG_DFL (if the default behavior is being used), or None (if the existing signal handler was registered from C, rather than Python).


In [4]:
import signal


def alarm_received(n, stack):
    return


signal.signal(signal.SIGALRM, alarm_received)

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

for s, name in sorted(signals_to_names.items()):
    handler = signal.getsignal(s)
    if handler is signal.SIG_DFL:
        handler = 'SIG_DFL'
    elif handler is signal.SIG_IGN:
        handler = 'SIG_IGN'
    print('{:<10} ({:2d}):'.format(name, s), handler)


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
SIGBUS     ( 7): SIG_DFL
SIGFPE     ( 8): SIG_DFL
SIGKILL    ( 9): SIG_DFL
SIGUSR1    (10): SIG_DFL
SIGSEGV    (11): SIG_DFL
SIGUSR2    (12): SIG_DFL
SIGPIPE    (13): SIG_IGN
SIGALRM    (14): <function alarm_received at 0x7f56802a1158>
SIGTERM    (15): SIG_DFL
SIGCLD     (17): SIG_DFL
SIGCONT    (18): SIG_DFL
SIGSTOP    (19): SIG_DFL
SIGTSTP    (20): SIG_DFL
SIGTTIN    (21): SIG_DFL
SIGTTOU    (22): SIG_DFL
SIGURG     (23): SIG_DFL
SIGXCPU    (24): SIG_DFL
SIGXFSZ    (25): SIG_IGN
SIGVTALRM  (26): SIG_DFL
SIGPROF    (27): SIG_DFL
SIGWINCH   (28): SIG_DFL
SIGPOLL    (29): SIG_DFL
SIGPWR     (30): SIG_DFL
SIGSYS     (31): SIG_DFL
SIGRTMIN   (34): SIG_DFL
SIGRTMAX   (64): SIG_DFL

Sending Signals

The function for sending signals from within Python is os.kill(). Its use is covered in the section on the os module, Creating Processes with os.fork().

Alarms

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


In [5]:
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())


Before: Fri Oct 20 13:53:30 2017
Alarm : Fri Oct 20 13:53:32 2017
After : Fri Oct 20 13:53:34 2017

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.

Don't execute the following code inside notebook, it will hang forever

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()

Signals and Threads

Signals and threads do not generally mix well because only the main thread of a process will receive signals. The following example sets up a signal handler, waits for the signal in one thread, and sends the signal to another


In [ ]:
import signal
import threading
import os
import time


def signal_handler(num, stack):
    print('Received signal {} in {}'.format(
        num, threading.currentThread().name))


signal.signal(signal.SIGUSR1, signal_handler)


def wait_for_signal():
    print('Waiting for signal in',
          threading.currentThread().name)
    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().name)
    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.name)
signal.alarm(2)
receiver.join()


Waiting for signal in receiver
Sending signal in senderReceived signal 10 in MainThread

Waiting for receiver

The signal handlers were all registered in the main thread because 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.

Although alarms can be set in any thread, they are always received by the main thread.


In [1]:
import signal
import time
import threading


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


signal.signal(signal.SIGALRM, signal_handler)


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


# 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.name)
alarm_thread.join()

print(time.ctime(), 'Exiting normally')


Fri Oct 20 14:11:45 2017 Setting alarm in alarm_thread
Fri Oct 20 14:11:45 2017 Sleeping in alarm_thread
Fri Oct 20 14:11:45 2017 Waiting for alarm_thread
Fri Oct 20 14:11:46 2017 Alarm in MainThread
Fri Oct 20 14:11:48 2017 Done with sleep in alarm_thread
Fri Oct 20 14:11:48 2017 Exiting normally

In [ ]: