Signal Handling in Linux: Understanding signal() vs sigaction() in Linux Internals

Signal Handling in Linux signal() vs sigaction()

Signal handling in Linux is a fundamental concept in Linux system programming, UNIX process management, and operating system internals. Linux signals provide a lightweight and efficient mechanism for asynchronous inter-process communication (IPC) between the Linux kernel and user-space applications. Whenever an important event occurs — such as an invalid memory access, process termination request, timer expiration, or keyboard interrupt — the Linux kernel signals the target process using a software interrupt called a signal.

For developers working with:

  • Linux internals
  • Embedded Linux
  • Kernel-level applications
  • Multi-threaded systems
  • Daemon processes
  • Network servers
  • Device drivers
  • System utilities

understanding Linux signal handling is essential for writing reliable and production-grade software.

This Linux signals tutorial explains:

  • What Linux process signals are
  • How signal handling in C works
  • The difference between signal() and sigaction()
  • POSIX signal handling techniques
  • Async-signal-safe programming
  • Signal masking using sigprocmask
  • Common Linux kernel signals
  • Signal handling best practices in Linux

This guide is designed for:

  • Beginners learning UNIX signal handling
  • Embedded systems engineers
  • Linux kernel enthusiasts
  • System programmers
  • C developers preparing for interviews
  • Developers building robust Linux applications

Signal handling in Linux is a core operating system mechanism used for asynchronous process communication and event notification. This guide explains Linux signals, signal handling in C, and the differences between signal() and sigaction() with practical examples. Learn POSIX signal handling, async-signal-safe functions, signal masking using sigprocmask, and best practices for production-grade Linux system programming.

What Are Linux Signals?

Linux signals are asynchronous notifications delivered to a process when specific events occur inside the operating system. Signals are managed by the Linux kernel and form one of the oldest and most important IPC mechanisms in UNIX-like operating systems. Signals allow:
  • The kernel to notify a process about exceptional conditions
  • One process to communicate with another process
  • User actions such as Ctrl+C to interrupt applications
  • Timers to notify applications after expiration
  • Child process state changes to be reported to parent processes
Linux process signals interrupt the normal execution flow of a program. When a signal arrives:
  • The currently executing instruction sequence pauses
  • The kernel invokes the registered signal handler
  • The handler executes asynchronously
  • Program execution resumes afterward
Because signals are asynchronous, signal handling in Linux requires careful programming practices to avoid:
  • Race conditions
  • Deadlocks
  • Re-entrancy bugs
  • Heap corruption
  • Inconsistent program states
Signals are heavily used in:
  • Shells
  • Daemons
  • Servers
  • Debuggers
  • Process supervisors
  • Embedded Linux applications
  • System monitoring tools
    registor_now_P    

How Linux Kernel Signals Work Internally

To understand Linux signal handling deeply, developers must know how Linux kernel signals are delivered internally. When an event occurs:
  • The Linux kernel generates a signal
  • The signal becomes pending for the target process
  • The kernel checks whether the signal is blocked
  • The kernel checks whether the signal is ignored
  • The kernel checks whether a custom handler exists
  • When the process resumes execution in user mode, the kernel delivers the signal
  • The process temporarily jumps to the signal handler
  • After execution, the process returns to its previous execution state
This mechanism makes Linux signals extremely lightweight compared to other IPC mechanisms such as:
  • Pipes
  • Message queues
  • Shared memory
  • Sockets
However, because signals can interrupt execution at any moment, UNIX signal handling must be implemented carefully.

Common Linux Process Signals

Signal Description
SIGINT Interrupt signal from keyboard (Ctrl+C)
SIGTERM Graceful termination request
SIGKILL Immediate forceful termination
SIGSEGV Invalid memory access
SIGABRT Process abort signal
SIGALRM Alarm timer expiration
SIGCHLD Child process state changed
SIGUSR1 User-defined signal 1
SIGUSR2 User-defined signal 2
SIGHUP Terminal hangup detected
SIGPIPE Write to broken pipe
Some Linux kernel signals can be:
  • Caught
  • Ignored
  • Blocked
However:
  • SIGKILL
  • SIGSTOP
cannot be caught or ignored because the kernel reserves complete control over these signals.

Explore Courses - Learn More  

Default Actions of Linux Signals

Every signal has a default kernel action. Possible default behaviors include:
  • Process termination
  • Core dump generation
  • Ignoring the signal
  • Stopping process execution
  • Continuing a stopped process
For example:
  • SIGINT terminates programs by default
  • SIGSEGV generates a core dump
  • SIGCHLD is usually ignored
Custom signal handlers allow developers to override these default behaviors.

signal() Function in Linux

The signal() API is the traditional UNIX signal handling mechanism used to register a signal handler. Although widely taught in beginner tutorials, it has several portability and reliability limitations.

signal() Prototype


#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(
    int signum,
    sighandler_t handler
);
The second argument may be:
  • A custom signal handler
  • SIG_DFL for default behavior
  • SIG_IGN to ignore the signal

signal() Example in Linux


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

void handle_sigint(int sig)
{
    printf("Caught SIGINT: %d\n", sig);
}

int main()
{
    signal(SIGINT, handle_sigint);

    while (1)
    {
        printf("Application running...\n");

        sleep(1);
    }

    return 0;
}
When the user presses Ctrl+C, the Linux kernel sends SIGINT to the process. Instead of terminating, the custom signal handler executes. This demonstrates basic signal handling in C.

Problems with signal() in Linux

Although simple, signal() has serious limitations in modern Linux system programming.

1. Historical UNIX Inconsistencies

Behavior differs across UNIX implementations. On older systems:
  • The handler resets automatically after execution
  • Developers must re-register handlers repeatedly
This creates race conditions and portability issues.

2. Interrupted System Calls

Signals may interrupt:
  • read()
  • write()
  • accept()
  • wait()
  • recv()
causing system calls to fail with:

EINTR
Without proper handling, applications become unstable.

3. No Signal Masking Support

signal() cannot safely block other signals during handler execution. This may result in:
  • Re-entrancy problems
  • Shared data corruption
  • Nested signal execution

4. No Additional Signal Context

The handler receives only:

int sig
It cannot access:
  • Sender PID
  • Fault address
  • User ID
  • Additional kernel metadata
Because of these limitations, POSIX recommends using sigaction() instead of signal().
    Talk to Academic Advisor  

sigaction in Linux

sigaction() is the modern POSIX-standard API for Linux signal handling and UNIX signal handling. It provides:
  • Reliable signal behavior
  • Signal masking support
  • Extended signal information
  • Restartable system calls
  • Fine-grained signal control
  • Better portability
Every production-grade Linux application should use sigaction in Linux.

sigaction() Prototype


#include <signal.h>

int sigaction(
    int signum,
    const struct sigaction *act,
    struct sigaction *oldact
);

Understanding struct sigaction


struct sigaction
{
    void (*sa_handler)(int);

    void (*sa_sigaction)(
        int,
        siginfo_t *,
        void *
    );

    sigset_t sa_mask;

    int sa_flags;

    void (*sa_restorer)(void);
};
Each member controls a different aspect of Linux signal handling.

sa_handler

Used for traditional signal handlers:

void handler(int sig);

sa_sigaction

Used with:

SA_SIGINFO
Provides:
  • Sender PID
  • Faulting address
  • Signal metadata

sa_mask

Defines additional signals blocked during handler execution. This prevents re-entrancy issues.

sa_flags

Controls signal behavior using flags such as:
  • SA_RESTART
  • SA_SIGINFO
  • SA_NODEFER
  • SA_RESETHAND

Important sigaction Flags

SA_RESTART

Automatically restarts interrupted system calls. Without SA_RESTART, system calls may fail with:

EINTR
This is critical for stable Linux system programming.

SA_SIGINFO

Enables extended signal information using:

siginfo_t
Useful for:
  • Debuggers
  • Profilers
  • Advanced signal processing
  • Crash analysis tools

SA_NODEFER

Prevents automatic blocking of the current signal during handler execution. Allows recursive signal delivery.

SA_RESETHAND

Resets the signal handler to default behavior after one invocation.

sigaction Example in Linux


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

void handle_sigint(int sig)
{
    const char msg[] = "Caught SIGINT\n";

    write(
        STDOUT_FILENO,
        msg,
        sizeof(msg) - 1
    );
}

int main()
{
    struct sigaction sa;

    memset(&sa, 0, sizeof(sa));

    sa.sa_handler = handle_sigint;

    sigemptyset(&sa.sa_mask);

    sa.sa_flags = SA_RESTART;

    if (
        sigaction(
            SIGINT,
            &sa,
            NULL
        ) == -1
    )
    {
        perror("sigaction");

        return 1;
    }

    while (1)
    {
        printf("Running...\n");

        sleep(1);
    }

    return 0;
}
This example demonstrates robust POSIX signal handling using sigaction in Linux.

Why printf() Is Unsafe Inside Signal Handlers

Signal handlers execute asynchronously and may interrupt:
  • malloc()
  • printf()
  • free()
  • Internal libc operations
printf() is not async-signal-safe because it:
  • Uses shared internal buffers
  • Uses locks
  • Is not reentrant
Calling it inside a signal handler may cause:
  • Deadlocks
  • Heap corruption
  • Undefined behavior
Instead, developers should use:

write()
which is POSIX async-signal-safe.

Async-Signal-Safe Functions

POSIX defines a set of async-signal-safe functions that can safely execute inside signal handlers. Examples include:
  • write()
  • read()
  • close()
  • _exit()
  • kill()
  • sigaction()
  • sigprocmask()
  • sem_post()
Unsafe functions include:
  • printf()
  • malloc()
  • free()
  • exit()
  • STL containers
  • Most C++ runtime functions
Understanding async-signal-safe programming is essential for reliable Linux signal handling.

SA_SIGINFO and Extended Signal Information

One of the most powerful features of sigaction in Linux is the ability to access additional signal metadata.

SA_SIGINFO Example


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

void rich_handler(
    int sig,
    siginfo_t *info,
    void *context
)
{
    printf("Signal: %d\n", sig);

    printf(
        "Sender PID: %d\n",
        info->si_pid
    );
}

int main()
{
    struct sigaction sa;

    memset(&sa, 0, sizeof(sa));

    sa.sa_sigaction = rich_handler;

    sigemptyset(&sa.sa_mask);

    sa.sa_flags = SA_SIGINFO;

    sigaction(
        SIGUSR1,
        &sa,
        NULL
    );

    printf("PID: %d\n", getpid());

    pause();

    return 0;
}
Send the signal using:

kill -SIGUSR1 <PID>
This is an advanced POSIX signal handling technique frequently used in Linux kernel programming and Linux internals development.

Signal Masking in Linux

Signal masking prevents selected Linux process signals from interrupting critical sections. This is essential in:

Using sa_mask for Signal Blocking


sigemptyset(&sa.sa_mask);

sigaddset(&sa.sa_mask, SIGINT);
This blocks SIGINT while the current handler executes.

sigprocmask Example in Linux

The sigprocmask() system call allows explicit signal blocking.

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

int main()
{
    sigset_t mask, oldmask;

    sigemptyset(&mask);

    sigaddset(&mask, SIGINT);

    sigprocmask(
        SIG_BLOCK,
        &mask,
        &oldmask
    );

    printf("SIGINT blocked\n");

    getchar();

    sigprocmask(
        SIG_SETMASK,
        &oldmask,
        NULL
    );

    printf("SIGINT restored\n");

    return 0;
}
This sigprocmask example demonstrates safe critical-section protection in Linux system programming.

signal() vs sigaction() Comparison

Feature signal() sigaction()
POSIX compliance Limited Full
Portability Inconsistent Reliable
Signal masking No Yes
Signal metadata No Yes
Restart interrupted syscalls Undefined SA_RESTART
Production suitability Poor Excellent
Race condition handling Weak Strong
Modern Linux applications should always prefer sigaction() over signal().

Best Practices for Signal Handling in Linux

Always Use sigaction()

Avoid using signal() in production applications.

Keep Signal Handlers Minimal

Signal handlers should:
  • Execute quickly
  • Avoid complex logic
  • Avoid dynamic memory allocation

Use SA_RESTART

Prevents interrupted system calls from failing unexpectedly.

Block Signals During Critical Sections

Use:
  • sa_mask
  • sigprocmask()
to avoid inconsistent states.

Use Only Async-Signal-Safe Functions

Never call unsafe functions inside signal handlers.

Test Signal Handling Thoroughly

Production Linux applications should test:
  • Concurrent signals
  • Rapid signal delivery
  • Interrupted I/O
  • Multi-threaded scenarios

Conclusion

Signal handling in Linux is one of the most important concepts in Linux system programming and UNIX internals. Linux kernel signals provide an efficient asynchronous communication mechanism between the kernel and processes. Although signal() is useful for learning basic UNIX signal handling, modern Linux applications should always use sigaction() because it provides:
  • Reliable POSIX signal handling
  • Signal masking support
  • Extended signal metadata
  • Better portability
  • Restartable system calls
  • Safer asynchronous execution
Understanding:
  • Linux process signals
  • Signal handling in C
  • Async-signal-safe programming
  • sigprocmask examples
  • POSIX signal handling
  • Linux kernel signals
helps developers build stable, scalable, and production-grade Linux software systems.
    Talk to Academic Advisor

FAQs

Signal handling in Linux is a mechanism that allows the Linux kernel or another process to notify a running process about events such as interrupts, segmentation faults, timer expirations, or termination requests. Signals are widely used in Linux system programming and UNIX process management for asynchronous event handling.

The signal() function is the older UNIX API for handling signals, while sigaction() is the modern POSIX-compliant API that provides reliable behavior, signal masking, extended signal information, and restartable system calls. Production-grade Linux applications should always prefer sigaction() over signal().

sigaction() is preferred because it offers better portability, safer signal handling, support for SA_RESTART, signal masking with sa_mask, and advanced features like SA_SIGINFO. It helps prevent race conditions, interrupted system call issues, and re-entrancy bugs in Linux applications.

Author

Embedded Systems trainer – IIES

Updated On: 11-05-26


10+ years of hands-on experience delivering practical training in Embedded Systems and it's design