Skip to Content
Course content

122: Signal Handling with signal.h

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When you first look at signal.h, it’s easy to think of a signal handler as just another callback function. You might assume that because it's written in C, you can do whatever you want inside that handler—print a "Goodbye" message, allocate some memory to log the event, or maybe update a complex data structure. I've seen many developers start exactly this way, and it's a recipe for bugs that are nearly impossible to debug because they only happen once every thousand runs.

The Myth: "I Can Use printf() and malloc() Inside a Handler"

Let's look at a snippet that looks perfectly reasonable on the surface. Imagine you're writing a long-running process and you want to print a status update when the user hits Ctrl+C (SIGINT).

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

void handle_sigint(int sig) {
    // This looks fine, right?
    printf("\nCaught signal %d. Cleaning up resources...\n", sig);
}

int main() {
    signal(SIGINT, handle_sigint);
    while(1) {
        printf("Working hard... ");
        fflush(stdout);
        sleep(1);
    }
    return 0;
}

Here is why this is dangerous: printf is not async-signal-safe. Under the hood, printf uses a global lock to ensure that multiple threads don't scramble the output. If your program is interrupted by SIGINT exactly while the main loop is inside a printf call, the lock is already held. When your handler then calls printf, it tries to acquire that same lock. Result? Your program deadlocks and hangs forever. You won't get a crash or an error message; it'll just stop responding.

The Reality: Async-Signal Safety and Atomic Flags

The rule is strict: you should only call functions that are explicitly documented as async-signal-safe inside a handler. This list is surprisingly short. It includes things like write() (the raw system call), _exit(), and a few others. But it does not include malloc, free, or any of the stdio.h functions.

The professional way to handle signals isn't to perform the logic inside the handler, but to use the handler to set a flag that the main loop periodically checks. However, you can't just use a regular int for that flag. The compiler might optimize the variable by keeping it in a register, and the main loop might never notice that the signal handler changed the value in memory.

This is where volatile sig_atomic_t comes in. The volatile keyword tells the compiler, "This value can change outside the normal flow of the program, so don't optimize it." The sig_atomic_t type ensures that reading and writing the variable happens in a single, uninterruptible operation.

Moving Beyond signal() to sigaction()

While signal() is simple, it's actually deprecated in many professional environments because its behavior varies across different Unix versions. Some systems reset the handler to the default after the first signal is caught; others don't. To be portable and robust, I always use sigaction().

Here is how I would actually implement that Ctrl+C logic to ensure it's safe and predictable:

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

// Global flag: volatile ensures it's read from memory, 
// sig_atomic_t ensures the operation is atomic.
volatile sig_atomic_t keep_running = 1;

void handle_sigint(int sig) {
    keep_running = 0; // Safe: just updating a simple flag
}

int main() {
    struct sigaction sa;
    sa.sa_handler = handle_sigint;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("Error setting up sigaction");
        return 1;
    }

    while (keep_running) {
        printf("Processing data... (Press Ctrl+C to stop)\n");
        sleep(1);
    }

    printf("\nClean shutdown initiated. Now I can safely use printf!\n");
    return 0;
}

Notice the shift in architecture. The handler does the absolute bare minimum—flipping a switch. The actual "work" of shutting down happens in the main execution flow where it's safe to call any function you like.




📋 Practical Task

Exercise: Implementing a Graceful Log-File Closer

You are building a background logger that writes timestamps to a file every second. If the program is killed via SIGTERM (the standard termination signal), it must close the file descriptor properly and write a final "Log Session Ended" entry before exiting. If it is killed via SIGINT (Ctrl+C), it should do the same.

Requirements:

  • Create a program that opens a file named session.log for writing.
  • Use sigaction to handle both SIGINT and SIGTERM.
  • The handlers must not call fprintf or fclose directly. Instead, they must trigger a volatile sig_atomic_t flag.
  • The main loop should check this flag. When triggered, the program must write "Log Session Ended" to the file, close the file, and exit gracefully.
  • Ensure the program writes a "Heartbeat: [timestamp]" entry to the file every second while running.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.