On this page

User-Space Signal Handling

Coverage: sigaction → sigframe (user stack) → SA_RESTART → signal stack (sigaltstack) → signalfd → timerfd → real-time signals Applicable to: glibc + Linux, user-space perspective (see kernel docs for kernel part)

Overview

Signals are the oldest yet still actively used IPC/notification mechanism in Unix. The key issue in user-space programming is not "how to send" (the kill system call), but "when signals are delivered, what the stack looks like when the handler executes, and where the program continues after the handler returns." This article focuses on the execution model of user-space signal handlers.

sigaction: Installing a Signal Handler

#include <signal.h>

struct sigaction sa = {
    .sa_sigaction = handler,   // void handler(int sig, siginfo_t *info, void *ucontext)
    .sa_flags     = SA_SIGINFO | SA_RESTART,
    .sa_mask      = 0,          // signals blocked during handler execution
};
sigemptyset(&sa.sa_mask);
sigaction(SIGINT, &sa, NULL);

// Key bits in sa_flags:
//   SA_SIGINFO:   Use 3-argument handler (obtains siginfo + ucontext)
//   SA_RESTART:   Interrupted syscalls are automatically retried
//   SA_NODEFER:   Do not automatically block this signal during handler execution
//   SA_RESETHAND: One-shot handler (restores SIG_DFL after execution)
//   SA_ONSTACK:   Use sigaltstack (independent signal stack)

Signal Frame: The "Ghost Stack" of Signal Handlers

// When a signal is delivered, the kernel creates a sigframe on the user stack:
//
// Normal stack (grows downward):
//   [user data]  ← original RSP
//   [sigframe]   ← "snapshot" constructed by the kernel
//     ├─ Saved registers: RIP, RSP, RFLAGS, general-purpose regs
//     ├─ siginfo_t (si_signo, si_code, si_addr, ...)
//     └─ FPU state (XSAVE)
//   ← new RSP (signal handler starts from here)

// Signal handler return:
//   sigreturn() → restore all registers from sigframe → program continues

Key: Signal handlers run on the user stack (not the kernel stack) — they are ordinary user-space functions. The kernel simply saves the entire register context before calling it, and restores it after the handler returns.

Signals and System Calls

// System calls are interrupted by signals during "slow" operations (read from pipe/socket):

ssize_t ret;
do {
    ret = read(fd, buf, size);
} while (ret == -1 && errno == EINTR);  // Manual retry

// Or: SA_RESTART → kernel automatically retries (but not all syscalls are restartable)
//   Restartable: read/write/wait/pause/select/poll/...
//   Non-restartable: sleep (returns remaining time), connect (returns error), accept

// pselect / ppoll / epoll_pwait:
//   Atomic operation: unblock signal → wait → block signal
//   Avoids race condition: signal arriving between unblock and wait

sigaltstack: Independent Signal Stack

// Problem: Stack overflow → SIGSEGV → signal handler needs to run on the stack → but the stack is full!
// Solution: Allocate an independent stack for signal handlers (sigaltstack)

stack_t ss = {
    .ss_sp    = sigstack_mem,          // Independent memory region
    .ss_size  = SIGSTKSZ,              // Typically 8KB+
    .ss_flags = 0,
};
sigaltstack(&ss, NULL);

struct sigaction sa = {
    .sa_sigaction = handler,
    .sa_flags     = SA_ONSTACK | SA_SIGINFO,  // ← SA_ONSTACK!
};
sigaction(SIGSEGV, &sa, NULL);

// Use cases:
//   - SIGSEGV handler (safely record crash information)
//   - Go runtime: goroutine preemption (SIGURG + independent signal stack)
//   - Profiling: SIGPROF handler

signalfd: Signals as File Descriptors

// Traditional signal problem: Asynchronous → handlers can only call async-signal-safe functions
// signalfd: Treat signals as readable fds (synchronous) → can be used with poll/epoll

#include <sys/signalfd.h>

sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);
sigprocmask(SIG_BLOCK, &mask, NULL);    // Block first (otherwise default behavior applies)

int sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);

// Event loop:
struct signalfd_siginfo fdsi;
read(sfd, &fdsi, sizeof(fdsi));
printf("got signal %d\n", fdsi.ssi_signo);

// Works with epoll → all events handled uniformly

timerfd: Timers as File Descriptors

#include <sys/timerfd.h>

int tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);

struct itimerspec its = {
    .it_interval = { .tv_sec = 1, .tv_nsec = 0 },  // Period: 1s
    .it_value    = { .tv_sec = 1, .tv_nsec = 0 },  // First expiration: 1s
};
timerfd_settime(tfd, 0, &its, NULL);

// Each expiration → fd becomes readable → read returns 1 (number of expirations)
uint64_t expirations;
read(tfd, &expirations, sizeof(expirations));

Real-Time Signals (SIGRTMIN ~ SIGRTMAX)

POSIX real-time signals vs. traditional signals:
  Traditional (SIGINT, SIGTERM, ...): Multiple sends → may be merged into a single delivery (queue limit = 1)
  Real-time signals (SIGRTMIN+):      Multiple sends → queued in order → no loss

Real-time signals can carry additional data:
  union sigval value;
  value.sival_int = 42;
  sigqueue(target_pid, SIGRTMIN, value);

Debugging

# Process signal status
cat /proc/<pid>/status | grep Sig
# SigBlk: blocked, SigIgn: ignored, SigCgt: caught (has handler)
# SigPnd: pending per-thread, ShdPnd: pending per-process

# Send signals
kill -s SIGUSR1 <pid>
kill -s SIGRTMIN+1 <pid>   # Real-time signal

# Trace signals with strace
strace -e trace=signal ./prog

References

  • man: sigaction(2), sigaltstack(2), signalfd(2), timerfd_create(2)
  • Source code: glibc signal/sigaction.c, sysdeps/unix/sysv/linux/signalfd.c
  • LWN: "Signalfd and timerfd", "POSIX real-time signals"

Keywords: sigaction, sigframe, SA_RESTART, sigaltstack, SA_ONSTACK, signalfd, timerfd, real-time signals, SIGEV_THREAD