Signal#

Introduction#

Signals are software interrupts that provide a mechanism for handling asynchronous events in Unix-like operating systems. When a signal is delivered to a process, the operating system interrupts the process’s normal execution flow and invokes a signal handler function.

Signals can originate from various sources: the kernel (e.g., SIGSEGV for memory access violations), other processes (via kill()), terminal input (e.g., Ctrl+C generates SIGINT), or the process itself (via raise() or abort()). Each signal has a default action—typically terminating the process, ignoring the signal, or stopping/continuing execution—but most signals can be caught and handled by user-defined functions.

Understanding signal handling is essential for writing robust Unix applications, particularly servers and daemons that must handle termination requests gracefully, manage child processes, and respond to timer events.

Basic Signal Handler#

Source:

src/signal/basic-handler

The signal() function registers a handler for a specific signal. When that signal is delivered, the kernel interrupts the process and calls the handler function with the signal number as its argument. Use SIG_IGN to ignore a signal or SIG_DFL to restore the default behavior. Note that signal() has portability issues across Unix variants; sigaction() is preferred for new code.

#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>

void handler(int signo) {
  printf("[%d] Got signal: %s\n", getpid(), strsignal(signo));
}

int main(void) {
  signal(SIGHUP, handler);
  signal(SIGINT, handler);
  signal(SIGALRM, handler);
  signal(SIGUSR1, SIG_IGN);  // ignore SIGUSR1
  while (1) sleep(3);
}
$ ./basic-handler &
[54652]
$ kill -HUP %1
[54652] Got signal: Hangup
$ kill -INT %1
[54652] Got signal: Interrupt

Signal Handling with sigaction#

Source:

src/signal/sigaction-handler

The sigaction() function provides more control over signal handling than signal(). It allows specifying a signal mask to block other signals during handler execution, flags to modify behavior (e.g., SA_RESTART to restart interrupted system calls), and access to additional signal information via SA_SIGINFO. Always prefer sigaction() over signal() for reliable, portable signal handling.

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void handler(int signo) {
  printf("Got signal: %s\n", sys_siglist[signo]);
}

int main(void) {
  struct sigaction sa = {0};
  sa.sa_handler = handler;
  sigemptyset(&sa.sa_mask);
  sa.sa_flags = 0;

  sigaction(SIGINT, NULL, &sa);  // get current
  if (sa.sa_handler != SIG_IGN)
    sigaction(SIGINT, &sa, NULL);

  sigaction(SIGHUP, NULL, &sa);
  if (sa.sa_handler != SIG_IGN)
    sigaction(SIGHUP, &sa, NULL);

  printf("PID: %d\n", getpid());
  while (1) sleep(3);
}
$ ./sigaction-handler &
PID: 57140
$ kill -HUP 57140
Got signal: Hangup
$ kill -INT 57140
Got signal: Interrupt

Pthread Signal Handling#

Source:

src/signal/pthread-signal

In multithreaded programs, signals can be delivered to any thread that hasn’t blocked them. A common pattern is to block signals in all threads except a dedicated signal-handling thread that uses sigwait() to synchronously receive signals. This avoids the complexity and restrictions of asynchronous signal handlers in threaded code.

#include <stdio.h>
#include <pthread.h>
#include <signal.h>

void *sig_thread(void *arg) {
  sigset_t *set = (sigset_t *)arg;
  int signo;
  for (;;) {
    sigwait(set, &signo);
    printf("Got signal[%d]: %s\n", signo, sys_siglist[signo]);
  }
}

int main(void) {
  sigset_t set;
  sigemptyset(&set);
  sigaddset(&set, SIGQUIT);
  sigaddset(&set, SIGUSR1);
  pthread_sigmask(SIG_BLOCK, &set, NULL);

  pthread_t t;
  pthread_create(&t, NULL, sig_thread, &set);
  pause();
}
$ ./pthread-signal &
[1] 21258
$ kill -USR1 %1
Got signal[10]: User defined signal 1
$ kill -QUIT %1
Got signal[3]: Quit

Monitoring Child Processes#

Source:

src/signal/child-monitor

When a child process terminates, the kernel sends SIGCHLD to the parent. By installing a handler for SIGCHLD, the parent can be notified immediately when children exit, rather than blocking in wait(). This is essential for servers that spawn worker processes and need to track their lifecycle without blocking the main event loop.

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void handler(int signo) {
  printf("[%d] Got signal[%d]: %s\n", getpid(), signo, sys_siglist[signo]);
}

int main(void) {
  signal(SIGCHLD, handler);
  pid_t pid = fork();

  if (pid == 0) {
    printf("Child[%d]\n", getpid());
    sleep(3);
  } else {
    printf("Parent[%d]\n", getpid());
    pause();
  }
}
$ ./child-monitor
Parent[59113]
Child[59114]
[59113] Got signal[20]: Child exited

Blocking and Unblocking Signals#

Source:

src/signal/signal-mask

The sigprocmask() function controls which signals are blocked (deferred) for the calling thread. Blocked signals remain pending until unblocked, at which point they are delivered. This is useful for protecting critical sections from interruption or for temporarily deferring signal handling. Combined with siglongjmp(), it enables complex control flow in signal handlers.

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <setjmp.h>

static sigjmp_buf jmpbuf;

void handler(int signo) {
  printf("Got signal[%d]: %s\n", signo, sys_siglist[signo]);
  if (signo == SIGUSR1)
    siglongjmp(jmpbuf, 1);
}

int main(void) {
  sigset_t mask;
  sigemptyset(&mask);
  sigaddset(&mask, SIGHUP);

  signal(SIGHUP, handler);
  signal(SIGALRM, handler);
  signal(SIGUSR1, handler);

  if (sigsetjmp(jmpbuf, 1))
    sigprocmask(SIG_UNBLOCK, &mask, NULL);  // unblock after jump
  else
    sigprocmask(SIG_BLOCK, &mask, NULL);    // block SIGHUP

  while (1) sleep(3);
}
$ ./signal-mask &
$ kill -HUP %1      # blocked, no output
$ kill -ALRM %1
Got signal[14]: Alarm clock
$ kill -USR1 %1     # triggers jump, unblocks SIGHUP
Got signal[10]: User defined signal 1
Got signal[1]: Hangup