# 49. Signals

# 49. Signals

Signals are operating system notifications delivered to a process. CPython exposes them through the `signal` module and integrates them with the interpreter’s evaluation loop, thread model, pending-call machinery, and exception system.

A signal is not a normal Python function call. It can arrive asynchronously from outside the program.

Common examples:

```text id="l7yq3r"
SIGINT     interrupt from Ctrl-C
SIGTERM    termination request
SIGALRM    timer alarm on Unix
SIGHUP     terminal hangup or reload request on Unix
SIGCHLD    child process state change on Unix
```

CPython cannot safely run arbitrary Python code at the exact machine instruction where a signal arrives. Instead, the low-level C signal handler records that a signal occurred. The interpreter later runs the Python signal handler at a safe point.

That design is the core of CPython signal handling.

## 49.1 What a Signal Is

At the operating system level, a signal is a small asynchronous notification sent to a process or thread.

Examples:

```bash id="tdu5pf"
kill -TERM 12345
```

sends `SIGTERM` to process `12345`.

Pressing Ctrl-C in a terminal usually sends `SIGINT` to the foreground process group.

In Python:

```python id="9sjktz"
import signal

print(signal.SIGINT)
print(signal.SIGTERM)
```

A signal has a number and a symbolic name. The exact set of signals depends on the operating system.

## 49.2 CPython’s Signal Handling Model

CPython uses a two-stage model.

```text id="jqmfm4"
1. Low-level C signal handler runs when the OS signal arrives.
2. The handler records the signal and returns quickly.
3. The interpreter notices the pending signal at a safe checkpoint.
4. CPython calls the Python-level signal handler in the main thread.
```

This avoids running complex Python code inside an unsafe low-level signal context.

The C signal handler must be minimal because many operations are unsafe inside an OS signal handler.

## 49.3 Python Signal Handlers

A Python signal handler is a callable taking two arguments:

```python id="angv2p"
def handler(signum, frame):
    ...
```

Register it with `signal.signal`:

```python id="ru5m0h"
import signal

def handle_sigint(signum, frame):
    print("received", signum)

signal.signal(signal.SIGINT, handle_sigint)
```

When `SIGINT` is processed, CPython calls:

```python id="8n6r8c"
handle_sigint(signum, frame)
```

The `frame` argument is the current Python frame where the signal was processed, not necessarily where the signal arrived at the OS level.

## 49.4 Default `SIGINT` Behavior

By default, Ctrl-C raises `KeyboardInterrupt`.

```python id="khrowg"
while True:
    pass
```

Pressing Ctrl-C usually interrupts the loop with:

```text id="n09l4d"
KeyboardInterrupt
```

This behavior is implemented by CPython’s default signal handling for `SIGINT`.

A program can catch it:

```python id="vnylsq"
try:
    run_forever()
except KeyboardInterrupt:
    shutdown()
```

This is often preferable to installing a custom signal handler unless the program needs special signal semantics.

## 49.5 Signals Run in the Main Thread

Python-level signal handlers run in the main Python thread of the main interpreter.

This is a critical rule.

If a worker thread is running when a signal arrives, CPython does not run the Python handler in that worker thread. The handler runs later in the main thread.

Example:

```python id="j035em"
import signal
import threading

def handler(signum, frame):
    print("handler thread:", threading.current_thread().name)

signal.signal(signal.SIGINT, handler)
```

The printed thread is normally the main thread.

This simplifies interpreter safety but affects threaded program design.

## 49.6 Only the Main Thread Can Set Handlers

In Python, signal handlers can only be installed from the main thread of the main interpreter.

This fails in a worker thread:

```python id="7c6rqw"
import signal
import threading

def worker():
    signal.signal(signal.SIGINT, lambda s, f: None)

threading.Thread(target=worker).start()
```

A `ValueError` is raised.

Signal handling is therefore usually part of application startup and shutdown coordination, not worker logic.

## 49.7 Signals and the GIL

Signal delivery interacts with the GIL but is separate from it.

The low-level signal handler can run asynchronously at the C level. It records pending signal state.

The Python handler runs only when the interpreter reaches a safe point and the main thread can execute Python code.

If a long-running C extension holds the GIL and does not check for signals, Python signal handling may be delayed.

This is why CPU-heavy C extensions should periodically release the GIL or check for signals when appropriate.

## 49.8 Safe Points

CPython checks for pending signals during interpreter execution.

Conceptually:

```text id="x4hzyo"
execute bytecode
check eval breaker / pending calls
if signal pending:
    run Python signal handler
continue execution
```

The exact machinery changes across versions, but the principle is stable: signal handlers run at interpreter-defined safe points.

A pure Python loop is usually interruptible because the interpreter keeps checking.

A blocking or long-running native call may delay signal processing.

## 49.9 Signals and Blocking I/O

Signals can interrupt blocking system calls. CPython and the standard library handle many of these cases carefully.

A program may be waiting in:

```text id="oyhiec"
read
write
select
poll
accept
sleep
wait
```

When a signal arrives, the OS may interrupt the call. Python may run the signal handler and then either retry or raise, depending on the system call, handler behavior, and Python’s restart policy.

This is one reason signal behavior can be platform-specific.

## 49.10 `KeyboardInterrupt`

`KeyboardInterrupt` is an exception raised by signal handling, most commonly from `SIGINT`.

```python id="qwivsg"
try:
    while True:
        work()
except KeyboardInterrupt:
    print("interrupted")
```

Because it is an exception, it can appear between normal bytecode operations.

Do not assume cleanup code will always complete unless it is protected.

Use `finally`:

```python id="wo131m"
try:
    run()
finally:
    cleanup()
```

Or use context managers:

```python id="pa3ddm"
with resource:
    run()
```

## 49.11 Graceful Shutdown Pattern

A common pattern is to set a shutdown flag in the signal handler and let the main loop exit normally.

```python id="yc116m"
import signal

stopping = False

def request_stop(signum, frame):
    global stopping
    stopping = True

signal.signal(signal.SIGTERM, request_stop)
signal.signal(signal.SIGINT, request_stop)

while not stopping:
    do_one_unit_of_work()

cleanup()
```

This avoids doing complex cleanup directly inside the handler.

For threaded programs, use `threading.Event`:

```python id="ggsqg0"
import signal
import threading

stop = threading.Event()

def request_stop(signum, frame):
    stop.set()

signal.signal(signal.SIGTERM, request_stop)
signal.signal(signal.SIGINT, request_stop)
```

Worker threads can check `stop.is_set()`.

## 49.12 Avoid Complex Handler Logic

Python handlers run at safe interpreter points, but they still interrupt normal control flow.

Keep handlers small.

Good handler:

```python id="xmxrmg"
def handler(signum, frame):
    stop.set()
```

Risky handler:

```python id="xe1dj5"
def handler(signum, frame):
    save_database()
    join_threads()
    import heavy_module
    acquire_many_locks()
```

Complex handlers can create reentrancy bugs, deadlocks, and partial state problems.

Signal handlers should usually request action, not perform all action.

## 49.13 Signals and Locks

A signal handler can run while the main thread is inside code that holds a lock.

If the handler tries to acquire the same lock, the program can deadlock.

Bad:

```python id="c5w98i"
lock = threading.Lock()

def handler(signum, frame):
    with lock:
        print("signal")
```

If the main thread already holds `lock` when the signal handler runs, the handler waits forever.

Prefer setting an event or a simple flag.

## 49.14 Signals and Exceptions

A signal handler may raise an exception.

```python id="6y8g4y"
def handler(signum, frame):
    raise RuntimeError("signal")
```

That exception is raised in the main thread at the point where signal processing occurs.

This can be useful but dangerous. The exception may interrupt code that did not expect it.

`KeyboardInterrupt` behaves this way.

For predictable shutdown, setting a flag is often easier to reason about than raising custom exceptions from signal handlers.

## 49.15 Ignoring Signals

A program can ignore some signals:

```python id="ryec7b"
import signal

signal.signal(signal.SIGINT, signal.SIG_IGN)
```

This tells Python to ignore `SIGINT`.

Use this carefully. Ignoring termination or interrupt signals can make programs hard to stop.

Some signals cannot be ignored or caught. For example, on Unix, `SIGKILL` cannot be handled.

## 49.16 Restoring Default Handlers

Use `signal.SIG_DFL` to restore default behavior.

```python id="cq6hzs"
signal.signal(signal.SIGINT, signal.SIG_DFL)
```

This can be useful when a parent process customizes signal handling but wants child behavior to return to normal.

## 49.17 `SIGTERM`

`SIGTERM` is a normal termination request on Unix-like systems.

A server should usually handle it gracefully:

```python id="qyv2q6"
import signal
import threading

stop = threading.Event()

def terminate(signum, frame):
    stop.set()

signal.signal(signal.SIGTERM, terminate)

while not stop.is_set():
    serve_once()

shutdown()
```

Container runtimes and process managers often send `SIGTERM` first, then send `SIGKILL` if the process does not exit in time.

## 49.18 `SIGKILL`

`SIGKILL` cannot be caught, blocked, or ignored on Unix.

If a process receives `SIGKILL`, it stops immediately.

Python code cannot run cleanup handlers for `SIGKILL`.

This means graceful shutdown must happen before a hard kill.

Design servers so they respond promptly to `SIGTERM`.

## 49.19 `SIGALRM`

On Unix, `SIGALRM` can be used for timer-based interruption.

```python id="yl1ch3"
import signal

def timeout(signum, frame):
    raise TimeoutError

signal.signal(signal.SIGALRM, timeout)
signal.alarm(5)

try:
    slow_operation()
finally:
    signal.alarm(0)
```

This is Unix-specific and process-wide. It can interact badly with libraries that also use alarms.

For portable timeout handling, prefer higher-level APIs when possible.

## 49.20 Interval Timers

Unix systems may support interval timers through `setitimer`.

```python id="n6ojvl"
import signal

signal.setitimer(signal.ITIMER_REAL, 1.0)
```

Timers can generate signals after a delay or repeatedly.

They are powerful but global to the process. Use them carefully in libraries because they can conflict with application-level signal handling.

## 49.21 `SIGPIPE`

On Unix, writing to a closed pipe can generate `SIGPIPE`.

Python often handles broken pipes as exceptions such as `BrokenPipeError` rather than letting `SIGPIPE` terminate the process in normal code.

This matters for command-line tools:

```bash id="v86jrw"
python produce_output.py | head
```

If `head` exits early, the Python producer may see a broken pipe.

Well-behaved CLI tools handle this without noisy tracebacks when appropriate.

## 49.22 `SIGCHLD`

`SIGCHLD` is sent when a child process changes state.

Programs that manage child processes may use it, but Python code often relies on `subprocess` and `wait` APIs rather than installing custom `SIGCHLD` handlers.

Custom `SIGCHLD` handling can be subtle because child process reaping must be coordinated.

## 49.23 Signals on Windows

Signal support differs on Windows.

Some Unix signals do not exist. Some names are present but behavior differs.

Portable Python programs should not assume Unix signal semantics.

Common portable cases:

```text id="7fqfny"
SIGINT from Ctrl-C
SIGTERM in some environments
KeyboardInterrupt behavior
```

For cross-platform service shutdown, combine signal handling with platform-specific service management or higher-level process control.

## 49.24 Signals and Threads

Signals and threads are a common source of wrong assumptions.

Important rules:

```text id="8upq35"
Python handlers run in the main thread
only main thread can install handlers
worker threads should observe shutdown state
signals are process-level, not worker-thread messages
```

A good threaded shutdown model:

```python id="va8xxw"
stop = threading.Event()

def handler(signum, frame):
    stop.set()

def worker():
    while not stop.is_set():
        do_work()
```

The signal requests shutdown. Worker threads cooperate.

## 49.25 Signals and Asyncio

`asyncio` has signal integration on Unix event loops.

Typical pattern:

```python id="v9toxk"
import asyncio
import signal

async def main():
    stop = asyncio.Event()

    loop = asyncio.get_running_loop()

    loop.add_signal_handler(signal.SIGTERM, stop.set)
    loop.add_signal_handler(signal.SIGINT, stop.set)

    await stop.wait()
```

This is Unix-specific for many event loop implementations. On Windows, support differs.

In async programs, use event-loop signal APIs when available. They integrate signal delivery with the event loop’s scheduling model.

## 49.26 Signals and Subprocesses

When a Python program starts child processes, signals may need to be forwarded.

Example server supervisor:

```text id="mwhu02"
parent receives SIGTERM
parent asks children to terminate
parent waits
parent escalates if needed
```

In Python:

```python id="d2btrf"
proc.terminate()
proc.wait(timeout=5)
```

If the child does not exit:

```python id="v6fuz2"
proc.kill()
```

Signal behavior differs across platforms. `terminate()` maps to different mechanisms depending on OS.

## 49.27 Process Groups

On Unix, terminals often send signals to a process group, not just one process.

This matters for shells, pipelines, and supervisors.

A Python parent process may need to manage process groups when spawning subprocess trees.

For simple programs, `subprocess.run` handles most cases.

For process supervisors, signal and process group handling must be designed explicitly.

## 49.28 Signal Masks

Some platforms allow threads to block or unblock signals with signal masks.

Python exposes some support through functions such as `pthread_sigmask` on Unix where available.

This is advanced. It is mainly useful for programs that need precise control over which thread receives low-level signals.

Even when signals are delivered at the OS level to a particular thread, Python-level signal handlers still follow CPython’s main-thread rule.

## 49.29 `set_wakeup_fd`

CPython supports `signal.set_wakeup_fd` for event loops and low-level reactors.

It writes a byte to a file descriptor when a signal arrives, allowing a selector or poll loop to wake up.

This is how signal handling can be integrated with I/O multiplexing.

This API is advanced and should normally be used by event loop implementations rather than application code.

## 49.30 Pending Calls

CPython has a pending-call mechanism for scheduling small callbacks to run in the main interpreter thread at safe points.

Signal handling uses related machinery.

The key idea:

```text id="csksnu"
low-level async event occurs
record pending work
interpreter reaches safe point
run pending work in Python-safe context
```

This pattern avoids doing unsafe work in the low-level signal handler.

## 49.31 Signals and C Extensions

C extensions that run for a long time should consider signal responsiveness.

A long native loop can periodically check for signals:

```c id="xz6l2y"
if (PyErr_CheckSignals() < 0) {
    return NULL;
}
```

If a signal handler raised an exception, `PyErr_CheckSignals` reports it.

Extensions that block in native code may also release the GIL around blocking calls, allowing the main thread to process signals.

## 49.32 Signals and `time.sleep`

`time.sleep` can be interrupted by signals, but Python generally handles restart behavior according to its system call policy.

Example:

```python id="8niaf6"
import time

try:
    time.sleep(100)
except KeyboardInterrupt:
    print("interrupted")
```

Ctrl-C usually raises `KeyboardInterrupt`.

Signal behavior during sleep depends on handler behavior and platform details.

## 49.33 Signals and Import

Avoid complex imports inside signal handlers.

Import can acquire locks, execute arbitrary module code, and mutate `sys.modules`.

A signal handler that imports modules can deadlock or fail if the main thread was interrupted while already importing.

Bad:

```python id="zd0cvd"
def handler(signum, frame):
    import logging
    logging.info("signal")
```

Better:

```python id="302nhy"
def handler(signum, frame):
    stop.set()
```

Then the main loop can log during normal control flow.

## 49.34 Signals and Logging

Logging can acquire locks and perform I/O.

Calling logging directly from a signal handler can be risky.

Prefer setting a flag, then logging outside the handler.

```python id="e7ie5p"
def handler(signum, frame):
    stop.set()

while not stop.is_set():
    work()

logger.info("shutdown requested")
```

This avoids reentrant logging lock problems.

## 49.35 Signals and Cleanup

Cleanup should happen in ordinary program flow.

Good:

```python id="sl3b9g"
stop = False

def handler(signum, frame):
    global stop
    stop = True

try:
    while not stop:
        work()
finally:
    cleanup()
```

The handler requests shutdown. The `finally` block performs cleanup.

This keeps cleanup in a predictable context.

## 49.36 Common Signal Bugs

| Bug | Cause | Fix |
|---|---|---|
| Worker does not stop | Signal handled only in main thread | Use shared shutdown event |
| Program hangs on Ctrl-C | Main thread blocked in C code or deadlock | Check C extension, use timeouts |
| Handler deadlocks | Handler acquires lock held by interrupted code | Keep handler minimal |
| Cleanup skipped | Process receives `SIGKILL` | Respond promptly to `SIGTERM` |
| Async server ignores signal | Event loop signal integration missing | Use loop signal APIs on Unix |
| Library breaks app alarms | Library uses process-wide `SIGALRM` | Avoid process-wide signal APIs in libraries |
| Noisy broken pipe | CLI writes after downstream closes | Handle `BrokenPipeError` |

## 49.37 Design Rules

Install signal handlers in the main thread during startup.

Keep handlers small.

Use handlers to set flags or events.

Perform cleanup outside the handler.

Do not import, log heavily, acquire complex locks, or call unknown code from handlers.

Use `KeyboardInterrupt` handling for simple CLI programs.

Use `SIGTERM` handling for servers and containers.

Use event-loop signal APIs for async servers on Unix.

Use processes for hard kill and crash isolation.

## 49.38 Minimal Signal Shutdown Example

```python id="i5epjj"
import signal
import threading
import time

stop = threading.Event()

def request_stop(signum, frame):
    stop.set()

signal.signal(signal.SIGINT, request_stop)
signal.signal(signal.SIGTERM, request_stop)

def worker():
    while not stop.is_set():
        print("working")
        time.sleep(1)

thread = threading.Thread(target=worker)
thread.start()

try:
    while not stop.is_set():
        time.sleep(0.5)
finally:
    stop.set()
    thread.join()
    print("stopped")
```

This example has the important properties:

```text id="m47glp"
main thread owns signal handling
handler only sets an event
worker exits cooperatively
main thread joins worker
cleanup runs in ordinary control flow
```

## 49.39 CPython Internals Mental Model

Use this model:

```text id="bcqxlr"
OS signal arrives.
A tiny C signal handler records it.
CPython marks pending signal work.
The evaluation loop reaches a safe checkpoint.
The main thread runs the Python handler.
The handler may set a flag or raise an exception.
Normal Python control flow continues or unwinds.
```

This explains why signal handling is delayed during long C calls, why handlers run in the main thread, and why handlers should stay small.

## 49.40 Key Points

Signals are asynchronous process-level notifications.

CPython does not run arbitrary Python code directly inside the low-level OS signal handler.

Python signal handlers run later at safe interpreter checkpoints.

Python signal handlers run in the main thread of the main interpreter.

Only the main thread can install Python signal handlers.

`SIGINT` normally raises `KeyboardInterrupt`.

`SIGTERM` should usually request graceful shutdown in servers.

`SIGKILL` cannot be caught or handled.

Signal handlers should set flags or events, not perform complex cleanup.

Threads, async event loops, subprocesses, and C extensions need explicit signal-aware design.
