The Windows Service Application Architecture

[Previous] [Next]

In this section, I explain the additional infrastructure that turns a server application into a service, thus allowing your application to be remotely administered. I've found Microsoft's service architecture to be a little difficult to understand at first. The difficulty is due to the fact that every service process always contains at least two threads, and these threads must communicate with one another. So you have to deal with thread synchronization issues and interthread communication issues.

Another issue you need to consider is that a single executable file can contain several services. If you look back at Table 3-1, you'll see that many services are contained inside the Services.exe file. Most of these services (such as DHCP Client, Messenger, and Alerter) are fairly simple in their implementation. It would be very inefficient if each of these services had to run as a separate process, with its own address space and additional process overhead. Because of this overhead, Microsoft allows a single executable to contain several services. The Services.exe file actually contains about 20 different services inside of it, including the three just mentioned.

When designing a service executable, you must concern yourself with three kinds of functions:

  • Process's entry-point function This function is your standard (w)main or (w)WinMain function with which you should be extremely familiar by now. For a service, this function initializes the process as a whole and then calls a special function that connects the process with the local system's SCM. At this point, the SCM takes control of your primary thread for its own purposes. Your code will regain control only when all of the services in the executable have stopped running.
  • Service's ServiceMain function You must implement a ServiceMain function for each service contained inside your executable file. To run a service, the SCM spawns a thread in your process that executes your ServiceMain function. When the thread returns from ServiceMain, the SCM considers the service stopped. Note that this function does not have to be called ServiceMain; you can give it any name you desire.
  • Service's HandlerEx function Each service must have a HandlerEx function associated with it. The SCM communicates with the service by calling the service's HandlerEx function. The code in the HandlerEx function is executed by your process's primary thread. The HandlerEx function either executes the necessary action, or it must communicate the SCM's instructions to the thread that is executing the service's ServiceMain function by using some form of interthread communication. Note that each service can have its own HandlerEx function, or multiple services (in a single executable) can share a single HandlerEx function. One of the parameters passed to the HandlerEx function indicates which service the SCM wishes to communicate with. Note that this function does not have to be called HandlerEx; you can give it any name you desire.

Figure 3-7 will help you put this architecture in perspective. It shows the functions necessary to implement a service executable that houses two services as well as the lines of interprocess communication (IPC) and interthread communication (ITC). In the upcoming sections, I will examine these three functions in detail and flesh out exactly what their responsibilities are. I recommend that you refer to this figure while reading.

click to view at full size.

Figure 3-7. Windows service application architecture

The Process Entry-Point Function: (w)main or (w)WinMain

When an administrator wants to start a service, the SCM determines whether or not the executable file containing the service is already running. If it is not running, the SCM spawns the executable file. The process's primary thread is responsible for performing the process-wide initialization. (Service-specific initialization should be done in the appropriate service's ServiceMain function.)

After the process is initialized, the entry-point function must contact the SCM, which now takes over control of the process. To contact the SCM, the entry-point function must first allocate and initialize an array of SERVICE_TABLE_ENTRY structures:

 typedef struct _SERVICE_TABLE_ENTRY {    PTSTR                   lpServiceName;   // Service's internal name    LPSERVICE_MAIN_FUNCTION lpServiceProc;   // Service's ServiceMain } SERVICE_TABLE_ENTRY, *LPSERVICE_TABLE_ENTRY; 

The first member indicates the internal, programmatic name of the service, and the second member is the address of the service's ServiceMain callback function. If the executable houses just one service, the array of SERVICE_TABLE_ENTRY structures must be initialized as follows:

 SERVICE_TABLE_ENTRY ServiceTable[] = {    { TEXT("ServiceName1"), ServiceMain1 },    { NULL, NULL }   // Marks end of array }; 

If your executable contains three services, you must initialize the array like this:

 SERVICE_TABLE_ENTRY ServiceTable[] = {    { TEXT("ServiceName1"), ServiceMain1 },    { TEXT("ServiceName2"), ServiceMain2 },    { TEXT("ServiceName3"), ServiceMain3 },    { NULL, NULL }   // Marks end of array }; 

The last structure in the array must have both members set to NULL to indicate the end of the array. Now the process connects itself to the SCM by calling StartServiceCtrlDispatcher:

 BOOL StartServiceCtrlDispatcher(    CONST SERVICE_TABLE_ENTRY* pServiceTable); 

Calling this function and passing in the address of the service table array is how the executable process indicates which services are contained within the process. At this point, the SCM knows which service it was trying to start and iterates through the array looking for it. Once the service is found, a thread is created and begins executing the service's ServiceMain function (whose address is obtained from the SERVICE_TABLE_ENTRY array).

NOTE
The SCM keeps close tabs on how a service is doing. For example, when the SCM spawns a service executable, the SCM waits for the primary thread in the executable to call StartServiceCtrlDispatcher. If StartServiceCtrlDispatcher is not called within 30 seconds, the SCM thinks that the service executable is malfunctioning and calls TerminateProcess to forcibly kill the process. For this reason, if your process requires more than 30 seconds to initialize, you must spawn another thread to handle the initialization so that the primary thread can quickly call StartServiceCtrlDispatcher. Note that I'm discussing process-wide initialization here. Individual services should initialize themselves using their own ServiceMain functions.

StartServiceCtrlDispatcher does not return until all services in the executable have stopped running. While at least one service is running, the SCM controls what the process's primary thread executes. Usually, this thread has nothing to do and just sleeps, not wasting precious CPU time. If the administrator attempts to start another service implemented in the same executable, the SCM does not spawn another instance of the executable. Instead, the SCM communicates to the executable's primary thread and has it iterate the list of services again, this time looking for the service that is being started. Once found, a new thread is spawned, which executes the appropriate service's ServiceMain function.

NOTE
When determining whether to spawn a new service process or a new thread in an existing service process, the SCM performs a strict comparison of the service pathname strings. For example, say that two services are implemented in a single executable file, MyServices.exe. The first service is added to the SCM's database using an executable pathname of "%windir%\System32\MyService.exe", but the second service is added to the database using "C:\WinNT\System32\MyService.exe". If both services are started, the SCM will spawn two separate processes, both of them running the same MyService.exe service application. To ensure the SCM uses a single process for all services in a single executable file, use the same pathname string when adding the services to the SCM's database.

Internally, the system is keeping track of which services within the process are executing. When each service exits (usually because the ServiceMain function returns), the system checks to see whether any services are still running. If no services are running, then and only then does the entry-point function's call to StartServiceCtrlDispatcher return. Your code should perform any process-wide cleanup, and then the entry-point function should return, causing the process to terminate. Note that you must complete your clean-up code in 30 seconds or the SCM kills the process.

The ServiceMain Function

Each service in the executable file must have its own ServiceMain function:

 VOID WINAPI ServiceMain(    DWORD  dwArgc,    PTSTR* pszArgv); 

The SCM starts a service by creating a new thread; this thread begins its execution with the ServiceMain function. As I mentioned earlier, I call this particular function ServiceMain, but the function can have any name you choose. The name you choose for the function is not important because you pass its address in the SERVICE_TABLE_ENTRY's lpServiceProc member. However, you can't have two ServiceMain functions with the same name in a single executable file; if you do, the compiler or linker will generate an error when you try to build your project.

Two parameters are passed to a ServiceMain function. These parameters create a mechanism that allows an administrator to start a service with some command-line parameters via the StartService function (discussed in the next chapter). Personally, I don't know of any service that references these parameters, and I encourage you to ignore them. Having a service configure itself by reading settings out of the following registry subkey is better than using parameters passed to ServiceMain. (The ServiceName portion of the key should be replaced with the actual name of the service.)

 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\ServiceName\Parameters 

Many services ship with a client application that allows an administrator to configure the service's settings. The client application simply saves these settings in the registry subkey. When the service starts, it retrieves the settings from the registry.

If a service is running when its configuration changes, three options are available to it:

  • The service ignores the changed configuration settings until the next time the service starts. This is the simplest choice, and many services existing today have taken this approach.
  • The service can be explicitly told that it should reconfigure itself. An SCP does this by calling the ServiceControl function, passing the SERVICE_CONTROL_PARAMCHANGE value. Chapter 4 describes how to do this.
  • The service can call the RegNotifyChangeKeyValue function to receive a notification when an external application has changed its registry settings. This allows a service to reconfigure itself on the fly. The RegNotify sample application in Chapter 5 shows how to accomplish this.

The first task that the ServiceMain function must perform is telling the SCM the address of the service's HandlerEx callback function. It does this by calling RegisterServiceCtrlHandlerEx:

 SERVICE_STATUS_HANDLE RegisterServiceCtrlHandlerEx(    PCTSTR                pszServiceName, // Service's internal name    LPHANDLER_FUNCTION_EX pfnHandler,     // Service's HandlerEx function    PVOID                 pvContext);     // User-defined value 

The first parameter indicates the service for which you are setting a HandlerEx function, and the second parameter is the address of the HandlerEx function. The pszServiceName parameter must match the name used when the array of SERVICE_TABLE_ENTRYs was initialized and passed to StartServiceCtrlDispatcher. The last parameter, pvContext, is a user-defined value that is passed to the service's HandlerEx function. I'll discuss the HandlerEx function in the next section.

RegisterServiceCtrlHandlerEx returns a SERVICE_STATUS_HANDLE, which is a value that uniquely identifies the service to the SCM. All future communication from the service to the SCM will require this handle instead of the service's internal string name.

NOTE
Unlike most handles in the system, the handle returned from RegisterServiceCtrlHandlerEx is never closed by you.

After RegisterServiceCtrlHandlerEx returns, the ServiceMain thread should immediately tell the SCM that the service is continuing to initialize. It does this by calling the SetServiceStatus function:

 BOOL SetServiceStatus(    SERVICE_STATUS_HANDLE hService,     LPSERVICE_STATUS      pServiceStatus); 

This function requires that you pass it the handle identifying your service (which is returned from the call to RegisterServiceCtrlHandlerEx) and the address of an initialized SERVICE_STATUS structure:

 typedef struct _SERVICE_STATUS {    DWORD dwServiceType;    DWORD dwCurrentState;     DWORD dwControlsAccepted;     DWORD dwWin32ExitCode;     DWORD dwServiceSpecificExitCode;     DWORD dwCheckPoint;     DWORD dwWaitHint;  } SERVICE_STATUS, *LPSERVICE_STATUS; 

The SERVICE_STATUS structure contains seven members that reflect the current status of the service. All of these members, described in the following list, must be set correctly before you pass the structure to SetServiceStatus.

  • dwServiceType This member indicates what type of service executable you have implemented. Set this member to SERVICE_WIN32_OWN_PROCESS when your executable houses a single service, or to SERVICE_WIN32_SHARE_PROCESS when your executable houses two or more services. In addition to these two flags, you can OR in the SERVICE_INTERACTIVE_PROCESS flag when your service needs to interact with the desktop. (You should avoid interactive services as much as possible.) The value of dwServiceType should never change during the lifetime of your service.
  • dwCurrentState This member is the most important member of the SERVICE_STATUS structure. It tells the SCM the current state of your service. To report that your service is still initializing, you should set this member to SERVICE_START_PENDING. I'll explain the other possible values when we talk about the HandlerEx function in the section "Codes Requiring Status Reporting."
  • dwControlsAccepted This member indicates what control notifications the service is willing to accept. If you allow a service control program to pause and continue your service, specify SERVICE_ACCEPT_PAUSE_CONTINUE. Many services do not support pausing and continuing; you have to decide if this functionality makes sense for your service. If you allow a service control program to stop your service, specify SERVICE_ACCEPT_STOP. If you want your service to be notified when the operating system is being shut down, specify SERVICE_ACCEPT_SHUTDOWN. You can also indicate whether you want to receive parameter change, hardware profile change, and power event notifications by specifying SERVICE_ACCEPT_PARAMCHANGE, SERVICE_ACCEPT_HARDWAREPROFILECHANGE, or SERVICE_ACCEPT_POWEREVENT, respectively.
  • Use the OR operator to combine the desired set of flags. Note that your service can change the controls it accepts while it is running. For example, I have written services that allowed themselves to be paused as long as no clients were connected to them.

  • dwWin32ExitCode and dwServiceSpecificExitCode These two members allow the service to report error codes. If a service wants to report a Win32 error code (as defined in WinError.h), it sets the dwWin32ExitCode member to the desired code. A service can also report errors that are specific to the service and do not map to a predefined Win32 error code. To make the service do this, you must set the dwWin32ExitCode member to ERROR_SERVICE_SPECIFIC_ERROR and then set the dwServiceSpecificExitCode member to the service-specific error code. Note that a customized SCP would be required to report this error code. Set the dwWin32ExitCode member to NO_ERROR when the service is running normally and has no error to report.
  • dwCheckPoint and dwWaitHint These members allow a service to report its progress. When you set dwCurrentState to SERVICE_START_PENDING, you should set dwCheckPoint to 1 and set dwWaitHint to the number of milliseconds required for the service to reach its next SetServiceStatus call. Once the service is fully initialized, you should re-initialize the SERVICE_STATUS structure's members so that dwCurrentState is SERVICE_RUNNING, and then set both dwCheckPoint and dwWaitHint to 0.
  • The dwCheckPoint member exists for your own benefit. It allows a service to report how far its processing has progressed. Each time you call SetServiceStatus, you should increment dwCheckPoint to a number that indicates what "step" your service has executed. It is totally up to you how frequently to report your service's progress. If you do decide to report each step of your service's initialization, the dwWaitHint member should be set to indicate how many milliseconds you think you need to reach the next step (checkpoint)—not the number of milliseconds required for the service to complete its processing.

NOTE
The ServiceMain function must call SetServiceStatus within 80 seconds of starting, or the SCM thinks that the service has failed to start. If no other services are running in the service process, the SCM kills the process.

NOTE
Just before creating your service's thread, the SCM sets your service's status to indicate a current state of START_PENDING, a checkpoint of 0, and a wait hint of 2000 milliseconds. If your ServiceMain function requires more than 2000 milliseconds to initialize, the very first time you call SetServiceStatus, you should indicate a current state of SERVICE_START_PENDING, a checkpoint of 1, and a wait hint as desired. Notice that the checkpoint should be set to 1. A very common mistake developers make is setting the checkpoint to 0 the first time they call SetServiceStatus, which can confuse the administrator's SCP program by causing it to believe the service is not responding properly. If your service requires more initialization, you can continue to report a state of SERVICE_START_PENDING, incrementing the checkpoint and setting the wait hint as desired.

After your service's initialization is complete, your service calls SetServiceStatus to indicate SERVICE_RUNNING (with the checkpoint and wait hint both set to 0). Now your service is running. Usually a service runs by placing itself in a loop. Inside the loop, the service thread suspends itself, waiting for a network request or a notification indicating that the service should pause, continue, stop, shut down, and so on. If a network request comes in, the service thread wakes up, processes the request, and loops back around to wait for the next request or notification.

If the service wakes because of a notification, it processes the notification. If the service receives a stop or shutdown notification, the loop is terminated, and the service's ServiceMain function returns, killing the thread. If the service is the last service running, the process also terminates.

NOTE
The SetServiceStatus function examines the SERVICE_STATUS structure's dwCurrentState member. If this member is set to SERVICE_STOPPED, SetServiceStatus closes the service's status handle (which is the first parameter to SetServiceStatus). This is why you never have to explicitly close the handle returned from RegisterServiceCtrlHandlerEx and, even more importantly, why you should never call SetServiceStatus after you have called it with a current state of SERVICE_STOPPED. Attempting to do so will raise an invalid handle exception when running the service under a debugger.

The HandlerEx Function

Each service in the executable file has to be associated with a HandlerEx function:

 DWORD WINAPI HandlerEx(    DWORD dwControl,    DWORD dwEventType,    PVOID pvEventData,    PVOID pvContext); 

I call this function HandlerEx, but the function can have any name you choose. The actual name is not important because you pass the function's address as a parameter to the RegisterServiceCtrlHandlerEx function. However, you can't have two HandlerEx functions with the same name in a single executable file; if you do, the compiler or linker will generate an error when you try to build your project.

Most of the time, the process's primary thread is suspended inside the call to StartServiceCtrlDispatcher. When an SCP program wants to control a service, the SCM communicates the control to the process's primary thread. The thread wakes up and calls the appropriate service's HandlerEx function. For example, when an administrator uses the Services snap-in to stop a service, the snap-in communicates the administrator's desire to the local or remote SCM. The SCM then wakes the service executable's primary thread, which calls the service's HandlerEx function, passing it a SERVICE_CONTROL_STOP control code.

The SCM also sends device, hardware profile, and power event notifications to a service's HandlerEx function. These notifications allow the service to reconfigure itself appropriately and to participate in granting or denying system changes.

NOTE
Because the process's primary thread executes every service's HandlerEx function, you should implement these HandlerEx functions so that they execute quickly. Failure to do so will prevent other services in the same process from receiving their desired actions in a reasonable amount of time.

Since the primary thread executes the HandlerEx function but the service is executed by another thread, it might be necessary for HandlerEx to communicate actions or notifications to the service thread. There is no standard way to perform this communication; the method really depends on how you implement the service. You can queue an asynchronous procedure call (APC), post an I/O completion status, post a window message, or whatever. I recommend that you choose a queuing mechanism such as the ones just mentioned to avoid synchronizing the HandlerEx thread with the ServiceMain thread. I always handle this communication by posting an I/O completion status.

The HandlerEx function is passed four parameters. The first parameter, dwControl, indicates the requested action or notification. If dwControl identifies a device, hardware profile, or power event notification, the dwEventType and pvEventData parameters offer more specific information about the action or notification. The pvContext parameter is simply the user-defined value originally passed to the RegisterServiceCtrlHandlerEx function. Using this value, you can create a single HandlerEx function that is used by all services in a single executable; the pvContext value could be used to determine the specific service that the HandlerEx function needs to communicate with. In the next section, I'll talk about how to handle these control codes and notifications.

The HandlerEx function's return value allows a service's handler to return some information back to the SCM. If the HandlerEx function doesn't handle a particular control code, return ERROR_CALL_NOT_IMPLEMENTED. If the HandlerEx function handles a device, hardware profile, or power event request, return NO_ERROR. To deny a request, return any other Win32 error code. For any other control codes, the HandlerEx function should return NO_ERROR.



Programming Server-Side Applications for Microsoft Windows 2000
Programming Server-Side Applications for Microsoft Windows 2000 (Microsoft Programming)
ISBN: 0735607532
EAN: 2147483647
Year: 2000
Pages: 126

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net