UNIX Signals


Notes on 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