Asynchronous Signal Safety
Writing asynchronous signal handlers can be very challenging due to data races. If a signal handler is delivered while a particular object is being accessed or modified, that signal handler must refrain from modifying or accessing (respectively) that same object, since this could result in undefined behavior.
It might seem possible to avoid this issue by blocking offending signals when accessing objects that are also accessed by a signal handler. However, this is not actually safe in practice. Common optimizations both at compile and runtime can reorder instructions, cache values of objects in registers for later reuse, and so on, without taking into account meddling by signal handlers.
In Standard C
Standard C supports only rudimentary asynchronous signal handling–signal masks are a POSIX extension. C supports the three basic signal dispositions of ignoring a signal, responding with a default action, and executing a signal handler. Many aspects of signal handling are unspecified in the standards, such as what happens if a signal is delivered during execution of a signal handler, whether a signal’s disposition is reset to the default action after its handler is executed or remains assigned to the signal handler, and so on.
Standard C is also very restrictive about what can be safely done in a signal handler. With only a couple of exceptions described below, accessing any static object (i.e. “global variables”) or calling any library function in a signal handler is undefined behavior.
This intent is elaborated in the C99 rationale, which states,
Signals are difficult to specify in a system-independent way. The C89 Committee concluded that about the only thing a strictly conforming program can do in a signal handler is to assign a value to a volatile static variable which can be written uninterruptedly and promptly return. (The header <signal.h> specifies a type sig_atomic_t which can be so written.)
As mentioned in the quote above, the signal.h
header defines a special sigatomic_t
data type, which is an integer type defined as supporting atomic access with respect to signal handlers. If a static object is declared as a volatile sigatomic_t
data type, a signal handler may safely access and modify it. This can be used to indicate to the program that the signal was delivered, and then a more complex action can then be performed after returning from the signal handler.
With respect to library functions, signal handlers may only safely call a small handful of functions, specifically those which immediately exit the program–abort()
, _Exit()
, and quick_exit()
. They may also call the signal()
function to change the signal disposition of the currently handled signal only. Since C does not specify whether a signal handler will be called again when the same signal is received a subsequent time, this allows signal handlers to re-register themselves to the handled signal.
As we can see, signal handling in C is very restrictive; this is by design, since C is meant to be portable to as many systems as possible, which may all have very different signal semantics.
POSIX Signal Extensions
POSIX extends the basic C signal handling facilities, adds additional guarantees and relaxes several of the restrictions placed on signal handlers by the C standards. As described above, signal blocking using sigprocmask()
and synchronously accepting signals with functions like sigwait()
is a POSIX extension. Additionally, POSIX adds more fine-grained control over signal behavior through the sigaction()
method, vs the C standard’s simple signal()
method. For example, signal handlers in POSIX can be set up to receive information about the signal that was delivered, such as the process ID of the sender; and, sigaction()
allows a process to save and restore a signal’s disposition to its original state–not possible in standard C. Additionally, sigaction()
supports many signal handling flags, which can be used to change how that signal is handled. As an example, the SA_RESART
flag is used to control whether interrupted system calls are restarted or whether they fail with errno = EINTR
when a particular signal handler is executed.
POSIX also introduces an additional concept called async-signal safety–a library function (or system call) is async-signal safe if it can be safely called within a signal handler. The POSIX standard defines a base set of async-signal safe functions and allows for implementations (e.g. Linux) to add additional ones to the base list. Since system calls are atomic with respect to signals–i.e. signals cannot be delivered in the middle of a system call, only just before returning to user-space–many system calls are async-signal safe, such as read()
and write()
. However, if a particular system call accesses or modifies static storage, it generally cannot be made async-signal safe. An example of this would be a system call which allocates storage, such as mmap()
. Additionally, the entire standard i/o library is not async-signal safe, because of the use of static data structures to manage open streams.
Most library methods and system calls set errno
on failure. In order to support the async-signal safety concept, signal handlers are also permitted to access and modify errno
–but they must restore it to its original value when they return. This usually manifests as storing the value of errno
in a local temporary variable at the beginning of a signal handler, calling some async-signal safe library methods that potentially modify errno
, and then restoring errno
from the saved value before returning from the signal handler. Signal handlers still aren’t allowed to access any other static (global) objects, unless those objects are volatile sigatomic_t
.