Unix


Portable Signal Handling Under UNIX

Martin Remy

UNIX-style signals are hardly an object-oriented concept, but that doesn't prevent you from imposing a class discipline on how they're used.


The Problem So Far

In most C++ UNIX programs, signal handling represents a kind of make shift bridge between objects and processes. Usually, a global signal handler is written that (among other operations) calls various member functions of the classes that constitute the process. This means that the knowledge about how each of the classes should handle certain signals is collected by the consumer of the classes. This knowledge is therefore external to the class, and is mixed with the same kind of knowledge about the other classes in the program. Another programmer who wants to use some of the same classes in a different program has to build a new signal handler from scratch. So much for reuse.

However, if you believe that Posix can provide portable signal handling, and you've written your classes for UNIX-to-UNIX portability only, there is a graceful way to the other side. (If you want to brush up on Posix signal handling before you cross that bridge, see the sidebar) .

One solution, of course, is to encapsulate signal handling knowledge within the class. For example, if you are writing a class (class Manager, say) that manages a critical resource, you simply provide a member function Manager::sighandler(int) that executes the appropriate response to each type of signal that class Manager is interested in. This response might consist of terminating child processes spawned by the resource manager, or manipulating the state of the object in some way to ensure the integrity of the resource in the situation indicated by the particular signal.

Some would argue that signal handling does not belong in classes. But since Posix addresses signals, I think this approach is defensible (because it can be made portable), at least for strictly UNIX/Posix classes.

To make this strategy robust and usable, I decided to employ an abstract base class (class SigAware) that provides "signal awareness" to objects of its subclasses. The idea is to keep track of the objects in a process, and to give each one a chance to handle the current signal. This shifts the focus of signal handling from the process to its constituent objects. There are several significant advantages in this approach. For example, as a class evolves during application development, its signal handling can be brought up to date by the owner of the class code. This leaves very little room for miscommunication between class author and class consumer on the subject of signals.

The class SigAware provides a static collection for keeping track of the objects in the process, and some static methods for routing the signal to the appropriate object's signal handler. It also provides a constructor that registers an object to receive signals as soon as it is constructed, as well as a virtual destructor that ensures that an object's pointer is removed from the collection as soon as the object is destroyed.

The strategy, then, is to derive signal-aware classes from this base class, which provides all of the necessary functionality except for the signal handler function itself, which is specific to the derived class.

Implementation of SigAware

The base class is shown in Listing 1. For a collection class it uses an STL set, of template class set. (You can, of course, substitute almost any collection class you like.) This set is static data because it manages SigAware objects process-wide. There is a matching static iterator for the set, which is used to traverse the set so that each registered object's signal handler function can be called. The constructor simply registers the object (by calling the registerForSignals function), and the destructor unregisters the object by calling the corresponding function unregisterForSignals. The static function SigAware::sigRouter passes the signal to each registered object, and must be installed as a signal handler (using sigaction — see the sidebar on Posix signal handling). The function registerForSignals just adds the pointer to the SigAware object to the set, and unregister accomplishes the reverse, erasing the pointer from the set.

The most important function in the class is sighandler, which is pure virtual. This means that if you derive a class (say, Manager) from SigAware, you will be forced to implement sighandler to process the signals that your new class Manager is interested in. The sighandler function usually consists just of a switch statement, with a case for each signal that the class is interested in.

If your class is not interested in a particular signal, say SIGUSR2, simply omit a case for SIGUSR2 in your switch statement. See Listing 2 for an example of a sighandler implementation that omits all signals except SIGTERM and SIGINT.

To illustrate what SigAware-derived classes look like, Listing 2 shows two examples. One is the Manager class I mentioned previously. The other is a class called Scribe, whose purpose is to log all signals that are sent to the program. The Manager class is interested only in SIGINT and SIGTERM in this example. Its implementation of the virtual function sighandler calls the member function writeCache in response to either signal. The sighandler for the Scribe class, on the other hand, acts as a signal monitor by logging every signal it receives. (A class like this may be useful in a real program for achieving conformance to a logging format).

Listing 3 shows a small program that instantiates a Scribe object and a Manager object. Next, it installs the static function SigAware::sigRouter as the signal handler for SIGINT, SIGTERM, and SIGUSR1. This implies that all other signals will follow the default behavior in this program (as you will see with SIGHUP in the test below).

It is important to point out here that the SigAware scheme of handling signals does not preclude other signal handling tricks. If you are using SigAware-derived classes in your program, you can still block signals and play other signal games from any other point within your code.

After installing the signal handlers, the example program in Listing 3 enters an infinite loop. If you run this program in the background, you can easily test its response to various signals by invoking the kill command from the shell.

For example, kill -USR1 causes the Scribe object to report the signal, but the Manager object remains silent since it is not interested in SIGUSR1. Typing kill -TERM again causes the Scribe object to report the signal. This time the Manager object calls its writeCache method. If you send a signal that is not being caught, such as SIGHUP, the program simply exits. Even the Scribe object does not report the signal, since SigAware::sigRouter was not installed as the signal handler for SIGHUP.

If you have several instances of a SigAware-derived class in your program, you will see that each one responds to the signal separately, just as you would expect.

Questions About SigAware

The benefits of encapsulating signal knowledge within classes are clear. Chief among them are encapsulation and code reuse. The SigAware class offers a simple, portable, way to reap these benefits in practice. But there are several questions that inevitably arise. One of them is, how do I make sure that the consumer of my class will install SigAware::sigRouter as a signal handler? The first answer, of course, is documentation. If you make it clear that this is a requirement of your class, you may have a good chance at being invited to the signal party in that particular program.

The second answer is that SigAware, as I have presented it here, is only the beginning. It is possible to add code to the SigAware constructor that automatically installs sigRouter as a signal handler. Another data member of type pointer-to-function has to be added so that the currently installed handler can be preserved and called before or after sigRouter.

Another important question that arises about the SigAware approach is, what if my class is already derived from another class, and I don't want to introduce multiple inheritance into my model just to get signal awareness? This question takes a lot more thought, but the answer has to be something like this: If you absolutely cannot see your way clear to using MI, you have to look for alternatives that still provide at least encapsulation and reusability. You could try to implement SigAware along the lines of the Visitor design pattern [1] . But my evaluation of that approach shows that the code required would at least triple in size, and that the burdens on the user of the Manager class would increase as well.

It is true that, in the case of the SigAware class, inheritance is used to model an "implements" relationship rather than an "is-a" relationship to its superclass. But C++ doesn't really offer an alternative for modeling "implements" relationships (unlike Java, which has interfaces for this kind of thing). I think that, even at the cost of using multiple inheritance, SigAware provides significant benefits to the C++ UNIX programmer who wants to be more "objective" about signal handling.

In fact, I have experimented with extending the basic SigAware class presented here, and have found it to be a good model for something like a "process-aware" class that could bridge the gulf between objects and processes in many more areas than just signals. Child processes, for example, could be managed on a per-object basis. This could be useful, again, in the context of a resource-manager class, which spawns child processes that respond to requests for data from the resource.

Suppose there are several different kinds of resource manager objects living in a single process, with each object spawning its own specialized child processes. In this situation it would make sense to let each object take "ownership" of its children. In effect, this would mean superimposing ownership by objects onto ownership by processes. Extending SigAware could achieve this. But for now, I use SigAware to make my signal handling less prone to the pitfalls of huge, monolithic UNIX programs, and more portable, reusable, and object-friendly.

Reference

[1] Erich Gamma, Richard Helm, et al. Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1995).

Bibliography

Graham Glass & Brett Schuchert. The STL <Primer> (Prentice-Hall, 1996).

Donald Lewine. Posix Programmer's Guide, "Writing Portable Unix Programs" (O'Reilly and Associates, 1991).

Kay A. Robbins & Steven Robbins. Practical Unix Programming, "A Guide to Concurrency, Communication, and Multithreading" (Prentice-Hall, 1996).

W. Richard Stevens. Advanced Programming in the Unix Environment (Addison-Wesley, 1992).

Martin Remy is a consultant at IBM in Boulder, Colorado, working on a large client/server project employing C++ under AIX. He holds an M.S. degree in Applied Mathematics as well as a B.S. degree in Computer Science from Santa Clara University. He may be reached at remy@dimensional.com.