Signal Concepts and Generation
Signals serve as a mechanism for the operating system to notify processes of asynchronous events. In a Linux environment, signals allow the OS or users to interrupt a running process to handle specific tasks, such as termination, suspension, or custom logic. Each signal has a unique name starting with 'SIG' (e.g., SIGINT, SIGKILL) and a corresponding integer value. The range 1-31 typically represents standard signals with distinct purposes.
Signals can be generated through various inputs. A common example is the terminal interrupt key combination Ctrl+C, which sends signal number 2 (SIGINT) to the foreground process group. Similarly, Ctrl+\ sends signal 3 (SIGQUIT). Processes can also send signals programmatically using system calls.
Handling Signals
When a signal is delivered, a process can respond in one of three ways:
- Default Action: The kernel performs the preset behavior (e.g., terminating the process).
- Ignore: The signal is discarded, and no action is taken.
- Catch: The process executes a user-defined function upon receiving the signal.
The signal function allows a process to register a custom handler. The following example demonstrates intercepting SIGINT to prevent immediate termination:
#include <iostream>
#include <csignal>
#include
void signalHandler(int signum) {
std::cout << "Received signal: " << signum << std::endl;
std::cout << "Performing cleanup..." << std::endl;
_exit(0);
}
int main() {
// Register handler for SIGINT (Ctrl+C)
signal(SIGINT, signalHandler);
while (true) {
std::cout << "Process running with PID: " << getpid() << std::endl;
sleep(1);
}
return 0;
}
Besides keyboard inputs, signals can be generated via the kill command or API. The kill system call sends a signal to a specific process ID. Additionally, raise sends a signal to the current process, and abort sends SIGABRT to force termination.
Hardware and Software Exceptions
Hardware anomalies often trigger signals. For instance, dividing an integer by zero causes the CPU to raise a hardware exception. The OS kernel catches this and sends SIGFPE (signal 8) to the offending process. Similarly, invalid memory access results in SIGSEGV (signal 11).
#include <iostream>
#include <csignal>
#include <cstdlib>
void floatingPointHandler(int signum) {
std::cout << "Arithmetic exception detected (Signal " << signum << ")" << std::endl;
exit(1);
}
int main() {
signal(SIGFPE, floatingPointHandler);
int value = 10;
int result = value / 0; // Triggers SIGFPE
return 0;
}
Software conditions also generate signals. For example, writing to a pipe with no reader triggers SIGPIPE (signal 13). The alarm function generates SIGALRM (signal 14) after a specified number of seconds.
Signal Management and Blocking
Signal States
The kernel maintains three distinct data structures for signal handling within a process's Process Control Block (PCB):
- Pending Set: A bitmap indicating signals that have been sent but not yet delivered.
- Block Set: A bitmap of signals the process has requested to block temporarily.
- Handler Table: An array of function pointers defining the action for each signal.
If a signal is blocked, it remains in the pending set until unblocked. A signal cannot be delivered if it is blocked, but it is not lost.
Signal Set Operations
Linux provides the sigset_t data type and a suite of functions to manipulate signal sets. Functions like sigemptyset, sigfillset, and sigaddset allow modification of these sets. The sigprocmask function is used to read or change the current block set.
#include <iostream>
#include <csignal>
#include
void showPendingSignals() {
sigset_t pendingSet;
sigpending(&pendingSet);
std::cout << "Pending signals: ";
for (int i = 1; i <= 31; ++i) {
if (sigismember(&pendingSet, i)) {
std::cout << "1";
} else {
std::cout << "0";
}
}
std::cout << std::endl;
}
int main() {
sigset_t blockSet, originalSet;
// Initialize and add SIGINT(2) to the block set
sigemptyset(&blockSet);
sigaddset(&blockSet, SIGINT);
// Block SIGINT
sigprocmask(SIG_BLOCK, &blockSet, &originalSet);
std::cout << "SIGINT blocked for 5 seconds. Try Ctrl+C." << std::endl;
int count = 0;
while (count < 5) {
showPendingSignals();
sleep(1);
count++;
}
// Restore original set (unblock)
sigprocmask(SIG_SETMASK, &originalSet, nullptr);
std::cout << "SIGINT unblocked." << std::endl;
return 0;
}
Advanced Handling with sigaction
The sigaction function provides more robust control than signal. It allows specifying a handler and managing additional behavior, such as blocking specific signals during the handler's execution. The struct sigaction contains the handler, a mask of signals to block during execution, and flags.
#include <iostream>
#include <csignal>
#include <cstring>
#include
void customHandler(int signum) {
std::cout << "Handled signal " << signum << " via sigaction." << std::endl;
}
int main() {
struct sigaction act, oldAct;
memset(&act, 0, sizeof(act));
act.sa_handler = customHandler;
sigemptyset(&act.sa_mask);
// Block SIGQUIT while handling SIGINT
sigaddset(&act.sa_mask, SIGQUIT);
act.sa_flags = 0;
sigaction(SIGINT, &act, &oldAct);
std::cout << "Process waiting for signals." << std::endl;
while(true) {
pause();
}
return 0;
}
When using sigaction, the kernel automatically clears the pending bit for the signal being handled before invoking the handler. If a signal is blocked using sa_mask, multiple instances of that signal arriving during the handler execution are queued in the pending set as a single occurrence for standard signals.