What does signal.set_wakeup_fd() solve that a plain signal.signal() handler cannot?
answer
- The Python handler is too late here
- Something must break the blocked wait
- The C handler can write a byte
- Self-pipe, but built into CPython
- Non-blocking descriptor, byte is the signal number
basics
~20 sIt makes CPython's C-level handler write the signal number to a file descriptor the moment the signal lands, so a thread blocked in a file-descriptor wait such as select.select wakes immediately instead of waiting for the bytecode loop.
solid answer
~50 sA Python handler runs only when the main thread reaches a bytecode boundary, which is no help when that thread is parked inside a C-level wait on file descriptors. `signal.set_wakeup_fd(fd)` closes the gap: CPython's *C* handler, which runs at delivery time, writes a single byte — the signal number — to `fd`. If `fd` is the write end of a pipe whose read end is in your `select.select()` set, the wait returns at once, the eval loop resumes, your Python handler runs, and the loop drains the byte and reacts on the normal code path. The file descriptor must be non-blocking, only the main thread may set it, `-1` disables it, and the call returns the previous descriptor. You still need `signal.signal()` for the signal, because the byte is written by the handler CPython installs there.
code
python · 14 linesimport os
import select
import signal
read_fd, write_fd = os.pipe()
os.set_blocking(write_fd, False)
signal.set_wakeup_fd(write_fd)
signal.signal(signal.SIGUSR1, lambda signum, frame: print("python handler ran"))
signal.raise_signal(signal.SIGUSR1)
ready, _, _ = select.select([read_fd], [], [], 0)
print("pipe readable:", bool(ready))
print("byte is the signal number:", os.read(read_fd, 1) == bytes([signal.SIGUSR1]))
signal.set_wakeup_fd(-1)go deeper
You are not expected to know this function. Take away the shape of the problem: a program sitting in a blocking wait on file descriptors needs something to break that wait when a signal arrives.
Be able to explain the mechanism in one breath: the C-level handler writes the signal number to a pipe you supply, so a select-style wait becomes readable immediately instead of waiting for the bytecode loop.
Know when it is worth it — a hand-written multiplexing loop that must fold signal arrival into one readiness path — and the operational rules: non-blocking descriptor, main thread only, one per process, drain it every iteration.
Judge whether hand-rolled multiplexing is the right level to be working at. Usually the answer is to adopt a loop implementation that already wires this correctly, and reserve the raw mechanism for components that genuinely cannot.
## The gap it fills CPython handles a signal in two stages: a C-level handler flags it at delivery time, and the Python-level handler runs later, when the main thread of the main interpreter reaches a bytecode boundary. That second stage is the problem for any program whose main thread spends its life blocked in a wait on file descriptors. The wait is C code; it does not return because a flag was set somewhere. `signal.set_wakeup_fd(fd)` gives the *first* stage something useful to do. Once registered, CPython's C handler writes one byte to `fd` immediately on delivery, and the byte's value is the signal number. Point that at the write end of a pipe, put the read end in the descriptor set your loop waits on, and the wait becomes readable the instant the signal arrives. This is the classic self-pipe trick, wired into the interpreter so you do not have to write the C handler yourself. ## The rules the API enforces * **Non-blocking is mandatory.** A blocking descriptor is rejected, because the C handler must never stall. If the buffer fills, the byte is dropped and a warning is issued unless you pass `warn_on_full_buffer=False`. * **Main thread of the main interpreter only** — the same restriction as `signal.signal()`. * **It returns the previous descriptor**, and `signal.set_wakeup_fd(-1)` disables the mechanism. Save and restore it if a library might already own it, because there is exactly one such descriptor per process. * **CPython never closes it for you**, and it does not keep the pipe alive; if the read end is closed and the buffer fills you are dropping bytes silently. * **You still call `signal.signal()`.** The write happens inside the C handler that `signal.signal()` installs, so a signal left at its default disposition writes nothing. * On Windows the argument is a socket handle rather than a pipe descriptor, because there is no `select` over pipes there. ## The byte is a hint, not a log One byte per delivery is written, and the buffer is small. Two things follow. First, treat the bytes as a wakeup, not as an event stream: read whatever is available, then consult your own state — the flags your Python handler set — for what actually happened. Second, drain the pipe every time round the loop, or you will eventually fill the buffer and get the warning about dropped wakeups. The reason the mechanism is still worth it is ordering: the wakeup is prompt and reliable enough to bound your loop's reaction time, which a plain handler cannot promise while the loop is blocked in C. ## Why it is not the only answer Since 3.5, an interrupted system call is retried automatically after the Python handler runs, so a main thread blocked in a simple read reacts perfectly well without any of this. The wakeup descriptor earns its place when your loop multiplexes many descriptors and must fold signal arrival into the *same* wait as everything else, so there is one readiness path rather than a wait plus a separate check. Frameworks that own an event loop set this up for you; hand-written multiplexing loops have to do it themselves. ## How it is asked This is a differentiator rather than a screening question — few candidates are marked down for not knowing the function's name. What earns credit is the reasoning: recognising that the two-stage handler leaves a blocked C-level wait unserved, and knowing that the standard library exposes a hook in the first stage to break that wait. Naming the non-blocking requirement and the fact that the byte carries the signal number tells an interviewer you have actually used it.
- If you register a wakeup file descriptor but never call signal.signal() for that signal, what happens?Nothing is written. The byte is written by the C-level handler that signal.signal() installs, so a signal still at its default disposition is handled by the kernel's default action and never reaches CPython's handler. The wakeup descriptor is an addition to a registered handler, not a replacement for one.
- Why must the file descriptor be non-blocking, and what happens when its buffer fills?The write happens inside the C-level signal handler, which must never stall; a blocking descriptor could pause arbitrary code at an arbitrary point, so the API rejects it. When the buffer is full the byte is discarded and a warning is issued, unless the call passed warn_on_full_buffer=False. That is why the loop must drain the read end on every iteration.
saying these in an interview costs you the question
- Thinks the Python handler itself writes the byte
- Registers a blocking descriptor and expects it to work
- Believes it replaces the need for signal.signal
- Treats the bytes as a reliable event log to count
- Forgets to drain the read end each iteration
- Tries to register the descriptor from a worker thread