CoreTechED

Loading

Signals in C Language: Complete Guide

Learn C signals: signal handling, raise, kill, signal types, and custom handlers. Master asynchronous event handling in C.

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

SignalDescriptionDefault Action
SIGINTInterrupt (Ctrl+C)Terminate
SIGTERMTermination requestTerminate
SIGKILLForceful terminationTerminate (cannot be caught)
SIGSEGVSegmentation faultTerminate (core dump)
SIGFPEFloating-point exceptionTerminate
SIGABRTAbnormal terminationTerminate (core dump)
SIGCHLDChild process terminatedIgnore
SIGCONTContinue stopped processContinue
SIGTSTPTerminal 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

  1. Default handlers: Signals terminate process unless caught.
  2. Custom handler: signalHandler sets flags and prints message.
  3. raise(): Sends signal to current process.
  4. kill(): Sends signal by PID.
  5. Multiple signals: Register and handle different signals.
  6. 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 (not printf/malloc).
  • volatile sig_atomic_t is 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_t for flags.

Best Practices

  1. Use write() instead of printf in handlers.
  2. Use volatile sig_atomic_t for flags.
  3. Keep handlers minimal (set flags, do simple I/O).
  4. Check signal return value for errors.
  5. Re-register handlers if the system resets them.
  6. Do not handle SIGKILL (impossible).
  7. Use sigaction instead of signal for portability.
  8. Block signals during critical sections using sigprocmask.
  9. Document signal behavior in comments.
  10. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *