Table Of Contents
- What Are Signals?
- What Are Signals Used For?
- Sending Signals To Processes
- Catching Signals - Signal Handlers
- Installing Signal Handlers
- Avoiding Signal Races - Masking Signals
- Implementing Timers Using Signals
- Summary - "Do" and "Don't" inside A Signal Handler
What Are Signals?
Signals, to be short, are various notifications sent to a process in order to notify it of various "important" events. By their nature, they interrupt whatever the process is doing at this minute, and force it to handle them immediately. Each signal has an integer number that represents it (1, 2 and so on), as well as a symbolic name that is usually defined in the file /usr/include/signal.h or one of the files included by it directly or indirectly (HUP
, INT
and so on. Use the command 'kill -l'
to see a list of signals supported by your system).
Each signal may have a signal handler, which is a function that gets called when the process receives that signal. The function is called in "asynchronous mode", meaning that no where in your program you have code that calls this function directly. Instead, when the signal is sent to the process, the operating system stops the execution of the process, and "forces" it to call the signal handler function. When that signal handler function returns, the process continues execution from wherever it happened to be before the signal was received, as if this interruption never occurred.
Note for "hardwarists": If you are familiar with interrupts (you are, right?), signals are very similar in their behavior. The difference is that while interrupts are sent to the operating system by the hardware, signals are sent to the process by the operating system, or by other processes. Note that signals have nothing to do with software interrupts, which are still sent by the hardware (the CPU itself, in this case).
What Are Signals Used For?
Signals are usually used by the operating system to notify processes that some event occurred, without these processes needing to poll for the event. Signals should then be handled, rather then used to create an event notification mechanism for a specific application.
When we say that "Signals are being handled", we mean that our program is ready to handle such signals that the operating system might be sending it (such as signals notifying that the user asked to terminate it, or that a network connection we tried writing into, was closed, etc). Failing to properly handle various signals, would likely cause our application to terminate, when it receives such signals.
Sending Signals To Processes
Sending Signals Using The Keyboard
The most common way of sending signals to processes is using the keyboard. There are certain key presses that are interpreted by the system as requests to send signals to the process with which we are interacting:
Ctrl-C
Pressing this key causes the system to send an
INT
signal (
SIGINT
) to the running process. By default, this signal causes the process to immediately terminate.
Ctrl-Z
Pressing this key causes the system to send a
TSTP
signal (
SIGTSTP
) to the running process. By default, this signal causes the process to suspend execution.
Ctrl-/
Pressing this key causes the system to send a
ABRT
signal (
SIGABRT
) to the running process. By default, this signal causes the process to immediately terminate. Note that this redundancy (i.e. Ctrl-/ doing the same as Ctrl-C) gives us some better flexibility. We'll explain that later on.
Sending Signals From The Command Line
Another way of sending signals to processes is done using various commands, usually internal to the shell:
kill
The kill command accepts two parameters: a signal name (or number), and a process ID. Usually the syntax for using it goes something like:
For example, in order to send the
INT
signal to process with PID 5342, type:
kill -INT 5342
This has the same affect as pressing Ctrl-C in the shell that runs that process.
If no signal name or number is specified, the default is to send a
TERM
signal to the process, which normally causes its termination, and hence the name of the
kill
command.
fg
On most shells, using the
'fg'
command will resume execution of the process (that was suspended with Ctrl-Z), by sending it a
CONT
signal.
Sending Signals Using System Calls
A third way of sending signals to processes is by using the kill system call. This is the normal way of sending a signal from one process to another. This system call is also used by the 'kill'
command or by the 'fg'
command. Here is an example code that causes a process to suspend its own execution by sending itself the STOP
signal:
An example of a situation when this code might prove useful, is inside a signal handler that catches the TSTP signal (Ctrl-Z, remember?) in order to do various tasks before actually suspending the process. We will see an example of this later on.
Catching Signals - Signal Handlers
Catchable And Non-Catchable Signals
Most signals may be caught by the process, but there are a few signals that the process cannot catch, and cause the process to terminate. For example, the KILL
signal (-9
on all unices I've met so far) is such a signal. This is why you usually see a process being shut down using this signal if it gets "wild". One process that uses this signal is a system shutdown process. It first sends a TERM
signal to all processes, waits a while, and after allowing them a "grace period" to shut down cleanly, it kills whichever are left using the KILL
signal.
STOP
is also a signal that a process cannot catch, and forces the process's suspension immediately. This is useful when debugging programs whose behavior depends on timing. Suppose that process A needs to send some data to process B, and you want to check some system parameters after the message is sent, but before it is received and processed by process B. One way to do that would be to send a STOP
signal to process B, thus causing its suspension, and then running process A and waiting until it sends its oh-so important message to process B. Now you can check whatever you want to, and later on you can use the CONT
signal to continue process B's execution, which will then receive and process the message sent from process A.
Now, many other signals are catchable, and this includes the famous SEGV
and BUS
signals. You probably have seen numerous occasions when a program has exited with a message such as 'Segmentation Violation - Core Dumped', or 'Bus Error - core dumped'. In the first occasion, a SEGV
signal was sent to your program due to accessing an illegal memory address. In the second case, a BUS
signal was sent to your program, due to accessing a memory address with invalid alignment. In both cases, it is possible to catch these signals in order to do some cleanup - kill child processes, perhaps remove temporary files, etc. Although in both cases, the memory used by your process is most likely corrupt, it's probable that only a small part of it was corrupt, so cleanup is still usually possible.
Default Signal Handlers
If you install no signal handlers of your own (remember what a signal handler is? yes, that function handling a signal?), the runtime environment sets up a set of default signal handlers for your program. For example, the default signal handler for the TERM
signal calls the exit()
system call. The default handler for the ABRT
is to dump the process's memory image into a file named 'core' in the process's current directory, and then exit. Note that the 'core' file might actually have some suffix (such as 'core.567') on some systems.
Installing Signal Handlers
There are several ways to install signal handlers. We'll use the most basic form here, and refer you to your manual pages for further reading.
The signal()
System Call
The signal()
system call is used to set a signal handler for a single signal type. signal()
accepts a signal number and a pointer to a signal handler function, and sets that handler to accept the given signal. As an example, here is a code snippest that causes the program to print the string "Don't do that" when a user presses Ctrl-C:
The complete source code for this program is found in the catch-ctrl-c.c file.
Notes:
- the
pause()
system call causes the process to halt execution, until a signal is received. it is surely better than a 'busy wait' infinite loop. - the name of a function in C/C++ is actually a pointer to the function, so when you're asked to supply a pointer to a function, you may simply specify its name instead.
- On some systems (such as Linux), when a signal handler is called, the system automatically resets the signal handler for that signal to the default handler. Thus, we re-assign the signal handler immediately when entering the handler function. Otherwise, the next time this signal is received, the process will exit (default behavior for
INT
signals). Even on systems that do not behave in this way, it still won't hurt, so adding this line always is a good idea.
Pre-defined Signal Handlers
For our convenience, there are two pre-defined signal handler functions that we can use, instead of writing our own: SIG_IGN
and SIG_DFL
.
SIG_IGN
:
Causes the process to ignore the specified signal. For example, in order to ignore Ctrl-C completely (useful for programs that must NOT be interrupted in the middle, or in critical sections), write this:
signal(SIGINT, SIG_IGN);
SIG_DFL
:
Causes the system to set the default signal handler for the given signal (i.e. the same handler the system would have assigned for the signal when the process started running):
signal(SIGTSTP, SIG_DFL);
Avoiding Signal Races - Masking Signals
One of the nasty problems that might occur when handling a signal, is the occurrence of a second signal while the signal handler function executes. Such a signal might be of a different type than the one being handled, or even of the same type. Thus, we should take some precautions inside the signal handler function, to avoid races.
Luckily, the system also contains some features that will allow us to block signals from being processed. These can be used in two 'contexts' - a global context which affects all signal handlers, or a per-signal type context - that only affects the signal handler for a specific signal type.
Masking signals with sigprocmask()
the (modern) "POSIX" function used to mask signals in the global context, is the sigprocmask()
system call. It allows us to specify a set of signals to block, and returns the list of signals that were previously blocked. This is useful when we'll want to restore the previous masking state once we're done with our critical section.
Note: each process on a unix system has its own signals mask, which is used by the operating system to specify which signals should be delivered to the proces, and which should be blocked. The sigprocmask
system call is used to take a signals mask we created in user space, and update the one in stored in the kernel, using this user-space mask. The mask stored in the kernel is the one later considered by the operating system, when deciding whether to deliver a signal to the process, or block it.
sigprocmask()
accepts 3 parameters:
int how
defines if we want to add signals to the current process's mask (
SIG_BLOCK
), remove them from the current mask (
SIG_UNBLOCK
), or completely replace the current mask with the new mask (
SIG_SETMASK
).
const sigset_t *set
The set of signals to be blocked, or to be added to the current mask, or removed from the current mask (depending on the 'how' parameter).
sigset_t *oldset
If this parameter is not
NULL
, then it'll contain the previous mask. We can later use this set to restore the situation back to how it was before we called
sigprocmask()
.
Note: Older systems do not support the sigprocmask()
system call. Instead, one should use the sigmask()
and sigsetmask()
system calls. If you have such an operating system handy, please read the manual pages for these system calls. They are simpler to use than sigprocmask, so it shouldn't be too hard understanding them once you've read this section.
You probably wonder what are these sigset_t
variables, and how they are manipulated. Well, i wondered too, so i went to the manual page of sigsetops
, and found the answer. There are several functions to handle these sets. Lets learn them using some example code:
Now that we know all these little secrets, lets see a short code example that counts the number of Ctrl-C signals a user has hit, and on the 5th time (note - this number was "Stolen" from some quite famous Unix program) asks the user if they really want to exit. Further more, if the user hits Ctrl-Z, the number of Ctrl-C presses is printed on the screen.
- Make it short
- Proper Signal Masking - don't be too lazy to define proper signal masking for a signal handler, preferably using the
sigaction()
system call. It takes a little more effort than just using thesignal()
system call, but it'll help you sleep better at night, knowing that you haven't left an extra place for race conditions to occur. Remember - if some bug has a probability of 1/10,000 to occur, it WILL occur when many people use that program many times, as tends to be the case with good programs (you write only good programs, no?). - Careful with "fault" signals - If you catch signals that indicate a program bug (
SIGBUS
,SIGSEGV
,SIGFPE
), don't try to be too smart and let the program continue, unless you know exactly what you are doing (which is a very rare case) - just do the minimal required cleanup, and exit, preferably with a core dump (using theabort()
function). Such signals usually indicate a bug in the program, that if ignored will most likely cause it to crush sooner or later, making you think the problem is somewhere else in the code. - Careful with timers - when you use timers, remember that you can only use one timer at a time, unless you also (ab)use the
VTALRM
signal. If you need to have more than one timer active at a time, don't use signals, or devise a set of functions that will allow you to have several virtual timers using a delta list of some sort. If you've no idea what I'm talking about, you probably don't need several simultaneous timers in the first place. - Signals are NOT an event driven framework