Skip to main content

Modules/signalmodule.c (part 2)

Source:

cpython 3.14 @ ab2d84fe1023/Modules/signalmodule.c

This annotation covers signal registration and delivery. See modules_signal_detail for signal.getsignal, signal.alarm, signal.pause, SIGCHLD, and the main thread restriction.

Map

LinesSymbolRole
1-100signal.signalRegister a Python handler for a Unix signal
101-220signal_handlerC-level trampoline called by the OS
221-360_PyEval_SignalReceivedNotify the eval loop that a signal is pending
361-480Wakeup fdWrite a byte to a pipe/socket on signal delivery
481-500SIG_DFL / SIG_IGN constantsRestore default or ignore

Reading

signal.signal

// CPython: Modules/signalmodule.c:560 signal_signal_impl
static PyObject *
signal_signal_impl(PyObject *module, int signalnum, PyObject *handler)
{
/* Only the main thread can set signal handlers */
if (!_Py_IsMainThread() || !_Py_IsMainInterpreter(tstate)) {
PyErr_SetString(PyExc_ValueError,
"signal only works in main thread of the main interpreter");
return NULL;
}
PyObject *old_handler = Handlers[signalnum].func;
if (handler == IgnoreHandler) {
PyOS_setsig(signalnum, SIG_IGN);
} else if (handler == DefaultHandler) {
PyOS_setsig(signalnum, SIG_DFL);
} else {
/* Install the C trampoline */
PyOS_setsig(signalnum, signal_handler);
}
Handlers[signalnum].func = Py_NewRef(handler);
return old_handler;
}

signal.signal(signal.SIGINT, my_handler) replaces the default KeyboardInterrupt handler. The C trampoline signal_handler is the actual OS handler; it just records that a signal arrived and notifies the eval loop.

signal_handler trampoline

// CPython: Modules/signalmodule.c:280 signal_handler
static void
signal_handler(int sig_num)
{
int save_errno = errno;
Handlers[sig_num].tripped = 1;
/* Notify the eval loop: set a flag in the thread state */
_PyEval_SignalReceived(tstate);
/* Write to wakeup fd if set (for select/poll loops) */
if (wakeup.fd != INVALID_FD) {
unsigned char byte = (unsigned char)sig_num;
(void)write(wakeup.fd, &byte, 1);
}
errno = save_errno;
}

The C handler does the minimum safe work: set an int flag (async-signal-safe), poke the eval loop, and write one byte to the wakeup fd. Python code runs later when _PyEval_MakePendingCalls is called at a safe point.

_PyEval_SignalReceived

// CPython: Python/ceval.c:220 _PyEval_SignalReceived
void
_PyEval_SignalReceived(PyThreadState *tstate)
{
/* Set eval_breaker so the eval loop checks pending calls at the next
opcode boundary. */
_Py_atomic_store_relaxed(&tstate->eval_breaker, 1);
_Py_atomic_store_relaxed(&tstate->pending_calls_to_do, 1);
}

eval_breaker is checked by the eval loop after every N instructions (default 100, controlled by sys.setswitchinterval). When set, the loop calls _PyEval_EvalFrameDefault's check_eval_breaker which dispatches pending calls including signal handlers.

gopy notes

signal.signal is module/signal.Signal in module/signal/module.go. The C trampoline is replaced by a Go channel write: signalCh <- sigNum. _PyEval_SignalReceived sets vm.EvalBreaker via an atomic store. The eval loop reads the channel in vm.CheckPendingSignals.