If you use subprocess.Popen()
to create child processes, and you do not want them to be killed by a SIGINT signal, use the preexec_fn
argument to set the SIGINT signal disposition to ignored before the new binary is executed:
child = subprocess.Popen(...,
preexec_fn = lambda: signal.signal(signal.SIGINT, signal.SIG_IGN))
where ...
is a placeholder for your current parameters.
If you use actual threads (either threads or threading module), Python's signal module sets everything up so that only the main/initial thread can receive signals or set signal handlers. So, proper threads are not really affected by signals in Python.
In the subprocess.Popen()
case, the child process initially inherits a copy of the process, including signal handlers. This means that there is a small window during which the child process may catch a signal, using the same code as the parent process; but, because it is a separate process, only its side effects are visible. (For example, if the signal handler calls sys.exit()
, only the child process will exit. The signal handler in the child process cannot change any variables in the parent process.)
To avoid this, the parent process can temporarily switch to a different signal handler, that only remembers if a signal is caught, for the duration of the subprocess creation:
import signal
# Global variables for sigint diversion
sigint_diverted = False # True if caught while diverted
sigint_original = None # Original signal handler
def sigint_divert_handler():
global sigint_diverted
sigint_diverted = True
def sigint_divert(interrupts=False):
"""Temporarily postpone SIGINT signal delivery."""
global sigint_diverted
global sigint_original
sigint_diverted = False
sigint_original = signal.signal(signal.SIGINT, sigint_divert_handler)
signal.siginterrupt(signal.SIGINT, interrupts)
def sigint_restore(interrupts=True):
"""Restore SIGINT signal delivery to original handler."""
global sigint_diverted
global sigint_original
original = sigint_original
sigint_original = None
if original is not None:
signal.signal(signal.SIGINT, original)
signal.siginterrupt(signal.SIGINT, interrupts)
diverted = sigint_diverted
sigint_diverted = False
if diverted and original is not None:
original(signal.SIGINT)
With the above helpers, the idea is that before creating a child process (using subprocess module, or some of the os module functions), you call sigint_divert()
. The child process inherits a copy of the diverted SIGINT handler. After creating the child process, you restore SIGINT handling by calling sigint_restore()
. (Note that if you called signal.siginterrupt(signal.SIGINT, False)
after setting your original SIGINT handler, so that its delivery won't raise IOError exceptions, you should call here sigint_restore(False)
instead.)
This way, the signal handler in the child process is the diverted signal handler, which only sets a global flag and does nothing else. Of course, you still want to use the preexec_fn =
parameter to subprocess.Popen()
, so that SIGINT signal is ignored completely when the actual binary is executed in the child process.
The sigint_restore()
not only restores the original signal handler, but if the diverted signal handler caught a SIGINT signal, it is "re-raised" by calling the original signal handler directly. This assumes that the original handler is one you've already installed; otherwise, you could use os.kill(os.getpid(), signal.SIGKILL)
instead.
Python 3.3 and later on non-Windows OSes exposes signal masks, which can be used to "block" signals for a duration. Blocking means that the delivery of the signal is postponed, until unblocked; not ignored. This is exactly what the above signal diversion code tries to accomplish.
The signals are not queued, so if one signal is already pending, any further signals of that same type are ignored. (So, only one signal of each type, say SIGINT, can be pending at the same time.)
This allows a pattern using two helper functions,
def block_signals(sigset = { signal.SIGINT }):
mask = signal.pthread_sigmask(signal.SIG_BLOCK, {})
signal.pthread_sigmask(signal.SIG_BLOCK, sigset)
return mask
def restore_signals(mask):
signal.pthread_sigmask(signal.SIG_SETMASK, mask)
so that one calls mask = block_signals()
before creating a thread or a subprocess, and restore_signals(mask)
afterwards. In the created thread or subprocess, the SIGINT signal is blocked by default.
Blocked SIGINT signal can also be consumed using signal.sigwait({signal.SIGINT})
(which blocks until one is delivered), or signal.sigtimedwait({signal.SIGINT}, 0)
which returns immediately with the signal if one is pending, and None
otherwise.
When a subprocess manages its own signal mask and signal handlers, we cannot make it ignore a SIGINT signal.
On Unix/POSIXy machines, we can stop the SIGINT from being sent to the child process, however, by detaching the child process from the controlling terminal, and running it in its own session.
There are two sets of changes needed in subprocess.Popen()
:
Execute the command or binary under setsid
: either [ "setsid", "program", args.. ]
or "setsid sh -c 'command'"
, depending on whether you supply the binary to be executed as a list or as a string.
setsid
is a command-line utility that runs the specified program with the specified arguments in a new session. The new session does not have a controlling terminal, which means it will not receive a SIGINT if the user presses Ctrl+C.
If the parent does not use a pipe for the subprocess' stdin
, stdout
, or stderr
, they should be explicitly opened to os.devnull
:
stdin=open(os.devnull, 'rb')
,
stdout=open(os.devnull, 'wb')
,
stderr=open(os.devnull, 'wb')
This ensures that the subprocess does not fall back under the controlling terminal. (It is the controlling terminal that sends the SIGINT signal to each process when user presses Ctrl+C.)
If the parent process wants to, it can send a SIGINT signal to the child process using os.kill(child.pid, signal.SIGINT)
.