UNIX/Linux systems offer special mechanisms to communicate between each individual process. One of these mechanisms are signals, and belong to the different methods of communication between processes (Inter Process Communication, abbreviated with IPC).
In short, signals are software interrupts that are sent to the program (or the process) to notify the program of significant events or requests to the program in order to run a special code sequence. A program that receives a signal either stops or continues the execution of its instructions, terminates either with or without a memory dump, or even simply ignores the signal.
Although it is defined in the POSIX standard, the reaction actually depends on how the developer wrote the script and implemented the handling of signals.
In this article we explain what are signals, show you how to sent a signal to another process from the command line as well as processing the received signal. Among other modules, the program code is mainly based on the signal module. This module connects the according C headers of your operating system with the Python world.
An Introduction to Signals
On UNIX-based systems, there are three categories of signals:
-
System signals (hardware and system errors): SIGILL, SIGTRAP, SIGBUS, SIGFPE, SIGKILL, SIGSEGV, SIGXCPU, SIGXFSZ, SIGIO
-
Device signals: SIGHUP, SIGINT, SIGPIPE, SIGALRM, SIGCHLD, SIGCONT, SIGSTOP, SIGTTIN, SIGTTOU, SIGURG, SIGWINCH, SIGIO
-
User-defined signals: SIGQUIT, SIGABRT, SIGUSR1, SIGUSR2, SIGTERM
Each signal is represented by an integer value, and the list of signals that are available is comparably long and not consistent between the different UNIX/Linux variants. On a Debian GNU/Linux system, the command kill -l
displays the list of signals as follows:
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
The signals 1 to 15 are roughly standardized, and have the following meaning on most of the Linux systems:
- 1 (SIGHUP): terminate a connection, or reload the configuration for daemons
- 2 (SIGINT): interrupt the session from the dialogue station
- 3 (SIGQUIT): terminate the session from the dialogue station
- 4 (SIGILL): illegal instruction was executed
- 5 (SIGTRAP): do a single instruction (trap)
- 6 (SIGABRT): abnormal termination
- 7 (SIGBUS): error on the system bus
- 8 (SIGFPE): floating point error
- 9 (SIGKILL): immmediately terminate the process
- 10 (SIGUSR1): user-defined signal
- 11 (SIGSEGV): segmentation fault due to illegal access of a memory segment
- 12 (SIGUSR2): user-defined signal
- 13 (SIGPIPE): writing into a pipe, and nobody is reading from it
- 14 (SIGALRM): the timer terminated (alarm)
- 15 (SIGTERM): terminate the process in a soft way
In order to send a signal to a process in a Linux terminal you invoke the kill
command with both the signal number (or signal name) from the list above and the id of the process (pid). The following example command sends the signal 15 (SIGTERM) to the process that has the pid 12345:
$ kill -15 12345
An equivalent way is to use the signal name instead of its number:
$ kill -SIGTERM 12345
Which way you choose depends on what is more convenient for you. Both ways have the same effect. As a result the process receives the signal SIGTERM, and terminates immediately.
Using the Python signal Library
Since Python 1.4, the signal
library is a regular component of every Python release. In order to use the signal
library, import the library into your Python program as follows, first:
import signal
Capturing and reacting properly on a received signal is done by a callback function - a so-called signal handler. A rather simple signal handler named receiveSignal()
can be written as follows:
def receiveSignal(signalNumber, frame):
print('Received:', signalNumber)
return
This signal handler does nothing else than reporting the number of the received signal. The next step is registering the signals that are caught by the signal handler. For Python programs, all the signals (but 9, SIGKILL) can be caught in your script:
if __name__ == '__main__':
# register the signals to be caught
signal.signal(signal.SIGHUP, receiveSignal)
signal.signal(signal.SIGINT, receiveSignal)
signal.signal(signal.SIGQUIT, receiveSignal)
signal.signal(signal.SIGILL, receiveSignal)
signal.signal(signal.SIGTRAP, receiveSignal)
signal.signal(signal.SIGABRT, receiveSignal)
signal.signal(signal.SIGBUS, receiveSignal)
signal.signal(signal.SIGFPE, receiveSignal)
#signal.signal(signal.SIGKILL, receiveSignal)
signal.signal(signal.SIGUSR1, receiveSignal)
signal.signal(signal.SIGSEGV, receiveSignal)
signal.signal(signal.SIGUSR2, receiveSignal)
signal.signal(signal.SIGPIPE, receiveSignal)
signal.signal(signal.SIGALRM, receiveSignal)
signal.signal(signal.SIGTERM, receiveSignal)
Next, we add the process information for the current process, and detect the process id using the methode getpid()
from the os
module. In an endless while
loop we wait for incoming signals. We implement this using two more Python modules - os and time. We import them at the beginning of our Python script, too:
import os
import time
In the while
loop of our main program the print statement outputs "Waiting...". The time.sleep()
function call makes the program wait for three seconds.
# output current process id
print('My PID is:', os.getpid())
# wait in an endless loop for signals
while True:
print('Waiting...')
time.sleep(3)
Finally, we have to test our script. Having saved the script as signal-handling.py
we can invoke it in a terminal as follows:
$ python3 signal-handling.py
My PID is: 5746
Waiting...
...
In a second terminal window we send a signal to the process. We identify our first process - the Python script - by the process id as printed on screen, above.
$ kill -1 5746
The signal event handler in our Python program receives the signal we have sent to the process. It reacts accordingly, and simply confirms the received signal:
...
Received: 1
...
Ignoring Signals
The signal module defines ways to ignore received signals. In order to do that the signal has to be connected with the predefined function signal.SIG_IGN
. The example below demonstrates that, and as a result the Python program cannot be interrupted by CTRL+C
anymore. To stop the Python script an alternative way has been implemented in the example script - the signal SIGUSR1 terminates the Python script. Furthermore, instead of an endless loop we use the method signal.pause()
. It just waits for a signal to be received.
import signal
import os
import time
def receiveSignal(signalNumber, frame):
print('Received:', signalNumber)
raise SystemExit('Exiting')
return
if __name__ == '__main__':
# register the signal to be caught
signal.signal(signal.SIGUSR1, receiveSignal)
# register the signal to be ignored
signal.signal(signal.SIGINT, signal.SIG_IGN)
# output current process id
print('My PID is:', os.getpid())
signal.pause()
Handling Signals Properly
The signal handler we have used up to now is rather simple, and just reports a received signal. This shows us that the interface of our Python script is working fine. Let's improve it.
Catching the signal is already a good basis but requires some improvement to comply with the rules of the POSIX standard. For a higher accuracy each signal needs a proper reaction (see list above). This means that the signal handler in our Python script needs to be extended by a specific routine per signal. This works best if we understand what a signal does, and what a common reaction is. A process that receives signal 1, 2, 9 or 15 terminates. In any other case it is expected to write a core dump, too.
Up to now we have implemented a single routine that covers all the signals, and handles them in the same way. The next step is to implement an individual routine per signal. The following example code demonstrates this for the signals 1 (SIGHUP) and 15 (SIGTERM).
def readConfiguration(signalNumber, frame):
print ('(SIGHUP) reading configuration')
return
def terminateProcess(signalNumber, frame):
print ('(SIGTERM) terminating the process')
sys.exit()
The two functions above are connected with the signals as follows:
signal.signal(signal.SIGHUP, readConfiguration)
signal.signal(signal.SIGTERM, terminateProcess)
Running the Python script, and sending the signal 1 (SIGHUP) followed by a signal 15 (SIGTERM) by the UNIX commands kill -1 16640
and kill -15 16640
results in the following output:
$ python3 daemon.py
My PID is: 16640
Waiting...
Waiting...
(SIGHUP) reading configuration
Waiting...
Waiting...
(SIGTERM) terminating the process
The script receives the signals, and handles them properly. For clarity, this is the entire script:
import signal
import os
import time
import sys
def readConfiguration(signalNumber, frame):
print ('(SIGHUP) reading configuration')
return
def terminateProcess(signalNumber, frame):
print ('(SIGTERM) terminating the process')
sys.exit()
def receiveSignal(signalNumber, frame):
print('Received:', signalNumber)
return
if __name__ == '__main__':
# register the signals to be caught
signal.signal(signal.SIGHUP, readConfiguration)
signal.signal(signal.SIGINT, receiveSignal)
signal.signal(signal.SIGQUIT, receiveSignal)
signal.signal(signal.SIGILL, receiveSignal)
signal.signal(signal.SIGTRAP, receiveSignal)
signal.signal(signal.SIGABRT, receiveSignal)
signal.signal(signal.SIGBUS, receiveSignal)
signal.signal(signal.SIGFPE, receiveSignal)
#signal.signal(signal.SIGKILL, receiveSignal)
signal.signal(signal.SIGUSR1, receiveSignal)
signal.signal(signal.SIGSEGV, receiveSignal)
signal.signal(signal.SIGUSR2, receiveSignal)
signal.signal(signal.SIGPIPE, receiveSignal)
signal.signal(signal.SIGALRM, receiveSignal)
signal.signal(signal.SIGTERM, terminateProcess)
# output current process id
print('My PID is:', os.getpid())
# wait in an endless loop for signals
while True:
print('Waiting...')
time.sleep(3)
Further Reading
Using the signal
module and an according event handler it is relatively easy to catch signals. Knowing the meaning of the different signals, and to react properly as defined in the POSIX standard is the next step. It requires that the event handler distinguishes between the different signals, and has a separate routine for all of them.
from Planet Python
via read more
No comments:
Post a Comment