1. Overview

In Linux systems, processes can receive a variety of signals, such as SIGINT or SIGKILL. Each signal is sent in different situations and each has different behavior.

In this article, we’ll talk about SIGINT, SIGTERM, SIGQUIT, and SIGKILL. We’ll also see the difference between them.

2. Introduction to Signals

The signals are a method of communication between processes. When a process receives a signal, the process interrupts its execution and a signal handler is executed.

How the program behaves usually depends on the type of signal received. After handling the signal, the process may or may not continue its normal execution.

The Linux kernel can send signals, for instance, when a process attempts to divide by zero it receives the SIGFPE signal.

We can also send signals using the kill program. Let’s run a simple script in the background and stop it:

$ (sleep 30; echo "Ready!") &
[1] 26929
$ kill -SIGSTOP 26929
[1]+  Stopped                 ( sleep 30; echo "Ready!" )

Now, we can resume it using SIGCONT:

$ kill -SIGCONT 26929
Ready!
[1]+  Done                    ( sleep 30; echo "Ready!" )

Alternatively, we can send signals in a terminal using key combinations. For instance, Ctrl+C sends SIGINT, Ctrl+S sends SIGSTOP, and Ctrl+Q sends SIGCONT.

Each signal has a default action, but a process can override the default action and handle it differently, or ignore it. However, some signals can’t be ignored nor handled differently and the default action is always executed.

We can handle signals in bash using the trap command. For instance, we can add trap date SIGINT in a script and it will print the date when SIGINT is received.

3. SIGINT

SIGINT is the signal sent when we press Ctrl+C. The default action is to terminate the process. However, some programs override this action and handle it differently.

One common example is the bash interpreter. When we press Ctrl+C it doesn’t quit, instead, it prints a new and empty prompt line. Another example is when we use gdb to debug a program. We can send SIGINT with Ctrl+C to stop the execution and return it to the gdb‘s interpreter.

We can think of SIGINT as an interruption request sent by the user. How it is handled usually depends on the process and the situation.

Let’s write handle_sigint.sh using the trap command to handle SIGINT and print the current date:

#!/bin/bash

trap date SIGINT

read input
echo User input: $input
echo Exiting now

We use read input to wait for the user interaction. Now, let’s run our script and let’s press Ctrl+C:

$ ./handle_sigint.sh 
^CSat Apr 10 15:32:07 -03 2021

We can see the script didn’t exist. We can now terminate the script by writing some input:

$ ./handle_sigint.sh 
^CSat Apr 10 15:32:07 -03 2021
live long and prosper
User input: live long and prosper
Exiting now

If we want to use a signal to terminate it, we can’t use SIGINT with this script. We should use SIGTERM, SIGQUIT, or SIGKILL instead.

4. SIGTERM and SIGQUIT

The SIGTERM and SIGQUIT signals are meant to terminate the process. In this case, we are specifically requesting to finish it. SIGTERM is the default signal when we use the kill command.

The default action of both signals is to terminate the process. However, SIGQUIT also generates a core dump before exiting.

When we send SIGTERM, the process sometimes executes a clean-up routine before exiting.

We can also handle SIGTERM to ask for confirmation before exiting. Let’s write a script called handle_sigterm.sh to terminate only If the user sends the signal twice:

#!/bin/bash

SIGTERM_REQUESTED=0
handle_sigterm() {
    if [ $SIGTERM_REQUESTED -eq 0 ]; then
        echo "Send SIGTERM again to terminate"
        SIGTERM_REQUESTED=1
    else
        echo "SIGTERM received, exiting now"
        exit 0
    fi
}

trap handle_sigterm SIGTERM

TIMEOUT=$(date +%s)
TIMEOUT=$(($TIMEOUT + 60))

echo "This script will exit in 60 seconds"
while [ $(date +%s) -lt $TIMEOUT ]; do
    sleep 1;
done
echo Timeout reached, exiting now

Now, let’s run it on background executing $ ./handle_sigterm.sh &. Then, we run $ kill twice:

$ ./handle_sigterm.sh &
[1] 6092
$ kill 6092
Send SIGTERM again to terminate
$ kill 6092
SIGTERM received, exiting now
[1]+  Done                    ./handle_sigterm.sh

As we can see, the script exited after it received the second SIGTERM.

5. SIGKILL

When a process receives SIGKILL it is terminated. This is a special signal as it can’t be ignored and we can’t change its behavior.

We use this signal to forcefully terminate the process. We should be careful as the process won’t be able to execute any clean-up routine.

One common way of using SIGKILL is to first send SIGTERM. We give the process some time to terminate, we may also send SIGTERM a couple of times. If the process doesn’t finish on its own, then we send SIGKILL to terminate it.

Let’s rewrite the previous example to try to handle SIGKILL and ask for confirmation:

#!/bin/bash

SIGKILL_REQUESTED=0
handle_sigkill() {
    if [ $SIGKILL_REQUESTED -eq 0 ]; then
        echo "Send SIGKILL again to terminate"
        SIGKILL_REQUESTED=1
    else
        echo "Exiting now"
        exit 0
    fi
}

trap handle_sigkill SIGKILL

read input
echo User input: $input

Now, let’s run it on a terminal, and let’s send SIGKILL only once with $ kill -SIGKILL :

$ ./handle_sigkill.sh
Killed
$

We can see it terminate right away without asking to re-send the signal.

6. How SIGINT Relates to SIGTERM, SIGQUIT and SIGKILL

Now that we understand more about signals, we can see how they relate to each other.

The default action for SIGINT, SIGTERM, SIGQUIT, and SIGKILL is to terminate the process. However, SIGTERM, SIGQUIT, and SIGKILL are defined as signals to terminate the process, but SIGINT is defined as an interruption requested by the user.

In particular, if we send SIGINT (or press Ctrl+C) depending on the process and the situation it can behave differently. So, we shouldn’t depend solely on SIGINT to finish a process.

As SIGINT is intended as a signal sent by the user, usually the processes communicate with each other using other signals. For instance, a parent process usually sends SIGTERM to its children to terminate them, even if SIGINT has the same effect.

In the case of SIGQUIT, it generates a core dump which is useful for debugging.

Now that we have this in mind, we can see we should choose SIGTERM on top of SIGKILL to terminate a process. SIGTERM is the preferred way as the process has the chance to terminate gracefully.

As a process can override the default action for SIGINT, SIGTERM, and SIGQUIT, it can be the case that neither of them finishes the process. Also, if the process is hung it may not respond to any of those signals. In that case, we have SIGKILL as the last resort to terminate the process.

7. Understanding Pending Signals in Linux

7.1. Signal Types

So far, we’ve discussed all the standard Linux signals used chiefly for interprocess communication. Linux also has real-time signals, those with SIGRTMIN (34) to SIGRTMAX, that offer more advanced features like queuing and ordered delivery, making them suitable for time-sensitive applications.

A signal becomes pending when it is generated but cannot be delivered to the process immediately. This usually happens if the target process is uninterruptable. Standard signals are represented in processes as a bitmap. It follows that they are either set or not, regardless of how often the signal has been sent without being handled by the process.

Real-time signals, on the other hand, are queued. If multiple instances of a real-time signal are sent to a process, they are queued in the order of arrival. This queuing system allows for accumulating multiple instances of the same signal, ensuring none are lost and handled promptly.

7.2. Queue Management and Limits

The Linux kernel limits the number of signals queued for a process. The RLIMIT_SIGPENDING resource limit defines this limit. This limit is per process; the value is inherited when creating child processes.

If the queue for real-time signals is complete and additional signals are sent, these extra signals are discarded. Any further real-time signals sent to the process will be lost when the queue reaches its maximum capacity. This is why we should implement proper signal handling in programs that deal with real-time signals.

We can query the limit using the getrlimit() and change it using the setrlimit() system calls. Managing this limit is crucial in applications where real-time signals are heavily used, as it directly impacts how many signals can be pending at any given time.

We can transiently change the limit in a running bash process using:

$ ulimit -i 1024

This will change the limit for the current bash process and any other process we start in this shell after changing the limit.

8. Conclusion

In this article, we learned about signals and the difference between SIGINT, SIGTERM, SIGQUIT, and SIGKILL. Also, we briefly learned how to handle signals in bash.

We saw how SIGINT sometimes doesn’t kill the process as it may a different meaning. On the other hand, the SIGKILL signal will always terminate the process.

We also learned that SIGQUIT generates a core dump by default and that SIGTERM is the preferred way to kill a process.

Finally, we took a quick look at real-time signals and how they are handled differently from the standard signals.