Search Knowledge

© 2026 LIBREUNI PROJECT

Operating Systems Internals / Advanced Topics & UNIX Deep Dive

UNIX Signals and Process Control

UNIX Signals and Process Control

In POSIX-compliant operating systems, a signal is a software interrupt delivered to a process by the kernel or another process. Signals notify processes that a specific hardware or software event has occurred, forcing execution flow to detour to handle the interrupt.

Signal Delivery and Handlers

When the kernel delivers a signal, it suspends execution of the target process’s instructions, saves its CPU registers on a special signal stack frame, and switches execution context. The process responds in one of three ways:

  1. Ignore the Signal: The process drops the notification. However, certain critical signals cannot be ignored.
  2. Perform the Default Action: The kernel executes a default behavior (e.g., terminating the process, ignoring the signal, or suspending its execution state).
  3. Catch the Signal: The process registers a custom function—a signal handler—which executes when the signal is delivered.

POSIX defines a standard set of signals, including:

  • SIGINT (2): Interrupt from keyboard (typically triggered via Ctrl+C). Default action: Terminate.
  • SIGKILL (9): Forceful termination. Cannot be caught, blocked, or ignored.
  • SIGSEGV (11): Segmentation fault (invalid memory reference). Default action: Terminate and generate a core dump.
  • SIGTERM (15): Graceful termination request. This is the default signal sent by the kill command.

The following shell commands demonstrate sending different signals to processes:

# Example: Sending signals to a running daemon using the kill command
$ ps aux | grep "my_daemon"
student  12345  0.1  0.5  12345  6789 pts/1    S    12:00   0:00 ./my_daemon

# Send SIGTERM (signal 15) to request a clean exit
$ kill 12345

# Send SIGKILL (signal 9) if the daemon is unresponsive and must be forced down
$ kill -9 12345

Implementing Signal Handling in C

Systems programmers use the modern sigaction API to manage handlers. Compared to the older signal() system call, sigaction provides reliable signal delivery and fine-grained control over signal masks.

The following C program registers a signal handler to clean up resources when receiving a termination signal:

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

/* Signal handler function */
void graceful_shutdown(int signum) {
    /* Log received signal and exit */
    const char *msg = "\nSignal received. Exiting...\n";
    write(STDOUT_FILENO, msg, 28);
    exit(0);
}

int main(void) {
    struct sigaction sa;

    /* Initialize sigaction structure */
    sa.sa_handler = graceful_shutdown;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    /* Register handler for SIGINT (Ctrl+C) and SIGTERM */
    if (sigaction(SIGINT, &sa, NULL) == -1 || sigaction(SIGTERM, &sa, NULL) == -1) {
        perror("Error registering signal handler");
        return 1;
    }

    printf("Daemon running. Waiting for signals (PID: %d)...\n", getpid());
    while (1) {
        sleep(1); 
    }
    return 0;
}

Using custom handlers prevents dangling lock files, unclosed network sockets, and database corruption.

Signal Safety (Async-Signal-Safe Functions)

Because signals are fully asynchronous, they can interrupt a process at any instruction boundary. This introduces severe concurrency risks. If the main program is interrupted while executing a non-reentrant standard library function (such as malloc() or printf()), and the signal handler attempts to call that same function, the internal state of the library will be corrupted.

POSIX restricts handler code to a small set of functions designated as async-signal-safe. These functions are guaranteed to be reentrant or non-interruptible.

The following C code shows the difference between a safe and unsafe signal handler:

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

/* UNSAFE: printf uses internal locks and static buffers, risking deadlocks */
void unsafe_handler(int signum) {
    printf("Interrupted by signal: %d\n", signum); 
}

/* SAFE: write() is a direct, async-signal-safe system call */
void safe_handler(int signum) {
    const char *msg = "Signal intercepted safely\n";
    write(STDOUT_FILENO, msg, strlen(msg));
}

To share state safely between the main program and a signal handler, developers use the atomic type volatile sig_atomic_t, which prevents compiler register caching and guarantees atomic reads and writes.

Exercise: Signal Handling Operations

Evaluate your understanding of asynchronous signal delivery and safety in the exercises below:

Case Study Setup

A systems developer is writing a network logging server in C. To ensure data is written to disk before termination, they register a SIGINT handler. Inside this handler, the developer attempts to allocate a crash dump buffer using `malloc()` and log status output using `printf()`.

Why does calling these standard library functions inside a signal handler violate POSIX safety standards?

Which of the following POSIX signals cannot be caught, blocked, or ignored by a user-space process?

Declaring Safe Flags in Signal Handlers

/* A flag modified in a handler must use this type to guarantee atomic access */
volatile  flag = 0;

References & Further Reading

For additional specifications and implementation details on POSIX signals, consult the following sources:

  • Stevens, W. R., & Rago, S. A. (2013). Advanced Programming in the UNIX Environment (3rd ed.). Addison-Wesley. (Covering Chapter 10: Signals, including signal masks, sigaction, and reentrant functions).
  • Kerrisk, M. (2010). The Linux Programming Interface. No Starch Press. (Covering Chapter 20: Signals: Fundamental Concepts and Chapter 21: Signals: Signal Handlers).
  • signal-safety(7) Manual Page. Linux man-pages project.
  • sigaction(2) Manual Page. Linux man-pages project.