Differences between revisions 3 and 5 (spanning 2 versions)
Revision 3 as of 2011-10-25 19:31:47
Size: 6610
Editor: GreyCat
Comment: use a function in the second example
Revision 5 as of 2011-12-22 14:01:33
Size: 7733
Editor: pgas
Comment: a note about when a trap is executed.
Deletions are marked like this. Additions are marked like this.
Line 70: Line 70:
Setting a trap overwrites a previous trap on a given signal. There's no direct way to access the command associated with a trap as a string in order to save and restore the state of a trap. However, trap is "supposed" to be able to create properly escaped output that's safe for reuse. The POSIX-recommended method is:
{{{
save_traps=$(trap)
...
eval "$save_traps"
}}}

== When is the signal handled? ==

Bash waits for the current foreground command to exit before executing the trap, this is important if you have a script like this:

{{{
trap 'echo doing some cleaning' INT
while true; do
 echo waiting a bit
 sleep 10000
done
}}}

If you kill the script, bash will wait for sleep to exit before printing the message often that's not what you expect. A workaround is to use a builtin that will be interrupted, namely wait:
{{{
trap 'echo doing some cleaning' INT
while true; do
 echo waiting a bit
 sleep 10000 & wait $! # will be interrupted as soon as the script exits.
done
}}}

Note that 'sleep 10000' will not be killed and will continue to run, some extra cleanup might be needed.

Sending and Trapping Signals

Signals are a basic tool for asynchronous interprocess communication. What that means is one process (A) can tell another process (B) to do something, at a time chosen by process A rather than process B. (Compare to process B looking for a file every few seconds; this is called polling, and the timing is controlled by process B, rather than process A.)

The operating system provides a finite number of signals which can be "sent" to tell a process to do something. The signals do not carry any additional information; the only information the process gets is which signal was received. The process does not even know who sent the signal.

Unless a process takes special action in advance, most signals are fatal; that is, the default action a process will perform upon receiving a signal is an immediate exit. (Exceptions: SIGCHLD is ignored by default, SIGSTOP pauses the process, and SIGCONT resumes the process.) Some signals (such as SIGQUIT) also cause a process to leave a core file, in addition to exiting.

1. Traps, or Signal Handlers

A process may choose to perform a different action, rather than exiting, upon receiving a signal. This is done by setting up a signal handler (or trap). The trap must be set before the signal is received. A process that receives a signal for which it has set a trap is said to have caught the signal.

The simplest signal handling a process can choose to perform is to ignore a signal. This is generally a bad idea, unless it is done for a very specific purpose. Ignoring signals often leads to runaway processes which consume all available CPU.

More commonly, traps can be set up to intercept a fatal signal, perform cleanup, and then exit gracefully. For example, a program that creates temporary files might wish to remove them before exiting. If the program is forced to exit by a signal, it won't be able to remove the files unless it catches the signal.

In a shell script, the command to set up a signal handler is trap. The trap command has 5 different ways to be used:

  • trap 'some code' signal list -- using this form, a signal handler is set up for each signal in the list. When one of these signals is received, the commands in the first argument will be executed.

  • trap '' signal list -- using this form, each signal in the list will be ignored. Most scripts should not do this.

  • trap - signal list -- using this form, each signal in the list will be restored to its default behavior.

  • trap signal -- using this form, the one signal listed will be restored to its default behavior. (This is legacy syntax.)

  • trap -- with no arguments, print a list of signal handlers.

Signals may be specified using a number, or using a symbolic name. The symbolic name is greatly preferred for POSIX or Bash scripts, because the mapping from signal numbers to actual signals can vary slightly across operating systems. For Bourne shells, the numbers may be required.

There is a core set of signals common to all Unix-like operating systems whose numbers realistically never change; the most common of these are:

Name

Number

Meaning

HUP

1

Hang Up. The controlling terminal has gone away.

INT

2

Interrupt. The user has pressed the interrupt key (usually Ctrl-C or DEL).

QUIT

3

Quit. The user has pressed the quit key (usually Ctrl-\). Exit and dump core.

KILL

9

Kill. This signal cannot be caught or ignored. Unconditionally fatal. No cleanup possible.

TERM

15

Terminate. This is the default signal sent by the kill command.

EXIT

0

Not really a signal. In a shell script, an EXIT trap is run on any exit, signalled or not.

The names used in a shell script lack the leading SIG portion. SIGHUP is trapped by using trap ... HUP and is sent by using kill -HUP process_ID.

The special name EXIT is defined by POSIX and is preferred for any signal handler that simply wants to clean up upon exiting, rather than doing anything complex. Using 0 instead of EXIT is also allowed in a trap command (but 0 is not a valid signal number, and kill -0 has a completely different meaning).

If you are asking a program to terminate, you should always use SIGTERM (simply kill process_ID). This will give the program a chance to catch the signal and clean up. If you use SIGKILL, the program cannot clean up, and may leave files in a corrupted state.

Please see ProcessManagement for a more thorough explanation of how processes work and interact.

2. Examples

This is the basic method for setting up a trap to clean up temporary files:

   1 #!/bin/sh
   2 tempfile=$(mktemp)
   3 trap 'rm -f "$tempfile"' EXIT
   4 ...

This example defines a signal handler that will re-read a configuration file on SIGHUP. This is a common technique used by long-running daemon processes, so that they do not need to be restarted from scratch when a configuration variable is changed.

   1 #!/bin/sh
   2 config=/etc/myscript/config
   3 read_config() { test -r "$config" && . "$config"; }
   4 read_config
   5 trap 'read_config' HUP
   6 while true; do
   7   ...

Setting a trap overwrites a previous trap on a given signal. There's no direct way to access the command associated with a trap as a string in order to save and restore the state of a trap. However, trap is "supposed" to be able to create properly escaped output that's safe for reuse. The POSIX-recommended method is:

save_traps=$(trap)
...
eval "$save_traps"

3. When is the signal handled?

Bash waits for the current foreground command to exit before executing the trap, this is important if you have a script like this:

trap 'echo doing some cleaning' INT
while true; do 
 echo waiting a bit
 sleep 10000
done

If you kill the script, bash will wait for sleep to exit before printing the message often that's not what you expect. A workaround is to use a builtin that will be interrupted, namely wait:

trap 'echo doing some cleaning' INT
while true; do 
 echo waiting a bit
 sleep 10000 & wait $! # will be interrupted as soon as the script exits.
done

Note that 'sleep 10000' will not be killed and will continue to run, some extra cleanup might be needed.

4. Special Note On SIGINT

If you choose to set up a handler for SIGINT (rather than using the EXIT trap), you should be aware that a process that exits in response to SIGINT should kill itself with SIGINT rather than simply exiting, to avoid causing problems for its caller. Thus:

trap 'rm -f "$tempfile"; trap - INT; kill -INT $$' INT

We can see the difference between a properly behaving process and a misbehaving process. On most operating systems, ping is an example of a misbehaving process. It traps SIGINT in order to display a summary at the end, before exiting. But it fails to kill itself with SIGINT, and so the calling shell does not know that it should abort as well. For example,

# Bash.  Linux ping sytnax.
for i in {1..254}; do
  ping -c 2 192.168.1.$i
done

Here, if the user presses Ctrl-C during the loop, it will terminate the current ping command, but it will not terminate the loop. This is because Linux's ping command does not kill itself with SIGINT in order to communicate to the caller that the SIGINT was fatal. (Linux is not unique in this respect; I do not know of any operating system whose ping command exhibits the correct behavior.)


CategoryShell

SignalTrap (last edited 2023-09-21 20:57:38 by 71)