“Longing on a large scale is what makes history.”

time.sleep and signal interrupts

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.

$ 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!