Introduction to Linux Process Signals
Signals in Daily Life
In our daily lives, signals are everywhere, such as:
- Traffic lights
- School bells
- Starting pistols
...
Because we have received education (we can recognize signals), when these signals are generated, we immediately think of the corresponding action. However, we might not process them immediately because we could be doing something more important, until an appropriate time to handle them arrives. For example: when the alarm rings, you are supposed to get up and study, but the bed is too comfortable to leave. Therefore, between the generation of the signal and its processing, the signal must be remembered (saved).
Understanding Process Signals
- Similarly, a process must also have the ability to recognize and handle signals. Programmers have built a set of instructions into the operating system; an instruction represents a special action, and these instructions are signals (process signals).
- When a process receives a specific signal, it may not process it immediately, but rather wait until an appropriate time.
- A process must save the signal from the time it is generated until it is processed, and handle it at an appropriate time.
In Linux, you can view the list of signals in the current system using the following command:
kill -l
There are a total of 62 process signals in the system. Signals 1-31 are standard signals (the primary focus); signals 34-64 are real-time signals (not covered here).
- Standard Signals: Scheduled fairly based on time slices, suitable for personal computers. (They do not need to be processed immediately upon recognition.)
- Real-time Signals: High responsiveness, suitable for platforms with fewer tasks that require fast processing, such as car infotainment systems or rocket launch consoles. (They must be processed immediately upon recognition.)
Note: In Linux, a signal is essentially just a number. Names like SIGKILL and SIGINT are actually macros.
The 31 Standard Signals
| Signal Number | Signal Name | Description |
|---|---|---|
1 |
SIGHUP |
If a terminal interface detects a disconnection, the default action is to terminate the process. |
2 |
SIGINT |
Stands for Interrupt. This signal is typically sent by the user via the keyboard (Ctrl + c), and the default action is to interrupt the current process. |
3 |
SIGQUIT |
When the user presses the key combination (usually Ctrl + \), this signal not only terminates the foreground proces group but also generates a core file. |
4 |
SIGILL |
Indicates that the process has executed an illegal instruction. The default action is to terminate the process and generate a core file. |
5 |
SIGTRAP |
Generated by a breakpoint instruction or other trap instructions. The default action is to terminate the process and generate a core file. |
6 |
SIGABRT |
Generated by calling the abort function. The process terminates abnormally, and a core file is generated. |
7 |
SIGBUS |
Often generated when certain types of memory faults occur. The default action is to terminate the process and generate a core file. |
8 |
SIGFPE |
Indicates an arithmetic operation exception, such as division by zero or floating-point overflow. The default action is to terminate the process and generate a core file. |
9 |
SIGKILL |
This signal cannot be caught or ignored. It provides a reliable method for system administrators to kill any process. |
10 |
SIGUSR1 |
A user-defined signal. Programmers can define and use this signal in their programs. The default action is to terminate the process. |
11 |
SIGSEGV |
Indicates that the process made an invalid memory access (e.g., accessing an uninitialized pointer). The default action is to terminate the process and generate a core file. |
12 |
SIGUSR2 |
Another user-defined signal, similar to SIGUSR1. The default action is to terminate the process. |
13 |
SIGPIPE |
Received if a write operation is performed on a pipe whose read process has terminated. The default action is to terminate the process. |
14 |
SIGALRM |
Generated when a timer set by the alarm function expires, or when the interval set by the setitimer function expires. |
15 |
SIGTERM |
This signal is caught by applications, giving the program a chance to perform cleanup before exiting. Unlike SIGKILL, it can be caught or ignored, and is typically used to indicate a normal program exit. |
16 |
SIGSTKFLT |
Indicates a stack fault on the coprocessor (unused). The default action is to terminate the process. |
17 |
SIGCHLD |
Sent to a parent process when a child process terminates or stops. By default, this signal is ignored. If the parent process wishes to be notified of such state changes in its child, it should catch this signal. The signal handler typically calls a wait function to obtain the child process ID and its termination status. |
18 |
SIGCONT |
Can be sent to make a stopped process continue running. |
19 |
SIGSTOP |
A job control signal used to stop a process, similar to the interactive stop signal (SIGTSTP), but it cannot be caught or ignored. |
20 |
SIGTSTP |
Interactive stop signal. Generated by the terminal driver and sent to every process in the foreground process group when the user presses the key combination (usually Ctrl + Z). |
21 |
SIGTTIN |
Generated by the terminal driver and sent to a background process when it attempts to read from the terminal console. The default action is to pause the process. |
22 |
SIGTTOU |
Generated by the terminal driver and sent to a background process when it attempts to output data to the terminal console. The default action is to pause the process. |
23 |
SIGURG |
Sent to the currently running process when urgent data arrives on a socket, reporting that urgent data has arrived. The default action is to ignore. |
24 |
SIGXCPU |
Generated and sent to the process by the system when the process execution time exceeds the allocated CPU time. The default action is to terminate the process and generate a core file. |
25 |
SIGXFSZ |
Received if a process writes to a file exceeding the maximum file size limit. The default action is to terminate the process and generate a core file. |
26 |
SIGVTALRM |
Generated when a virtual timer expires. Similar to SIGALRM, but it only counts the CPU time used by the process in user mode. The default action is to terminate the process. |
27 |
SIGPROF |
Similar to SIGVTALRM, but it includes both the CPU time used by the process and the time spent executing system calls. The default action is to terminate the process. |
28 |
SIGWINCH |
Sent by the kernel to the foreground process group when the terminal window size changes. The default action is to ignore. |
29 |
SIGIO |
Indicates an asynchronous I/O event. The default action is to terminate the process. |
30 |
SIGPWR |
Power failure. The default action is to terminate the process. |
31 |
SIGSYS |
Indicates an invalid system call. The default action is to terminate the process and generate a core file. |
Phenomena When a Process Receives a Signal
Foreground and Background Processes
- Foreground Process: By default, processes run in the foreground, sending output directly to the terminal. Typically, the process currently interacting with the user is the foreground process. Users can terminate a foreground process by pressing
Ctrl + cin the terminal or by using thekillcommand to send signal 9 (SIGKILL).
kill -9 <pid>
- Background Process: When running in the background, a process does not occupy the terminal and will not block user input/output. Users can place a process in the background using the following command. However, to kill it, you must use the
killcommand (as shown above).
./myprocess &
Notes:
- In Linux, a terminal is paired with a command-line interpreter (
bash), and a terminal only allows one foreground process, but multiple background processes. - By default,
bashis the foreground process. When a foreground process starts,bashbecomes a background process. - Keyboard input is received by the foreground process first.
Ctrl + cdoes not terminate thebashprocess becausebashhandles it specially.
The Ctrl + c Hotkey
Consider an infinite loop program like this:
#include <iostream>
int main() {
for (;;) {
std::cout << "Process executing an infinite loop..." << std::endl;
}
return 0;
}
When this program runs, it will rapidly print to the screen. As commonly taught, pressing Ctrl + c will terminate the program. Why does this happen?
When the infinite loop program runs, it becomes the foreground process, and bash becomes the background process. Since keyboard input is received by the foreground process first, Ctrl + c is essentially interpreted by the foreground process as receiving a specific signal.
Actually, this triggers Signal 2: SIGINT.
There are three ways to handle a process signal:
- System default action
- Ignore the signal (no action)
- User-defined handling (signal catching)
By default, the action for Ctrl + c is to terminate the process. Can we customize the signal handling action? Yes, we can.
The signal system call is used to set a custom signal handler. Its prototype is:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
Explanation:
signum: The number of the signal to be handled. Passing the signal name is also acceptable because the name is a macro for the number.handler: A function pointer (function address).
void terminationHandler(int receivedSignal) {
// Custom handling logic
}
Clearly, the signal function acts as a callback. When a specific signal is caught, it invokes the corresponding function to execute the action. The signum is passed as the argument to the handler function.
Notes:
- The
signalfunction can be set anywhere, but it is generally recommended to set it at the beginning of the program. - Once set via
signal, it remains valid for the entire lifecycle of the process. - The
signalfunction itself is not triggered by its declaration; it is triggered during subsequent code execution (the handler is only called when the process receivessignum). - Not all signals can be custom-handled. For example,
SIGKILLandSIGSTOPcannot be caught or ignored. - If the
handlerfunction body is empty, the signal handling might fall back to the system default depending on implementation, but typically an empty handler means the signal is ignored upon receipt.
For instance, we can modify the handling of Ctrl + c. The custom action will be: print the received signal number and then exit the process.
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
void terminationHandler(int receivedSignal) {
std::cout << "Received signal: " << receivedSignal << std::endl;
std::exit(EXIT_FAILURE);
}
int main() {
// Register the custom signal handler at the beginning
std::signal(SIGINT, terminationHandler);
for (;;) {
std::cout << "Process is running in a continuous loop..." << std::endl;
sleep(1);
}
return 0;
}
Hardware Interrupts
How does keyboard data (Ctrl + c) get input to the operating system? How does the OS know when there is data from the keyboard?
- At the data level, the CPU does not fetch data directly from peripherals; it fetches from memory (because memory access is much faster).
- At the control level, the CPU can read the status of peripherals by communicating with external device controllers. These controllers manage interaction with peripherals, including data transmission and status monitoring.
- When the keyboard has data, the OS is notified first through a hardware interrupt mechanism. Input devices communicate with the CPU and OS via interrupts. When a peripheral has data, the OS uses an interrupt to notify the CPU of an important event, which includes a hardware interrupt number (an identifier used by the CPU to distinguish interrupt types, stored in CPU registers).
- Upon receiving an interrupt request, the CPU pauses its current task and looks up the corresponding entry in the Interrupt Vector Table (essentially an array) based on the interrupt number. The Interrupt Vector Table is a data structure stored in memory and maintained by the OS. Its entries are addresses of methods that call hardware device drivers, facilitating hardware interaction. For example, a hard disk interrupt service routine might call a hard disk driver function to
readorwritedata. - The interrupt handler calls the appropriate hardware method to process the input data and stores it in the memory's input buffer. Finally, it is transferred to the corresponding upper-layer application via file descriptors. For example, a terminal program reads user commands and operations from the OS via file descriptors.
Therefore, the process signals we learn about are essentially simulated using hardware interrupts.
How does Ctrl + c become a signal?
- In the OS, keyboard input can consist of regular characters (like letters) and special control characters (like
Ctrl + c). The former can be echoed to the screen, while the latter are not echoed. - The OS first determines the type of character input. Regular characters are copied directly from the keyboard file to the kernel buffer; if it's a special control character, the OS interprets it as a specific signal and sends it to the foreground process.
Concept of Signals
Signal generation and the execution of your own code are asynchronous. Signals belong to the category of software interrupts.
- Asynchronous: The program does not need to execute in a strict sequential order. For example, a teacher sends a student to the office to fetch a microphone and continues teaching without waiting for the student to return.
- Synchronous: The program executes in a predetermined sequence, where each step must wait for the previous one to complete before executing. For example, a teacher sends a student to fetch a microphone and waits for them to return before starting the lesson.
- In other words, we do not know exactly when a signal will be generated, but we just continue running our code, waiting for the signal to arrive at some future moment.
Note: Signals are always sent to a process by the Operating System (the manager).