time.sleep and signal interrupts
- Python has two signal handlers
- Blocking a signal is different than ignoring it
- Python doesn't pass
SA_RESTART - Safe signal idioms
- Python signal handlers are unexpectedly reentrant
- Is it safe to call
printin a Python signal handler? time.sleepand signal interrupts- Python
siginterruptis confusing
Consider this Python program:
import os
import signal
import subprocess
import time
# Set a signal handler.
signal.signal(signal.SIGUSR1, lambda _signo, _frame: None)
# After 3 seconds, send signal to self.
subprocess.Popen(f"sleep 3 && kill -USR1 {os.getpid()}", shell=True)
# Go to sleep.
time.sleep(5)
How long does it take to run? The answer depends on how you think the signal interrupt and sleep interact.
- If the signal ends the sleep early, it should take 3 seconds.
- If the sleep restarts from where it left off after the signal, it should take 5 seconds.
- If the sleep restarts from the beginning after the signal, it should take 8 seconds.
$ time python3 sleepy.py
real 0m5.031s
user 0m0.030s
sys 0m0.000s
This is the most sensible result – you asked to sleep 5 seconds and you slept 5 seconds – but how does time.sleep do it? If the syscall is restarted, why does it not start from the beginning and cause the sleep to last 8 seconds instead of 5?
signal(7) explains that sleep functions are not automatically restarted by SA_RESTART (and recall that Python doesn't pass SA_RESTART anyway). So it is up to userspace to restart the sleep after handling the signals.
clock_nanosleep, the syscall that implements time.sleep, has a couple of ways to do this.
The third argument, remain, is an out pointer that is set to the remaining sleep time when clock_nanosleep is interrupted – in our case, it would be set to approximately 2 seconds. You can then call clock_nanosleep again with the value in remain to complete the sleep.
What does CPython do? We can use strace to print the calls to clock_nanosleep:
$ strace -e clock_nanosleep python3 sleepy.py
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, {tv_sec=268904, tv_nsec=94641921}, NULL) = ? ERESTARTNOHAND (To be restarted if no handler)
--- SIGUSR1 {si_signo=SIGUSR1, si_code=SI_USER, si_pid=462964, si_uid=501} ---
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=462964, si_uid=501, si_status=0, si_utime=0, si_stime=0} ---
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, {tv_sec=268904, tv_nsec=94641921}, NULL) = 0
+++ exited with 0 +++
We can see that CPython calls clock_nanosleep with TIMER_ABSTIME and leaves the remain parameter null. TIMER_ABSTIME works better with Python signal handling because the Python signal handler runs before the syscall is retried. If the Python signal handler takes 5 seconds to run, and remain was only set to 3 seconds, then retrying clock_nanosleep with remain causes the program to sleep an extra 3 seconds, while retrying it with TIMER_ABSTIME correctly returns immediately.
This does mean that time.sleep will sleep for max(sleep_duration, signal_handler_duration) seconds, though I hope this point is solely of academic interest as your signal handlers should not be taking tens of seconds to execute!