Catch Ctrl-C in C

Learn catch ctrl-c in c with practical examples, diagrams, and best practices. Covers c, signals development techniques with visual explanations.

Graceful Termination: Catching Ctrl-C in C Programs

Hero image for Catch Ctrl-C in C

Learn how to intercept and handle the Ctrl-C (SIGINT) signal in C programs for clean shutdowns, resource cleanup, and robust application behavior.

In C programming, pressing Ctrl-C in the terminal typically terminates a running program abruptly. While this is often the desired behavior, there are many scenarios where an application needs to perform cleanup operations, save data, or release resources before exiting. This article will guide you through the process of catching and handling the SIGINT signal, which is generated by Ctrl-C, to ensure your C programs terminate gracefully.

Understanding Signals and SIGINT

Signals are a limited form of inter-process communication (IPC) used in Unix-like operating systems to notify a process of an event. When you press Ctrl-C, the terminal sends a specific signal called SIGINT (Signal Interrupt) to the foreground process. By default, the action associated with SIGINT is to terminate the process. However, C provides mechanisms to change this default behavior and execute custom code when SIGINT is received.

flowchart TD
    A[User presses Ctrl-C] --> B{Terminal sends SIGINT}
    B --> C{Process receives SIGINT}
    C --> D{Is custom handler registered?}
    D -- Yes --> E[Execute signal handler function]
    D -- No --> F[Default action: Terminate process]
    E --> G[Perform cleanup (e.g., close files, free memory)]
    G --> H[Exit program gracefully]

Flowchart of SIGINT signal handling in a C program.

Implementing a Signal Handler with signal()

The simplest way to catch SIGINT is by using the signal() function from the <signal.h> header. This function allows you to associate a custom function (the signal handler) with a specific signal. When the signal is received, your handler function will be executed instead of the default action.

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

volatile sig_atomic_t keep_running = 1;

void intHandler(int dummy)
{
    printf("\nCtrl-C detected! Performing cleanup...\n");
    keep_running = 0;
}

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

    printf("Program running. Press Ctrl-C to interrupt.\n");

    while (keep_running) {
        // Simulate some work
        printf("Working...\n");
        sleep(1);
    }

    printf("Cleanup complete. Exiting gracefully.\n");
    return 0;
}

Advanced Signal Handling with sigaction()

While signal() is straightforward, it has some limitations and portability issues. For more robust and portable signal handling, especially in complex applications, sigaction() is preferred. sigaction() provides more control over signal behavior, such as blocking other signals during handler execution, restarting interrupted system calls, and specifying flags.

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

volatile sig_atomic_t keep_running_sa = 1;

void sigint_handler_sa(int signum)
{
    printf("\nSIGINT received via sigaction! Initiating shutdown...\n");
    keep_running_sa = 0;
}

int main()
{
    struct sigaction sa;

    // Initialize sigaction struct
    sa.sa_handler = sigint_handler_sa; // Set our handler function
    sigemptyset(&sa.sa_mask);         // Clear all signals from sa_mask
    sa.sa_flags = 0;                  // No special flags

    // Register the signal handler for SIGINT
    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("Error setting up signal handler");
        return 1;
    }

    printf("Program running with sigaction. Press Ctrl-C to interrupt.\n");

    while (keep_running_sa) {
        printf("Working with sigaction...\n");
        sleep(1);
    }

    printf("Cleanup complete via sigaction. Exiting gracefully.\n");
    return 0;
}

Best Practices for Signal Handling

Proper signal handling is critical for robust applications. Consider these best practices:

  1. Keep Handlers Simple: Signal handlers should do as little as possible. Typically, they should just set a flag (like keep_running) and return. The main loop of your program can then check this flag and initiate the cleanup process.
  2. Use sigaction(): For new code and robust applications, sigaction() is generally preferred over signal() due to its better-defined semantics and control.
  3. Async-Signal-Safety: Be extremely careful about what functions you call inside a signal handler. Only use async-signal-safe functions.
  4. Re-establishing Handlers: Some older signal() implementations might reset the signal handler to its default after it's invoked. sigaction() with SA_RESETHAND flag can replicate this, but by default, sigaction() handlers remain installed.
  5. Blocking Signals: Use sigprocmask() to temporarily block signals if you have critical sections of code that must not be interrupted.

1. Compile the C code

Save the code (e.g., catch_ctrl_c.c) and compile it using a C compiler like GCC:

gcc catch_ctrl_c.c -o catch_ctrl_c

2. Run the executable

Execute the compiled program from your terminal:

./catch_ctrl_c

3. Test Ctrl-C interruption

While the program is running and printing "Working...", press Ctrl-C. Observe that the program prints your custom message and then exits gracefully, rather than terminating immediately.