Signals
Introduction to interrupts and signals
An interrupt is an event that alters the normal execution flow of a program and can be generated by hardware devices or even by the CPU itself. When an interrupt occurs the current flow of execution is suspended and the interrupt handler runs. After the interrupt handler runs the previous execution flow is resumed. There are three types of events that can cause the CPU to interrupt: hardware interrupts, software interrupts, and exceptions.
Signals are nothing but software interrupts that notifies a process that an event has occurred. These events might be requests from users or indications that a system problem (such as a memory access error) has occurred. Every signal has a signal number and a default action defined. A process can react to them in any of the following ways:
- a default (OS-provided) way
- catch the signal and handle them in a program-defined way
- ignore the signal entirely
Signal Groups
Signals fall into two broad categories. The first set constitutes the traditional or standard signals, which are used by the kernel to notify processes of events. On Linux, the standard signals are numbered from 1 to 31. The other set of signals consists of the realtime signals. Linux supports both POSIX reliable signals (hereinafter "standard signals") and POSIX real-time signals.
Realtime Signals
Realtime signals were defined in POSIX.1b to remedy a number of limitations of standard signals. They have the following advantages over standard signals:
- Realtime signals provide an increased range of signals that can be used for application-defined purposes. Only two standard signals are freely available for application-defined purposes: SIGUSR1 and SIGUSR2.
- Realtime signals are queued. If multiple instances of a realtime signal are sent to a process, then the signal is delivered multiple times. By contrast, if we send further instances of a standard signal that is already pending for a process, that signal is delivered only once.
- When sending a realtime signal, it is possible to specify data (an integer or pointer value) that accompanies the signal. The signal handler in the receiving process can retrieve this data.
- The order of delivery of different realtime signals is guaranteed. If multiple different realtime signals are pending, then the lowest-numbered signal is delivered first. In other words, signals are prioritized, with lower-numbered signals having higher priority. When multiple signals of the same type are queued, they are delivered—along with their accompanying data—in the order in which they were sent.
Standard Signals
The standard signals are the classical signals that have been there since the early days of Unix. Further here, we will be discussing about standard signals.
Signal Overview
A signal is said to be generated by some event. Once generated, a signal is later delivered to a process, which then takes some action in response to the signal. Between the time it is generated and the time it is delivered, a signal is said to be pending.
Normally, a pending signal is delivered to a process as soon as it is next scheduled to run, or immediately if the process is already running (e.g., if the process sent a signal to itself). Sometimes, however, we need to ensure that a segment of code is not interrupted by the delivery of a signal. To do this, we can add a signal to the process’s signal mask - a set of signals whose delivery is currently blocked. If a signal is generated while it is blocked, it remains pending until it is later unblocked (removed from the signal mask). Various system calls allow a process to add and remove signals from its signal mask.
Upon delivery of a signal, a process carries out one of the following default actions, depending on the signal:
- The signal is ignored; that is, it is discarded by the kernel and has no effect on the process. (The process never even knows that it occurred.)
- The process is terminated (killed). This is sometimes referred to as abnormal process termination, as opposed to the normal process termination that occurs when a process terminates using exit().
- A core dump file is generated, and the process is terminated. A core dump file contains an image of the virtual memory of the process, which can be loaded into a debugger in order to inspect the state of the process at the time that it terminated.
- The process is stopped—execution of the process is suspended.
- Execution of the process is resumed after previously being stopped.
Instead of accepting the default for a particular signal, a program can change the action that occurs when the signal is delivered. This is known as setting the disposition of the signal. To read more about disposition, refer here. A program can set one of the following dispositions for a signal:
- The default action should occur. This is useful to undo an earlier change of the disposition of the signal to something other than its default.
- The signal is ignored. This is useful for a signal whose default action would be to terminate the process.
- A signal handler is executed.
A signal handler is a function, written by the programmer, that performs appropriate tasks in response to the delivery of a signal. For example, the shell has a handler for the SIGINT signal (generated by the interrupt character, Control-C) that causes it to stop what it is currently doing and return control to the main input loop, so that the user is once more presented with the shell prompt. Notifying the kernel that a handler function should be invoked is usually referred to as installing or establishing a signal handler. When a signal handler is invoked in response to the delivery of a signal, we say that the signal has been handled or, synonymously, caught.
Note that it isn’t possible to set the disposition of a signal to terminate or dump core (unless one of these is the default disposition of the signal). The nearest we can get to this is to install a handler for the signal that then calls either exit() or abort(). The abort() function generates a SIGABRT signal for the process, which causes it to dump core and terminate.
Types of signals
To list available signals in a Linux system, you can use the command kill -l
.
The table below lists the signals 1 to 20. To get a full list of signals, you can refer here.
Signal name | Signal number | Default Action | Meaning |
---|---|---|---|
SIGHUP | 1 | Terminate | Hangup detected on controlling terminal or death of controlling process |
SIGINT | 2 | Terminate | Interrupt from keyboard |
SIGQUIT | 3 | Core dump | Quit from keyboard |
SIGILL | 4 | Core dump | Illegal instruction |
SIGTRAP | 5 | Core dump | Trace/breakpoint trap for debugging |
SIGABRT , SIGIOT | 6 | Core dump | Abnormal termination |
SIGBUS | 7 | Core dump | Bus error |
SIGFPE | 8 | Core dump | Floating point exception |
SIGKILL | 9 | Terminate | Kill signal(cannot be caught or ignored) |
SIGUSR1 | 10 | Terminate | User-defined signal 1 |
SIGSEGV | 11 | Core dump | Invalid memory reference |
SIGUSR2 | 12 | Terminate | User-defined signal 2 |
SIGPIPE | 13 | Terminate | Broken pipe;write pipe with no readers |
SIGALRM | 14 | Terminate | Timer signal from alarm |
SIGTERM | 15 | Terminate | Process termination |
SIGSTKFLT | 16 | Terminate | Stack fault on math co-processor |
SIGCHLD | 17 | Ignore | Child stopped or terminated |
SIGCONT | 18 | Continue | Continue if stopped |
SIGSTOP | 19 | Stop | Stop process (can not be caught or ignore) |
SIGTSTP | 20 | Stop | Stop types at tty |
Sending signals to process
There are three different ways to send signals to processes:
- Sending signal to process using kill
Kill command can be used to send signals to process. By default a SIGTERM signal is sent but a different type of signal can be sent to the process by defining the signal number(or signal name).
For example, the command kill -9 367
sends SIGKILL to the process with PID 367
- Sending signal to process via keyboard
Signals can be sent to a running process by pressing some specific keys. For example, holding Ctrl+C sends SIGINT to the process which terminates it.
- Sending signal to process via another process
A process can send a signal to another process via the kill() system call. In this use, signals can be employed as a synchronization technique, or even as a
primitive form of interprocess communication (IPC). It is also possible for a process to send a signal to itself.
int kill(pid_t pid, int sig)
system call takes 2 arguments, pid of the process you wish to send the signal to and the signal number of the desired signal.
Handling signals
Referring to the table of signals in the previous section, you can see that there are default handlers attached to all signals when the program is started. When we invoke signal to attach our own handler, we are over-riding the default behaviour of the program in response to that signal. Specifically, if we attach a handler to SIGINT, the program will no longer terminate when you press
Let’s take an example of handling SIGINT signal and terminating a program. We will use Python’s signal library to achieve this.
When we press Ctrl+C, SIGINT signal is sent. From the signals table, we see that the default action for SIGINT is to terminate the process. To illustrate how the process reacts to the default action and a signal handler, let us consider the below example.
Default Action of SIGINT:
Let us first run the below lines in a python environment:
while 1:
continue
Now let us press "Ctrl+C".
On pressing "Ctrl+C" , a SIGINT interrupt is sent to the process and the default action for SIGINT as per the table we saw in the previous section is to terminate the process. We see that the while loop is terminated and we get the below on our console:
^CTraceback (most recent call last):
File "<stdin>", line 2, in <module>
KeyboardInterrupt
The process terminated(default action) since it received a SIGINT(Keyboard Interrupt) when we pressed Ctrl+C.
Signal Handler for SIGINT:
Let us run the below lines of code in the Python environment.
import signal
import sys
#Start of signal_handler function
def signal_handler(signal, frame):
print ('You pressed Ctrl+C!')
# End of signal_handler function
signal.signal(signal.SIGINT, signal_handler)
This is an example of a program that defines its own signal handler for SIGINT , overriding the default action.
Now let us run the while and continue statement as we did previously.
while 1:
continue
Do we see any changes when Ctrl+C is pressed? Does the program terminate? We see the below output:
^CYou pressed Ctrl+C!
Everytime we press Ctrl+C, we just the see the above message and the program does not terminate. Inorder to terminate the program, you can press Ctrl+Z which sends the SIGSTOP signal whose default action is to stop the process.
In the case of the signal handler, we define a function signal_handler() which prints “You pressed Ctrl+C!” and does not terminate the program. The handler is called with two arguments, the signal number and the current stack frame (None or a frame object). signal.signal() allows defining custom handlers to be executed when a signal is received. Its two arguments are the signal number(name) you want to trap and the name of the signal handler.
Role of signals in system calls with the example of wait()
The wait() system call waits for one of the children of the calling process to terminate and returns the termination status of that child in the buffer pointed to by statusPtr.
- If the parent process calls the wait() system call, then the execution of the parent is suspended until the child is terminated.
- At the termination of the child, a SIGCHLD signal is generated which is delivered to the parent by the kernel. SIGCHLD signal indicates to the parent that there is some information on the child that needs to be collected.
- Parent, on receipt of SIGCHLD , reaps the status of the child from the process table. Even though the child is terminated, there is an entry in the process table corresponding to the child where the process entry and PID is stored.
- When the parent collects the status, this entry is deleted. Thus, all the traces of the child process are removed from the system.
Zombie and Orphane States
If the parent decides not to wait for the child’s termination and it executes its subsequent task, or fails to read the exit status of the child, there remains an entry in the process table even after the termination of the child. This state of the child process is known as the Zombie state. In order to avoid long-lasting zombies, we need to have code that calls wait() after the child process is created. It is generally good to create a signal handler for the SIGCHLD signal, calling one of the wait-family functions in a loop, until no uncollected child data remains.
A child process becomes orphaned, if its parent process terminates before the child .The orphaned child is adopted by init/systemd, the ancestor of all processes, whose process ID is 1. Further calls to fetch the parent pid of this process returns 1.