Inside Windows NT Services

Windows NT services are implemented as otherwise-normal executables that follow a specific protocol to allow proper interaction with the service control manager (SCM). Marshall discusses how you can build, create, and install Win32 services in Windows NT.


May 01, 1994
URL:http://www.drdobbs.com/windows/inside-windows-nt-services/184409235

Figure 1


Copyright © 1994, Dr. Dobb's Journal

MAY94: Inside Windows NT Services

Inside Windows NT Services

Somewhere between daemons and TSRs

Marshall Brain

Marshall works for Interface Technologies (Wake Forest, NC), which does software design, consulting, and programmer training in Windows NT, Motif, C++, and object-oriented design. He is the lead author for Prentice Hall's five-book series on Windows, which includes Win32 System Services: The Heart of Windows NT. He can be reached at [email protected].


Every operating system needs a way to execute background tasks that run continuously, regardless of who is using the machine. These background tasks can perform various services important to the system or its users. For example, a messaging system might monitor the network and display a dialog box whenever it receives a message from another machine. An application that sends and receives faxes needs to start up at boot time and then continuously monitor the fax modem for fax machines dialing in. A home or office security program, or code that controls a piece of test equipment, may need to poll sensors periodically and respond to them when appropriate. All of these tasks require CPU time to perform their jobs but should not affect a user working at the keyboard because they require so little of the total CPU power available.

In MS-DOS, background tasks like these are handled by terminate-and-stay-resident (TSR) programs. These programs are usually started in the AUTOEXEC.BAT file. In UNIX, background tasks are handled by daemons. Standard UNIX daemons such as cron or finger are usually started at the end of the UNIX boot sequence, before the system lets the first user log in. In Windows NT, background tasks are called "services" and can start automatically when NT boots; they remain running in the background regardless of who is logged in.

Windows NT services are implemented as otherwise-normal executables that follow a specific protocol allowing them to interact properly with the service control manager (SCM). In this article, I'll discuss how to create and install simple Win32 services in Windows NT. Once you understand simple services, it is easy to build your own, because all services, no matter how complicated, must contain the same basic SCM interface code. Once the basic requirements of the SCM protocol are met, there's no real difference between the executable for a service and that of a regular program.

A good working knowledge of NT services is important to both programmers and system administrators. Programmers obviously benefit because they can create their own services. The benefit to administrators is more subtle but equally important. Background tasks, in general, can be dangerous. Both MS-DOS and Macintosh systems make good viral hosts because, through their lack of security, they allow any person or program to create background tasks at any time. Windows NT and UNIX systems are secure, so only a system administrator can add background tasks to the system. However, if the administrator adds a destructive background task, then it is free to do its damage. The administrator who understands the mechanisms and privileges available to Windows NT services can be more selective in installing potentially harmful background tasks.

The Basics of NT Services

Services come in two different varieties. Driver services use device-driver protocols to interface NT to specific pieces of hardware. Win32 services, on the other hand, implement general background tasks using the normal Win32 API. This article focuses on Win32 services because of their general utility and ease of creation. Any NT programmer with the normal NT SDK (or Visual C++) and administrative access to an NT machine can implement and install Win32 services. If you need to create a program that starts at boot time and runs continuously as a background task in Windows NT, you will likely use the Win32 service protocol.

Services are accessible to user manipulation via the NT Control Panel, which has a Services applet that displays a list of all available Win32 services. This applet lets you start, stop, pause, and resume services. A second dialog, accessed by pressing the Start button, lets you change the startup behavior as well as the default account used by the service. A service can start automatically at boot time, it can be totally disabled, or it can be set to start manually. When starting a service manually, a user can supply startup parameters. You need to be logged in as the administrator or a power user to do anything with this applet.

Windows NT ships with a number of pre-installed services that handle such things as network messaging, command scheduling with the "at" command, and distributed RPC naming. When you create your own services, you must perform a separate installation step to insert them into the list managed by the services applet. The installation process adds information about a new service--its name, the name of its executable, its startup type, and the like--into the registry so that the SCM knows about the new service the next time the machine boots.

Creating a New Service

A program that acts as a service is a normal EXE file, but it must meet special requirements so that it interfaces properly with the SCM. The designers of NT have carefully choreographed the flow of function calls, and you must follow that plan closely or the service will not work. I'll summarize the flow here.

The first step in the SCM protocol is for your service to call the StartServiceCrtlDispatcher function. Your service should call this function right at the beginning of your program's main (or WinMain) routine. This function provides the SCM with the address of your ServiceMain function, which the SCM will call when it starts the service. (For details on the functions discussed here, including all function parameters, consult the Win32 programmer reference manuals or the api32wh.hlp help file. You can find additional information on services in my book, Win32 System Services: The Heart of Windows NT.)

The next step in the protocol is for the SCM to call your ServiceMain function when it wants to start the service. This happens, for example, when the system administrator presses the Start button in the Services applet; the SCM will execute ServiceMain in a separate thread. Your ServiceMain should call RegisterServiceCtrlHandler, which registers a Handler function with the SCM. This Handler function is used by the SCM for control requests. You can name the Handler function anything you like, but it is listed in the documentation under Handler. The RegisterServiceCtrlHandler function returns a handle that your service uses when sending status messages to the SCM.

Your ServiceMain function must also start the thread that does the actual work of the service. ServiceMain should not return until it's time for the service to stop. When it returns, the service has stopped.

The last major piece of the SCM protocol consists of your Handler function. This function usually contains a switch statement that parses control requests received from the SCM. By default, the SCM can send any of the requests in Table 1, each of which is identified by a defined constant. You can also specify custom constants (which have integer values ranging from 128 to 255) and send them through the SCM to the service.

A complete NT service, therefore, consists of an EXE containing the main, ServiceMain, and Handler functions, as well as a function that contains the thread for the service itself. Figure 1 summarizes the interactions between these different functions and the SCM.

A Simple Service

Listing One (page 100) shows the simplest possible service--a service that simply beeps. By default, it beeps every two seconds. You can optionally modify the beep interval with startup parameters. This service is complete in that it will appropriately respond to the SCM for every control signal possible. Because of that, you can use this code as a template for creating your own services.

The main function calls StartServiceCtrlDispatcher to register the ServiceMain function using an array of SERVICE_TABLE_ENTRY structures. In this case, the program contains just one service, so there is only one entry in the table. However, it is possible for you to implement several services within a single EXE file, and in such cases, the table identifies the appropriate ServiceMain function for each service.

Your initialization code can be placed in the main function prior to the call to StartServiceCtrlDispatcher, but if it does not complete in less than 30 seconds, the SCM aborts the service on the assumption that something went wrong.

The ServiceMain function gets called when the SCM wants to start the service--either during the boot process or because of a manual start. ServiceMain always contains the following steps:

  1. RegisterServiceCtrlHandler is called to register the Handler function with the SCM as this service's Handler function.
  2. The SendStatusToSCM function is called to notify the SCM of progress. The fourth parameter is a "click-count" value, which increments each time the program updates the status. The SCM and other programs can look at the click count and see that progress is being made during initialization. The last parameter is a "wait hint" that tells the SCM how long (in milliseconds) it should expect to wait before the click count gets updated again.
  3. ServiceMain creates an event that will be used at the bottom of the function to prevent it from returning until the SCM issues a STOP request.
  4. ServiceMain checks for startup parameters. These can be passed in by the user during a manual start (using the Startup Parameters line in the Service applet). Any parameters are passed to the ServiceMain function via an argv-style array.
  5. If your service needs to perform other initialization tasks, they should be placed here, just prior to the call to InitService.
  6. The InitService function is called, starting the thread that does the actual work of the service. If it succeeds, then ServiceMain lets the SCM know that the service has successfully started.
  7. ServiceMain now calls WaitForSingleObject, which waits efficiently for the terminateEvent event object to be set in the Handler function. Once it is, ServiceMain calls the terminate function to clean up and then returns to stop the service.
There isn't much flexibility in this sequence: With the exception of step #5, you must perform each of the tasks in the order indicated for the service to start properly.

The terminate function cleans up any open handles and sends a status message to the SCM to tell it that the service is stopped. The SCM calls the Handler function whenever it wants to pause, resume, interrogate, or stop the service. To stop the service, the handler sets terminateEvent. By doing this, it causes ServiceMain, which is executing as a separate thread, to terminate and return. Once ServiceMain returns, the service is stopped.

The SendStatusToSCM function consolidates all of the statements necessary to send the service's current status to the SCM. The InitService function gets called by ServiceMain when it needs to start the service's thread. This function calls CreateThread to create a new thread for the service.

ServiceThread contains the actual work to be performed by the service. In this case, the thread consists of an infinite loop that beeps and then sleeps for a predetermined interval. When creating your own services, you can place any code that you like in this thread, calling either Win32 functions or your own functions.

Installing and Removing Services

In order to use the beep service, you have to install it. Installation makes the SCM aware of the service and causes the SCM to add it to the list of services that appears in the Services applet of the Control Panel. INSTALL.CPP (Listing Two, page 101) demonstrates how to install a service. The code begins by opening a connection to the SCM using the OpenSCManager function. In the call to OpenSCManager, you must specify what you want to do so that the SCM can validate that activity. If the account you are logged in under does not have sufficient privilege, then the call will return NULL.

Installing the new service is accomplished by a call to CreateService. This call uses the pointer to the SCM returned by OpenSCManager; additional parameters include the name, label, and EXE file specified on the command line, along with a set of standard parameters to fill in all of the other values. The use of SERVICE_WIN32_OWN_PROCESS indicates that the service's EXE file contains just one service, and the SERVICE_

DEMAND_START parameter indicates that the service is started manually rather than automatically. A typical invocation of the install program from the command line might be: install BeepService "Beeper" c:\winnt\beep.exe.

The first parameter for this command specifies the name of the service used internally by the SCM. You will use this name later to remove the service. The second parameter specifies the label used to display the service in the Services applet. The third parameter gives the fully qualified path to the service's executable. After you install the service, start it using the Services applet in the Control Panel. You can look up the error codes in the online help file for the Win32 API.

To remove a service, follow the steps in REMOVE.CPP (Listing Three, page 101), which starts by opening a connection to the SCM. It then opens a connection to the service using the OpenService function and queries the service to find out if it is currently stopped. If it is not, REMOVE.CPP stops it. The DeleteService function removes the service from the Services applet in the Control Panel. The usual way of invoking the removal program is: remove BeepService.

Conclusion

Services are an essential part of Windows NT because they allow you to extend the operating system. Using my code as a template, you will find that it is easy to create new services of your own.

Table 1: Control requests sent by the SCM to your service.

======================================================================
 Request                                   Description
======================================================================
 SERVICE_CONTROL_STOP                      Tells service to stop.
 SERVICE_CONTROL_PAUSE                     Tells service to pause.
 SERVICE_CONTROL_CONTINUE                  Tells service to resume.
 SERVICE_CONTROL_INTERROGATE               Tells service to immediately report its status.
 SERVICE_CONTROL_SHUTDOWN                  Tells service shutdown is imminent.
======================================================================

Figure 1: The relationship between the SCM, the service's EXE, and the Install program.

[LISTING ONE]



//*************************************************************************
// BEEPSERV.CPP -- the simplest possible service for Windows NT. By Marshall
//  Brain. This service beeps every 2 seconds, or at a user-specified interval.
//  Code is also in the book "Win32 System Services: The Heart of Windows NT"
//*************************************************************************

#include <windows.h>
#include <stdio.h>
#include <iostream.h>
#include <stdlib.h>

#define DEFAULT_BEEP_DELAY 2000

//------- Global variables --------
char *SERVICE_NAME = "BeepService"; // The name of the service
HANDLE terminateEvent = NULL;       // for holding ServiceMain from completing
int beepDelay = DEFAULT_BEEP_DELAY; // The beep interval in ms.
BOOL pauseService   = FALSE;        // Flags holding current state of service
BOOL runningService = FALSE;
HANDLE threadHandle = 0;            // Thread for the actual work
SERVICE_STATUS_HANDLE serviceStatusHandle;  // Handle used to communicate
                                    // status info with the SCM. Created
                                    // by RegisterServiceCtrlHandler.
//------------------------------------------------------------------
void ErrorHandler(char *s, DWORD err)
{       cout << s << endl << "Error number: " << err << endl;
        ExitProcess(err);
}
//--- SendStatusToSCM() - Consolidates status updates sent via SetServiceStatus
BOOL SendStatusToSCM (DWORD dwCurrentState,
        DWORD  dwWin32ExitCode,
        DWORD  dwServiceSpecificExitCode,
        DWORD  dwCheckPoint,
        DWORD  dwWaitHint)
{       BOOL   success;
        SERVICE_STATUS serviceStatus;
                           // Fill in all of the SERVICE_STATUS fields
        serviceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
        serviceStatus.dwCurrentState = dwCurrentState;
                          // If in the process of doing something, then accept
                          // no control events, else accept anything.
        if (dwCurrentState == SERVICE_START_PENDING) {
                serviceStatus.dwControlsAccepted = 0;
        }
        else {  serviceStatus.dwControlsAccepted =
                             SERVICE_ACCEPT_STOP |
                             SERVICE_ACCEPT_PAUSE_CONTINUE |
                             SERVICE_ACCEPT_SHUTDOWN;
        }
        // If a specific exit code is defined, set up win32 exit code properly
        if (dwServiceSpecificExitCode == 0) {
                serviceStatus.dwWin32ExitCode = dwWin32ExitCode;
        }
        else {  serviceStatus.dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR;
        }
        serviceStatus.dwServiceSpecificExitCode = dwServiceSpecificExitCode;
        serviceStatus.dwCheckPoint = dwCheckPoint;
        serviceStatus.dwWaitHint   = dwWaitHint;
                               // Pass the status record to the SCM
        success = SetServiceStatus (serviceStatusHandle, &serviceStatus);
        return success;
}
//------------------------------------------------------------------
DWORD ServiceThread(LPDWORD param)
{       while (1)
        {       Beep(200,200);  Sleep(beepDelay);
        }
        return 0;
}
//---- InitService() -- initializes the service by starting its thread ----
BOOL InitService()
{       DWORD id;
        threadHandle = CreateThread(0, 0,    // Start the service's thread
                                   (LPTHREAD_START_ROUTINE) ServiceThread,
                                    0, 0, &id);
        if (threadHandle==0)       { return FALSE; }
        else { runningService = TRUE; return TRUE; }
}
//---- Handler()  -- dispatches events received from SCM ----
VOID Handler (DWORD controlCode)
{       DWORD currentState = 0;
        BOOL success;              // There is no START option because
        switch(controlCode)        // ServiceMain gets called on a start.
        {                                   // Stop the service.
        case SERVICE_CONTROL_STOP:          // Tell SCM what's happening.
                success = SendStatusToSCM(SERVICE_STOP_PENDING,
                                            NO_ERROR, 0, 1, 5000);
                runningService=FALSE;       // Set the event that is holding
                                            // ServiceMain, so that
                SetEvent(terminateEvent);   // ServiceMain can return.
                return;
        case SERVICE_CONTROL_PAUSE:          // Pause the service
                if (runningService && !pauseService)
                {                            // Tell SCM what's happening.
                        success = SendStatusToSCM( SERVICE_PAUSE_PENDING,
                                                   NO_ERROR, 0, 1, 1000);
                        pauseService = TRUE;
                        SuspendThread(threadHandle);
                        currentState = SERVICE_PAUSED;
                }

                break;
        case SERVICE_CONTROL_CONTINUE:       // Resume from a pause
                if (runningService && pauseService)
                {       // Tell the SCM what's happening
                        success = SendStatusToSCM( SERVICE_CONTINUE_PENDING,
                                                   NO_ERROR, 0, 1, 1000);
                        pauseService=FALSE;
                        ResumeThread(threadHandle);
                        currentState = SERVICE_RUNNING;
                }
                break;
        case SERVICE_CONTROL_INTERROGATE:      // Update current status
                break; // it will fall to bottom and send status
        case SERVICE_CONTROL_SHUTDOWN:
                // Do nothing in a shutdown. Could do cleanup but must be quick
                return;
        default: break;
        }
        SendStatusToSCM(currentState, NO_ERROR, 0, 0, 0);
}
//---- terminate() -- handle an error from ServiceMain by cleaning up
//----                and telling SCM that the service didn't start.
VOID terminate(DWORD error)
{                           // If terminateEvent has been created, close it.
        if (terminateEvent) CloseHandle(terminateEvent);
                            // Send a message to SCM to tell about stoppage.
        if (serviceStatusHandle) {
                SendStatusToSCM(SERVICE_STOPPED, error,0, 0, 0);
        }                   // If the thread has started, kill it off.
        if (threadHandle)   CloseHandle(threadHandle);
                            // Do not need to close serviceStatusHandle.
}
//---- ServiceMain is called when SCM wants to start service. When it returns,
// the service has stopped. It therefore waits on an event just before the end
// of the function, and that event gets set when it is time to stop. It also
// returns on any error because the service cannot start if there is an error.
VOID ServiceMain(DWORD argc, LPTSTR *argv)
{       BOOL success;             //
                                  // immediately call Registration function
        serviceStatusHandle = RegisterServiceCtrlHandler(SERVICE_NAME,Handler);
        if (!serviceStatusHandle) { terminate(GetLastError()); return; }
                                  // Notify SCM of progress
        success = SendStatusToSCM(SERVICE_START_PENDING, NO_ERROR, 0, 1, 5000);
        if (!success) { terminate(GetLastError()); return; }

                                  // create the termination event
        terminateEvent = CreateEvent (0, TRUE, FALSE, 0);
        if (!terminateEvent) { terminate(GetLastError()); return; }
                                  // Notify SCM of progress
        success = SendStatusToSCM(SERVICE_START_PENDING,
                                  NO_ERROR, 0, 2, 1000);
        if (!success) { terminate(GetLastError()); return; }
        if (argc == 2)            // Check for startup params
        {       int temp = atoi(argv[1]);
                if (temp < 1000)  beepDelay = DEFAULT_BEEP_DELAY;
                else              beepDelay = temp;
        }                         //
                                  // Notify SCM of progress
        success = SendStatusToSCM(SERVICE_START_PENDING, NO_ERROR, 0, 3, 5000);
        if (!success) { terminate(GetLastError()); return; }
                                  // Start the service itself
        success = InitService();  //
        if (!success) { terminate(GetLastError()); return; }
                                  // The service is now running.
                                  // Notify SCM of progress
        success = SendStatusToSCM(SERVICE_RUNNING, NO_ERROR, 0, 0, 0);
        if (!success)  { terminate(GetLastError()); return; }
                                  //
                                  // Wait for stop signal, and then terminate
        WaitForSingleObject (terminateEvent, INFINITE);
        terminate(0);
}
//------------------------------------------------------------------
VOID main(VOID)
{       SERVICE_TABLE_ENTRY serviceTable[] =
        {    { SERVICE_NAME, (LPSERVICE_MAIN_FUNCTION) ServiceMain },
             { NULL,                                   NULL        }};
        BOOL success;
        //
        // Register with the SCM
        success = StartServiceCtrlDispatcher(serviceTable);
        if (!success)  ErrorHandler("In StartServiceCtrlDispatcher",
                                                     GetLastError());
}


[LISTING TWO]



//-----------------------------------------------------------
// INSTALL.CPP -- this code installs a service. By Marshall Brain.
//-----------------------------------------------------------
#include <windows.h>
#include <iostream.h>
//-----------------------------------------------------------
void main(int argc, char *argv[])
{       SC_HANDLE newService, scm;
        if (argc != 4)
        {       cout << "Usage:\n";
                cout << "   install service_name service_label executable\n";
                cout << "         service_name is the name used internally"
                                                                  " by SCM\n";
                cout << "         service_label is the name that appears"
                                                  " in the Services applet\n";
                cout << "         (for multiple words, put them in"
                                                          " double quotes)\n";
                cout << "         executable is the full path to the EXE\n";
        cout << "\n";
                return;
        }
        // Open a connection to the SCM
        scm = OpenSCManager( 0, 0, SC_MANAGER_CREATE_SERVICE);
        if (!scm) ErrorHandler("In OpenScManager",GetLastError());
        // Install the new service
        newService = CreateService( scm, argv[1],    // eg "beep_srv"
                                 argv[2],            // eg "Beep Service"
                                 SERVICE_ALL_ACCESS,
                                 SERVICE_WIN32_OWN_PROCESS,
                                 SERVICE_DEMAND_START,
                                 SERVICE_ERROR_NORMAL,
                                 argv[3],      // eg "c:\winnt\xxx.exe"
                                 0, 0, 0, 0, 0);
        if (!newService) ErrorHandler("In CreateService", GetLastError());
        else             cout << "Service installed\n";
        // Clean up
        CloseServiceHandle(newService);
        CloseServiceHandle(scm);
}


[LISTING THREE]



//-------------------------------------------------------------
// REMOVE.CPP -- this code removes a service from the Services
// applet in the Windows NT Control Panel. By Marshall Brain.
//-------------------------------------------------------------
#include <windows.h>
#include <iostream.h>
//-----------------------------------------------------------
void main(int argc, char *argv[])
{       SC_HANDLE service, scm;
        BOOL success;
        SERVICE_STATUS status;
        if (argc != 2)
        {       cout << "Usage:\n   remove service_name\n"; return;
        }
        // Open a connection to the SCM
        scm = OpenSCManager(0, 0, SC_MANAGER_CREATE_SERVICE);
        if (!scm) ErrorHandler("In OpenScManager", GetLastError());
        // Get the service's handle
        service = OpenService(scm, argv[1], SERVICE_ALL_ACCESS | DELETE);
        if (!service)  ErrorHandler("In OpenService", GetLastError());
        // Stop the service if necessary
        success = QueryServiceStatus(service, &status);
        if (!success)
                ErrorHandler("In QueryServiceStatus", GetLastError());
        if (status.dwCurrentState != SERVICE_STOPPED)
        {       cout << "Stopping service...\n";
                success = ControlService(service,  SERVICE_CONTROL_STOP,
                                                               &status);
                if (!success) ErrorHandler("In ControlService",
                                                         GetLastError());
        }
        // Remove the service
        success = DeleteService(service);
        if (success) cout << "Service removed\n";
        else ErrorHandler("In DeleteService", GetLastError());
        // Clean up
        CloseServiceHandle(service);
        CloseServiceHandle(scm);
}


Copyright © 1994, Dr. Dobb's Journal

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.