What happens if a Python program receives the same signal twice in quick succession, so that the second signal is received while the first signal's handler is still executing?
We can test this by writing a signal handler that artificially takes a long time by calling time.sleep, then calling kill twice in a subprocess. We'll have the signal handler print the end of the call stack.
import os, signal, subprocess, time, traceback
def sighandler(_signo, _frame):
# Print the last two functions on the call stack.
print("".join(traceback.format_stack(limit=2)))
time.sleep(3)
signal.signal(signal.SIGUSR1, sighandler)
# Raise SIGUSR1 to this process twice.
pid = os.getpid()
subprocess.run(f"kill -USR1 {pid}; sleep 1; kill -USR1 {pid}", shell=True)
sigaction(2) says (emphasis mine):
sa_maskspecifies a mask of signals which should be blocked (i.e., added to the signal mask of the thread in which the signal handler is invoked) during execution of the signal handler. In addition, the signal which triggered the handler will be blocked, unless theSA_NODEFERflag is used.
So we should expect that the first call to sighandler completes before the second call starts. (We do expect a second call: blocking a signal is different than ignoring it.)
But that is not what happens:
File "lib/python3.11/subprocess.py", line 2004, in _try_wait
(pid, sts) = os.waitpid(self.pid, wait_flags)
File "reentrant.py", line 5, in sighandler
print("".join(traceback.format_stack(limit=2)))
File "reentrant.py", line 6, in sighandler
time.sleep(3)
File "reentrant.py", line 5, in sighandler
print("".join(traceback.format_stack(limit=2)))
Look at the second backtrace. The signal handler is being called inside of itself! (And for further evidence, time the program and observe that it takes 4 seconds, not 6.)
The equivalent C program shows the expected behavior:
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void sighandler(int signo, siginfo_t*, void*) {
#define print(msg) write(1, msg, sizeof msg)
print("sighandler enter\n");
sleep(3);
print("sighandler exit\n");
}
int main() {
struct sigaction act = { 0 };
act.sa_sigaction = sighandler;
sigaction(SIGUSR1, &act, NULL);
printf("%d\n", getpid());
getchar();
return 0;
}
It prints:
sighandler enter
sighandler exit
sighandler enter
sighandler exit
Why is Python's behavior different from C (and from what the man page describes)?
The reason is that Python has two signal handlers: a low-level signal handler running in C, and the user-supplied signal handler running in Python. The man page describes the behavior of the C signal handler, not the Python signal handler. Python gives no guarantees about the reentrancy of user-supplied signal handlers.
Does this matter in practice? I doubt it. The automatic protection against reentrancy in C only applies to instances of the same signal, so handlers for different signals could still be reentrant. And if you follow safe signal idioms, then you are not doing real work inside the signal handler, and so you should not have to worry about reentrancy.