C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
122: Signal Handling with signal.h
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.logfor writing. - Use
sigactionto handle bothSIGINTandSIGTERM. - The handlers must not call
fprintforfclosedirectly. Instead, they must trigger avolatile sig_atomic_tflag. - 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.
There are no comments for now.