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:

  1. Execute the default action. Every signal has a default disposition. For many signals, like SIGSEGV (segmentation fault) or SIGILL (illegal instruction), the default action is to terminate the process and potentially generate a core dump file for debugging. For other signals, like SIGCHLD, the default action is to be ignored.
  2. Ignore the signal. A process can explicitly choose to ignore a signal, with two notable exceptions: SIGKILL and SIGSTOP. 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.
  3. 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.

Signal Name Number Default Action Common Use Case
SIGHUP 1 Terminate Reload configuration files in a daemon.
SIGINT 2 Terminate Interrupt from terminal (Ctrl+C). Used for graceful shutdown.
SIGKILL 9 Terminate Forcibly terminate a process. Cannot be caught or ignored.
SIGTERM 15 Terminate Generic termination signal. The standard way to request a graceful exit.
SIGCHLD 17 Ignore A child process has terminated, stopped, or continued.
SIGUSR1 / SIGUSR2 10 / 12 Terminate User-defined signals for custom application behavior.
SIGALRM 14 Terminate Timer set by alarm() has expired.

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:

C
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:

C
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:

C
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 the sa_flags field. If the SA_SIGINFO flag is not set, then sa_handler is used. It points to a simple handler function, just like the one used with signal(). If SA_SIGINFO is set, then sa_sigaction is used. This points to a more advanced handler that receives two additional arguments: a pointer to a siginfo_t structure and a pointer to a ucontext_t (void pointer for portability). The siginfo_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. The sa_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 the sa_sigaction handler instead of sa_handler.
    • SA_RESTART: This flag affects how system calls behave when interrupted by a signal. Some slow system calls, like read() or write() on a network socket, can be interrupted before they complete. If SA_RESTART is set, the kernel will automatically try to restart the interrupted system call. Without it, the call would fail with the error EINTR. This flag can simplify application logic by automatically handling these interruptions.
    • SA_NOCLDSTOP: If signum is SIGCHLD, 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 or SA_RESETHAND: This flag restores the signal’s action to the default once the handler is invoked, mimicking the old, unreliable behavior of signal(). It is rarely needed.
Flag Description
SA_RESTART Automatically restart certain system calls (like read, write) if they are interrupted by this signal handler. This can simplify error handling.
SA_SIGINFO Use the sa_sigaction field as the handler instead of sa_handler. This provides the handler with extra information about the signal event via a siginfo_t structure.
SA_NOCLDSTOP If the signal is SIGCHLD, do not generate a signal for child processes that stop or continue, only for those that terminate.
SA_RESETHAND Restore the signal action to the default upon entry to the signal handler. This mimics the old, unreliable behavior of signal() and is rarely used. Also known as SA_ONESHOT.

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:

C
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 in set are added to the current process signal mask.
  • SIG_UNBLOCK: The signals in set are removed from the current process signal mask.
  • SIG_SETMASK: The process signal mask is replaced with set.

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:

  1. Initialize a signal set (sigset_t) using helper functions.
  2. Call sigprocmask(SIG_BLOCK, ...) to block the desired signals and save the old mask.
  3. Execute the critical section of code, safe in the knowledge that the blocked signals cannot interrupt it.
  4. 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:

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:

Bash
gcc -o simple_signal simple_signal.c -Wall

2. Run the application:

Bash
./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:

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:

Bash
gcc -o robust_shutdown robust_shutdown.c -Wall

2. Run the application:

Bash
./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:

Bash
# 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:

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:

Bash
gcc -o critical_section critical_section.c -Wall

2. Run the application:

Bash
./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”).

Bash
# 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 that sigprocmask 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.

Mistake / Issue Symptom(s) Troubleshooting / Solution
Using non-async-signal-safe functions in a handler. Random crashes, heap corruption, deadlocks. The program behaves erratically after a signal is handled. Solution: The handler should only set a volatile sig_atomic_t flag. All complex processing (including I/O like printf) must be moved to the main application loop, which checks the flag.
Forgetting to check return values. Signal handler never runs. Program terminates unexpectedly on a signal because the handler was never registered. Solution: Always check the return value of sigaction(), sigprocmask(), etc. If they return -1, use perror(“descriptive message”) to see why it failed and exit.
Race condition on shared data. Handler reads partially updated, inconsistent data. Program logic fails in subtle ways. Solution: Protect critical sections in your main code by blocking the relevant signal with sigprocmask(SIG_BLOCK, …) before accessing the data, and unblocking it with sigprocmask(SIG_SETMASK, …) afterwards.
Not initializing struct sigaction. Unpredictable behavior. The handler might not be called, or it might have strange side effects due to random values in sa_flags or sa_mask. Solution: Always zero-out the structure immediately after declaration. Use memset(&sa, 0, sizeof(sa)); or struct initialization struct sigaction sa = {0};.
System call fails with EINTR. A “slow” system call (e.g., read() on a socket) returns -1, and errno is set to EINTR. The program may exit thinking a real error occurred. Solution: Either use the SA_RESTART flag in sigaction to handle this automatically, or manually check for errno == EINTR after a system call and retry the call in a loop.
Creating zombie processes. Child processes terminate but remain in the system’s process table (as “defunct”). This leaks resources over time. Solution: The parent process must handle SIGCHLD. The handler should call waitpid(-1, &status, WNOHANG) in a loop to reap all terminated children.

Exercises

  1. Simple Counter: Write a program that uses sigaction() to catch the SIGUSR1 signal. The handler should not print anything but should increment a global volatile 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 multiple SIGUSR1 signals from another terminal.
  2. 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. When SIGHUP 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.
  3. 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 for SIGCHLD. The handler should use the waitpid() 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.
  4. Signal-Driven State Machine: Design a small application that models a state machine with three states: IDLERUNNING, and PAUSED. The program starts in the IDLE state.
    • Receiving SIGUSR1 should transition it from IDLE to RUNNING.
    • Receiving SIGUSR2 should transition it from RUNNING to PAUSED.
    • Receiving SIGUSR1 again should transition it from PAUSED back to RUNNING.
    • Receiving SIGTERM in any state should cause a graceful exit.The main loop should print the current state whenever it changes.
  5. Real-Time Signal Queue: Investigate real-time signals (signals with numbers from SIGRTMIN to SIGRTMAX). Unlike standard signals, real-time signals are queued and can carry data. Modify the sigaction() setup to use the SA_SIGINFO flag. Write a program where a sender process sends multiple SIGRTMIN+5 signals to a receiver process using the sigqueue() function, passing a different integer value with each signal. The receiver’s handler (sa_sigaction) should extract this integer from the siginfo_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 the struct sigaction.
  • signal mask, managed with sigprocmask() and the sa_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

  1. signal(7) Linux Programmer’s Manual: Provides a comprehensive overview of signals on Linux. Access via man 7 signal.
  2. sigaction(2) Linux Programmer’s Manual: The definitive reference for the sigaction system call. Access via man 2 sigaction.
  3. The Linux Programming Interface by Michael Kerrisk: Chapters 20-23 provide an exhaustive and authoritative treatment of signals.
  4. 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.
  5. POSIX.1-2017 standard, section on Signal Management: The official standard defining the behavior of sigactionsigprocmask, and related functions. (https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/).
  6. “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.
  7. 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/

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top