Chapter 63: Signal Handling: Catching Signals with signal()
and sigaction()
Chapter Objectives
By the end of this chapter, you will be able to:
- Understand the fundamental role of signals as an inter-process communication (IPC) mechanism in Linux.
- Implement basic signal handlers using the traditional
signal()
system call and recognize its limitations. - Develop robust, portable, and reliable signal handlers using the POSIX
sigaction()
system call. - Configure and manipulate signal masks to block signals during critical sections of code.
- Analyze and debug common issues in signal handling, particularly those related to reentrancy and race conditions.
- Apply these concepts to build responsive and resilient embedded applications on a Raspberry Pi 5.
Introduction
In the world of embedded systems, reliability and responsiveness are not just features; they are fundamental requirements. An embedded device, whether it’s a medical monitor, an industrial controller, or a consumer gadget, must react predictably to external events and internal state changes. One of the most fundamental mechanisms that Linux provides for managing these asynchronous events is the signal. Signals are a classic form of inter-process communication (IPC), serving as software interrupts that notify a process of a significant event. These events can range from a user pressing Ctrl+C
in the console (SIGINT
), to a child process terminating (SIGCHLD
), to a hardware alarm or a power-failure warning from a UPS (SIGPWR
).
Understanding how to properly handle signals is a cornerstone of professional embedded Linux development. An unhandled signal can lead to the default action, which often is the abrupt termination of your application. This can leave the system in an unstable state, with resources unreleased, files corrupted, or hardware improperly configured. Conversely, by writing custom signal handlers, you can intercept these notifications and define a precise, controlled response. This allows you to implement graceful shutdowns, reload configuration files on the fly, manage child processes, or respond to urgent hardware events in a deterministic manner. This chapter will guide you through the theory and practice of signal handling, starting with the simple, historical signal()
call and moving to the powerful and preferred sigaction()
interface. You will learn not just how to catch signals, but how to manage them with surgical precision using signal masks, ensuring your embedded applications are both robust and reliable.
Technical Background
The Nature of Linux Signals
At its core, a signal is a notification sent to a process to alert it that an event has occurred. Think of it as a software-level interrupt. When a signal is sent to a process, the operating system kernel interrupts the process’s normal flow of execution to deliver the signal. The process must then take one of three actions:
- Execute the default action. Every signal has a default disposition. For many signals, like
SIGSEGV
(segmentation fault) orSIGILL
(illegal instruction), the default action is to terminate the process and potentially generate a core dump file for debugging. For other signals, likeSIGCHLD
, the default action is to be ignored. - Ignore the signal. A process can explicitly choose to ignore a signal, with two notable exceptions:
SIGKILL
andSIGSTOP
. These two signals are special; they are always delivered and cannot be caught or ignored, providing the system administrator with an infallible way to stop a rogue process. - Catch the signal. A process can register a custom function, called a signal handler, to be executed when the signal is delivered. This is the mechanism that allows an application to respond to events in a controlled manner.
Signals can be generated from several sources. The kernel can generate signals to notify a process of a hardware exception (e.g., division by zero, SIGFPE
) or a software event (e.g., a timer expiring, SIGALRM
). Other processes can send signals using the kill()
system call, which is a common way for processes to communicate with each other. Finally, a user at a terminal can generate signals with specific key combinations, such as Ctrl+C
(SIGINT
) to interrupt a process or Ctrl+Z
(SIGTSTP
) to suspend it.
Each signal is identified by an integer, defined in <signal.h>
. You can view a full list of signals on your system by running kill -l
in the terminal. These signals are standardized by POSIX, ensuring a high degree of portability across UNIX-like systems.
The Simple Approach: The signal()
System Call
The original and simplest interface for setting a signal handler is the signal()
function, which has been part of UNIX systems since their early days. Its prototype is:
void (*signal(int signum, void (*handler)(int)))(int);
This function signature can appear daunting at first. It’s a function named signal
that takes two arguments: signum
, the integer number of the signal to catch, and handler
, a pointer to a function that will handle the signal. The handler function itself takes an integer argument (the signal number) and returns void. The signal()
function returns a pointer to the previous handler for that signal, or SIG_ERR
on error.
While signal()
is easy to use for very basic tasks, its simplicity hides significant problems that make it unsuitable for serious application development. The primary issue is its unreliability across different UNIX implementations. The POSIX standard allows for two different behaviors when a handler is invoked via signal()
. On some older systems, once a handler for a signal is called, the system resets the signal’s disposition to its default action. If you want to catch the signal again, you must re-register the handler as the first action within the handler itself. This creates a tiny window of time—a race condition—between the handler’s invocation and the re-registration, during which a second instance of the same signal could arrive and trigger the default action, likely terminating the process.
graph TD subgraph "Modern sigaction() - Robust" SA[Start: Process Running] --> SB{"Signal Arrives (e.g., SIGINT)"}; SB --> SC[Handler Executes]; SC --> SD{Handler Remains Registered}; SD --> SE{Second SIGINT Arrives}; SE --> SC; end subgraph "Legacy signal() - Unreliable" A[Start: Process Running] --> B{"Signal Arrives (e.g., SIGINT)"}; B --> C[Handler Executes]; C --> D{"System Resets Handler to Default (on some systems)"}; D --> E{<font color=#ef4444><b>Race Condition Window!</b></font>}; E --> F{Second SIGINT Arrives}; F --> G["Default Action (Process Terminated!)"]; C --> H["Handler must re-register itself: signal(SIGINT, handler)"]; end style A fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff style SA fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff style B fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff style SB fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff style C fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff style SC fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff style D fill:#eab308,stroke:#eab308,stroke-width:1px,color:#1f2937 style SD fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff style E fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff style F fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff style SE fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff style G fill:#ef4444,stroke:#ef4444,stroke-width:2px,color:#ffffff style H fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff
Modern systems, including Linux, typically do not reset the handler. However, the POSIX standard is vague enough that relying on this behavior is not portable. Furthermore, signal()
does not give the programmer any control over which other signals might be delivered while the handler is executing. This can lead to complex and hard-to-debug scenarios where one handler is interrupted by another. Because of these portability and reliability issues, the use of signal()
is strongly discouraged in new code. It is introduced here primarily for historical context and to help you understand older codebases you may encounter.
The Robust Solution: The sigaction()
System Call
To address the shortcomings of signal()
, the POSIX standard introduced the sigaction()
system call. It is more complex but provides a reliable, portable, and powerful interface for signal management. Its function signature is:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
The sigaction()
function configures the action for the signal signum
. The new action is specified in the act
argument, which is a pointer to a struct sigaction
. If oldact
is not NULL
, the system will store the previous action in the structure it points to. This is useful for saving and restoring signal dispositions.
The real power of sigaction()
lies in the struct sigaction
itself. This structure provides fine-grained control over the signal handling process. Its key fields are:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void); // Deprecated
};
Let’s break down these fields.
sa_handler
vs.sa_sigaction
: These two fields are mutually exclusive and are selected by thesa_flags
field. If theSA_SIGINFO
flag is not set, thensa_handler
is used. It points to a simple handler function, just like the one used withsignal()
. IfSA_SIGINFO
is set, thensa_sigaction
is used. This points to a more advanced handler that receives two additional arguments: a pointer to asiginfo_t
structure and a pointer to aucontext_t
(void pointer for portability). Thesiginfo_t
structure is incredibly useful as it contains detailed information about why the signal was generated, such as the process ID of the sender, the real user ID of the sender, the address of the memory fault, and more. This additional context is invaluable for complex debugging and IPC scenarios.sa_mask
: This field specifies a signal mask—a set of signals that should be blocked from delivery while the handler for this signal is executing. This is a critical feature for preventing race conditions. When your handler is invoked, the kernel automatically adds the signal that triggered it to the process’s signal mask. Thesa_mask
allows you to specify additional signals to block. For instance, if you have a handler that modifies a data structure that is also touched by another handler, you can block the second signal while the first handler runs, ensuring the data remains consistent. This provides a powerful mechanism for creating “critical sections” within your signal handling logic.sa_flags
: This field is a bitmask of flags that modify the behavior of the signal. Some of the most important flags include:SA_SIGINFO
: As mentioned, this flag selects thesa_sigaction
handler instead ofsa_handler
.SA_RESTART
: This flag affects how system calls behave when interrupted by a signal. Some slow system calls, likeread()
orwrite()
on a network socket, can be interrupted before they complete. IfSA_RESTART
is set, the kernel will automatically try to restart the interrupted system call. Without it, the call would fail with the errorEINTR
. This flag can simplify application logic by automatically handling these interruptions.SA_NOCLDSTOP
: Ifsignum
isSIGCHLD
, this flag prevents the process from receiving a notification when its child processes stop or continue (e.g., due to job control signals). It will only be notified when a child terminates.SA_ONESHOT
orSA_RESETHAND
: This flag restores the signal’s action to the default once the handler is invoked, mimicking the old, unreliable behavior ofsignal()
. It is rarely needed.
By using sigaction()
, you gain complete control over the signal handling environment, making your applications more robust and portable.
Managing Signal Masks: sigprocmask()
The sa_mask
field in sigaction
allows you to define a mask that is active only during the execution of a specific handler. However, there are many situations where you might want to block signals during a critical section of your main application code, not just within a handler. For example, you might be updating a global data structure that is also read or modified by a signal handler. If a signal arrives in the middle of your update, the handler could read inconsistent or corrupted data.
To prevent this, you can manipulate the process’s signal mask directly using the sigprocmask()
function:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
The set
argument is a pointer to a signal set (sigset_t
) that you want to apply to the current process mask. The how
argument determines how this set is applied:
SIG_BLOCK
: The signals inset
are added to the current process signal mask.SIG_UNBLOCK
: The signals inset
are removed from the current process signal mask.SIG_SETMASK
: The process signal mask is replaced withset
.
If oldset
is not NULL
, the previous signal mask is stored there. This is essential for restoring the mask to its original state after your critical section is complete.
A typical use case looks like this:
- Initialize a signal set (
sigset_t
) using helper functions. - Call
sigprocmask(SIG_BLOCK, ...)
to block the desired signals and save the old mask. - Execute the critical section of code, safe in the knowledge that the blocked signals cannot interrupt it.
- Call
sigprocmask(SIG_SETMASK, ...)
with the saved old mask to restore the previous state.
To work with sigset_t
objects, you use a set of dedicated functions:
sigemptyset(sigset_t *set)
: Initializes the set to be empty.sigfillset(sigset_t *set)
: Initializes the set to contain all signals.sigaddset(sigset_t *set, int signum)
: Adds a single signal to the set.sigdelset(sigset_t *set, int signum)
: Removes a single signal from the set.sigismember(const sigset_t *set, int signum)
: Tests if a signal is a member of the set.
These tools provide a complete framework for controlling exactly when and where signals can be delivered to your process.
The Challenge of Reentrancy and Async-Signal-Safety
Perhaps the most subtle and dangerous aspect of writing signal handlers is the concept of reentrancy. A signal can arrive at any time, interrupting your program’s main execution flow at any instruction. This means your signal handler code must be written with extreme care.
A function is considered async-signal-safe if it can be called safely from within a signal handler. The problem is that very few standard library functions meet this criterion. Consider malloc()
. Most implementations of malloc()
manage the heap using a global linked list or other complex data structure. If your main program is in the middle of a malloc()
call—modifying that global structure—and a signal arrives, your handler cannot also call malloc()
. Doing so would likely corrupt the heap, leading to a crash later in the program’s execution. The same problem applies to many other seemingly innocuous functions, including most of the stdio
library (like printf()
), which often buffers data internally.
The POSIX standard defines a specific list of functions that are guaranteed to be async-signal-safe. These include functions like _exit()
, write()
(to a limited extent), kill()
, raise()
, and the sig*
functions themselves. A common pattern is to have the signal handler do the absolute minimum amount of work possible. Often, the best approach is for the handler to simply set a global volatile sig_atomic_t
flag and then return. The main program loop can then periodically check this flag and perform the actual processing in a safe context, outside of the handler.
Tip: The
sig_atomic_t
type is an integer type that is guaranteed by the C standard to be readable and writable in a single, atomic operation. Using a variable of this type for a flag prevents race conditions where the main loop reads a partially updated value from the handler.
This “set a flag and handle it later” strategy is a cornerstone of robust signal handling design in embedded systems. It neatly separates the immediate, time-critical notification from the more complex, non-reentrant processing logic.
graph TD subgraph "Safe Handler (Set Flag and Return)" SA[Main Program] -- "calls malloc()" --> SB("Inside malloc()"); SB -- "<b>Signal Arrives!</b>" --> SC{Signal Handler Starts}; SC -- "sets flag: volatile sig_atomic_t done = 1" --> SD(Returns Immediately); SD -- "Execution returns to main program" --> SE("malloc() call completes"); SE --> SF[Main Loop Checks Flag]; SF -- "done == 1" --> SG[Process signal safely, outside of handler]; end subgraph "Unsafe Handler (Risk of Deadlock/Corruption)" A[Main Program] -- "calls malloc()" --> B("Inside malloc(), modifying heap"); B -- "<b>Signal Arrives!</b>" --> C{Signal Handler Starts}; C -- "calls printf()" --> D("printf internally calls malloc()"); D -- "tries to lock heap" --> E((<font color=#ef4444><b>DEADLOCK!</b></font><br>Heap is already locked by main program)); end style A fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff style B fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff style C fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff style D fill:#eab308,stroke:#eab308,stroke-width:1px,color:#1f2937 style E fill:#ef4444,stroke:#ef4444,stroke-width:2px,color:#ffffff style SA fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff style SB fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff style SC fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff style SD fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff style SE fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff style SF fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff style SG fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff
Practical Examples
This section provides hands-on examples for the Raspberry Pi 5. We will assume you have a cross-compilation toolchain set up, or you are compiling directly on the Pi. The examples will use standard C libraries and system calls.
Example 1: A Simple (and Flawed) SIGINT
Handler
Our first example demonstrates the basic usage of the signal()
call to catch SIGINT
(the signal sent when you press Ctrl+C
). While we’ve established that signal()
is flawed, seeing it in action helps to appreciate the improvements offered by sigaction()
.
Code Snippet
Save the following code as simple_signal.c
:
// simple_signal.c: A basic example using the signal() function.
// WARNING: This approach has portability and reliability issues.
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
// Global flag to indicate if the program should exit
volatile sig_atomic_t keep_running = 1;
// The signal handler function
void sigint_handler(int signum) {
// Note: printf is not async-signal-safe, but used here for simple demonstration.
// In a real application, this is a dangerous practice.
printf("\nCaught signal %d (SIGINT). Press Ctrl+C again to exit.\n", signum);
// On some older systems, the handler would need to be re-registered here.
// signal(SIGINT, sigint_handler);
// On the second signal, we will exit.
static int signal_count = 0;
signal_count++;
if (signal_count > 1) {
keep_running = 0;
}
}
int main() {
// Register the signal handler for SIGINT
if (signal(SIGINT, sigint_handler) == SIG_ERR) {
perror("Failed to register signal handler");
return 1;
}
printf("Process ID: %d\n", getpid());
printf("Waiting for signals. Press Ctrl+C to trigger the handler.\n");
// Main application loop
while (keep_running) {
// In a real application, this is where work would be done.
sleep(1);
}
printf("Exiting gracefully.\n");
return 0;
}
Build and Run Steps
1. Compile the code:
gcc -o simple_signal simple_signal.c -Wall
2. Run the application:
./simple_signal
The program will print its Process ID and wait.
3. Test the handler:
Press Ctrl+C in your terminal. You should see the message from the handler:
Caught signal 2 (SIGINT). Press Ctrl+C again to exit.
The program will continue running.
4. Exit the program:
Press Ctrl+C a second time. The keep_running flag will be set to 0, the while loop will terminate, and the program will print “Exiting gracefully.”
This example demonstrates the basic concept, but as noted in the code comments, using printf()
inside a handler is unsafe. We will now move to the correct approach using sigaction()
.
Example 2: Robust Graceful Shutdown with sigaction()
This example creates a more realistic embedded application that performs a simulated cleanup task. It will handle both SIGINT
and SIGTERM
(a generic termination signal often sent by system utilities like kill
or systemd
).
Code Snippet
Save the following code as robust_shutdown.c
:
// robust_shutdown.c: A robust signal handling example using sigaction().
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
// Global flag to be modified by the handler.
// volatile sig_atomic_t is guaranteed to be safe for atomic access.
volatile sig_atomic_t stop_requested = 0;
// The robust signal handler
void shutdown_handler(int signum) {
// This is the safe way to communicate from a handler to the main loop.
// We simply set a flag. All complex logic is handled outside the handler.
stop_requested = 1;
}
void setup_signal_handler() {
struct sigaction sa;
// Zero out the sigaction struct
memset(&sa, 0, sizeof(sa));
// Set the handler function
sa.sa_handler = shutdown_handler;
// Use sigemptyset to initialize the mask to empty.
// No signals will be blocked other than the one that triggered the handler.
if (sigemptyset(&sa.sa_mask) == -1) {
perror("sigemptyset failed");
exit(1);
}
// We don't need any special flags, but we set it to 0 for clarity.
// For production code, SA_RESTART is often a good choice.
sa.sa_flags = 0;
// Register the handler for SIGINT (Ctrl+C)
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction failed for SIGINT");
exit(1);
}
// Register the same handler for SIGTERM (sent by 'kill' command)
if (sigaction(SIGTERM, &sa, NULL) == -1) {
perror("sigaction failed for SIGTERM");
exit(1);
}
}
int main() {
setup_signal_handler();
printf("Process ID: %d\n", getpid());
printf("Application running. Send SIGINT (Ctrl+C) or SIGTERM to exit gracefully.\n");
// Main application loop
while (!stop_requested) {
printf("Working...\n");
// In a real embedded app, you might be polling sensors,
// processing data, or updating a display here.
sleep(2);
}
// Cleanup phase triggered by the signal
printf("\nSignal received. Starting graceful shutdown...\n");
// Simulate cleanup tasks
printf("Closing files...\n");
sleep(1);
printf("Releasing hardware resources...\n");
sleep(1);
printf("Shutdown complete. Exiting.\n");
return 0;
}
Build and Run Steps
1. Compile the code:
gcc -o robust_shutdown robust_shutdown.c -Wall
2. Run the application:
./robust_shutdown
The program will print “Working…” every two seconds.
3. Test with SIGINT:
Press Ctrl+C. The while loop will terminate, and you will see the graceful shutdown messages.
4. Test with SIGTERM:Run the application again in one terminal. In a second terminal, find the Process ID (PID) of the application and send it the SIGTERM signal:
# In terminal 2
kill <PID_of_robust_shutdown>
You will see the same graceful shutdown sequence in the first terminal. This demonstrates how your application can respond correctly to system-initiated shutdown requests.
Example 3: Protecting a Critical Section with sigprocmask()
This example shows how to protect a critical data update from being interrupted by a signal. We will have a signal handler that prints the state of a shared data structure, and a main loop that updates this structure. We will use sigprocmask()
to ensure the update is atomic.
Code Snippet
Save the following code as critical_section.c
:
// critical_section.c: Demonstrates using sigprocmask to protect a critical section.
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
// A shared data structure
struct SharedData {
long counter;
char message[50];
};
struct SharedData g_data = {0, "Initialized"};
// A handler for SIGUSR1 that prints the data
void print_data_handler(int signum) {
// Using write() as it is async-signal-safe, unlike printf()
char buffer[100];
snprintf(buffer, sizeof(buffer), "\n--- SIGUSR1 Handler ---\nCounter: %ld, Message: %s\n-----------------------\n", g_data.counter, g_data.message);
write(STDOUT_FILENO, buffer, strlen(buffer));
}
void setup_sigusr1_handler() {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = print_data_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART; // Restart interrupted system calls
if (sigaction(SIGUSR1, &sa, NULL) == -1) {
perror("sigaction for SIGUSR1");
exit(1);
}
}
int main() {
setup_sigusr1_handler();
pid_t pid = getpid();
printf("Process ID: %d\n", pid);
printf("Send SIGUSR1 to this process to print data status: kill -USR1 %d\n", pid);
printf("The main loop will update the data structure every 3 seconds.\n\n");
sigset_t block_mask, old_mask;
// Create a signal set with just SIGUSR1
sigemptyset(&block_mask);
sigaddset(&block_mask, SIGUSR1);
while (1) {
sleep(3);
// --- Start of Critical Section ---
printf("Main loop: Entering critical section. Blocking SIGUSR1.\n");
if (sigprocmask(SIG_BLOCK, &block_mask, &old_mask) == -1) {
perror("sigprocmask block");
exit(1);
}
// Simulate a non-atomic update
g_data.counter++;
printf("Main loop: Counter incremented to %ld.\n", g_data.counter);
sleep(1); // Simulate more work
snprintf(g_data.message, sizeof(g_data.message), "Update #%ld", g_data.counter);
printf("Main loop: Message updated.\n");
// --- End of Critical Section ---
printf("Main loop: Exiting critical section. Unblocking SIGUSR1.\n\n");
if (sigprocmask(SIG_SETMASK, &old_mask, NULL) == -1) {
perror("sigprocmask unblock");
exit(1);
}
// --- Any pending SIGUSR1 will be delivered now ---
}
return 0;
}
Build and Run Steps
1. Compile the code:
gcc -o critical_section critical_section.c -Wall
2. Run the application:
./critical_section
3. Test the blocking:The application will print its PID. In a second terminal, send the SIGUSR1 signal repeatedly while the main loop is in its “critical section” (the 4-second period after it prints “Entering critical section”).
# In terminal 2, run this multiple times
kill -USR1 <PID>
You will notice that the handler’s output does not appear immediately. It is delayed until the main loop unblocks the signal. When it does, you will see the handler’s output, and it will always show a consistent state of the g_data
structure (counter and message will match).
Experiment: Try commenting out the two
sigprocmask()
calls, recompile, and run the experiment again. If you send the signal at just the right moment, you might see the handler print a state where the counter has been incremented but the message has not yet been updated, demonstrating the data inconsistency thatsigprocmask
prevents.
Common Mistakes & Troubleshooting
Signal handling is powerful, but its asynchronous nature creates several common pitfalls. Being aware of them is the first step to avoiding them.
Exercises
- Simple Counter: Write a program that uses
sigaction()
to catch theSIGUSR1
signal. The handler should not print anything but should increment a globalvolatile sig_atomic_t
counter. The main loop should print the value of the counter every 5 seconds and then reset it to zero. Test your program by sending multipleSIGUSR1
signals from another terminal. - Configuration Reload: Create an application that simulates reading a configuration file at startup. The program should then enter a loop. It should handle the
SIGHUP
(hangup) signal. WhenSIGHUP
is received, the handler should set a flag. The main loop, upon seeing the flag, should print “Reloading configuration…” and simulate rereading its settings. This pattern is common in server applications. - Child Process Manager: Write a program that uses
fork()
to create a child process. The child process should simply print a message and exit after a few seconds. The parent process must install a handler forSIGCHLD
. The handler should use thewaitpid()
system call to reap the terminated child process, preventing it from becoming a zombie. The handler should print a message confirming that the child has been cleaned up. - Signal-Driven State Machine: Design a small application that models a state machine with three states:
IDLE
,RUNNING
, andPAUSED
. The program starts in theIDLE
state.- Receiving
SIGUSR1
should transition it fromIDLE
toRUNNING
. - Receiving
SIGUSR2
should transition it fromRUNNING
toPAUSED
. - Receiving
SIGUSR1
again should transition it fromPAUSED
back toRUNNING
. - Receiving SIGTERM in any state should cause a graceful exit.The main loop should print the current state whenever it changes.
- Receiving
- Real-Time Signal Queue: Investigate real-time signals (signals with numbers from
SIGRTMIN
toSIGRTMAX
). Unlike standard signals, real-time signals are queued and can carry data. Modify thesigaction()
setup to use theSA_SIGINFO
flag. Write a program where a sender process sends multipleSIGRTMIN+5
signals to a receiver process using thesigqueue()
function, passing a different integer value with each signal. The receiver’s handler (sa_sigaction
) should extract this integer from thesiginfo_t
structure and print it. This demonstrates how signals can be used for more than just notification.
Summary
- Signals are a fundamental IPC mechanism in Linux, used to notify a process of an asynchronous event.
- The legacy
signal()
call is simple but suffers from portability issues and race conditions, and its use in new applications is discouraged. - The POSIX
sigaction()
system call is the robust, reliable, and portable way to manage signals, offering fine-grained control through thestruct sigaction
. - A signal mask, managed with
sigprocmask()
and thesa_mask
field, is crucial for blocking signals during critical sections to prevent race conditions. - Signal handlers must be async-signal-safe. Most standard library functions are not safe to call from a handler. The safest strategy is for the handler to set a
volatile sig_atomic_t
flag and let the main loop perform the actual work. - Proper signal handling is essential for writing resilient embedded applications that can perform graceful shutdowns, respond to external events, and maintain a stable state.
Further Reading
signal(7)
Linux Programmer’s Manual: Provides a comprehensive overview of signals on Linux. Access viaman 7 signal
.sigaction(2)
Linux Programmer’s Manual: The definitive reference for thesigaction
system call. Access viaman 2 sigaction
.- The Linux Programming Interface by Michael Kerrisk: Chapters 20-23 provide an exhaustive and authoritative treatment of signals.
- Advanced Programming in the UNIX Environment by W. Richard Stevens and Stephen A. Rago: A classic text with deep insights into UNIX system programming, including detailed chapters on signal handling.
- POSIX.1-2017 standard, section on Signal Management: The official standard defining the behavior of
sigaction
,sigprocmask
, and related functions. (https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/). - “Async-signal-safe functions” – The Open Group Base Specifications: A list and explanation of functions that are safe to call from within a signal handler.
- Raspberry Pi Documentation: While not specific to signals, the official documentation provides essential information on the hardware and base OS for any practical application. https://www.raspberrypi.com/documentation/