Catch Ctrl-C in C
Categories:
Graceful Termination: Catching Ctrl-C in C Programs

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;
}
volatile sig_atomic_t
type for keep_running
is crucial. volatile
tells the compiler that the variable's value can change unexpectedly (e.g., by a signal handler), preventing optimizations that might lead to incorrect behavior. sig_atomic_t
ensures that reads and writes to the variable are atomic, meaning they are completed in a single, uninterruptible operation, which is important in signal handlers.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;
}
write()
, _exit()
, signal()
, and sigaction()
itself. Avoid complex operations like malloc()
, printf()
(though often used for debugging, it's technically not async-signal-safe), or file I/O that isn't explicitly safe.Best Practices for Signal Handling
Proper signal handling is critical for robust applications. Consider these best practices:
- 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. - Use
sigaction()
: For new code and robust applications,sigaction()
is generally preferred oversignal()
due to its better-defined semantics and control. - Async-Signal-Safety: Be extremely careful about what functions you call inside a signal handler. Only use async-signal-safe functions.
- Re-establishing Handlers: Some older
signal()
implementations might reset the signal handler to its default after it's invoked.sigaction()
withSA_RESETHAND
flag can replicate this, but by default,sigaction()
handlers remain installed. - 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.