Windows服务程序的一种简捷实现方法

时间:2022-04-12 13:42:35

Windows服务程序的一种简捷实现方法

1.   代码编写

Windows 服务被 设计 用于需要在后台运行的应用程序以及实现没有用户交互的任务。为了学习这种控制台应用程序的基础 知识 C (不是C++ )是最佳选择 。本文将建立并实现一个简单的服务程序,其功能是查询系统中可用物理内存数量,然后将结果写入一个文本文件。 最后 ,你可以用所学知识编写自己的 Windows 服务。

  MSDN 上一篇 Nigel Thompson 写的文章:“Creating a Simple Win32 Service in C++ 这篇文章 ,附带一个 C++ 例子。虽然这篇文章很好地解释了服务的 开发 过程,但是,我仍然感觉缺少我需要的重要信息 我想理解通过什么框架,调用什么函数,以及何时调用,但 C++ 在这方面没有让我轻松多少。面向对象的方法固然方便,但由于用类对底层 Win32 函数调用进行了封装,它不利于学习服务程序的基本知识。这就是为什么我觉得 C 更加适合于编写初级服务程序或者实现简单后台任务的服务。在你对服务程序有了充分透彻的理解之后,用 C++ 编写才能游刃有余。当我离开原来的工作岗位,不得不向另一个人转移我的知识的时候,利用我用 C 所写的例子就非常容易解释 NT 服务之所以然。  

  本文将首先解释如何创建 一个定期查询可用物理内存并将结果写入某个文本文件的服务。然后完成生成,安装和实现服务的整个过程。

1.1.  主函数和全局定义

首先,包含所需的头文件。例子要调用 Win32 函数(windows.h )和磁盘文件写入(stdio.h ):

#include <windows.h>

#include <stdio.h>

接着,定义两个常量:

#define SLEEP_TIME 5000

#define LOGFILE "C://MyServices//memstatus.txt"

SLEEP_TIME 指定两次连续查询可用内存之间的毫秒间隔。在第二步中编写服务工作循环的时候要使用该常量。

LOGFILE 定义日志文件的路径,你将会用 WriteToLog 函数将内存查询的结果输出到该文件,WriteToLog 函数定义如下:

int WriteToLog(char* str)

{

    FILE* log;

    log = fopen(LOGFILE, "a+");

    if (log == NULL)

    return -1;

    fprintf(log, "%s/n", str);

    fclose(log);

    return 0;

}

声明几个全局变量,以便在程序的多个函数之间共享它们值。此外,做一个函数的前向定义:

 

SERVICE_STATUS ServiceStatus;

SERVICE_STATUS_HANDLE hStatus;

 

void ServiceMain(int argc, char** argv);

void ControlHandler(DWORD request);

int InitService();

  现在,准备工作已经就绪,你可以开始编码了。服务程序控制台程序的一个子集。因此,开始你可以定义一个 main 函数,它是程序的入口点。对于服务程序来说,main 的代码令人惊讶地简短,因为它只创建分派表并启动控制分派机。

 

void main()

{

    SERVICE_TABLE_ENTRY ServiceTable[2];

    ServiceTable[0].lpServiceName = "MemStatus ";

    ServiceTable[0].lpServiceProc = (LPSERVICE_MAIN_FUNCTION)ServiceMain;

   

    ServiceTable[1].lpServiceName = NULL;

    ServiceTable[1].lpServiceProc = NULL;

 

    // 启动服务的控制分派机线程

    StartServiceCtrlDispatcher(ServiceTable);

}

  一个程序可能包含若干个服务。每一个服务都必须列于专门的分派表中(为此该程序定义了一个 ServiceTable 结构数组)。这个表中的每一项都要在 SERVICE_TABLE_ENTRY 结构之中。它有两个域:

1)         lpServiceName: 指向表示服务名称字符串的指针;当定义了多个服务时,那么这个域必须指定;

2)         lpServiceProc: 指向服务主函数的指针(服务入口点);

  分派表的最后一项必须是服务名和服务主函数域的 NULL 指针,文本例子程序中只宿主一个服务,所以服务名的定义是可选的。

  服务控制管理器(SCM Services Control Manager)是一个管理系统所有服务的进程。当 SCM 启动某个服务时,它等待某个进程的主线程来调用 StartServiceCtrlDispatcher 函数。将分派表传递给 StartServiceCtrlDispatcher 。这将把调用进程的主线程转换为控制分派器。该分派器启动一个新线程,该线程运行分派表中每个服务的 ServiceMain 函数(本文例子中只有一个服务)分派器还监视程序中所有服务的执行情况。然后分派器将控制请求从 SCM 传给服务。

注意:如果 StartServiceCtrlDispatcher 函数30 秒没有被调用,便会报错。为了避免这种情况,我们必须在 ServiceMain 函数中(参见本文例子)或在非主函数的单独线程中初始化服务分派表。本文所描述的服务不需要防范这样的情况。

分派表中所有的服务执行完之后(例如,用户通过“ 服务” 控制面板程序停止它们),或者发生错误时。StartServiceCtrlDispatcher 调用返回。然后主进程终止。  

Windows服务程序的生命周期时序图

Windows服务程序的一种简捷实现方法

1.2.  ServiceMain 函数

  Listing 1 展示了 ServiceMain 的代码。该函数是服务的入口点。它运行在一个单独的线程当中,这个线程是由控制分派器创建的。ServiceMain 应该尽可能早的为服务注册控制处理器。这要通过调用 RegisterServiceCtrlHadler 函数来实现。你要将两个参数传递给此函数:服务名和指向 ControlHandlerfunction 的指针。

  它指示控制分派器调用 ControlHandler 函数处理 SCM 控制请求。注册完控制处理器之后,获得状态句柄(hStatus )。通过调用 SetServiceStatus 函数,用 hStatus SCM 报告服务的状态。

Listing 1 展示了如何指定服务特征和其当前状态来初始化 ServiceStatus 结构,ServiceStatus 结构的每个域都有其用途:

²  dwServiceType :指示服务类型,创建 Win32 服务。赋值 SERVICE_WIN32

²  dwCurrentState :指定服务的当前状态。因为服务的初始化在这里没有完成,所以这里的状态为 SERVICE_START_PENDING

²  dwControlsAccepted :这个域通知 SCM 服务接受哪个域。本文例子是允许 STOP SHUTDOWN 请求。处理控制请求将在第三步讨论;

²  dwWin32ExitCode dwServiceSpecificExitCode :这两个域在你终止服务并报告退出细节时很有用。初始化服务时并不退出,因此,它们的值为 0

²  dwCheckPoint dwWaitHint :这两个域表示初始化某个服务进程时要30 秒以上。本文例子服务的初始化过程很短,所以这两个域的值都为 0

  调用 SetServiceStatus 函数向 SCM 报告服务的状态时。要提供 hStatus 句柄和 ServiceStatus 结构。注意 ServiceStatus 一个全局变量,所以你可以跨多个函数使用它。ServiceMain 函数中,你给结构的几个域赋值,它们在服务运行的整个过程中都保持不变,比如:dwServiceType

  在报告了服务状态之后,你可以调用 InitService 函数来完成初始化。这个函数只是添加一个说明性字符串到日志文件。如下面代码所示:

// 服务初始化

int InitService()

{

    int result;

    result = WriteToLog("Monitoring started.");

    return(result);

}

  在 ServiceMain 中,检查 InitService 函数的返回值。如果初始化有错(因为有可能写日志文件失败),则将服务状态置为终止并退出 ServiceMain

 

error = InitService();

if (error)

{

    // 初始化失败,终止服务

    ServiceStatus.dwCurrentState = SERVICE_STOPPED;

    ServiceStatus.dwWin32ExitCode = -1;

    SetServiceStatus(hStatus, &ServiceStatus);

    // 退出 ServiceMain

    return;

}

如果初始化成功,则向 SCM 报告状态:

// SCM 报告运行状态

ServiceStatus.dwCurrentState = SERVICE_RUNNING;

SetServiceStatus (hStatus, &ServiceStatus);

接着,启动工作循环。每五秒钟查询一个可用物理内存并将结果写入日志文件。

Listing 1 所示,循环一直到服务的状态为 SERVICE_RUNNING 或日志文件写入出错为止。状态可能在 ControlHandler 函数响应 SCM 控制请求时修改。

1.3.  处理控制请求

  在第二步中,你用 ServiceMain 函数注册了控制处理器函数。控制处理器与处理各种 Windows 消息的窗口回调函数非常类似。它检查 SCM 发送了什么请求并采取相应行动。

  每次你调用 SetServiceStatus 函数的时候,必须指定服务接收 STOP SHUTDOWN请求。Listing 2 示范了如何在 ControlHandler 函数中处理它们。

  STOP 请求是 SCM 终止服务的时候发送的。例如,如果用户在“ 服务” 控制面板中手动终止服务。SHUTDOWN 请求是关闭机器时,由 SCM 发送给所有运行中服务的请求。两种情况的处理方式相同:

写日志文件,监视停止;

SCM 报告 SERVICE_STOPPED 状态;

  由于 ServiceStatus 结构对于整个程序而言为全局量,ServiceStatus 中的工作循环在当前状态改变或服务终止后停止。其它的控制请求如:PAUSE CONTINUE 在本文的例子没有处理。

控制处理器函数必须报告服务状态,即便 SCM 每次发送控制请求的时候状态保持相同。因此,不管响应什么请求,都要调用 SetServiceStatus

CSDN 的博客上找到了相关的完整源码,来自http://blog.csdn.net/birdme007/archive/2008/04/11/2282792.aspx

这个源码没有说明应该引用的lib 文件(StartServiceCtrlDispatcher 等函数) ,并且定义的LOGFILE 变量中的字符串的转义符稍微有点问题。

因此,我改正了这两点,并将源码贴出来,整个源码的权利属于原作者,我只是稍作修改而已。对于关键部分没有任何改变。原理部分请参见上边文字或者MSDN.

 

#include <windows.h>

#include <stdio.h>

#pragma comment(lib, "Advapi32")

#define SLEEP_TIME 5000

#define LOGFILE _T(".//memstatus.txt")

#include <tchar.h>

 

SERVICE_STATUS svcStat = { 0 };

SERVICE_STATUS_HANDLE hSvcStat = NULL;;

void ServiceMain(int argc, TCHAR** argv);

void ControlHandler(DWORD dwRequest);

bool InitService();

int WriteToLog(TCHAR* ptszStr);

 

int main(int argc, TCHAR* argv[])

{

    SERVICE_TABLE_ENTRY svcTableArr[2] = { 0 };

    svcTableArr[0].lpServiceName = _T("MemStatus ");

    svcTableArr[0].lpServiceProc = (LPSERVICE_MAIN_FUNCTION)ServiceMain;

 

    svcTableArr[1].lpServiceName = NULL;

    svcTableArr[1].lpServiceProc = NULL;

    // 启动服务的控制分派机线程

    StartServiceCtrlDispatcher(svcTableArr);

}

 

void ServiceMain(int argc, TCHAR** argv)

{

    Sleep(20000);

    bool bErr;

 

    svcStat.dwServiceType =

        SERVICE_WIN32;

    svcStat.dwCurrentState =

        SERVICE_START_PENDING;

    svcStat.dwControlsAccepted   = 

        SERVICE_ACCEPT_STOP |

        SERVICE_ACCEPT_SHUTDOWN;

    svcStat.dwWin32ExitCode = 0;

    svcStat.dwServiceSpecificExitCode = 0;

    svcStat.dwCheckPoint = 0;

    svcStat.dwWaitHint = 0;

 

    hSvcStat = RegisterServiceCtrlHandler(

        _T("MemStatus "),

        (LPHANDLER_FUNCTION)ControlHandler);

    if (hSvcStat == (SERVICE_STATUS_HANDLE)0)

    {

        // Registering Control Handler failed

        return;

    } 

    // Initialize Service

    bErr = InitService();

    if (!bErr)

    {

        // Initialization failed

        svcStat.dwCurrentState =

            SERVICE_STOPPED;

        svcStat.dwWin32ExitCode = -1;

        SetServiceStatus(hSvcStat, &svcStat);

        return;

    }

    // We report the running status to SCM.

    svcStat.dwCurrentState =

        SERVICE_RUNNING;

    SetServiceStatus (hSvcStat, &svcStat);

 

    MEMSTATUS  memStat;

    TCHAR tszLogEntryArr[16] = { 0 };

    int iRet = 0;

    // The worker loop of a service

    while (svcStat.dwCurrentState ==

        SERVICE_RUNNING)

    {

        GlobalMemStatus (&memStat);

        _stprintf_s(tszLogEntryArr, _T("%d"), memStat.dwMemoryLoad);

        iRet = WriteToLog(tszLogEntryArr);

        if (iRet)

        {

            svcStat.dwCurrentState =

                SERVICE_STOPPED;

            svcStat.dwWin32ExitCode      = -1;

            SetServiceStatus(hSvcStat,

                &svcStat);

            return;

        }

        Sleep(SLEEP_TIME);

    }

    return;

}

 

void ControlHandler(DWORD dwRequest)

{

    switch(dwRequest)

    {

    case SERVICE_CONTROL_STOP:

        WriteToLog(_T("/nMonitoring stopped."));

        svcStat.dwWin32ExitCode = 0;

        svcStat.dwCurrentState = SERVICE_STOPPED;

        SetServiceStatus (hSvcStat, &svcStat);

        return;

 

    case SERVICE_CONTROL_SHUTDOWN:

        WriteToLog(_T("/nMonitoring stopped."));

        svcStat.dwWin32ExitCode = 0;

        svcStat.dwCurrentState = SERVICE_STOPPED;

        SetServiceStatus (hSvcStat, &svcStat);

        return;

 

    default:

        break;

    }

 

    // Report current status

    SetServiceStatus (hSvcStat, &svcStat);

 

    return;

}

 

int WriteToLog(TCHAR* ptszStr)

{

    FILE* pFILELog = NULL;

    errno_t err;

    err = _tfopen_s(&pFILELog, LOGFILE, _T("a+"));

    if (pFILELog == NULL)

        return -1;

    _ftprintf(pFILELog, _T("%s "), ptszStr);

    fclose(pFILELog);

    return 0;

}

bool InitService()

{

    WriteToLog(_T("Monitoring started./n"));

    return true;

} 

2.   编译

同普通app, Build生成.exe文件。

3.   安装、卸载

 本文例子创建的文件叫 Memtatus.exe ,将它拷贝到 C:/MyServices 文件夹。为了在机器上安装这个服务,需要用 SC.EXE 可执行文件,它是 Win32 Platform SDK 中附带的一个工具。(译者注:Visaul Studio 2005 IDE 环境中也有这个工具,具体存放位置在:C:/Program Files/Microsoft Visual Studio 8/Common7/Tools/Bin/winnt )。使用这个实用工具可以安装和移除服务。其它控制操作将通过服务控制面板来完成。以下是用命令行安装 Memtatus 服务的方法:

sc create MemStatus binpath= c:/MyServices/MemStatus.exe

  发出此创建命令。指定服务名和二进制文件的路径(注意 binpath= 和路径之间的那个空格)。安装成功后,便可以用服务控制面板来控制这个服务。用控制面板的工具栏启动和终止这个服务。

 MemStatus的启动类型是手动,也就是说根据需要来启动这个服务。右键单击该服务,然后选择上下文菜单中的“ 属性” 菜单项,此时显示该服务的属性窗口。在这里可以修改 启动类型以及其它设置。你还可以从“ 常规” 标签中启动/ 停止服务。以下是从系统中移除服务的方法:

sc delete MemStatus

指定 delete 选项和服务名。此服务将被标记为删除,下次系统重启后,该服务将被完全移除。

4.   运行、停止、暂停、重启

服务是一个运行在后台并实现勿需用户交互的任务的控制台程序。Windows NT/2000/7 操作系统提供为服务程序提供专门的支持。人们可以用服务控制面板来配置安装好的服务程序,也就是 Windows 2000/XP 控制面板| 管理工具中的“ 服务” (或在“ 开始”| 运行” 对话框中输入 services.msc /s—— 译者注)。可以将服务配置成操作系统启动时自动启动,这样你就不必每次再重启系统后还要手动启动服务。

Win7下的任务管理器,“服务”标签的邮件菜单里有服务管理项,但是似乎不起作用。

5.   测试

  从服务控制面板启动 MemStatus  服务。如果初始化不出错,表示启动成功。过一会儿将服务停止。检查一下 C:/MyServices 文件夹中 memstatus.txt 文件的服务输出。在我的机器上输出是这样的:

Monitoring started.

273469440

273379328

273133568

273084416

Monitoring stopped.

  为了测试 MemStatus  服务在出错情况下的行为,可以将 memstatus.txt 文件设置成只读。这样一来,服务应该无法启动。

  去掉只读属性,启动服务,在将文件设成只读。服务将停止执行,因为此时日志文件写入失败。如果你更新服务控制面板的内容,会发现服务状态是已经停止。

假设我们编译好了程序,在d 盘,名为MemStatus.exe ,使用SC.exe 工具(VS2005 Microsoft Visual Studio 8/Common7/Tools/Bin/winnt )命令行下:

sc create MemStatus  binPath= d:/MemStatus.exe

向服务列表添加一个服务名为MemStatus,注意option 的等号后边有一个空格。sc 还有其他的option ,我没有研究了。

查看“ 服务” ,可以看到MemStatus  这个服务,开启之后就会看到F 盘的产生了一个memstatus.txt 文件,其中记录着剩余内存情况。

使用sc delete MemStatus  命令可以将该服务从服务里表中删除(关闭服务之后)。

6.   调试

服务程序的调试,与普通app不同。它不能直接F5 debug, 需要绑定到进程。

6.1.  下断点

首先,在IDE中,找到你要调试的地方,加上断点,然后安装好服务。
服务的安装,其实就是设计到注册表的操作。在.NET平台下,我一般习惯用installutil这个程序来安装服务。安装好之后,我们打开控制面板的“Administrator Tools”,找到"Services"选项,双击打开。在服务列表中,我们能找到我们安装的服务。然后运行这个服务。

6.2.  服务启动后暂停,以便于调试

现在到了关键的地方,在服务开始的时候,就会执行你的代码。为了调试的方便,我是在服务的代码开始处让,程序先Sleep20秒,这样的好处看后面你就知道了。

6.3.  将服务绑定到进程

切换回我们的IDE,在Debug菜单,里面有个Attach to process(绑定到进程)选项。点击打开,在对话框的进程列表中,选择我们的服务相对应的进程,然后点击旁边的Attach(附加)按钮。这样,IDE就开始加载我们要调试的进程了。加载完毕之后,程序会停顿一下。(为什么?因为我们在服务的开始代码处,Sleep20秒)之后,黄色的光标,跳动到你的断点处,现在就可以像以往一样,来调试这个windows服务了。
之所以在程序开始的地方Sleep20秒,是为我们在IDE附加这个Service进程的时候,留下一段缓冲的时间。不然当你的服务开始运行的时候,你的断点处的代码早就被执行过了,你断点自然不起作用了。

7.   问题及解决

1)         服务程序修改后,Rebuild失败

Ø  我是Rebuild前忘了先停止服务。由于服务的调试不能直接从VS IDE启动,而是需要先从SCM启动服务,所以比较容易忽视这一点。看VSoutput信息。

1>MemStatus : error PRJ0008 : Could not delete file 'd:/WindowsServiceSample/Debug/MemStatus.exe'.

1>Make sure that the file is not open by another process and is not write-protected.

2)         程序无法进入调试状态。

Ø  注意是否debug版本的程序。和普通app一样,服务程序也必须是debug版才能调试。

3)         安装、卸载失败

Ø  我是因为安装时忘了先卸载掉旧版本的服务。看SC.exe命令的错误提示。

[SC] CreateService FAILED 1073:

4)         服务启动失败

Ø  多半是程序写的有问题。我是因为调试时在ServiceMain的开头加了DebugBreak,忘了删除。结果安装上以后,启动时进度条走到中间就弹错误了。