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
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:
However:
cannot be caught or ignored because the kernel reserves complete control over these signals.
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().
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:
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:
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.