Unix


A Signal Command and Control Class for Unix

Patrick Bailey

Signals are a useful way to communicate under Unix, provided you have enough to go around.


This article presents two C++ classes you can use to leverage Unix signals. The two classes combine the power of signals, Unix message queues, and the Standard C++ library map container template class. To help readers appreciate the capability offered by these classes, I provide a brief overview of signals and their implementation.

Overview of Signals

Signals under Unix System V, Release 4 provide a powerful mechanism for externally controlling applications. A signal is an interprocess communication (IPC) mechanism, which interrupts an application to perform an action. Signal types are identified by integers. The operating system provides a default action in response to most signals. Default actions include ignoring the signal as well as stopping the application.

In general, the Unix "kill" utility or the Unix system call kill are used to send signals to processes. The signal sender must provide the ID number of the signal being sent and the PID (Process ID) of the receiving process. Below is an example of the Unix command line to send a TERMINATE signal (15) to a process with PID 1234. (The TERMINATE signal is usually used to notify applications that the operating system is about to be shut down.)

kill  -TERM  1234

The Unix system call signal allows developers to customize actions taken for many of the standard signals that applications receive. The prototype for the signal call is in the header file <sys/signal.h> and is declared as follows:

int signal (int signal_id, void (*)());

The signal call takes two arguments. The first is an integer to identify the signal type, and the second is a function pointer to the custom routine. Using the example above, a developer may provide a special function named stop_gracefully to do some cleanup before stopping the application. The following snippet illustrates how this is accomplished:

#include <sys/signal.h>                        
void stop_gracefully(int);        
int main(void){        
    signal(SIGTERM, stop_gracefully);
    // more code follows              
}

Notice that the stop_gracefully function prototype takes a single integer argument. When the application receives a signal, the argument to stop_gracefully will be set to the type of signal sent. This implementation requirement was a key consideration in designing the CmdControl class, described later.

Two signals defined in <sys/signal.h> are specifically for customized use: SIGUSR1 and SIGUSR2. For example, I knew one developer who incorporated these signals into a run-time diagnostic routine. If a higher level of diagnostics was desired, the application was sent the SIGUSR1 signal; the SIGUSR2 signal had the reverse effect.

The Command Control Classes

SIGUSR1 and SIGUSR2 are convenient, but if they have already been used (not uncommon), the developer must find some other means to activate other features in the application. Even if the developer uses other signals offered by the operating system, sooner or later the supply of signals will be depleted. This limitation is addressed with the two classes presented in this article.

The CmdControl Class

The first class, CmdControl, is the "Command and Control" class. Using CmdControl, a developer can assign an almost unlimited number of actions to a signal. The second class, CmdQMgr is the "Command Control Queue Message Manager." As its name implies, it manages communications between the CmdControl class and the outside world.

I'll review the design and implementation of the CmdControl class first. CmdControl.h (Listing 1) provides the header file for the CmdControl class. CmdControl.c (Listing 2) shows implementation of its member functions. The code listings for this article were compiled and tested with the GNU C++ compiler g++ under Caldera's OpenLinux (v1.2).

The CmdControl class is best described as a class that creates associations. It creates associations that identify which signal belongs to which instance of the class. Furthermore, the CmdControl class associates symbolic references with actions. These associations are supported with C++'s standard template library container map class. CmdControl's member object cmd_sigobjref provides the association between signals and the instance of the class that handles them. This explains why it is declared as a static member. The cmd_runstring object associates a function within an application with a symbolic string.

The CmdControl constructor (Listing 2) requires a single parameter. This is the identification number of the Unix signal it will manage for an application. An application should contain only a single instance of CmdControl per signal.

When the CmdControl class is instantiated, it invokes Unix's system call signal. This associates the signal with the static member function CmdControl::cmd_handler, a generic function for signal handling.

One of the things I found particularly challenging in designing this class was dealing with non-member function pointers. As noted above, the prototype of the signal call requires a pointer to a function. However, this function must be either a non-member function or declared as static within the class. Stroustrup points out that a pointer to a member function is actually an offset and does not point directly to a piece of memory [2]. Since a static member function isn't associated with a specific instance of an object, its address can be used as an ordinary pointer. Taking this into consideration, cmd_handler was declared as a static member within the class to retain encapsulation.

The second step in the constructor is associating the signal with the specific instance of the class. To create this association, the constructor adds an element to the CmdControl::cmd_sigobjref map. This element must consist of a key and a value. The constructor uses the signal ID as the key and it uses the pointer to the current instance (this) as the value.

Finally, the constructor creates an instance of the CmdQMgr object and assigns its address to the pointer cmd_msg_object. If the CmdQMgr constructor fails, it throws an exception. The CmdControl constructor catches this exception and throws its own exception, a string with the contents "Error", to the calling application.

The CmdQMgr Class

Before describing how all of this comes together, I provide an overview of the CmdQMgr class. (See Listing 3, CmdQMgr.h, and Listing 4, CmdQMgr.c.) As I previously mentioned, the CmdControl class associates an application function with a symbolic string. CmdControl invokes the function when it receives the symbolic string in a message. It is the CmdQMgr class that links those messages between a separate user application and a process's instance of CmdControl created for a particular signal.

The basic building block for exchanging messages used by CmdQMgr is the Unix message queue IPC resource. Briefly, a Unix queue is created by a calling application, which provides the queue a unique key value. Messages on a queue have a basic structure, defined in the standard header file <sys/msg.h>:

struct  msgbuf{
    long type;
    char data[1];   
};

The type member provides an identification number to a message. In the implementation of this structure, the data can be one or more bytes in length. Using this model the following data type definition was created:

typedef struct {
    long type;
    char data[max_message + 1];
} msgbuff;

The message size for this application is set to the value of the constant max_message, also defined in the source file for CmdQMgr. The additional byte was added to support the null string terminator.

The CmdQMgr object can operate in one of two contexts. The first context is as a creator of a service. A creator controls the queue's existence. Any instance of CmdQMgr created by the CmdControl class acts as a creator of a service. The second context is that of the sender of a message. The sender merely identifies an existing queue to which it can send messages.

The context of a CmdQMgr instance is based on the constructor used. The first constructor format is used in the creator context. This constructor requires a single integer argument, which is used as a signal number. This signal number is the identification number of the signal that will be managed. The signal ID is assigned to the private member cmdqmgr_type. The instance of the CmdQMgr class created in this context will concern itself only with queue messages that have a type value equivalent to cmdqmgr_type.

The creator's constructor version gets the application's current process ID through the Unix system call getpid. The constructor creates a unique key identifier for the queue by adding the process ID to the constant queuebaseid. This key identifier is assigned to member cmdqmgr_key. Using the constant as a base should provide unique values within a given range to avoid conflicts with queue IDs for other applications on a Unix system. (However, this can never be absolutely guaranteed. There are methods to make the base configurable for a system. An example would be setting an environment value in a startup shell script and capturing it with a system call to getenv. I use this constant in this article for the sake of simplicity.)

Finally, the constructor invokes the Unix system call msgget. The last argument, IPC_CREAT ORed with the octal value 0666, requests the operating system to create a new queue using the ID provided in the first argument and to assign read and write permissions to the queue. After a successful creation, or if the queue already exists, the operating system returns a handle, which is assigned to CmdQMgr's private member cmdqmgr_queue_id.

Using the constructor prototyped to receive two parameters creates an instance of the CmdQMgr in the sender context. Applications establish an instance of CmdQMgr in the sender context to send signals and messages to processes using the CmdControl class. The first constructor parameter is the ID of the signal that will be sent to a process, which will also be incorporated into the msgbuff structure for the type of message that will be sent. The second parameter identifies the process the signal and messages will be sent to. Again, We have created an association!

The Signal Manager in Action

The source code for democ.c (Listing 5) exercises the features of the CmdControl class. The source code for tqsnd.c (Listing 6) provides an example of a separate application that sends messages to processes using the CmdControl class. The mechanics of implementing the CmdControl and CmdQMgr classes will be described through the use of these two listings.

democ.c provides two instances of the CmdControl class. One instance will manage messages for the process any time it receives the SIGUSR1 signal. The other instance will do the same for SIGUSR2. The separate instances for SIGUSR1 and SIGUSR2 signals are dynamically allocated with pointers assigned to usr1hdle and usr2hdle, respectively.

In this fictitious scenario, democ is an application that will provide run-time reporting capabilities whenever it receives the SIGUSR1 signal. Four separate functions can be invoked depending on the user's need. Each function is associated with a string using the set_action method of the CmdControl class. This method creates the association within CmdControl's embedded map member run_string. For this example, SIGUSR2 is used to invoke any one of four generic demonstration functions. I mention this to highlight the fact that multiple signals in an application can be managed with the CmdControl class, provided there is one instance of the class per signal. Any subsequent instantiations of the class for the same signal would reassign the value of the object pointer in the CmdControl constructor statement

cmd_sigobjref[cmd_signal] = this;

The democ application also exercises other member functions in the CmdControl class that manage the associations or provide information. Un-setting the previously made association between a function and the string "DEMO4" is done with a call to the unset_action member function. A test is performed with two calls to the member function is_used to verify that unset_action worked correctly, and that a remaining association for "REPORT" exists.

The tqsnd application (Listing 6) provides the user a front end to the democ application. With tqsnd, the user can request democ to invoke one of the specific services associated with a signal while it is running. tqsnd's usage is similar to the Unix "kill" utility. However, it requires three command-line arguments: the process ID, the signal that should be sent, and the message to send to the process. For example, if democ was running with a process ID of 4567 and I wanted it to produce a run-time diagnostic report, I would use tqsnd on the command line as follows. (Note: On my system, the literal value for the SIGUSR1 signal is defined as 10.)

tqsnd  4567 10 "REPORT"

An instance of the CmdQMgr object is created by tqsnd. Since CmdQMgr will be sending a request to a process to perform a service, tqsnd creates the object with the constructor that implies the sender context. The pointer Qobj maintains the reference to the CmdQMgr object.

To request the service, tqsnd invokes CmdQMgr's member function send_msg (Listing 2). send_msg requires a string argument, and sets up the message buffer by assigning the value of the signal to msgbuff.type and copying the string into msgbuff.data. send_msg calls the standard Unix system call msgsnd to place the message on the queue that was identified in the constructor. Immediately after that, send_msg calls the Unix system call kill, using the process ID and signal the CmdQMgr instance is associated with. Consequently, democ is interrupted.

Within the democ application (Listing 5), the CmdControl class static member function cmd_handler (Listing 2) is called through democ's object usr1hdle. The operating system provides the signal ID for the call. To ensure that the processing for the currently received signal is not interrupted for the same signal, cmd_handler makes a call to the signal with the SIG_IGN (as in ignore the signal) parameter.

Next, cmd_handler determines which instance of the CmdControl class is set up to manage this signal by first checking if the association was ever created. cmd_handler calls the find function on the map member cmd_sigobjref, using the signal ID as the search key. I could also have used the map subscript operation [] to search for the instance. Although it is not an error to use subscripting on a map for elements that may not exist, this can involve extra overhead if find is not used at first. Per Stroustrup, in the event an element does not exist for a subscripted value, a map container will create an element with a default value [2]. If several invalid subscripts were provided, the map container could become unnecessarily large.

If the association does not exist, cmd_handler generates an error message. If the association does exist, cmd_handler subscripts the map with the signal ID and assigns the address of the associated instance to the pointer cmd_objhandle.

cmd_handler then calls the run_cmd member function on this associated instance. This instance of CmdControl contains a pointer to a QmdQMgr object; run_cmd (Listing 2) calls the member function receive_msg (Listing 4) on this QmdQMgr object, which in turn invokes the Unix system call msgrcv. This call specifically requests the next message with a type value equivalent to the signal received. This message is passed back to run_cmd through the argument of receive_msg.

run_cmd uses this message string to search for an associated function pointer in the embedded map run_string. (Once again, I precede the subscript operation with a find.) If an association does exist, the address of the function to be invoked is assigned to the pointer run_function. The service requested is called with the following:

(*run_function)();

Upon completion of the service, control returns to CmdControl::cmd_handler. cmd_handler's last operation is to reset itself as the signal handler for the signal just received. If this isn't done, the operating system's default behavior for the signal will be invoked the next time the application is interrupted.

To exercise the applications, the shell script cmddemo.sh (Listing 7) was created with multiple calls to tqsnd. The calls exercise both of the signals set up for management within democ. Further, some messages for which democ associates no action are sent to test the error recovery ability of the CmdControl class. The script is executed from the command line with the process ID for the instance of democ of interest. For example, if the process ID is 5678, the command line would appear as follows:

cmddemo.sh 5678

Figure 1 shows the output of democ based on the messages it received from cmddemo.sh. The first two messages are due to the self checks within democ. The remaining messages have a one-to-one correspondence to the lines in the cmddemo.sh script.

Concluding Remarks

Although the democ application demonstrates the robustness of the CmdControl class in supporting multiple signals, you needn't use more than one instance of it in an application. Since one signal can now have multiple actions associated with it, multiple instances of the CmdControl class are unnecessary.

The listings in this article were designed to illustrate the basic approach to leveraging a Unix resource. This application could be expanded to include a more complex messaging system that would allow application-level parameters as well.

Most of the literature I have encountered that addresses the issue of incorporating function pointers with Unix system calls usually provides sample code of wrappers that are external to a class. CmdControl provides a technique to encapsulate the wrapper within the class.

Once again, the object-oriented approach provides a solution in which the interface can change with minimal impact on the implementation. In this case, the interface for exchanging messages can change without impacting the CmdControl class. Although the CmdQMgr class uses the standard message queue, it can be reconstructed to use sockets or any other form of IPC.

Bibliography

[1] Caldera's OpenLinux (v 1.2) manual pages.

[2] Bjarne Stroustrup. The C++ Programming Language (Addison-Wesley, 1997).

Patrick Bailey is a former U.S. Army officer and has spent the past 13 years in Iowa as a Senior Software Engineer with consulting companies such as Keane and CTG. He recently joined Meijer Stores of Grand Rapids, Michigan as a Senior Systems Analyst and Designer. He can be reached at patbly@radiks.net.