Monday, August 26

Linux Fu: It’s a Trap!

It is easy to think that a Linux shell like Bash is just a way to enter commands at a terminal. But, in fact, it is also a powerful programming language as we’ve seen from projects ranging from web servers to simple utilities to make dangerous commands safer. Like most programming languages, though, there are multiple layers of complexity. You can spend a little time and get by or you can invest more time and learn about the language and, hopefully, write more robust programs.

Signals

If you are running a Linux program, even a shell script, it is subject to receiving a signal under certain conditions. For example, SIGINT, or signal 2, is what happens when you press Control+C. There are plenty of other signals, though. A very few signals, like signal 9 which is the SIGKILL will terminate your program no questions asked and you can’t stop it. But most of the other signals can be caught. You can either ignore them or take some action.

Some signals come from the system. Here’s a list of common signals and their number.

1- SIGHUP (Hangup)

2 – SIGINT (Interrupt; Control+C)

3 – SIGQUIT (Quit)

4 – SIGILL (Illegal instruction)

9 – SIGKILL (Kill)

If you want to see a long list, try trap - l from the command prompt. My system lists 64 different signal names.

You can use the kill or killall command to send signals to processes:

kill -1 4234
killall -9 emacs
kill -SIGHUP 3152

In addition to the standard signals, Bash has a few special ones, too. Here’s a list, but you should check out the Bash manual under trap to get the details:

  • EXIT – When shell exits
  • ERR – When an error occurs (see the Bash manual for specifics)
  • RETURN – When a shell function or sourced script finishes
  • DEBUG – Before each command executes

What Happens?

Most of the time, when your program or script gets a signal, it will stop. There are a few exceptions and it depends on other things. For example, using nohup will protect your program from SIGHUP.

In a shell script, you can use the trap command to “catch” a signal or a list of signals. You have three options:

  1. Provide no action which sets the signal to the default handler
  2. Provide an empty action (e.g., ”)  which sets the program to ignore the signal
  3. Provide a bit of code to run if the signal occurs

For example, to ignore SIGQUIT and SIGHUP, you could write:

trap "" SIGQUIT SIGHUP

Or if you aren’t in the mood to ignore, you could write:

trap "echo Bye; exit" SIGINT

To return to the default, use:

trap SIGINT

Simple, right? Try this:

#!/bin/bash
trap "echo ; echo Bye ; echo ; exit" SIGINT
while true
do
   sleep 1
done

Run that and then press Control+C.

Easy, But…

That’s simple enough, but there is a slight inconvenience. If you trap more than one signal with the same code, you have no simple way to figure out which signal caused the trap. It would be nice if you could have a trap function that serviced a bunch of different traps that could understand which signal occurred using a case or if statement, for example.

This isn’t built into Bash, but you can do it with a little work. In fact, I wrote trappist to do just that for you. Here’s how it works: You include the trappist.sh file in your script and then write a function called trappist_trap. It will take a single argument that tells you what signal fired. If you don’t provide one, a dumb default will be there that you can override later.

You can call trappist_init in several ways. If you don’t provide any arguments, then all signals you can catch will direct to your trap function. If you like, you can pass an @ as the first argument, followed by a list of signal names with a + or – in front of them. Like this:

trappist_init  @ +SIGINT -SIGQUIT -SIGHUP

The order of the signals doesn’t matter. This command line catches all signals, but uses the default handler for SIGINT and ignores SIGQUIT and SIGHUP. You can also omit the @ sign if you like.

Another way to call the init function is with an equal sign:

trappist_init = SIGINT SIGQUIT +SIGHUP -SIGUSR1

In this case, only SIGINT and SIGQUIT will go to the trap function. SIGHUP will get the default handler and SIGUSR1 will be ignored.

A typical trap function might look like this:

function trappist_trap()
{
case $1 in
SIGALRM)
TRAP_DOWNCT=3 # After 10 seconds go back to 3
echo ^C reset
;; . . . 

Internals

The script is pretty easy to figure out. At the heart is a loop that adds traps to the system, one at a time, with arguments attached. The only two tricky things are how the script tries to detect your trap handler and you don’t have one, it uses eval to create a simple function for you.

The actual setup turns into:

trap "trappist_trap $t" $t

This line takes a signal named in t, traps it, and causes the correct signal name to pass as an argument. After that, it is pretty easy to see how things work.

If you think about it, the signals are a lot like interrupts, although some of them don’t fire right away — in other words, only a few of the signals mentioned occur immediately. However, by default, each “interrupt” has an entry in a vector table. Trappist populates the table to push everything to a single “interrupt service routine.”

Note that trappist wouldn’t be necessary if there was a way for the script to figure out the signal. You could write: trap trappist_trap SIGQUIT SIGINT SIGHUP … You would then have to figure out the signal in the trap function. Of course, if you want to treat all signals the same, you don’t have to worry about that.

We’ve talked about some of the ins and outs of stopping hangups before. We’ve also looked at scripting with binary files.

No comments:

Post a Comment