Signals in C Language: Complete Guide
Signals in C Language: A Comprehensive Tutorial
A signal is a software-generated interrupt sent to a process by the operating system or another process to notify it about an event. Signals provide a mechanism for inter-process communication and handling asynchronous events. Understanding signals is essential for writing robust programs that respond gracefully to user actions and system events.
This tutorial provides an in-depth exploration of signals in C: signal handling, default handlers, user-defined handlers, raise(), kill(), signal types, common pitfalls, and best practices.
Conceptual Introduction & Fundamentals
What is a Signal?
A signal is a notification delivered to a process by the operating system or another process. Signals interrupt the normal flow of execution to handle events such as:
- User actions (pressing Ctrl+C)
- Hardware exceptions (division by zero)
- Software errors (segmentation fault)
- Process control (termination requests)
Why Signals Matter
- Event notification: Inform processes about important events.
- Asynchronous handling: Handle events at any time during execution.
- Process management: Terminate, stop, or continue processes.
- Error recovery: Handle hardware/software exceptions.
- Inter-process communication: Send messages between processes.
Signal Characteristics
- Each signal has a unique integer identifier (e.g.,
SIGINT= 2). - Signals can be generated by the OS, user, or other processes.
- Default actions: terminate, ignore, stop, or continue.
- Custom handlers can replace default behavior (except
SIGKILL).
Common Signal Types
| Signal | Description | Default Action |
|---|---|---|
SIGINT | Interrupt (Ctrl+C) | Terminate |
SIGTERM | Termination request | Terminate |
SIGKILL | Forceful termination | Terminate (cannot be caught) |
SIGSEGV | Segmentation fault | Terminate (core dump) |
SIGFPE | Floating-point exception | Terminate |
SIGABRT | Abnormal termination | Terminate (core dump) |
SIGCHLD | Child process terminated | Ignore |
SIGCONT | Continue stopped process | Continue |
SIGTSTP | Terminal stop (Ctrl+Z) | Stop |
Syntax & Core Mechanics
Default Signal Handling
Without custom handlers, signals use their default actions.
c
#include <stdio.h> int main(void) { while (1) { printf("Hello World\n"); } return 0; } Output (infinite loop, terminated by Ctrl+C):
text
Hello World Hello World Hello World ... ^C
Pressing Ctrl+C sends SIGINT, which terminates the process.
User-Defined Signal Handlers
Use signal() to register custom handlers.
Syntax:
c
void (*signal(int sig, void (*handler)(int)))(int);
Example:
c
#include <stdio.h> #include <signal.h> #include <stdlib.h> #include <unistd.h> void signalHandler(int sig) { const char msg[] = "Caught SIGINT\n"; write(STDOUT_FILENO, msg, sizeof(msg) - 1); _exit(sig); } int main(void) { signal(SIGINT, signalHandler); while (1) { printf("Hello World!\n"); sleep(1); } return 0; } Output (Ctrl+C pressed):
text
Hello World! Hello World! Hello World! ^CCaught SIGINT
Note: Use write() (not printf) in signal handlers for safety.
Registering Multiple Signal Handlers
c
#include <stdio.h> #include <signal.h> #include <stdlib.h> #include <unistd.h> void handleSignal(int sig) { switch (sig) { case SIGINT: write(STDOUT_FILENO, "Caught SIGINT\n", 14); break; case SIGTERM: write(STDOUT_FILENO, "Caught SIGTERM\n", 15); break; case SIGUSR1: write(STDOUT_FILENO, "Caught SIGUSR1\n", 15); break; } } int main(void) { signal(SIGINT, handleSignal); signal(SIGTERM, handleSignal); signal(SIGUSR1, handleSignal); printf("PID: %d\n", getpid()); printf("Press Ctrl+C or send signals.\n"); while (1) { sleep(1); } return 0; } raise() — Send Signal to Self
Sends a signal to the current process.
Syntax:
c
int raise(int sig);
Example:
c
#include <stdio.h> #include <stdlib.h> #include <signal.h> void signalHandler(int sig) { printf("Interrupt handled: %d\n", sig); exit(sig); } int main(void) { signal(SIGINT, signalHandler); printf("Raising SIGINT...\n"); raise(SIGINT); /* This line won't be reached */ printf("After raise\n"); return 0; } Expected output:
text
Raising SIGINT... Interrupt handled: 2
kill() — Send Signal to Process
Sends a signal to another process using its PID.
Syntax:
c
int kill(pid_t pid, int sig);
Example:
c
#include <stdio.h> #include <stdlib.h> #include <signal.h> #include <unistd.h> void handleSignal(int sig) { printf("Received signal: %d\n", sig); } int main(void) { signal(SIGINT, handleSignal); pid_t pid = getpid(); printf("Sending SIGINT to PID %d\n", pid); kill(pid, SIGINT); return 0; } Expected output:
text
Sending SIGINT to PID 12345 Received signal: 2
Full Practical Code Example with Explanation
This program demonstrates signals in a realistic scenario: a robust daemon-like process with signal handling.
Program: Signal Handling Demo
c
/** * signal_demo.c * Demonstrates signal handling with custom handlers, raise, and kill. * Includes graceful shutdown and multiple signal types. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <unistd.h> #include <errno.h> /* Global flags for signal handling */ volatile sig_atomic_t signalReceived = 0; volatile sig_atomic_t signalNumber = 0; /* Signal handler function */ void signalHandler(int sig) { signalReceived = 1; signalNumber = sig; /* Use write() for safety in signal handlers */ const char *msg = "Signal received!\n"; write(STDOUT_FILENO, msg, strlen(msg)); } /* Function prototypes */ void demonstrateDefaultSignals(void); void demonstrateCustomHandler(void); void demonstrateRaise(void); void demonstrateKill(void); void demonstrateMultipleSignals(void); const char *getSignalName(int sig); int main(void) { int choice; char buffer[100]; printf("=== Signal Handling Demo ===\n"); printf("Process PID: %d\n", getpid()); while (1) { printf("\n--- Menu ---\n"); printf("1. Demonstrate Default Signals\n"); printf("2. Demonstrate Custom Handler\n"); printf("3. Demonstrate raise()\n"); printf("4. Demonstrate kill()\n"); printf("5. Demonstrate Multiple Signals\n"); printf("6. Exit\n"); printf("Enter choice: "); fflush(stdout); if (fgets(buffer, sizeof(buffer), stdin) == NULL) { fprintf(stderr, "Error reading choice.\n"); return EXIT_FAILURE; } choice = atoi(buffer); switch (choice) { case 1: demonstrateDefaultSignals(); break; case 2: demonstrateCustomHandler(); break; case 3: demonstrateRaise(); break; case 4: demonstrateKill(); break; case 5: demonstrateMultipleSignals(); break; case 6: printf("Exiting.\n"); return EXIT_SUCCESS; default: printf("Invalid choice. Please try again.\n"); break; } } return EXIT_SUCCESS; } /* Demonstrates default signal behavior */ void demonstrateDefaultSignals(void) { printf("\n=== Default Signals ===\n"); printf("This program would terminate on Ctrl+C.\n"); printf("But we won't wait for that.\n"); } /* Demonstrates custom signal handler */ void demonstrateCustomHandler(void) { printf("\n=== Custom Signal Handler ===\n"); /* Register handler for SIGINT */ signal(SIGINT, signalHandler); printf("SIGINT handler registered. Press Ctrl+C to test.\n"); printf("Waiting for signal...\n"); /* Wait for signal */ int count = 0; while (signalReceived == 0 && count < 10) { sleep(1); printf("Waiting... (%d/10)\n", ++count); } if (signalReceived) { printf("Signal '%s' was received.\n", getSignalName(signalNumber)); } else { printf("No signal received (timeout).\n"); } /* Reset */ signalReceived = 0; signal(SIGINT, SIG_DFL); } /* Demonstrates raise() */ void demonstrateRaise(void) { printf("\n=== raise() ===\n"); signal(SIGUSR1, signalHandler); printf("Raising SIGUSR1...\n"); if (raise(SIGUSR1) != 0) { perror("raise"); } if (signalReceived) { printf("Signal '%s' raised successfully.\n", getSignalName(signalNumber)); } signalReceived = 0; signal(SIGUSR1, SIG_DFL); } /* Demonstrates kill() */ void demonstrateKill(void) { printf("\n=== kill() ===\n"); signal(SIGUSR2, signalHandler); pid_t pid = getpid(); printf("Sending SIGUSR2 to PID %d...\n", pid); if (kill(pid, SIGUSR2) != 0) { perror("kill"); } if (signalReceived) { printf("Signal '%s' sent successfully.\n", getSignalName(signalNumber)); } signalReceived = 0; signal(SIGUSR2, SIG_DFL); } /* Demonstrates multiple signal handlers */ void demonstrateMultipleSignals(void) { printf("\n=== Multiple Signals ===\n"); /* Register multiple handlers */ signal(SIGINT, signalHandler); signal(SIGTERM, signalHandler); signal(SIGUSR1, signalHandler); printf("Registered handlers for SIGINT, SIGTERM, SIGUSR1.\n"); printf("Sending all three signals...\n"); /* Send signals using raise */ raise(SIGINT); signalReceived = 0; raise(SIGTERM); signalReceived = 0; raise(SIGUSR1); signalReceived = 0; printf("All signals handled.\n"); /* Reset handlers */ signal(SIGINT, SIG_DFL); signal(SIGTERM, SIG_DFL); signal(SIGUSR1, SIG_DFL); } /* Returns string representation of signal */ const char *getSignalName(int sig) { switch (sig) { case SIGINT: return "SIGINT"; case SIGTERM: return "SIGTERM"; case SIGUSR1: return "SIGUSR1"; case SIGUSR2: return "SIGUSR2"; case SIGSEGV: return "SIGSEGV"; case SIGFPE: return "SIGFPE"; default: return "UNKNOWN"; } } Explanation of Signal Usage
- Default handlers: Signals terminate process unless caught.
- Custom handler:
signalHandlersets flags and prints message. raise(): Sends signal to current process.kill(): Sends signal by PID.- Multiple signals: Register and handle different signals.
volatile sig_atomic_t: Safe flag type for signal handlers.
Expected Output (sample run)
text
=== Signal Handling Demo === Process PID: 12345 --- Menu --- 1. Demonstrate Default Signals 2. Demonstrate Custom Handler 3. Demonstrate raise() 4. Demonstrate kill() 5. Demonstrate Multiple Signals 6. Exit Enter choice: 2 === Custom Signal Handler === SIGINT handler registered. Press Ctrl+C to test. Waiting for signal... Waiting... (1/10) Waiting... (2/10) ^CSignal received! Signal 'SIGINT' was received. --- Menu --- ... Enter choice: 3 === raise() === Raising SIGUSR1... Signal received! Signal 'SIGUSR1' raised successfully. --- Menu --- ... Enter choice: 5 === Multiple Signals === Registered handlers for SIGINT, SIGTERM, SIGUSR1. Sending all three signals... Signal received! Signal received! Signal received! All signals handled.
Common Pitfalls, Edge Cases & Memory Considerations
1. Using printf in Signal Handlers
printf is not async-signal-safe; using it in handlers can cause corruption.
c
void handler(int sig) { printf("Signal %d\n", sig); // BUG: not safe } Solution: Use write() or set a flag and handle later.
2. Not Using volatile sig_atomic_t
Shared variables between handler and main code must be volatile sig_atomic_t.
c
int flag = 0; // BUG: not volatile void handler(int sig) { flag = 1; } Solution: Use volatile sig_atomic_t flag = 0;
3. Resetting Signal Handler
Some systems reset the handler after invocation; re-register inside handler.
c
void handler(int sig) { signal(sig, handler); // Re-register /* Handle signal */ } Solution: Re-register handler or use sigaction for persistent handlers.
4. Catching SIGKILL
SIGKILL cannot be caught, blocked, or ignored.
c
signal(SIGKILL, handler); // No effect
Solution: Do not attempt to handle SIGKILL.
5. Not Checking signal Return
signal returns SIG_ERR on failure.
c
signal(SIGINT, handler); // No check
Solution: Check if (signal(SIGINT, handler) == SIG_ERR).
6. Using raise Without Handler
If no handler is registered, raise uses default action (terminate).
c
raise(SIGINT); // Terminates if no handler
Solution: Register handler before raising signals.
7. kill with Invalid PID
Sending signals to non-existent processes fails.
c
kill(999999, SIGTERM); // May fail
Solution: Check kill return value and handle errors.
8. Race Conditions
Signal handlers can interrupt at any point, causing race conditions.
Solution: Use sig_atomic_t flags and minimize handler complexity.
9. Blocking Signals
Use sigprocmask to block signals temporarily.
c
sigset_t set; sigemptyset(&set); sigaddset(&set, SIGINT); sigprocmask(SIG_BLOCK, &set, NULL); /* Critical section */ sigprocmask(SIG_UNBLOCK, &set, NULL);
10. Memory Considerations
- Signal handlers should allocate minimal resources.
- Use
write()for output (notprintf/malloc). volatile sig_atomic_tis typically an integer.- Avoid global state mutation in handlers where possible.
Summary & Best Practices
Signals provide a mechanism for asynchronous event notification in C. Proper handling ensures programs respond gracefully to user actions and system events.
Key Takeaways
- Signals are software interrupts with integer identifiers.
- Default actions: terminate, ignore, stop, continue.
signal()registers custom handlers.raise()sends signals to self.kill()sends signals to other processes.- Use
write()in handlers for safety. - Use
volatile sig_atomic_tfor flags.
Best Practices
- Use
write()instead ofprintfin handlers. - Use
volatile sig_atomic_tfor flags. - Keep handlers minimal (set flags, do simple I/O).
- Check
signalreturn value for errors. - Re-register handlers if the system resets them.
- Do not handle
SIGKILL(impossible). - Use
sigactioninstead ofsignalfor portability. - Block signals during critical sections using
sigprocmask. - Document signal behavior in comments.
- Test with different signals and edge cases.
By mastering signals, you’ll write robust C programs that handle asynchronous events and communicate effectively with the operating system.



