When an operator runs:
the operating system does not immediately call the server's cleanup function. It sends a small asynchronous notification named SIGTERM.
The notification can arrive while the server is parsing a request, modifying a data structure, waiting for input, or executing inside a library. The server must decide what SIGTERM means and handle it without corrupting its own state.
This notification mechanism is called a signal.
A signal tells a process or thread that an event has occurred. It can interrupt the normal flow of execution, invoke a handler, or trigger a predefined operating-system action.
Signals support termination requests, terminal interaction, child-process notifications, timers, fault reporting, and application-defined events. Their compact interface is powerful, but their asynchronous nature requires care.
A signal moves through three conceptual stages:
Generation and delivery are separate events. A signal can sit in the pending state for a long time, and what happens at the end depends on the disposition the process has set.
Generation and delivery are not necessarily simultaneous. If the destination thread currently blocks that signal, it remains pending until it becomes unblocked.
A signal carries a type, represented in programs by a symbolic name such as SIGTERM or SIGINT. Programs should use these names rather than hard-coded signal numbers because numeric assignments can vary across systems and processor architectures.
Signals can originate from several places.
The kernel generates signals for events such as an invalid memory access, an expired timer, or a child-process state change. A terminal can generate a signal when the user types a control character such as Ctrl+C. One process can request delivery to another process, and a process can signal itself.
Examples include:
SIGINT for a terminal-controlled program.SIGPIPE.SIGSEGV.SIGCHLD for its parent.SIGTERM to request shutdown.Signals report that something happened. They are not general-purpose messages containing arbitrary application data, and most standard signals do not count repeated occurrences reliably.
Every signal has a default action. The most useful signal names are consistent across POSIX systems, although some details remain operating-system-specific.
| Signal | Typical cause or purpose | Default action |
|---|---|---|
SIGTERM | Polite termination request | Terminate |
SIGINT | Interactive interruption, commonly Ctrl+C | Terminate |
SIGQUIT | Interactive quit request | Terminate and produce a core dump |
SIGHUP | Terminal disconnection; often repurposed for reload | Terminate |
SIGCHLD | A child changed state | Ignore |
SIGPIPE | Write to a pipe or socket with no reader | Terminate |
SIGSEGV | Invalid memory access | Terminate and produce a core dump |
SIGSTOP | Unconditional stop request | Stop |
SIGCONT | Resume a stopped process | Continue |
SIGKILL | Unconditional termination request | Terminate |
The descriptions indicate common use, not universal application policy. For example, many daemons choose to interpret SIGHUP as a configuration-reload request, but that meaning comes from the program's installed handler rather than from the kernel.
Core-dump production also depends on system configuration and resource limits. “Core” describes the signal's default action even when no dump file is ultimately written.
Loading simulation...
Each signal has a disposition that determines what happens when it is delivered.
A process can generally choose one of three dispositions:
The disposition is process-wide. In a multithreaded program, every thread sees the same installed disposition for a given signal.
Two signals are deliberately outside this control:
SIGKILL cannot be caught, ignored, or blocked.SIGSTOP cannot be caught, ignored, or blocked.These guarantees give the operating system and authorized administrators a final way to stop or terminate a process, even if its signal-handling code is broken.
SIGTERM is different. A process can catch it, ignore it, or block it. That makes graceful shutdown possible, but it also means that sending SIGTERM is a request rather than a guarantee of immediate termination.
The shell's kill command sends signals despite its destructive-sounding name:
Signal names make intent clearer than commands such as kill -9. SIGKILL should usually be a last resort because the target gets no opportunity to flush application buffers, release distributed leases, finish requests, or report why it stopped.
In C, a positive PID targets one process:
Sending signal number 0 performs existence and permission checks without delivering a signal:
This is only a momentary observation. The process can exit immediately afterward, and its PID can eventually be reused. It is not a durable process handle.
The sender must also have permission. A process normally cannot signal an arbitrary process belonging to another user merely because it knows the PID.
sigaction() for Signal HandlersPOSIX provides sigaction() to inspect or change a signal's disposition:
The older signal() function has historically varied in behavior across Unix systems. sigaction() expresses the handler, temporary mask, and behavior flags explicitly and is the appropriate interface for robust code.
Its main fields are:
sa_mask lists additional signals to block while the handler executes. The signal currently being handled is also normally blocked during its own handler, preventing unbounded recursive entry.
sa_flags enables optional behavior. Important examples include SA_RESTART, which requests automatic restart for some interrupted operations, and SA_SIGINFO, which selects a three-argument handler that receives additional information about the signal.
A signal handler can run between almost any two instructions in the main program. The safest pattern is therefore to make the handler do almost nothing: record the request and return.
Compile and exercise it:
The handler only assigns to stop_requested. Normal program flow notices the flag and performs cleanup where the full C library is available.
sig_atomic_t is an integer type that can be read and written atomically with respect to a signal handler. volatile prevents the compiler from assuming that the value cannot change unexpectedly.
This combination does not make the variable a general synchronization primitive for multiple threads. Thread-to-thread communication requires the language's atomic or synchronization facilities.
A handler can interrupt code that already holds an internal library lock or is halfway through modifying shared state.
Imagine that the main flow is inside printf() updating a buffered stream. A signal arrives, and the handler calls printf() again. The second call can encounter partially updated state or attempt to acquire a lock already held by the interrupted first call.
For that reason, POSIX defines a limited set of async-signal-safe functions. Only those functions are guaranteed to be callable from an asynchronous signal handler.
Common operations that should not be performed in a handler include:
printf() and most standard buffered I/Omalloc() and free()write() and _exit() are examples of async-signal-safe operations, but even safe calls should be kept minimal. A handler that uses errno must save its incoming value and restore it before returning.
The simple rule for application code is:
Record the event in the handler; process the event in normal control flow.
When the kernel is about to return to user mode, it checks for a pending signal that the current thread can receive.
If a handler is installed, the kernel arranges a user-space stack frame containing the interrupted execution context and begins the handler. When the handler returns, a signal-return mechanism restores that context.
Conceptually:
Normal code runs, a signal is delivered, the handler executes, and the interrupted code then resumes.
The interrupted code does not call the handler in the ordinary function-call sense. It may have been inside an unrelated function with no expectation that application logic would run at that instant.
This explains both the usefulness and danger of handlers. They let a process react promptly, but they introduce asynchronous control flow into nearly any point in the program.
EINTRSuppose a thread is blocked in an operating-system operation when a handler runs. After the handler returns, one of two broad outcomes is possible:
errno set to EINTR.The result depends on the operation, operating system, and flags used when installing the handler.
SA_RESTART asks the system to restart certain interrupted calls:
It does not restart every possible operation. Some waits and timeout-related interfaces still report EINTR, and an I/O operation that already transferred data may return a partial success instead of an error.
Robust code handles the documented contract of each operation. The shutdown example deliberately leaves SA_RESTART unset, detects EINTR from nanosleep(), and checks the shutdown flag before sleeping again.
Blindly retrying every EINTR can also be wrong. If interruption represents a shutdown request, immediately repeating a long blocking operation without consulting program state can delay shutdown indefinitely.
Each thread has a signal mask, which is the set of signals currently blocked for that thread.
Blocking is not the same as ignoring:
SIGTERM is generated.SIGTERM is currently blocked, so it remains pending.SIGTERM.SIGTERM becomes eligible for delivery.A single-threaded program can change its mask with sigprocmask(). A multithreaded program should use pthread_sigmask(), which explicitly changes the calling thread's mask.
Signal sets are manipulated with functions rather than integer bit operations:
The kernel silently prevents SIGKILL and SIGSTOP from being added to an effective blocked set.
Suppose SIGUSR1 is blocked and is generated five times before it becomes unblocked.
For a standard signal, the kernel generally records that SIGUSR1 is pending, not that five separate instances arrived. When it is unblocked, the program may observe only one delivery.
Five arrivals produce one pending state, not a queue of five. A handler written to assume one run per generated signal will undercount.
This means a standard signal should not be used as a reliable job counter. A handler that increments a counter still cannot recover generations that were coalesced before delivery.
POSIX real-time signals provide queued delivery, defined ordering, and optional accompanying values. They are useful for specialized designs, but a pipe, queue, event descriptor, or other normal communication mechanism is often easier for transmitting application data.
When real-time signals are used, refer to them relative to SIGRTMIN and verify that the chosen value does not exceed SIGRTMAX; their absolute numbers must not be hard-coded.
Loading simulation...
This pattern contains a race:
The signal can arrive after the condition is checked but before pause() begins. The handler sets event_arrived, returns, and then the program sleeps while waiting for a signal that has already been handled.
Correct interfaces combine mask changes with waiting atomically. sigsuspend() temporarily installs a mask and sleeps in one operation. pselect() and ppoll() provide the same kind of atomic mask transition while waiting for file-descriptor activity.
The general pattern is:
This is the signal equivalent of protecting an ordinary shared-state condition from a lost wakeup.
Signal dispositions belong to the process, but signal masks belong to individual threads.
A process-directed signal targets the process as a whole. If it is caught, the kernel selects one thread that does not block it to perform the delivery. Code should not assume that an arbitrary process-directed signal will run on a particular worker thread.
A thread-directed signal targets one specific thread. Hardware-fault signals such as SIGSEGV are normally directed to the thread that executed the faulting instruction. Interfaces such as pthread_kill() can explicitly target a thread.
These rules create a common source of bugs:
The disposition is process-wide while the masks are per-thread, and the combination means delivery can occur on any eligible thread.
Installing one handler does not make one thread the permanent “signal thread.” A disciplined multithreaded design must control masks across all threads.
sigwait()Instead of running an asynchronous handler, a thread can accept blocked signals synchronously with sigwait().
The important sequence is:
sigwait().The following program uses its main thread as the signal coordinator:
Compile it with thread support:
Press Ctrl+C or send SIGTERM from another terminal. sigwait() returns the signal number as ordinary control flow, so the coordinator can safely use printf(), atomics, mutexes, and other normal facilities.
The initial pthread_sigmask() call must occur before pthread_create(). A new thread inherits a copy of its creator's mask, which ensures that the selected signals remain blocked in the worker and are available for the coordinator's sigwait().
SIGCHLD: Notification, Not ReapingWhen a child changes state, its parent can receive SIGCHLD. That tells the parent to check its children; it does not by itself collect a terminated child's exit information.
Because standard signals can coalesce, several children may exit while only one SIGCHLD delivery is observed. Correct child management drains all currently completed children:
The loop, rather than the number of signal deliveries, determines how many children are ready to be collected.
Linux and POSIX also support configurations that prevent children from becoming zombies, such as explicitly ignoring SIGCHLD or using SA_NOCLDWAIT. Those choices discard normal waitable termination records and therefore change how the parent can obtain exit status.
A subtle point is that the default action for SIGCHLD is listed as “ignore,” but explicitly setting its disposition to SIG_IGN has special child-reaping semantics. The default state and an explicit ignore are not interchangeable here.
SIGPIPE in Backend ServicesIf a process writes to a pipe or connected socket after the peer has closed its reading side, the write can generate SIGPIPE. Its default action terminates the process.
That behavior is convenient in a simple command pipeline: an upstream command can disappear automatically when no downstream reader remains. It can be surprising in a network service, where one disconnected client should not normally terminate the entire server.
Servers commonly prevent the default termination and handle the write failure through an error result such as EPIPE. Depending on the interface and platform, this may involve ignoring SIGPIPE, changing its disposition, or using a send option that suppresses it for one operation.
Ignoring SIGPIPE globally affects every thread and library in the process, so the decision belongs in the application's I/O policy rather than in an isolated helper function.
Signals such as SIGSEGV, SIGBUS, SIGILL, and SIGFPE can report faults caused by the currently executing instruction.
It is tempting to install a SIGSEGV handler, log the error with ordinary application code, and continue. That is generally unsafe:
Low-level runtimes and debuggers sometimes use advanced recovery techniques with carefully controlled execution contexts. Ordinary applications should treat these signals as crash conditions.
A minimal crash path may record a small amount of information using safe operations and then terminate, but preserving the default core-dump behavior is often more useful for diagnosis than attempting elaborate cleanup in a damaged process.
fork() and exec()Signal state does not have one uniform inheritance rule.
| Signal property | After fork() | After successful exec() |
|---|---|---|
| Dispositions | Child receives copies | Caught dispositions reset to default; ignored dispositions remain ignored |
| Signal mask | Child receives a copy | Preserved |
| Pending signals | Child starts with none | Preserved on Linux |
Resetting caught handlers during exec() is necessary because the handler's code belonged to the replaced program image. An ignored disposition can remain meaningful without referring to old code.
Preserving the mask can cause a subtle bug. If a launcher blocks SIGTERM, executes another program, and forgets to restore the old mask first, the new program begins with SIGTERM blocked even though its caught handler was reset.
Code that prepares to execute another program should therefore review both inherited dispositions and the current mask.
Linux exposes signal state in /proc:
The fields are hexadecimal bit masks:
SigPnd shows signals pending for the inspected thread.ShdPnd shows process-directed pending signals.SigBlk shows blocked signals.SigIgn shows ignored signals.SigCgt shows signals with installed handlers.For a specific thread in a multithreaded process, inspect:
The kill -l command lists signal names known to the shell. On Linux, tracing tools can also display signal generation and delivery:
Attaching a tracer changes timing and may require elevated permission, so use its output as diagnostic evidence rather than as a perfectly passive observation.
SIGKILL and SIGSTOP cannot be overridden or blocked.sigwait().SIGCHLD, SIGPIPE, interrupted operations, and inherited signal state.5 quizzes