Signals are a powerful tool in Bash scripting. They allow you to send a signal to a process, or to all processes on a system, and have them take specific actions. This can be useful for handling situations where you need to interrupt a process or stop it from doing something harmful. To use signals in your scripts, first you need to know what signals are available. There are nine different signals in the Linux kernel: SIGHUP, SIGINT, SIGQUIT, SIGILL, SIGTRAP, SIGABRT, SIGFPE, and SIGKILL. Each one has a different purpose and can be used for different tasks. Here are some examples of how you might use signals in your scripts:
- To stop a process gracefully: if you want to stop an application without causing any damage or leaving it in an unusable state, you can use the kill command with the signal name as the argument. For example: kill -9 <process_name> will kill the process with pid <process_name> using the signal 9 (SIGKILL).
- To interrupt a process: if you need to get someone’s attention quickly but don’t want them to lose their work completely, you can use the alarm command with the signal name as the argument. For example: alarm 10 will cause an alarm clock application running on your system to ring once every 10 minutes (SIGALRM).
- To stop all processes on a system: if you want to clean up after yourself and prevent any other processes from running while you’re working on something important, you can use the killall command with the signal name as one of its arguments. For example: killall -9 will cause all processes on your system to terminate (SIGKILL). ..
Signals and Processes
Signals are short, fast, one-way messages sent to processes such as scripts, programs, and daemons. They let the process know about something that has happened. The user may have hit Ctrl+C, or the application may have tried to write to memory it doesn’t have access to.
If the author of the process has anticipated that a certain signal might be sent to it, they can write a routine into the program or script to handle that signal. Such a routine is called a signal handler. It catches or traps the signal, and performs some action in response to it.
Linux uses a lot of signals, as we shall see, but from a scripting point of view, there’s only a small subset of signals that you’re likely to be interested in. In particular, in non-trivial scripts, signals that tell the script to shut down should be trapped (where possible) and a graceful shutdown performed.
For example, scripts that create temporary files or open firewall ports can be given the chance to delete the temporary files or to close the ports before they shut down. If the script just dies the instant it receives the signal, your computer can be left in an unpredictable state.
Here’s how you can handle signals in your own scripts.
Meet the Signals
Some Linux commands have cryptic names. Not so the command that traps signals. It’s called trap. We can also use trap with the -l (list) option to show us the entire list of signals that Linux uses.
Although our numbered list finishes at 64, there are actually 62 signals. Signals 32 and 33 are missing. They’re not implemented in Linux. They’ve been replaced by functionality in the gcc compiler for handling real-time threads. Everything from signal 34, SIGRTMIN, to signal 64, SIGRTMAX, are real-time signals.
You’ll see different lists on different Unix-like operating systems. On OpenIndiana for example, signals 32 and 33 are present, along with a bunch of extra signals taking the total count to 73.
Signals can be referenced by name, number, or by their shortened name. Their shortened name is simply their name with the leading “SIG” removed.
Signals are raised for many different reasons. If you can decipher them, their purpose is contained in their name. The impact of a signal falls into one of a few categories:
Terminate: The process is terminated. Ignore: The signal does not affect the process. This is an information-only signal. Core: A dump-core file is created. This is usually done because the process has transgressed in some way, such as a memory violation. Stop: The process is stopped. That is, it is paused, not terminated. Continue: Tells a stopped process to continue execution.
These are the signals you’ll encounter most frequently.
SIGHUP: Signal 1. The connection to a remote host—such as an SSH server—has unexpectedly dropped or the user has logged out. A script receiving this signal might terminate gracefully, or may choose to attempt to reconnect to the remote host. SIGINT: Signal 2. The user has pressed the Ctrl+C combination to force a process to close, or the kill command has been used with signal 2. Technically, this is an interrupt signal, not a termination signal, but an interrupted script without a signal handler will usually terminate. SIGQUIT: Signal 3. The user has pressed the Ctrl+D combination to force a process to quit, or the kill command has been used with signal 3. SIGFPE: Signal 8. The process tried to perform an illegal (impossible) mathematical operation, such as division by zero. SIGKILL: Signal 9. This is the signal equivalent of a guillotine. You can’t catch it or ignore it, and it happens instantly. The process is terminated immediately. SIGTERM: Signal 15. This is the more considerate version of SIGKILL. SIGTERM also tells a process to terminate, but it can be trapped and the process can run its clean-up processes before closing down. This allows a graceful shutdown. This is the default signal raised by the kill command.
Signals on the Command Line
One way to trap a signal is to use trap with the number or name of the signal, and a response that you want to happen if the signal is received. We can demonstrate this in a terminal window.
This command traps the SIGINT signal. The response is to print a line of text to the terminal window. We’re using the -e (enable escapes) option with echo so we can use the “\n” format specifier.
Our line of text is printed each time we hit the Ctrl+C combination.
To see if a trap is set on a signal, use the -p (print trap) option.
Using trap with no options does the same thing.
To reset the signal to its untrapped, normal state, use a hyphen “-” and the name of the trapped signal.
No output from the trap -p command indicates there is no trap set on that signal.
Trapping Signals in Scripts
We can use the same general format trap command inside a script. This script traps three different signals, SIGINT, SIGQUIT, and SIGTERM.
The three trap statements are at the top of the script. Note that we’ve included the exit command inside the response to each of the signals. This means the script reacts to the signal and then exits.
Copy the text into your editor and save it in a file called “simple-loop.sh”, and make it executable using the chmod command. You’ll need to do that to all of the scripts in this article if you want to follow along on your own computer. Just use the name of the appropriate script in each case.
The rest of the script is very simple. We need to know the process ID of the script, so we have the script echo that to us. The $$ variable holds the process ID of the script.
We create a variable called counter and set it to zero.
The while loop will run forever unless it is forcibly stopped. It increments the counter variable, echoes it to the screen, and sleeps for a second.
Let’s run the script and send different signals to it.
When we hit “Ctrl+C” our message is printed to the terminal window and the script is terminated.
Let’s run it again and send the SIGQUIT signal using the kill command. We’ll need to do that from another terminal window. You’ll need to use the process ID that was reported by your own script.
As expected the script reports the signal arriving then terminates. And finally, to prove the point, we’ll do it again with the SIGTERM signal.
We’ve verified we can trap multiple signals in a script, and react to each one independently. The step that promotes all of this from interesting to useful is adding signal handlers.
Handling Signals in Scripts
We can replace the response string with the name of a function in your script. The trap command then calls that function when the signal is detected.
Copy this text into an editor and save it as a file called “grace.sh”, and make it executable with chmod.
The script sets a trap for three different signals— SIGHUP, SIGINT, and SIGTERM—using a single trap statement. The response is the name of the graceful_shutdown() function. The function is called whenever one of the three trapped signals is received.
The script creates a temporary file in the “/tmp” directory, using mktemp. The filename template is “tmp.XXXXXXXXXX”, so the name of the file will be “tmp.” followed by ten random alphanumeric characters. The name of the file is echoed on the screen.
The rest of the script is the same as the previous one, with a counter variable and an infinite while loop.
When the file is sent a signal that causes it to close, the graceful_shutdown() function is called. This deletes our single temporary file. In a real-world situation, it could perform whatever clean-up your script requires.
Also, we bundled all of our trapped signals together and handled them with a single function. You can trap signals individually and send them to their own dedicated handler functions.
Copy this text and save it in a file called “triple.sh”, and make it executable using the chmod command.
We define three traps at the top of the script.
One traps SIGINT and has a handler called sigint_handler(). The second traps a signal called SIGUSR1 and uses a handler called sigusr1_handler() . Trap number three traps the EXIT signal. This signal is raised by the script itself when it closes. Setting a signal handler for EXIT means you can set a function that’ll always be called when the script terminates (unless it is killed with signal SIGKILL). Our handler is called exit_handler() .
SIGUSR1 and SIGUSR2 are signals provided so that you can send custom signals to your scripts. How you interpret and react to them is entirely up to you.
Leaving the signal handlers aside for now, the body of the script should be familiar to you. It echoes the process ID to the terminal window and creates some variables. Variable sigusr1_count records the number of times SIGUSR1 was handled, and sigint_count records the number of times SIGINT was handled. The loop_flag variable is set to zero.
The while loop is not an infinite loop. It will stop looping if the loop_flag variable is set to any non-zero value. Each spin of the while loop uses kill to send the SIGUSR1 signal to this script, by sending it to the process ID of the script. Scripts can send signals to themselves!
The sigusr1_handler() function increments the sigusr1_count variable and sends a message to the terminal window.
Each time the SIGINT signal is received, the siguint_handler() function increments the sigint_count variable and echoes its value to the terminal window.
If the sigint_count variable equals three, the loop_flag variable is set to one and a message is sent to the terminal window letting the user know the shutdown process has started.
Because loop_flag is no longer equal to zero, the while loop terminates and the script is finished. But that action automatically raises the EXIT signal and the exit_handler() function is called.
After three Ctrl+C presses, the script terminates and automatically invokes the exit_handler() function.
Read the Signals
By trapping signals and dealing with them in straightforward handler functions, you can make your Bash scripts tidy up behind themselves even if they’re unexpectedly terminated. That gives you a cleaner filesystem. It also prevents instability the next time you run the script, and—depending on what the purpose of your script is—it could even prevent security holes.
RELATED: How to Audit Your Linux System’s Security with Lynis