Unix signals are fundamental to process management and inter-process communication in Linux and Unix-like operating systems. They serve as software interrupts that notify processes of asynchronous events, making them essential for system administration and software development.

Understanding Unix Signals

What Are Unix Signals?

Signals are software interrupts delivered to processes to announce asynchronous events. They can be generated by the kernel, other processes, or the process itself. Modern Unix systems typically support 64 different signals, each identified by a symbolic name beginning with “SIG” and a corresponding number.

When a signal is delivered to a process, it interrupts the normal flow of execution. The process can then:

  • Ignore the signal
  • Catch and handle the signal with a custom function
  • Allow the default action to occur

Common Signal Types and Their Purpose

Termination Signals

SIGTERM (15) - The polite termination request. This signal asks a process to terminate gracefully, allowing it to clean up resources, save state, and exit cleanly.

SIGKILL (9) - The forceful termination. This signal cannot be caught, blocked, or ignored. It immediately terminates the process without cleanup.

SIGINT (2) - The interrupt signal, typically generated when a user presses Ctrl+C. It’s catchable, allowing programs to perform cleanup before termination.

Process Control Signals

SIGSTOP (19) - Pauses a process. Like SIGKILL, this signal cannot be caught or ignored.

SIGCONT (18) - Resumes a paused process. Used in conjunction with SIGSTOP for job control.

SIGTSTP (20) - Terminal stop signal, usually triggered by Ctrl+Z. Unlike SIGSTOP, this can be caught and handled.

Error Signals

SIGSEGV (11) - Segmentation violation. Sent when a process attempts to access memory it shouldn’t, typically resulting in a core dump.

SIGFPE (8) - Floating-point exception. Generated on arithmetic errors like division by zero.

SIGILL (4) - Illegal instruction. Sent when a process attempts to execute an invalid machine instruction.

Timing and Notification Signals

SIGALRM (14) - Alarm clock signal. Generated when a timer set by alarm() expires.

SIGCHLD (17) - Child status change. Sent to a parent process when a child process terminates or stops.

SIGHUP (1) - Hangup. Originally meant the terminal disconnected, now often used to trigger configuration reloads.

Signal Handling in Practice

Basic Signal Handler Implementation

Here’s a simple example of catching and handling SIGINT:

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

void sig_handler(int signo) {
    if (signo == SIGINT) {
        printf("\nReceived SIGINT! Cleaning up...\n");
        // Perform cleanup operations here
        exit(0);
    }
}

int main(void) {
    // Register signal handler
    if (signal(SIGINT, sig_handler) == SIG_ERR) {
        printf("Can't catch SIGINT\n");
        return 1;
    }
    
    printf("Running... Press Ctrl+C to interrupt\n");
    while(1) {
        sleep(1);
    }
    
    return 0;
}

Advanced Signal Handling with sigaction()

For more robust signal handling, use sigaction() instead of signal():

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

void handle_signal(int sig, siginfo_t *siginfo, void *context) {
    printf("Received signal %d from PID %d\n", sig, siginfo->si_pid);
}

int main() {
    struct sigaction act;
    
    memset(&act, 0, sizeof(act));
    act.sa_sigaction = &handle_signal;
    act.sa_flags = SA_SIGINFO;
    
    if (sigaction(SIGUSR1, &act, NULL) < 0) {
        perror("sigaction");
        return 1;
    }
    
    printf("PID: %d - Waiting for SIGUSR1...\n", getpid());
    while(1) {
        pause();
    }
    
    return 0;
}

Sending Signals

From the Command Line

# Send SIGTERM to process 1234
kill 1234

# Send SIGKILL to process 1234
kill -9 1234

# Send SIGUSR1 to process 1234
kill -USR1 1234

# Send signal to all processes in a process group
kill -TERM -1234

Programmatically

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

// Send signal to specific process
kill(pid, SIGUSR1);

// Send signal to current process
raise(SIGTERM);

// Send signal to process group
killpg(pgrp, SIGHUP);

Signal Masks and Blocking

Processes can temporarily block signals using signal masks:

sigset_t mask, oldmask;

// Initialize signal set
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);

// Block signals
sigprocmask(SIG_BLOCK, &mask, &oldmask);

// Critical section - SIGINT and SIGTERM are blocked
do_critical_work();

// Restore original mask
sigprocmask(SIG_SETMASK, &oldmask, NULL);

Real-World Applications

Graceful Daemon Shutdown

volatile sig_atomic_t shutdown_requested = 0;

void handle_shutdown(int sig) {
    shutdown_requested = 1;
}

int main() {
    signal(SIGTERM, handle_shutdown);
    signal(SIGINT, handle_shutdown);
    
    while (!shutdown_requested) {
        // Main daemon work
        process_requests();
    }
    
    // Cleanup
    close_connections();
    save_state();
    return 0;
}

Configuration Reload without Restart

Many daemons use SIGHUP to reload configuration:

void handle_sighup(int sig) {
    syslog(LOG_INFO, "Received SIGHUP, reloading configuration");
    reload_config();
}

Best Practices

  1. Keep Signal Handlers Simple: Signal handlers should do minimal work. Set a flag and handle the event in the main program flow.

  2. Use Async-Signal-Safe Functions: Only call functions guaranteed to be safe in signal handlers. Check signal-safety(7) for a complete list.

  3. Handle EINTR: System calls can be interrupted by signals. Always check for EINTR and retry if appropriate.

  4. Avoid Race Conditions: Use atomic operations and proper synchronization when accessing shared data from signal handlers.

  5. Document Signal Usage: Clearly document which signals your application handles and their effects.

Debugging Signal Issues

# Monitor signals sent to a process
strace -e signal -p <pid>

# List pending signals
cat /proc/<pid>/status | grep Sig

# Send test signals
kill -l  # List all signals
kill -0 <pid>  # Test if process exists

Conclusion

Understanding Unix signals is crucial for robust system programming and effective system administration. They provide a powerful mechanism for process communication and control, enabling graceful shutdowns, dynamic reconfiguration, and proper error handling. By mastering signal handling, developers can create more resilient and responsive applications that integrate seamlessly with the Unix philosophy of process management.

Whether you’re building system daemons, debugging application crashes, or managing production services, a solid understanding of signals will serve you well in your journey through Unix and Linux systems.