In a C program on Linux, what may a signal handler installed with `sigaction()` safely do, and why can calling something like `printf()` from a handler deadlock the process?
answer
- the handler interrupts arbitrary code
- re-entering a lock you already hold
- there is a documented safe list
- do nothing; flag it and return
- turn the signal into a file descriptor
basics
~20 sOnly async-signal-safe functions, listed in the signal-safety(7) manual page — chiefly write() and _exit() — plus setting a volatile sig_atomic_t flag. printf() takes locks the interrupted code may already hold, so re-entering it can deadlock.
solid answer
~50 sA handler runs on top of whatever the thread was already doing, so it may only call functions that are safe to re-enter — the async-signal-safe list in `signal-safety(7)`: `write`, `_exit`, `kill`, `sigaction`, `sigprocmask` and friends. `printf` is not on it. The stdio layer takes a lock, the allocator underneath takes another, and if the signal arrived while the interrupted code held that same lock, re-entering deadlocks the process — a hang that reproduces once a week and never in a test. The safe pattern is to do almost nothing in the handler: set a `volatile sig_atomic_t` flag, or `write` a byte to a self-pipe the event loop already polls, and do the real shutdown work back in the main loop. Better still, block the signals everywhere and read them synchronously with `signalfd` or `sigwaitinfo`. Save and restore `errno` if you call anything that can change it.
code
c · 31 lines#include <signal.h>
#include <stdio.h>
#include <unistd.h>
static volatile sig_atomic_t stop_requested = 0;
static void on_term(int signo)
{
ssize_t n;
(void)signo;
stop_requested = 1;
n = write(STDERR_FILENO, "SIGTERM received\n", 17);
(void)n;
}
int main(void)
{
struct sigaction sa;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = on_term;
sigaction(SIGTERM, &sa, NULL);
while (!stop_requested)
pause();
printf("draining and exiting cleanly\n");
return 0;
}go deeper
Know that a signal handler runs at an unpredictable point in the program and that it should do as little as possible — typically set a flag that the main loop notices — rather than performing the shutdown itself.
Explain re-entrancy: the handler interrupts arbitrary code, so calling a function that takes a lock the interrupted code already holds deadlocks the thread. Name the safe list in signal-safety(7), and the volatile sig_atomic_t flag idiom.
Show you have debugged this class of hang. Talk about the self-pipe trick and about blocking signals process-wide and consuming them through signalfd or sigwaitinfo, plus the practical details — saving errno, and retrying on EINTR regardless of SA_RESTART.
Own the guidance for the codebase: signal handling is a small, reviewed, centralised piece of infrastructure rather than something each component invents. Be ready to argue for a synchronous signal-consumption design so that no application code ever runs in an asynchronous context in the first place.
## Where a handler actually runs A signal handler is not a callback on a queue. When the kernel acts on a signal, it arranges for the target thread to jump into the handler *between two arbitrary machine instructions* of whatever it was doing, then return there afterwards. The interrupted code did not stop at a safe point; it was frozen wherever it happened to be — possibly halfway through updating a data structure, and possibly holding a lock. That single fact generates every rule below. ## Re-entrancy and the deadlock `printf` looks harmless. Underneath, the stdio implementation locks the `FILE` object so that concurrent threads do not interleave their output, and it may call the allocator, which takes its own internal locks. Now suppose the main flow was inside `printf` when SIGTERM arrived. The stdio lock is held. Your handler calls `printf`, which tries to take the same lock, on the same thread, which will not release it until the handler returns — and the handler cannot return, because it is blocked. The process hangs, holding no clue as to why. The same argument applies to `malloc` and `free`, to most logging libraries, to anything that touches a global cache, and to a surprising amount of code you would never suspect. This is not a theoretical hazard, it is a scheduling lottery: it fires only when the signal lands inside the small window where the lock is held, so it reproduces rarely, never under test, and always in production. ## The async-signal-safe list POSIX therefore defines a set of functions guaranteed safe to call from a handler, and Linux documents it in `signal-safety(7)`. The practically important members are: - `write(2)` — the one way to emit output from a handler; - `_exit(2)` — terminate immediately, skipping `atexit` handlers and stdio flushing (which is exactly why it is safe, and why `exit(3)` is not); - `kill(2)`, `raise(3)`, `sigaction(2)`, `sigprocmask(2)`, `signal(2)`; - basic file-descriptor calls such as `open`, `read`, `close`; - `time(2)`, `getpid(2)` and similar cheap syscall wrappers. Anything absent from that list is off limits — including `printf`, `malloc`, `free`, `syslog(3)` and any library you did not write. ## Two more rules people forget **Save `errno`.** A handler that calls `write` can change `errno` out from under the interrupted code, which may then misreport a completely unrelated failure. Save it at handler entry and restore it before returning. **Use `volatile sig_atomic_t` for the flag.** `sig_atomic_t` is a type whose loads and stores cannot be torn by a signal arriving mid-write, and `volatile` stops the compiler caching the value in a register across the loop that reads it. A plain `int` read in a tight loop can legitimately be optimised into a load-once, never noticing the change. ## The pattern that works Do as close to nothing as possible in the handler and let the main flow do the work: ```c static volatile sig_atomic_t stop_requested = 0; static void on_term(int signo) { (void)signo; stop_requested = 1; } ``` The main loop checks the flag and shuts down at a point of its own choosing, where every unsafe function is available again. A program built around `poll`/`epoll` needs a variant, because it may be blocked in the event loop rather than checking flags. The classic solution is the **self-pipe trick**: create a pipe at startup, add the read end to the event set, and have the handler `write` a single byte to the write end. `write` is async-signal-safe, and the wakeup turns an asynchronous signal into an ordinary readable file descriptor the loop already knows how to handle. The modern Linux answer removes handlers altogether: block the signals in every thread with `pthread_sigmask`, then either create a `signalfd(2)` — a file descriptor that becomes readable when a blocked signal is pending, so it drops straight into the event loop — or dedicate a thread to `sigwaitinfo(2)`. Because you are now reading signals synchronously, no code runs at an arbitrary point and the whole safety problem disappears. ## Two related gotchas A signal delivered while a thread is blocked in a slow syscall makes that call return `EINTR`. Setting `SA_RESTART` in `sigaction` makes the kernel restart many of those calls automatically, but not all — `poll` and `select` are never restarted — so robust code retries on `EINTR` anyway. And in a multithreaded process, a process-directed signal is delivered to any one thread that has not blocked it. The handler is a process-wide property; the blocked mask is per-thread. That is what makes "block in all threads, handle in one" a coherent strategy in the first place.
- In a multithreaded process, which thread runs the handler?Any thread that has not blocked the signal — the choice is not yours. The handler itself is a process-wide property, but the blocked mask is per-thread, which is what makes the standard strategy possible: block the signals in every thread with `pthread_sigmask`, then have one dedicated thread consume them synchronously through `sigwaitinfo` or a `signalfd`.
- What is `EINTR`, and what does `SA_RESTART` change?When a signal is acted on while a thread is blocked in a slow system call, that call can return early with `errno` set to `EINTR`. Setting `SA_RESTART` in `sigaction` asks the kernel to restart many such calls transparently, but the exemptions are real — `poll` and `select` are never restarted — so correct code still retries on `EINTR` rather than trusting the flag.
- Why is `_exit()` safe from a handler when `exit()` is not?`exit()` runs `atexit` handlers and flushes stdio streams, which means taking stdio locks and running arbitrary registered code — exactly the re-entrancy hazard you are trying to avoid. `_exit()` goes straight to the kernel and terminates the process without touching any of that, which is why it appears on the async-signal-safe list and `exit()` does not.
saying these in an interview costs you the question
- Says a handler may call anything, just keep it short
- Logs from the handler with the app's logging library
- Uses a plain int flag with no volatile qualifier
- Calls exit() rather than _exit() inside a handler
- Assumes SA_RESTART makes EINTR handling unnecessary