UNIX Signals
Signals are asynchronous notifications sent to processes by the kernel, potentially in response to a hardware exception or interrupt. The kernel interrupts the flow of execution of a process during any non-atomic instruction to deliver the signal.
Signals are not queued. Processes have binary flags to represent whether a signal is pending. When a signal is delivered, the corresponding binary flag will be set to true. The signal will be delivered as soon as the signal is not blocked and the binary flag will be reset to false. If a signal is delivered multiple times, the corresponding binary flag is set to true, and the signal is delivered once unless the binary flag is set to false before the new instance of the signal is delivered.
Signal handlers are only executed when transitioning from kernel mode to user mode. The kernel checks the pending signals when deciding to schedule a process to run. When delivering a signal, the kernel stores the current execution context into the user-space stack, creates a stack frame for the signal handler, and jumps into the signal handler in user mode. After a signal handler executes, the kernel takes over control in order to restore the execution context.
Signal handlers
A process may specify signal handlers to respond to, ignore, or block
signals other than SIGKILL
and SIGSTOP
. If not handled, the default signal
handlers are executed (documented in sigaction(2)
).
Programs may setup signal handlers using sigaction(2)
, or its easier-to-use
wrapper signal(3)
. The handler functions receive the signal number as an
argument.
Safe handlers
Signals may be delivered while a signal handler is running. For this reason, it is recommended for signal handlers to be reentrant (it can safely be executed by multiple threads in parallel) and/or not be interruptible by signal handlers.
Linux maintains a list of “async-signal-safe” functions that are safe to use in
signal handlers in
signal-safety(7)
.
Global variables
POSIX defines an integer type sig_atomic_t
that is safe to use as a global
variable shared between a program and its signal handlers. Read and write to
sig_atomic_t
variables are atomic operations, but ++
and --
are not. It
is recommended to declare sig_atomic_t
variables as volatile
to prevent
optimizer tricks.
Handler stack
Signal handlers typically use the process’ stack, but they can be configured to
use a custom stack with sigaltstack(2)
. This can be useful when handling the
SIGSEGV
signal, which can occur when the process stack space is exhausted.
Nested signals
A signal may be delivered while its own signal handler is running. The signal
that triggered the handler is blocked by default when the handler runs, unless
the SA_NODEFER
is set, in which case the new instance of the signal will be
set to pending and will be delivered once the running signal handler exits.
Passing data
A process may send a signal along with a piece of data (an integer or a
pointer) by using sigqueue(3)
. The corresponding handler can access this data
if it was setup with the SA_SIGINFO
flag of sigaction(2)
.
Working with signals
Sending signals
Processes may send signals using kill(2)
. A process only has permission to
send a signal if the real or effective user ID match, or the user has
super-user privileges. As an exception, the SIGCONT
can always be sent to any
descendant of the process. kill(2)
will fail if the sender is configured to
ignore the signal.
The kill(2)
function allows a process to send a signal to a specific process
(ignoring its descendants), to all the processes that belong to the sender’s
process group ID, or (given super-user privileges) to all the processes
excluding system processes.
The shell sends interrup signals, like SIGINT
, to the foregroung process group.
Blocking (masking) signals
Signals, other than SIGKILL
and SIGSTOP
, can be blocked with
sigprocmask(2)
. Blocked signals are delivered when the signal is unblocked.
The list of pending signals can be obtained with sigpending(2)
. Blocking
signals is useful to prevent interrupts during critical code paths. Signal
masks are per thread, and not per process.
The
SIGCONT
signal can’t be blocked on Linux
The sigaction(2)
function accepts a set of signals that must be blocked when
executing a particular signal handler. Also, the signal that executed the
current signal handler will be blocked unless the SA_NODEFER
flag is set.
If a signal handler is updated while a signal is pending, then the updated handler will be executed. The operating system only checks how a process wants to react to a handler when delivering the signal, and not when setting the signal’s binary flag.
Ignoring signals
Signals, other than SIGKILL
and SIGSTOP
, can be ignored using signal(3)
along with SIG_IGN
.
Resetting signals
Processes may reset a signal handler to its default by using signal(3)
along
with SIG_DFL
. The sigaction(2)
function also supports a SA_RESETHAND
flag
to implement one-shot signal handlers where the handler is reset after the
first handler execution.
Waiting for signals
A process may wait for a signal to occur with the sigwaitinfo(2)
or
sigtimedwait(2)
(which supports a timeout argument) functions.
Pausing and resuming processes
A process may be paused with SIGSTOP
(sent when pressing Ctrl-Z
on a shell)
and then resumed with SIGCONT
.
Terminating a process
There are many signals to terminate a process. SIGTERM
allows a process to
gracefully terminate. SIGKILL
inconditionally aborts the process and can be
used as a last resort. SIGQUIT
terminates a process, potentially creating a
core dump.
Sub-processes
A forked process inherits the signal mask and all signal handlers. The
execve(2)
resets all signal handlers but keeps the set of ignored and blocked
signals.
A parent process receives SIGCHLD
when a child exits, unless the
SA_NOCLDSTOP
flag of sigaction(2)
is set. Handling SIGCHLD
is not a
reliable way to wait for more than one children to exit, as multiple children
might exit while SIGCHLD
is being handled, but SIGCHLD
would only be
delivered once.
Threads
If a process has more than one thread, then the signal handler is sent to only
one of its threads. This decision is implementation-specific. Programs may send
a signal to a specific thread using pthread_kill(3)
.
Some signals generated a result of an specific instruction, such as SIGSEGV
and SIGFPE
, will always be sent to the thread that executed such instruction.
Blocking operations (EINTR
)
If a signal is delivered while a blocking system call (like read(2)
) is
running, then the system call will fail with EINTR
. The calling program may
deal with EINTR
by manually restarting the system call. Affected system calls
may also be automatically restarted after signal interruption if the signal is
setup with the SA_RESTART
flag.
The full list of
affected system calls is documented in sigaction(2)
:
The affected system calls include open(2), read(2), write(2), sendto(2), recvfrom(2), sendmsg(2) and recvmsg(2)
References
- https://en.wikipedia.org/wiki/Signal_(IPC)
- https://www.amazon.com/Design-UNIX-Operating-System/dp/0132017997
- https://man7.org/training/download/lusp_sighandlers_slides.pdf
- https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xsh_chap02.html#tag_22_02_04_02
- https://www.gnu.org/software/libc/manual/html_node/Job-Control-Signals.html