One major difference between Windows and Unix-based operating systems
is the support for handling programmer-defined signals. Although the
Standard C library provides some basic support for signal handling [2],
it is not enough for the programmer who needs signals mainly for
implementing interprocess communication. Indeed, the absence of such a
mechanism in the context of Windows makes the asynchronous
communication between the processes (threads) difficult and requires
the usage of special data structures, like events, and the creation of
dedicated threads that poll continuously the status of some conditions
[6]. In this artice, I introduce SignalsLib, a library for the support
of signal handling on Win32 programming platforms. The heart of the
library is a device driver that provides the mechanisms needed to cause
a signal handler in a target process to execute asynchronously, even if
the target process is not in an alertable state.
This article provides a general overview of signal handling, an outline
of the support Windows provides for signals and some basic mechanisms,
the design and implementation of the library and driver, performance
measurements of the implemented mechanisms, an API that is exported to
the programmer, and possible uses and extensions of this work.
Overview of Signals
Signals are a mechanism for informing processes about asynchronous
events. The basic idea is that each distinct signal has an associated
integer code, and any process can register a handler (a callback
function) for any signal (by specifying its integer code). When one
process sends a specific signal to another process that has registered
a handler for that signal, the target process will stop whatever it is
doing and execute the handler function registered for that signal.
The signal mechanism is analogous to interrupt handling, in that a
signal interrupts whatever code is currently executing in the target
process. Just like interrupt handlers, signal handlers require careful
coding — ordinary code must not access the same data as signal handlers
unless they both use synchronization primitives to avoid corrupting
each other’s work. Signals provide simple and convenient interprocess
communication. Common traditional uses include: notifying a service
that it should rotate its log files, notifying a parent process that
the child process has completed initialization and is ready to start
doing useful work, notifying a process that it should temporarily pause
operation, notifying a process that it should perform a clean shutdown
as soon as possible, and so on.
Each signal is associated with an action that will be executed by the
kernel on behalf of the process that has received the signal. For most
signals, the default action is the termination of the process, although
a process can require some alternative action from the system. The
various possible alternatives are:
1. Ignore the signal. In this case, the process is not informed about the signal.
2. Restore the default signal action.
3. Execute a specific signal-handling function. This is the case in
which the process wants to perform some custom action when a specific
signal arrives. The process registers a special function that gets
invoked asynchronously when the associated signal occurs. After the
signal handling function returns, whatever code it interrupted will
resume executing.
Windows Support for Signals
Win32 offers a very specific kind of signal support in the form of SetConsoleCtrlHandler().
This function lets a console process catch a variety of
system-generated signals (the user pressed Ctrl-C, the user is logging
off, etc.). It does not offer any programmer-defined signals, nor does
it offer any interprocess communication — it is strictly a means for
the operating system to notify a process of a few specific events.
The only other signal-like mechanism that Windows offers is structured
exception handling. However, Standard C requires support for the
well-known signal()/raise() Unix functions and a constrained number of signals [2]. signal() sets the signal-handling function to be called for a given signal. raise()
sends a specific signal to the current process, invoking either
whatever handler was registered for that signal, or else the default
action associated with that signal. Again, these signals are not
extensible, and they are local to a given process. (Standard C defines
no function for sending a signal to another process.)
Instead of signals, Windows supports APCs (Asynchronous Procedure
Calls). An APC is a kernel-defined control object that represents a
procedure that is called asynchronously. APCs have the following
features [7]:
1. An APC always runs in a specific thread context.
2. An APC runs at OS predetermined times.
3. APCs can preempt the currently running thread.
4. APC routines can themselves be preempted.
There are three different types of APCs in the kernel [2,3]:
User-mode APCs. User-mode APCs are, by default, disabled; that
is, they are queued to the user-mode thread, but are not executed,
except at well-defined points in the program. Specifically, they can be
executed only when an application has either called a wait service and
has enabled alerts to occur or, called the test-alert service.
Normal kernel-mode APCs. These are much like user-mode APCs,
except that they are executable by default. That is, they are enabled
except when the thread is already executing a kernel-mode APC or is
inside a critical code section.
Special kernel-mode APCs. These cannot be blocked, except by
running at a raised IRQL (interrupt request level). Special kernel-mode
APCs are executed at IRQL APC_LEVEL, in kernel mode. They are
used by the system to force a thread to execute a procedure in a
thread’s context. A special kernel-mode APC can preempt the execution
of a normal kernel-mode APC.
The Win32 API [4] provides QueueUserAPC(), which allows
an application to queue an APC object to a thread. The queuing of an
APC is a request for the thread to call the APC function. When a
user-mode APC is queued, the thread is not directed to call the APC
function unless it is in an alertable state. Unfortunately, a thread
enters an alertable state only by using one of the following Win32 API
functions: SleepEx(), SignalObjectAndWait(), WaitForSingleObjectEx(), WaitForMultipleObjectEx(), or MsgWaitForMultipleObjectsEx().
In kernel mode [1], the programmer initializes an APC object using KeInitializeApc(),
defining the target thread, one kernel-mode and one user-mode callback
routine, the type of the APC (kernel or user), and finally an argument
to these two functions. Next, the APC is queued (KeInsertQueueApc()) to the target thread and can be executed without having the thread be in an alertable state.
The SignalsLib Interface
The library provides the necessary structures and mechanisms for
elementary support of signal handling exporting an appropriate
interface to the programmer. During the building process, the user can
define the option for support per thread or the usage of a global table
of signal handlers.
signals.h (Listing 1) defines the available
signals, from zero to MAX_SIGNALS-1. This header file also defines
the two functions that make up the interface to the signals library.
SetSignalHandler() sets a handler for a given signal. It returns zero for failure, or non-zero for success.
SendSignalToThread() sends a signal to the specified thread. You
must specify the handle of the thread you want to signal, as well as
the signal (integer code) you want to send. It returns zero for
failure, or non-zero for success.
testapp.c (Listing 2) demonstrates how
to use the signals library. This application creates a thread that sets a
handler for a specific signal, and the main thread then sends that signal
to the thread, resulting in the execution of the installed signal handler.
Of course, the driver must be installed into the system and loaded, or else
an appropriate message is printed, when the library’s DLLMain()
is executed.
Design and Implementation
SignalsLib consists of a DLL and a kernel-mode device driver. The DLL
provides a user-mode interface for applications, while the device
driver is necessary for accessing the kernel-mode functions used to
queue a kernel-mode APC to the target thread. The calling application
simply makes calls to SetSignalHandler() and SendSignalToThread(), however — the DLL hides all the details of communicating with the device driver.
SetSignalHandler() is simple — it just stores the supplied function
pointer in the appropriate position of a global array of signal handlers.
When a signal actually occurs, the internal routine SignalsDriverRoutine()
will get invoked and use this global array to determine what signal handler
to call. Both of these routines are in signals.c (Listing
3).
SendSignalToThread() is where the DLL communicates with the device driver. DllMain() obtains a handle for the device driver when the DLL is first loaded and releases that handle when the DLL is unloaded. SendSignalToThread() uses that handle in a call to DeviceIoControl() to pass the device driver a SIGINFO structure:
typedef struct _SIGINFO
{
HANDLE hThread; /* target thread */
ULONG SigNo; /* signal number */
ULONG SigFunc; /* address of DriverRoutine */
} SIGINFO, *PSIGINFO;
Note that SigFunc is not the address of an individual signal handler, but rather the address of SignalsDriverRoutine(), a function in the DLL that looks up and invokes the correct signal handler.
When SendSignalToThread() passes this information to DeviceIoControl(),
it will cause the driver’s interrupt service routine to be invoked. The
main source code for the driver is in sigdrv.c (Listing
4). The driver’s interrupt service routine invokes SigDriverSendTheSignal(),
which is responsible for queuing an appropriate kernel-mode APC to the target
thread. SigDriverSendTheSignal() takes a pointer to the ETHREAD
data structure [2] of the target thread. It calls KeInitializeApc()
to initialize a kernel-mode APC and then calls KeInsertQueueApc() to
queue that APC for the target thread.
The queued APC contains a pointer to another function in sigdrv.c
(Listing 4): UserApcCallBack(). This function
will get invoked in user mode and passed the SIGINFO structure. UserApcCallBack()
uses the information in SIGINFO to invoke the DLL function SignalsDriverRoutine(),
which will in turn look up and invoke the signal handler associated with the
given signal.
Performance Evaluation
The choice of normal rather than special kernel-mode APCs has to do
with the desired functionality, not with performance. If it’s important
that the signal handler be preempted if another signal occurs, then you
should use special kernel-mode APCs rather than normal kernel-mode APCs.
The APC mechanism performs extremely well; the signal handler is
invoked as soon as the target thread is scheduled, typically within a
few microseconds. It is important to mention that kernel-mode APCs
achieve immediate signal delivery, independently of the system load
(number of threads that run on the system). You might be able to reduce
the response time further via thread priorities. For example, SendSignalToThread() could increase the priority of the target thread.
Conclusions
I’ve implemented the basic mechanisms of user-defined signals, mainly
in order to enable asynchronous communication between threads of the
same or different Win32 applications. The resulting DLL and device
driver combination provides the two major functions for signal
handling, similar to the signal() and kill() Unix system calls, and for signals similar to SIGUSR1 and SIGUSR2.
A possible extension of this library includes the implementation of the handlers for some Unix signals, like SIGSTOP, SIGCONT, and SIGTERM,
and the support of the POSIX standard. It would also be interesting to
integrate these mechanisms with the Standard C library. The library
assists in implementing the pthread_kill() POSIX function and
makes easier the development of applications that require notification
mechanisms between their threads in user mode or from kernel to user
mode, as the device driver needs only to know the address of the driver
routine. Although the POSIX standard defines global signal handlers for
all the threads of an application, the library can easily support
per-thread signal handlers by using thread local storage; for
simplicity, that was not done in this version.
To keep the code simple for publication, this implementation assumes
that all participating processes will share a single instance of signals.dll. More specifically, SendSignalToThread() always passes the address of SignalsDriverRoutine()
(in the context of the calling process) to the device driver, but the
device driver will attempt to use that address in the context of the
target process (which will likely be different). That will be
disastrous if the target process loaded signals.dll at a different address than the calling process did. If a particular process cannot load signals.dll
at its default address, you can always select another default address
until you find one that does not collide with anything already loaded.
You could solve the problem more elegantly by revising the interface so
that the device driver has enough information to locate the correct
callback address for a given thread.
Finally, an alternative implementation, which is also not presented here, is based on user-mode APCs. SendSignalToThread() uses the Win32’s QueueUserAPC()
and next, through a kernel-mode device driver, manages to set the
target thread in alertable state. This is possible by setting to one
the memory address that corresponds to 0x4a bytes offset [5] from the base address of the ETHREAD data structure [2].
References
[1] E. N. Dekker and J. M. Newcomer. Developing Windows NT Device Drivers: A Programmer’s Handbook (Addison-Wesley, 1999).
[2] Microsoft Corporation. Microsoft Developer Network Library, msdn.microsoft.com/library.
[3] D. A. Solomon. Inside Windows NT, Second Edition (Microsoft Press, 1998).
[4] J. Ritcher. Advanced Windows: The Professional Developer’s Guide to the Win32 API for Windows NT 4.0 and Windows 95 (Microsoft Press, 1995).
[5] www.cmkrnl.com/arc-userapc.html
[6] www.microsoft.com/msj/0799/nerd/nerd0709.html
[7] www.osr.com/insider/1998/apc.html
[8] Microsoft Visual Studio\VC98\CRT\SRC\WINSIG.C
Panagiotis Hadjidoukas is a postgraduate student at High
Performance Information Systems Laboratory, Department of Computer
Engineering & Informatics, at the University of Patras in Greece.
You can reach him by email at peh@hpclab.ceid.upatras.gr or at the web
page of his laboratory at http://www.hpclab.ceid.upatras.gr.