Signals
Asynchronous notifications delivered to processes
Signal basics
A signal is an integer (1–64 on x86-64) sent to a process or thread. The kernel delivers it by interrupting the target process at the next opportunity (syscall return, interrupt return).
Sender Kernel Receiver
────── ────── ────────
kill(pid, SIGTERM) → pending bit set → process checks TIF_SIGPENDING
on return to user do_signal()
mode: → runs signal handler
→ or default action (terminate)
Standard signals (1–31) are not queued — multiple sends before delivery collapse to one. Real-time signals (32–64, SIGRTMIN..SIGRTMAX) are queued.
Signal numbers and default actions
SIGHUP (1) — hangup (terminal closed) → terminate
SIGINT (2) — interrupt (Ctrl+C) → terminate
SIGQUIT (3) — quit (Ctrl+\) → core dump
SIGILL (4) — illegal instruction → core dump
SIGTRAP (5) — debug trap → core dump
SIGABRT (6) — abort() → core dump
SIGFPE (8) — arithmetic exception → core dump
SIGKILL (9) — kill (cannot be caught/ignored) → terminate
SIGSEGV (11) — invalid memory access → core dump
SIGPIPE (13) — write to closed pipe → terminate
SIGALRM (14) — timer (alarm()) → terminate
SIGTERM (15) — termination request → terminate
SIGCHLD (17) — child status change → ignore
SIGCONT (18) — continue if stopped → continue
SIGSTOP (19) — stop (cannot be caught/ignored) → stop
SIGTSTP (20) — terminal stop (Ctrl+Z) → stop
SIGUSR1 (10) — user defined → terminate
SIGUSR2 (12) — user defined → terminate
SIGRTMIN..SIGRTMAX (32..64) — real-time → terminate
Sending signals
/* kill: send to process or process group */
kill(pid, SIGTERM); /* pid>0: specific process */
kill(-pgrp, SIGTERM); /* pid<0: process group */
kill(0, SIGTERM); /* current process group */
kill(-1, SIGTERM); /* all processes (except init) — requires privilege */
/* tkill: send to specific thread (deprecated, use tgkill) */
tkill(tid, SIGTERM);
/* tgkill: send to specific thread in specific process (Linux-specific) */
tgkill(tgid, tid, SIGTERM);
/* sigqueue: send with data (for real-time signals) */
union sigval value;
value.sival_int = 42;
sigqueue(pid, SIGRTMIN, value);
/* Receiver gets si_value.sival_int == 42 in siginfo_t */
/* raise: send to current thread */
raise(SIGINT);
/* From kernel: */
send_sig(SIGTERM, task, 0); /* send to process */
send_sig_info(SIGTERM, &info, task); /* with siginfo_t */
kill_pgrp(pgrp, SIGTERM, 0);
Kernel signal delivery path
/* kernel/signal.c */
/* 1. do_send_sig_info(): queues signal to target task */
do_send_sig_info(sig, info, task, type)
/* Process-directed (kill): goes to task->signal->shared_pending */
/* Thread-directed (tgkill): goes to task->pending */
→ sigaddset(&task->signal->shared_pending.signal, sig) /* process-directed */
→ signal_wake_up(task, sig == SIGKILL) /* wake if sleeping, set TIF_SIGPENDING */
/* 2. On return to userspace (syscall return, interrupt return): */
exit_to_user_mode_loop()
→ if TIF_SIGPENDING: arch_do_signal_or_restart(regs)
→ get_signal(&ksig) /* dequeue next signal */
→ if handled: handle_signal(&ksig) /* set up userspace stack frame */
→ if ignored: continue
→ if default: do_group_exit() or coredump etc.
/* 3. handle_signal: set up signal frame on userspace stack */
handle_signal(ksig, regs)
→ setup_rt_frame(ksig, regs)
/* pushes: siginfo_t, ucontext_t, trampoline code onto user stack */
/* sets rip = signal handler, rsp = new user stack */
/* sets up SIGRETURN trampoline (calls rt_sigreturn syscall) */
After the signal handler returns, the trampoline calls rt_sigreturn which restores the saved ucontext_t (CPU registers before signal delivery) and resumes normal execution.
struct sigaction: installing handlers
#include <signal.h>
/* sa_handler or sa_sigaction (not both) */
struct sigaction {
union {
void (*sa_handler)(int signum);
void (*sa_sigaction)(int signum, siginfo_t *info, void *ucontext);
};
sigset_t sa_mask; /* signals to block during handler */
int sa_flags; /* SA_SIGINFO, SA_RESTART, SA_NODEFER, ... */
void (*sa_restorer)(void); /* internal use */
};
/* Install a signal handler */
struct sigaction sa = {
.sa_sigaction = my_handler,
.sa_flags = SA_SIGINFO | SA_RESTART,
};
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask, SIGTERM); /* block SIGTERM during handler */
sigaction(SIGUSR1, &sa, NULL);
/* Handler: */
static void my_handler(int sig, siginfo_t *info, void *ctx)
{
/* info->si_pid: sender's PID */
/* info->si_code: SI_USER, SI_QUEUE, SI_TIMER, ... */
/* info->si_value: data from sigqueue() */
printf("signal %d from pid %d\n", sig, info->si_pid);
}
Important flags:
- SA_SIGINFO — use sa_sigaction (3-arg handler) instead of sa_handler
- SA_RESTART — auto-restart interrupted syscalls (EINTR hidden from app)
- SA_NODEFER — don't block the signal during its own handler (reentrant)
- SA_RESETHAND — reset to default after one delivery
Signal masks
Each thread has a signal mask — blocked signals are not delivered until unblocked:
/* Block SIGINT and SIGTERM temporarily */
sigset_t block_set, old_set;
sigemptyset(&block_set);
sigaddset(&block_set, SIGINT);
sigaddset(&block_set, SIGTERM);
sigprocmask(SIG_BLOCK, &block_set, &old_set);
/* ... critical section ... */
/* Restore original mask */
sigprocmask(SIG_SETMASK, &old_set, NULL);
/* Atomically: wait for specific signal while unblocking */
sigsuspend(&old_set); /* replaces mask, sleeps until any signal */
Kernel data structures
/* Per-process signal state */
struct signal_struct {
/* pending signals shared by all threads: */
struct sigpending shared_pending;
/* ... */
};
/* Per-thread signal state */
struct task_struct {
/* thread-private pending signals: */
struct sigpending pending;
sigset_t blocked; /* current signal mask */
sigset_t real_blocked; /* saved mask (for sigwaitinfo) */
sigset_t saved_sigmask; /* restored on sigreturn */
/* ... */
};
struct sigpending {
struct list_head list; /* queued siginfo_t for RT signals */
sigset_t signal; /* bitmask of pending standard signals */
};
Real-time signals
Real-time signals (SIGRTMIN..SIGRTMAX) differ from standard signals:
- Queued: multiple sends before delivery are all delivered (FIFO order)
- Value: can carry an integer or pointer (via sigqueue + si_value)
- Priority: lower signal number = higher priority within real-time range
/* Send with data */
union sigval val = { .sival_int = 100 };
sigqueue(pid, SIGRTMIN+1, val);
/* Receive multiple */
struct sigaction sa = {
.sa_sigaction = rt_handler,
.sa_flags = SA_SIGINFO,
};
sigaction(SIGRTMIN+1, &sa, NULL);
/* Synchronously wait for signal */
sigset_t wait_set;
sigemptyset(&wait_set);
sigaddset(&wait_set, SIGRTMIN+1);
sigprocmask(SIG_BLOCK, &wait_set, NULL); /* must block before sigwaitinfo */
siginfo_t info;
sigwaitinfo(&wait_set, &info);
/* info.si_value.sival_int == 100 */
signalfd: signals as file descriptors
signalfd converts signal delivery into read() calls, enabling use with epoll:
#include <sys/signalfd.h>
#include <sys/epoll.h>
/* Block signals we want to receive via signalfd */
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);
sigprocmask(SIG_BLOCK, &mask, NULL);
/* Create signalfd */
int sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);
/* Add to epoll */
struct epoll_event ev = { .events = EPOLLIN, .data.fd = sfd };
epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev);
/* Read signal info */
struct signalfd_siginfo si;
read(sfd, &si, sizeof(si));
if (si.ssi_signo == SIGTERM) {
/* clean shutdown */
}
SIGCHLD and waitpid
SIGCHLD is sent to a parent when a child changes state (exits, stops, continues). The parent must call waitpid to reap the zombie:
static void sigchld_handler(int sig, siginfo_t *info, void *ctx)
{
int status;
pid_t pid;
/* Reap all exited children (loop for multiple children) */
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
if (WIFEXITED(status))
printf("child %d exited with %d\n", pid, WEXITSTATUS(status));
else if (WIFSIGNALED(status))
printf("child %d killed by signal %d\n", pid, WTERMSIG(status));
}
}
Further reading
- Pipes and FIFOs —
SIGPIPEwhen writing to a broken pipe - Futex Internals — How signals interact with futex waits
kernel/signal.c— complete signal implementationman 7 signal— signal list and default dispositions