Chapter 62: Signal Handling: Introduction to Signals and Default Actions

Chapter Objectives

Upon completing this chapter, you will be able to:

  • Understand the fundamental role of signals as an asynchronous inter-process communication mechanism in Linux.
  • Identify common signals such as SIGINTSIGTERM, and SIGCHLD, and describe their default actions and typical use cases.
  • Implement custom signal handlers in C to gracefully manage events like user interruptions and process termination.
  • Configure a parent process to correctly handle the SIGCHLD signal to manage child process lifecycle and prevent zombie processes.
  • Debug common issues related to signal handling, including the use of non-reentrant functions and race conditions.
  • Apply signal handling concepts to build more robust and reliable embedded applications on a Raspberry Pi 5.

Introduction

In the world of embedded systems, events do not always follow a predictable, synchronous schedule. A user might press an emergency stop button, a sensor might detect a critical failure, or a remote command might request a graceful shutdown. These asynchronous events require an immediate and reliable response from the software. This is where the Linux signal mechanism becomes indispensable. Signals are one of the oldest and most fundamental forms of Inter-Process Communication (IPC) on UNIX-like operating systems, serving as a software notification to a process that an event has occurred.

Understanding signals is not merely an academic exercise; it is a critical skill for any embedded Linux developer. A properly handled signal can be the difference between a device that corrupts its filesystem during a power loss and one that saves its state and shuts down cleanly. It can distinguish a system that recovers gracefully from a fault from one that requires a hard reboot. For example, a SIGTERM signal allows an application controlling a robotic arm to return to a safe home position before exiting, while an unhandled signal might leave the hardware in an unpredictable and potentially dangerous state. In this chapter, we will explore the theory behind signals, their default behaviors, and how to harness their power to create resilient embedded applications. Using the Raspberry Pi 5, you will move from theory to practice, writing code that catches user input, manages child processes, and lays the foundation for building truly robust systems.

Technical Background

The Nature of Asynchronous Events

To appreciate the design of signals, one must first understand the problem they solve: handling asynchronous events. A running program, or process, executes instructions sequentially, following a logical path defined by its code. However, the world outside the process is not always so orderly. Events can originate from various sources at any time, irrespective of the process’s current state. The operating system kernel is the primary originator of many signals, often in response to hardware events or exceptional conditions. For instance, if a process attempts to access an invalid memory address, the Memory Management Unit (MMU) in the CPU triggers a hardware exception. The kernel catches this exception and notifies the offending process by sending it a SIGSEGV (Segmentation Fault) signal.

Another common source is the user. When you press Ctrl+C in a terminal, the terminal driver recognizes this key combination and, through the kernel, sends a SIGINT (Interrupt) signal to the foreground process. The process is interrupted from whatever it was doing to handle this event. Other processes can also explicitly send signals to one another using system calls like kill(), providing a direct mechanism for inter-process communication and control.

This asynchronous nature presents a significant challenge. The process’s main line of execution is paused without warning, and control is transferred to a different piece of code—the signal handler. This is conceptually similar to a hardware interrupt, where the CPU stops its current task to execute an interrupt service routine (ISR). Just as an ISR must be carefully written to be fast and avoid disrupting the system, a signal handler must be crafted with similar care to maintain program consistency and stability.

graph TD
    subgraph Main Program Execution
        A[Start Process] --> B{"Executing Main Code...<br><i>(e.g., data logging, motor control)</i>"};
        B --> C{Continue Main Code...};
        C --> B;
    end

    subgraph Signal Handling
        D{"Signal Handler Execution<br><b>(Minimal & Safe Code)</b><br>e.g., set volatile flag"} --> E[Return from Handler];
    end

    F(Asynchronous Event<br><i>e.g., User presses Ctrl+C</i>) -- Triggers --> G[Kernel Delivers SIGINT];
    G -- Interrupts Process --> D;
    E -- Resumes Execution --> B;

    %% Styling
    classDef default fill:#f8fafc,stroke:#64748b,stroke-width:1px,color:#1f2937;
    classDef startNode fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff;
    classDef processNode fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff;
    classDef eventNode fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff;
    classDef kernelNode fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff;
    classDef handlerNode fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff;

    class A startNode;
    class B,C processNode;
    class F eventNode;
    class G kernelNode;
    class D,E handlerNode;

Signal Lifecycle: Generation, Delivery, and Disposition

Every signal follows a distinct lifecycle consisting of three phases: generation, delivery, and disposition.

graph TD
    A[Start: Event Occurs] --> B(<b>1. Generation</b><br>Signal is generated and marked as 'pending' for the process.<br><i>Sources: Kernel, other processes, user input.</i>);
    B --> C{<b>2. Delivery</b><br>Kernel checks for pending signals before resuming process in user mode.};
    C --> D{<b>3. Disposition</b><br>What does the process do?};

    D -- "Choice 1" --> E(<b>Default Action</b><br>System-defined behavior<br>e.g., Terminate, Ignore, Core Dump);
    D -- "Choice 2" --> F(<b>Catch Signal</b><br>Execute a custom signal handler function.<br><i>Most flexible option.</i>);
    D -- "Choice 3" --> G(<b>Ignore Signal</b><br>The signal is discarded; the process is unaffected.);

    subgraph Unavoidable Signals
        H(<b>SIGKILL & SIGSTOP</b><br>Cannot be Caught or Ignored);
    end

    F --> I[End: Handler Completes];
    E --> J[End: Action Performed];
    G --> K[End: Signal Discarded];

    %% Styling
    classDef default fill:#f8fafc,stroke:#64748b,stroke-width:1px,color:#1f2937,font-family:'Open Sans';
    classDef startNode fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff;
    classDef processNode fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff;
    classDef decisionNode fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff;
    classDef endNode fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff;
    classDef warningNode fill:#eab308,stroke:#eab308,stroke-width:1px,color:#1f2937;


    class A startNode;
    class B,E,F,G processNode;
    class C,D decisionNode;
    class I,J,K endNode;
    class H warningNode;
  1. Generation: A signal is generated (or sent) when the event that causes it occurs. As discussed, this can be a hardware exception, a user action, or a direct system call from another process. When a signal is generated for a process, it is typically marked as pending. A process can have multiple signals pending at once, but generally, there is only one pending signal for each signal type. For example, if a process receives ten SIGINT signals in rapid succession before it has a chance to run, only one of them is guaranteed to be delivered. Real-time signals are an exception to this, as they can be queued, but the standard signals we discuss here are not.
  2. Delivery: A pending signal is delivered to a process when the kernel alters the process’s execution to deal with the signal. This happens just before the process is scheduled to resume execution in user mode. Before returning control to the process’s main instruction stream, the kernel checks for any pending signals. If one exists, the kernel arranges for the process to handle it. The process is temporarily “hijacked” to deal with the signal.
  3. Disposition: The disposition of a signal refers to the action the process takes upon its delivery. For every signal, a process has three choices:
    • Default Action: Every signal has a default action defined by the system. For many signals, like SIGSEGV or SIGILL (Illegal Instruction), the default action is to terminate the process, often creating a core dump file for debugging. For other signals, like SIGCHLD, the default action is to be ignored. For SIGINT, the default is to terminate the process.
    • Catch the Signal: A process can establish a custom function, a signal handler, to be executed when the signal is delivered. This allows the application to define its own behavior, such as performing cleanup operations before shutting down. This is the most powerful and common way to use signals in application development.
    • Ignore the Signal: A process can explicitly choose to ignore a signal. When an ignored signal is delivered, the system simply discards it, and the process’s execution is unaffected. However, two signals, SIGKILL and SIGSTOP, are special; they cannot be caught or ignored. They provide a guaranteed way for the system administrator (and the kernel) to terminate or stop a process, respectively. This is a crucial failsafe mechanism.

A process can control a signal’s disposition using the signal() or the more modern and recommended sigaction() system call. These calls allow the process to specify a handler function or to set the disposition to ignore or revert to the default action.

Common Signals and Their Default Actions

While the Linux kernel defines over thirty different signals, a handful are particularly relevant in the context of embedded systems programming. Understanding their purpose and default behavior is fundamental.

  • SIGINT (Signal Interrupt): This signal is sent to a process by its controlling terminal when a user presses the interrupt key, typically Ctrl+C. Its default action is to terminate the process. In embedded development, this is commonly caught to allow for a graceful shutdown of an application being tested from a command line. For example, a data logging application might catch SIGINT to ensure all buffered data is written to the disk and files are closed properly before exiting.
  • SIGTERM (Signal Terminate): This is the standard, generic signal used to request the termination of a process. Unlike SIGKILLSIGTERM can be caught and handled, giving the application a chance to perform cleanup. This is the signal sent by the kill command by default. In a system managed by an init system like systemdSIGTERM is sent to a service to ask it to stop. A well-behaved daemon should always handle SIGTERM to shut down gracefully.
  • SIGKILL (Signal Kill): This signal causes the immediate termination of a process. It cannot be caught or ignored. It is the “last resort” for terminating a misbehaving or unresponsive process. While effective, it should be used with caution in embedded systems because it gives the application no opportunity to save its state, release resources (like hardware locks), or put hardware into a safe state. Relying on SIGKILL is often a sign of a design flaw.
  • SIGHUP (Signal Hang Up): This signal’s name is a relic of the days of serial modems. It was originally sent to a process when its controlling terminal was disconnected (the line was “hung up”). Today, it is commonly used as a mechanism to signal a daemon process to reload its configuration file without shutting down. An embedded web server, for instance, might reload its settings upon receiving SIGHUP. The default action is to terminate the process.
  • SIGCHLD (Signal Child): This signal is sent to a parent process whenever one of its child processes terminates, stops, or resumes after being stopped. The default action is to ignore it. However, this signal is critically important for preventing zombie processes. A zombie is a process that has completed execution but still has an entry in the process table. This entry is needed so the parent process can read its child’s exit status. If the parent never reads this status (by calling wait() or a related system call), the zombie will persist, consuming a process ID and a small amount of kernel memory. While a few zombies are harmless, a large number can exhaust the system’s PIDs, preventing new processes from being created. A robust parent process must therefore catch SIGCHLD and use its handler to call wait(), thereby “reaping” the child and allowing the kernel to remove it from the process table.
sequenceDiagram
    actor Parent
    participant Child
    participant Kernel

    Parent->>Kernel: fork()
    activate Kernel
    Kernel-->>Parent: returns child_pid
    deactivate Kernel
    Kernel-->>Child: returns 0
    activate Child

    Parent->>Parent: Continues other work...

    Child->>Child: Executes its task...
    Child->>Kernel: exit()
    activate Kernel
    Note right of Child: Child is now terminated<br>but is a Zombie Process.
    Kernel->>Parent: Sends SIGCHLD signal
    deactivate Child
    
    alt SIGCHLD is Handled (Correct Way)
        Parent->>Parent: SIGCHLD handler is invoked
        activate Parent
        Parent->>Kernel: waitpid(child_pid, ...)
        Kernel-->>Parent: Returns child's exit status
        deactivate Kernel
        Note right of Kernel: Kernel cleans up zombie<br>process from process table.
        deactivate Parent
    else SIGCHLD is Ignored (Wrong Way)
        Parent->>Parent: Ignores SIGCHLD
        Note over Parent,Kernel: Child remains a Zombie process,<br>consuming a PID until Parent exits.
    end
  • SIGUSR1 and SIGUSR2 (User-defined Signals): These signals are left without a predefined meaning, allowing developers to use them for custom purposes within an application or between cooperating processes. For example, one could use SIGUSR1 to toggle a debugging mode on or off in a running application, or SIGUSR2 to trigger an immediate data dump to a log file. Their default action is to terminate the process, so they must be caught to be useful.

Common Signals and Default Actions

Signal Name Typical Trigger Default Action Common Use Case
SIGINT Interrupt User presses Ctrl+C in terminal Terminate process Catch for graceful shutdown from command line.
SIGTERM Terminate kill command, init systems (systemd) Terminate process Standard signal for services to perform cleanup and exit.
SIGKILL Kill kill -9 command Terminate process (Cannot be caught or ignored) Force-kill an unresponsive or malicious process.
SIGHUP Hang Up Controlling terminal closes, or explicitly sent Terminate process Signal a daemon to reload its configuration file without restarting.
SIGCHLD Child Status Changed A child process terminates, stops, or resumes Ignore Essential for parent to catch to reap child processes and prevent zombies.
SIGUSR1 / SIGUSR2 User-defined Explicitly sent by another process via kill() Terminate process Custom application-specific actions (e.g., toggle debug mode, dump state).
SIGSEGV Segmentation Fault Process attempts invalid memory access Terminate & Core Dump Indicates a critical bug (e.g., null pointer dereference) for debugging.

The Challenge of Signal Handlers: Reentrancy

When a signal handler is invoked, the main program’s execution is paused at an arbitrary point. The handler code begins to execute, using the same process memory and resources. This creates a dangerous possibility: what if the signal arrived just after the main program had acquired a lock or was in the middle of updating a complex data structure? If the signal handler then tries to acquire the same lock or access the same partially-updated structure, the program could deadlock or suffer from data corruption.

To prevent this, functions called from within a signal handler must be async-signal-safe. This means the function is guaranteed to execute correctly even when called from a signal handler that interrupted another operation. A very limited set of standard library functions are guaranteed to be async-signal-safe. Functions like printf()malloc()free(), and most other standard I/O functions are not safe. They often rely on global data structures or locks that could be in an inconsistent state when the signal is delivered.

Tip: The general best practice for signal handlers is to keep them as short and simple as possible. A common and safe pattern is to have the handler set a global volatile sig_atomic_t flag and then return. The main program loop can then periodically check this flag and perform the necessary actions in a safe context, outside of the handler.

This constraint is fundamental to writing reliable signal-handling code. Ignoring it is a frequent source of subtle, hard-to-diagnose bugs in multithreaded and signal-driven applications.

Practical Examples

Now, let’s translate this theory into practice on the Raspberry Pi 5. These examples will demonstrate how to capture signals and use them to control application behavior. You will need a Raspberry Pi 5 running a standard Raspberry Pi OS (or another Linux distribution) and access to its command line.

Example 1: Graceful Shutdown with SIGINT

This first example shows how to write a simple C program that catches the SIGINT signal (Ctrl+C) and performs a clean shutdown instead of terminating abruptly.

Code Snippet

Create a file named sigint_handler.c and enter the following code. This program simulates a main loop that is doing work, and when interrupted, it prints a message and exits cleanly.

C
// sigint_handler.c
// Demonstrates a simple signal handler for SIGINT on a Raspberry Pi 5.

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

// A global flag to indicate if a signal has been received.
// 'volatile' tells the compiler that this variable can be changed by external means
// (i.e., the signal handler) and prevents certain optimizations.
// 'sig_atomic_t' is an integer type that is guaranteed to be read/written atomically,
// preventing partial updates in the presence of a signal.
volatile sig_atomic_t g_signal_received = 0;

/**
 * @brief Signal handler for SIGINT.
 *
 * This function is called when the process receives a SIGINT signal (e.g., from Ctrl+C).
 * It sets a global flag and prints a message. Note the use of printf() here is
 * technically not async-signal-safe, but is used for simple demonstration.
 * In a real-world application, you would avoid complex I/O in a handler.
 *
 * @param signum The signal number that was caught.
 */
void sigint_handler(int signum) {
    // Set the flag that the main loop will check.
    g_signal_received = 1;
}

int main() {
    printf("Process started with PID: %d\n", getpid());
    printf("Press Ctrl+C to trigger the signal handler and exit gracefully.\n");

    // Set up the signal handler using sigaction for robustness.
    // sigaction is preferred over the older signal() call.
    struct sigaction sa;
    sa.sa_handler = sigint_handler; // Set our custom handler function
    sigemptyset(&sa.sa_mask);       // Do not block any other signals during execution of this handler
    sa.sa_flags = 0;                // No special flags

    // Register the handler for SIGINT
    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("Error: cannot handle SIGINT");
        exit(EXIT_FAILURE);
    }

    // Main application loop.
    // It will continue until the signal handler sets the flag.
    while (!g_signal_received) {
        printf("Doing work...\n");
        sleep(1); // Simulate work being done
    }

    // This part of the code is executed after the loop breaks.
    printf("\nSIGINT received. Shutting down gracefully.\n");
    // Here you would perform cleanup tasks, e.g., closing files, releasing resources.
    printf("Cleanup complete. Exiting.\n");

    return EXIT_SUCCESS;
}

Build and Execution Steps

1. Compile the Code: Open a terminal on your Raspberry Pi 5 and compile the program using GCC.

Bash
gcc -o sigint_handler sigint_handler.c -Wall


The -o sigint_handler flag specifies the output executable name, and -Wall enables all compiler warnings, which is good practice.

2. Run the Program: Execute the compiled program.

Bash
./sigint_handler

3. Expected Output and Interaction: The program will start and print its Process ID (PID). It will then print “Doing work…” every second.

Plaintext
Process started with PID: 12345
Press Ctrl+C to trigger the signal handler and exit gracefully.
Doing work...
Doing work...
Doing work...

4. Trigger the Signal: While the program is running, press Ctrl+C. Instead of immediately terminating, you will see the following output as the signal handler is invoked and the main loop exits:

Plaintext
^C
SIGINT received. Shutting down gracefully.
Cleanup complete. Exiting.


Notice that the printf from within the handler itself might not appear, as the main loop detects the flag change and prints the shutdown messages. The ^C is printed by the terminal to indicate the keypress. The program exits cleanly on its own terms.

Example 2: Preventing Zombie Processes with SIGCHLD

This example demonstrates a more advanced and critical use case in system programming: managing child processes. The parent process will fork a child, and then use a SIGCHLD handler to wait for the child to terminate, preventing it from becoming a zombie.

Code Snippet

Create a file named sigchld_handler.c.

C
// sigchld_handler.c
// Demonstrates handling SIGCHLD to prevent zombie processes.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

/**
 * @brief Signal handler for SIGCHLD.
 *
 * This handler is designed to reap all terminated child processes.
 * It uses a loop with waitpid() and the WNOHANG option to handle multiple
 * children terminating in quick succession, ensuring no zombies are left.
 *
 * @param signum The signal number.
 */
void sigchld_handler(int signum) {
    int saved_errno = errno; // waitpid() might change errno
    // Reap all terminated children.
    // WNOHANG ensures that waitpid returns immediately if no child has exited.
    while (waitpid(-1, NULL, WNOHANG) > 0) {
        // This loop will continue as long as there are terminated children to reap.
    }
    errno = saved_errno;
}

int main() {
    printf("Parent process started with PID: %d\n", getpid());

    // Register the handler for SIGCHLD.
    struct sigaction sa;
    sa.sa_handler = sigchld_handler;
    sigemptyset(&sa.sa_mask);
    // SA_RESTART flag causes certain system calls (like read) to be automatically
    // restarted if they are interrupted by this signal handler.
    sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
    if (sigaction(SIGCHLD, &sa, NULL) == -1) {
        perror("Error: cannot handle SIGCHLD");
        exit(EXIT_FAILURE);
    }

    // Fork a child process.
    pid_t pid = fork();

    if (pid < 0) {
        // Fork failed
        perror("fork failed");
        exit(EXIT_FAILURE);
    }

    if (pid == 0) {
        // --- This is the child process ---
        printf("Child process (PID: %d) is running.\n", getpid());
        // Simulate doing some work
        sleep(2);
        printf("Child process is exiting.\n");
        exit(EXIT_SUCCESS);
    } else {
        // --- This is the parent process ---
        printf("Parent process created child with PID: %d\n", pid);
        printf("Parent is now doing other work. The SIGCHLD handler will reap the child.\n");

        // The parent can now do other things.
        // We'll just sleep and let the signal handler do its job.
        sleep(5);

        printf("Parent process has finished its work and is now exiting.\n");
    }

    return EXIT_SUCCESS;
}

Build, Flash, and Boot Procedures

1. Compile the Code:

Bash
gcc -o sigchld_handler sigchld_handler.c -Wall

2. Run and Monitor: Execute the program. While it is running, open a second terminal to monitor the process states.

In Terminal 1:

Bash
./sigchld_handler

In Terminal 2, quickly run this command repeatedly while the program is active. Replace sigchld_handler with the PID of the child process if you can catch it fast enough. The grep command will show the child process and its state.

Bash
ps -ef | grep sigchld_handler | grep -v grep

Expected Output:

In Terminal 1, you will see:

Plaintext
Parent process started with PID: 23456
Parent process created child with PID: 23457
Parent is now doing other work. The SIGCHLD handler will reap the child.
Child process (PID: 23457) is running.
Child process is exiting.
Parent process has finished its work and is now exiting.

In Terminal 2, if you are fast, you might briefly see the child process listed. After it exits, you might see it for a split second in the Z (zombie) state, indicated by <defunct> in some ps outputs. However, because our SIGCHLD handler is in place, it will be cleaned up almost instantly. If you were to comment out the sigaction call and recompile, the child process would remain as a zombie for the entire 5-second duration of the parent’s sleep. Our handler prevents this.

This example correctly demonstrates the asynchronous nature of process management and the essential role of SIGCHLD in maintaining a clean system state.

Common Mistakes & Troubleshooting

Signal handling is powerful, but it’s also easy to make mistakes that lead to subtle bugs. Here are some common pitfalls and how to avoid them.

Mistake / Issue Symptom(s) Troubleshooting / Solution
Using non-safe functions in a handler
(e.g., malloc, printf)
Random crashes, deadlocks, corrupted data, or hangs that are hard to reproduce. Keep handlers minimal: The handler should only set a volatile sig_atomic_t flag and return. Process the flag in the main application loop.
Forgetting to reap child processes System slowly becomes unresponsive. Running ps shows many processes in ‘Z’ (zombie) state. Eventually, new processes cannot be created. Handle SIGCHLD: Install a signal handler for SIGCHLD. Inside the handler, call waitpid(-1, NULL, WNOHANG) in a loop to clean up all terminated children.
Using the legacy signal() call Application works most of the time but occasionally terminates unexpectedly when a signal arrives. Always use sigaction(): It provides reliable, standardized behavior and avoids race conditions present in the old signal() implementation.
Assuming immediate signal delivery An action triggered by a signal appears delayed or doesn’t happen when expected. Design for latency: Understand that a signal is only delivered when the process is scheduled to run. Do not use standard signals for hard real-time synchronization.

Exercises

These exercises will help you solidify your understanding of signal handling on your Raspberry Pi 5.

  1. Handle SIGHUP for Configuration Reload:
    • Objective: Modify the sigint_handler.c program to catch SIGHUP in addition to SIGINT.
    • Guidance:
      1. Create a new signal handler function, sighup_handler. Inside this handler, simply print a message like “SIGHUP received. Reloading configuration…”.
      2. In main(), register this new handler for the SIGHUP signal using another sigaction call.
      3. Run the program in one terminal. From a second terminal, find its PID using pgrep sigint_handler.
      4. Send the SIGHUP signal using the kill command: kill -HUP <PID>.
    • Verification: The program should print the “Reloading configuration” message and continue running. Pressing Ctrl+C should still shut it down gracefully.
  2. Basic Inter-Process Communication with SIGUSR1:
    • Objective: Write a program where a parent process sends a SIGUSR1 signal to its child to make it perform an action.
    • Guidance:
      1. The program should fork().
      2. The child process should install a signal handler for SIGUSR1. The handler should print a message like “Child received SIGUSR1!”. After setting up the handler, the child should enter an infinite loop (while(1) sleep(1);).
      3. The parent process should sleep for 2 seconds (to give the child time to set up its handler) and then send SIGUSR1 to the child using kill(child_pid, SIGUSR1).
      4. The parent should then wait for a few more seconds before sending SIGKILL to the child to terminate it, and then exit.
    • Verification: You should see the “Child received SIGUSR1!” message printed to the console.
  3. Creating a Self-Terminating Process with SIGALRM:
    • Objective: Write a program that terminates itself after a set amount of time using an alarm.
    • Guidance:
      1. Write a signal handler for SIGALRM that prints “Time’s up! Exiting.” and then calls exit(0).
      2. Register this handler.
      3. In main(), call alarm(5). This system call instructs the kernel to send a SIGALRM signal to the process after 5 seconds.
      4. After the alarm() call, enter an infinite loop that prints “Working…” every second.
    • Verification: The program should print “Working…” five times and then print the “Time’s up!” message before exiting automatically.
  4. Investigating SIGCHLD and Zombie Processes:
    • Objective: Prove that failing to handle SIGCHLD creates a zombie process.
    • Guidance:
      1. Take the sigchld_handler.c code from the example.
      2. Comment out the line if (sigaction(SIGCHLD, &sa, NULL) == -1) { ... }. This disables the signal handler.
      3. Increase the parent’s sleep(5) to sleep(30) to give you more time to observe.
      4. Compile and run the modified program.
      5. While it’s running, use ps aux | grep <program_name> in another terminal.
    • Verification: After the child exits (around the 2-second mark), you should see its entry in the ps output marked with a Z or <defunct> status. This zombie process will persist until the parent process exits after 30 seconds. This demonstrates visually what the SIGCHLD handler prevents.

Summary

  • Signals are Asynchronous Notifications: They are a fundamental Linux mechanism for notifying a process that an event has occurred, interrupting its normal flow of execution.
  • Signal Disposition: A process can handle a signal by performing the default action, ignoring the signal, or catching it with a custom signal handler.
  • sigaction is the Preferred Tool: The sigaction() system call provides a robust and reliable way to set a signal’s disposition, avoiding the race conditions associated with the older signal() call.
  • Key Signals: SIGINT is for user interruption (Ctrl+C), SIGTERM is for graceful termination requests, SIGKILL is for forced termination, and SIGHUP is often used for configuration reloads.
  • SIGCHLD is Critical for Process Management: Handling SIGCHLD and reaping terminated child processes with waitpid() is essential to prevent the accumulation of zombie processes.
  • Signal Handlers Must Be Simple: To avoid race conditions and data corruption, handlers must be written carefully using only async-signal-safe functions. The best practice is to set a flag in the handler and act on it in the main program loop.

Further Reading

  1. The Linux Programming Interface by Michael Kerrisk – Chapters 20-22 provide an exhaustive and authoritative reference on signals.
  2. Advanced Programming in the UNIX Environment by W. Richard Stevens and Stephen A. Rago – Chapter 10 is a classic and highly respected text on signal handling.
  3. signal(7) Linux Manual Page – Provides a comprehensive overview of all standard signals. Access via man 7 signal.
  4. sigaction(2) Linux Manual Page – The definitive documentation for the sigaction system call. Access via man 2 sigaction.
  5. POSIX.1-2017 Standard, Section 3.3, Signals – The official standard defining how signals should behave on compliant systems. Available from the IEEE/The Open Group. https://www.open-std.org/jtc1/sc22/open/n4217.pdf
  6. “Signals” – Beej’s Guide to Unix Interprocess Communication – A very accessible, practical tutorial on signal concepts.

Leave a Comment

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

Scroll to Top