There are a few exceptions to this rule. For example, the UNIX SIGPIPE signal is provided as an optimization to terminate the writer of a pipe when the pipe's reader has stopped listening. If the writer might have other important side effects to accomplish when this signal is seen, then the signal should be ignored (or better yet, it should arrange for the affected pipe(s) not to be written any more). Another example is that a program that normally runs as a daemon (without an associated terminal) might disposition the SIGHUP signal to take some action such as reloading the configuration file.
It is preferable for a program running in an interactive mode to respond to the UNIX SIGINT signal by terminating the current user request, rather than the entire process. On the other hand, SIGTERM, SIGHUP and SIGPWR should terminate the whole process even if it's not in batch mode. Other systems may make similar distinctions among signals.
Some signals are intended to cause the process to terminate immediately, without any sort of handling at all. For example, the UNIX SIGABRT and SIGQUIT signals are intended as debugging facilities, and therefore should not even trust signal handling routines. Synchronous violation signals, such as UNIX SIGSEGV, SIGBUS and SIGFPE, should almost always be handled the same way, but there are cases in which the program can be made substantially more efficient by trapping them to invoke rarely-used alternative code when the stack trace is recognizable. (For example, in order to tighten its inner loop, UNIX uncompress doesn't check some of its pointers. If the incoming data stream is corrupted, a segmentation violation signal is sent, and results in an appropriate error message.) Because this involves accessing the stack, it tends to be expensive to port, and it works only when the return address of the signal handler call has a guaranteed relationship to the violating instruction.
A persistent resource is anything that a process allocates and is not automatically deallocated by the system when the process exits. For example, files and database locks are persistent resources. Temporary persistent resources are resources that belong to some scope of the program, and therefore should be deallocated by the process before it exits, either normally or in response to an ordinary termination signal. In UNIX, memory is not a persistent resource, but in some other systems it is, so the default dispositions may not work portably if you use dynamic memory allocation.
If the signaled process is required to take some action (such as deallocating temporary persistent resources before exiting) in response to one or more signals, then the more general approach outlined in the remainder of this document should be used instead.
One possible approach to synchronization is to disable interrupts in the critical sections where the shared state is accessed. However, this is a bad idea, both because it requires frequent costly system calls, and because it is error-prone. Instead, we rely on the atomicity of setting and resetting flags of type int. (In particular, you can't use vector<bool>, because resetting a single flag may be implemented as a non-atomic read-modify-write, in which case a flag that is set by the signal handler after the read but before the write will be lost. Similarly, it is unreliable to try to keep track of the number of times a signal was received by incrementing a count rather than setting a flag.)
In general, each signal handler sets a flag corresponding to the signal received, and then sets another flag to indicate that a signal was received. The main thread then periodically checks to see if a signal was received, and if so takes the appropriate action and clears the associated flags. Such polling should occur at least ten times per second in a compute-intensive loop, and must occur immediately before any system call that might block for a noticeable amount of time. (Even then there's a small problem -- see UNIX Signal Handling Dilemma .)
Here's some sample C++ code under POSIX:
#include <signal>
int interrupted=0;
int sigpend[NUM_SIGNALS]={0}; // NUM_SIGNALS is nonstandard
extern "C" {
void remember(int);
void poll();
void poll_();
void disposition();
}
void handle(int); // We'll talk about what this does later
void remember(int signo) {
sigpend[signo]=1;
interrupted=1;
}
inline void poll() {
// For efficiency
while(interrupted) { poll_(); }
}
void poll_() {
interrupted=0;
for(int i=0; i<NUM_SIGNALS; i++) {
if(sigpend[i]) {
sigpend[i]=0;
handle(i);
}
}
}
void disposition() {
(void) signal(SIGINT, remember);
(void) signal(SIGTERM, remember);
// and so on...
}
It's not safe for the signal handler to take any action other than setting flags, because other parts of the program state might be incoherent. For example, if a temporary file is created, then the existence of the file will be recorded in the program state either before of after the file creation. A signal handler that is responsible for removing temporary files will always do the wrong thing if it happens to be called in the intervening state.
On the other hand, a synchronous violation handler always executes at specific points in the main thread, and is most likely required to execute before the main thread can continue. Therefore, synchronous violation signals, if they are trapped at all, should be dealt with entirely within the signal handler.
This takes care of the synchronization problem. Now let's deal with exiting the program gracefully when that's what the signal calls for.
All temporary persistent resources should correspond to objects belonging to some program scope, where the destructors of those objects will deallocate the resources. This is a good idea even when there aren't any signals, because it limits the amount of clean-up code you need to write, and will do the right thing when exceptions are thrown in general.
If an outstanding termination signal is detected by the polling routine, a corresponding exception is thrown. Functions that promise not to throw such exceptions (for example, destructors) should enclose any polling (or calls that might poll) in a try block. Any caught signal exceptions should cause those signals to be resent to the current process after all polling is completed.
Termination signal exceptions are then caught in the top-level routine, which then exits without any further processing. The final wrinkle is that some systems, such as UNIX, require the signal that causes the termination to be reflected in the program's exit status, such that the parent process can also react to the signal. (If you don't do this, then the parent process is supposed to assume that the program ignored the received signal and then later terminated normally.) In UNIX, you do this by dispositioning the signal to terminate the process, and then sending the signal to yourself.
Here's some more sample code:
class Signal {
int number_;
Signal(); // Not allowed
public:
number() const { return number_; }
explicit Signal(int signo) : number_(signo) {}
};
class IntSignal : public Signal {
public:
IntSignal() : Signal(SIGINT) {}
};
void handle(int signo) {
switch(signo) {
case SIGINT:
throw IntSignal();
break;
case SIGHUP:
case SIGTERM:
case SIGPIPE:
throw Signal(signo);
break;
case SIGUSR1: // for example
// Respond without throwing anything
break;
}
}
int main() {
disposition();
try {
run(); // Calls poll() every 100ms or less
poll();
}
catch(Signal &sig) {
(void) signal(sig.number(), SIG_ERR);
(void) raise(sig.number());
}
return 0;
}
Here's what run() looks like if the program is running in interactive mode:
#include <iostream>
class Command; // The details of this don't concern us
bool do_command(const Command&); // True iff it's the Exit command
Command get_command();
void run() {
bool done=false;
while(!done) {
try {
poll();
done=do_command(get_command()); // Calls poll()
}
catch(IntSignal &sig) {
std::cerr << "Interrupted\n";
}
}
}
More commonly, you can deal with this by maintaining a registry of outstanding temporary persistent resources, and arranging for the polling routine to deallocate those resources and exit without returning when a termination signal is received. In C, it is recommended that you register your cleanup routine with atexit(), define a global variable to indicate the termination signal number (or zero if none has been received) to be set by the polling routine before it exits, and have the cleanup routine kill its own process with that signal, if nonzero. Interactive programs can deal with SIGINT using setjmp() and longjmp().
Here's some sample C code:
#include <stdlib.h>
#include <setjmp.h>
#include <stdio.h>
typedef struct TempFile_ {
char name[MAX_FILENAME]; // MAX_FILENAME is nonstandard
} TempFile;
typedef struct Resource_ {
enum {
temp_file, /* ... */
} type;
union {
TempFile file;
/* ... */
} x;
struct Resource_ *next;
} Resource;
/* The resources should really be grouped by context, such that
* query loops can nest, but let's not get too fancy here.
*/
Resource *temp_resources=NULL;
jmp_buf query_loop;
int query_loop_active=0;
int termination_signo=0;
void cleanup() {
while(temp_resources) {
Resource *tmp=temp_resources;
switch(tmp->type) {
case temp_file:
(void) remove(tmp->x.file.name);
break;
/* ... */
}
temp_resources=temp_resources->next;
free(tmp);
}
if(termination_signo) {
(void) signal(termination_signo, SIG_ERR);
(void) raise(termination_signo);
}
}
void handle(int signo) {
switch(signo) {
case SIGINT:
if(query_loop_active) {
fprintf(stderr, "Interrupted\n");
longjmp(query_loop, 1);
}
/* fall thru otherwise */
case SIGHUP:
case SIGTERM:
case SIGPIPE:
termination_signo=signo;
exit(EXIT_FAILURE);
break;
case SIGUSR1: /* for example */
/* Respond to the signal */
break;
}
}
void run_program() { /* Interactive mode */
int done=0;
(void) setjmp(query_loop);
query_loop_active=1;
while(!done) {
done=do_command(get_command()); // Calls poll()
}
query_loop_active=0;
}
int main() {
if(atexit(cleanup)) { exit(EXIT_FAILURE); }
disposition();
run_program(); Calls poll() every 100ms or less
return 0;
}
Anders Johnson, last modified $Date: 2003/01/08 $