1. Understanding Signals

Signals are a limited form of inter-process communication in Unix, Unix-like, and other POSIX-compliant operating systems. They are an asynchronous notification mechanism used to alert a process that an event has occurred. When a signal is sent to a process, the operating system interrupts the process’s normal control flow — any non-atomic operation will be interrupted at that point. If the process has defined a signal handler, it will be executed; otherwise, the default handler is invoked.

Signals are the only asynchronous IPC mechanism — they serve as asynchronous notifications telling the receiving process that something has happened. In simple terms, signals can be thought of as a form of software interrupt.

2. Signal Sources

Generally, signals originate from three sources:

  • Hardware: Hardware exceptions such as division by zero or invalid memory access generate signals. These events are typically detected by hardware (e.g., the CPU) and reported to the Linux kernel, which then generates the corresponding signal and delivers it to the process running when the event occurred.
  • Software: A user running the kill command in a terminal to send a signal; a process calling kill or sigqueue to send a signal; or a software condition becoming true — for example, when a timer set by alarm or settimer expires, generating SIGALRM.
  • Keyboard input: Pressing certain keys in a terminal generates signals. For example, Ctrl+C generates SIGINT, and Ctrl+\ generates SIGQUIT.

3. Signal Types

Run kill -l to list the signals supported by Linux:

sl@Li:~/Works$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX

Linux supports a total of 64 signals. Signals 1–31 are standard signals (also called unreliable signals), while signals 34–64 are real-time signals (reliable signals).

The difference between reliable and unreliable signals:

  • Unreliable signals do not support signal queuing: when multiple signals arrive at a process faster than it can handle them, the unprocessed signals are simply dropped, leaving only one pending instance.
  • Reliable signals are queued: when multiple signals arrive faster than the process can handle, unprocessed signals are placed into a queue. When the process gets a chance to handle them, it processes them one by one — no signals are lost.

Here are a few commonly used signals:

SignalDescription
SIGHUPSent to all processes started from a terminal when the user logs out. Default action: terminate the process.
SIGINTProgram interrupt signal, sent when the user types the INTR character (usually Ctrl+C). Used to notify the foreground process group to terminate.
SIGQUITSimilar to SIGINT, but triggered by the QUIT character (usually Ctrl+\). A process exiting due to SIGQUIT produces a core file, making it similar to a program error signal.
SIGKILLImmediately terminates the program. This signal cannot be blocked, handled, or ignored.
SIGTERMProgram terminate signal. Unlike SIGKILL, this signal can be blocked and handled. Typically used to request a graceful program exit.
SIGSTOPStops (suspends) process execution. Unlike terminate and interrupt, the process is not ended — only paused. This signal cannot be blocked, handled, or ignored.

4. Reentrant Functions

Signal handlers must be reentrant. Since a signal handler can itself be interrupted by another signal — causing the program to jump to another signal handler and then return — special care must be taken when writing custom signal handlers. Thinking of signals as “soft interrupts” makes the concept of reentrant functions easy to understand.

5. Signal Delivery

The kernel handles pending signals for a process when that process transitions from kernel mode back to user mode. Therefore, when a process is running in kernel mode, soft-interrupt signals do not take effect immediately — they are handled only when the process is about to return to user mode. A process only returns to user mode after all signals have been handled; in user mode, a process never has unhandled signals pending.

The kernel handles a process’s pending signals within that process’s context, so the process must be in the running state. When a process receives a signal it ignores, it simply discards it and continues running as if nothing happened.

When a process receives a signal it is set to catch, it executes the user-defined handler upon returning from kernel to user mode. The implementation is clever: the kernel creates a new frame on the user stack, setting the return address in that frame to the address of the user-defined handler. When the process returns from the kernel and pops the stack, it lands at the user-defined handler. When the handler returns and pops the stack again, it returns to where it was before entering the kernel. This approach is necessary because user-defined handlers cannot and must not run in kernel mode (if user-defined functions could run in kernel mode, a user could gain arbitrary privileges).

Here is a concrete example illustrating the process:

  1. The user program registers sighandler as the handler for SIGQUIT.
  2. While main is executing, an interrupt or exception causes a switch to kernel mode.
  3. Before returning to main in user mode after handling the interrupt, the kernel detects that SIGQUIT has been delivered.
  4. Instead of restoring main’s context, the kernel decides to execute sighandler. sighandler and main use different stack spaces — they are independent control flows, not a caller-callee relationship.
  5. When sighandler returns, it automatically executes the special sigreturn system call to re-enter kernel mode.
  6. If no new signals need to be delivered, the kernel returns to user mode by restoring main’s context to continue execution.

6. Code Example 1

The following code catches program termination signals and invokes a user-defined handler instead of the system default.

#include<stdlib.h>
#include<stdio.h>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>

void sig_handle(int sig) {
    printf("received signal: %d, quit.\n", sig);
    exit(0);
}

int main () {
    signal(SIGINT, sig_handle);
    signal(SIGKILL, sig_handle);
    signal(SIGSEGV, sig_handle);
    signal(SIGTERM, sig_handle);

    int i = 0;
    while (1) {
        printf("%d\n", ++i);
        sleep(2);
    }

    printf("main quit.");

    return 0;
}

Output:

1
2
received signal: 15, quit.

7. Code Example 2

This example is functionally similar to the one above. Signals can carry parameters, but they are primarily a notification mechanism — the amount of information that can be passed is very limited. For larger data transfers, consider other IPC methods.

#include<signal.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>

void new_op(int, siginfo_t*, void*);

int main() {
    if (NULL == freopen("sigproc.log", "w", stdout)) {
        fprintf(stderr, "error redirecting stdout\n");
    }

    struct sigaction act;

    sigemptyset(&act.sa_mask);  // sa_mask specifies which signals should be blocked during handler execution. By default the current signal itself is blocked to prevent nested delivery
    sigaddset(&act.sa_mask, SIGTERM);
    sigaddset(&act.sa_mask, SIGINT);
    act.sa_flags = SA_SIGINFO;  // SA_SIGINFO: when set, signal-attached parameters can be passed to the handler
    act.sa_sigaction = new_op;

    if (sigaction(SIGINT, &act, NULL) < 0) {
        printf("install sigal error\n");
    }

    if (sigaction(SIGTERM, &act, NULL) < 0) {
        printf("install sigal error\n");
    }

    if (sigaction(SIGHUP, &act, NULL) < 0) {
        printf("install sigal error\n");
    }

    int i = 0;
    while (1) {
        printf("%d\n", ++i);
        sleep(1);
    }

    printf("end.");

    return 0;
}

void new_op(int signum, siginfo_t *info, void *myact) {
    printf("receive signal %d\n", signum);
    for (int i = 0; i < 5; ++i) {
        printf("signal processing: %d\n", i);
        sleep(1);
    }
    printf("process quit.");

    exit(0);
}

Output:

1
2
3
receive signal 15
signal processing: 0
signal processing: 1
signal processing: 2
signal processing: 3
signal processing: 4
process quit.