Windows Services 开发系列 (一) -- 开篇编写一个简单的Service程序

Windows Service 开发系列 (一) – 开篇编写一个简单的Service程序

【1】Windows Service 开发 – 本示例使用的WIN API详解:

SERVICE_STATUS 结构体:

typedef struct _SERVICE_STATUS {
DWORD dwServiceType;
DWORD dwCurrentState;
DWORD dwControlsAccepted;
DWORD dwWin32ExitCode;
DWORD dwServiceSpecificExitCode;
DWORD dwCheckPoint;
DWORD dwWaitHint;
} SERVICE_STATUS, *LPSERVICE_STATUS;
该结构体包含一个Service的所有状态信息;
成员说明:
dwServiceType:Service的类型;
取值:
SERVICE_FILE_SYSTEM_DRIVER:该服务为文件系统驱动;
SERVICE_KERNEL_DRIVER:该服务为设备驱动;
SERVICE_WIN32_OWN_PROCESS:该服务以自己的进程独立运行;
SERVICE_WIN32_SHARE_PROCESS:该服务与其他服务共享进程;
SERVICE_USER_OWN_PROCESS:该服务在用户登陆账号下以自己的进程独立运行;
SERVICE_USER_SHARE_PROCESS:该服务在用户登陆账号下与其他服务共享进程;
SERVICE_INTERACTIVE_PROCESS:该服务可以与桌面交互;

dwCurrentState:服务的当前状态;
取值:
SERVICE_CONTINUE_PENDING:服务的持续状态是待定的;
SERVICE_PAUSE_PENDING:服务的终止状态是待定的;
SERVICE_PAUSED:服务终止;
SERVICE_RUNNING:服务运行;
SERVICE_START_PENDING:服务正在启动;
SERVICE_STOP_PENDING:服务正在停止;
SERVICE_STOPPED:服务停止;

dwControlsAccepted:服务接受以及在处理函数中处理的控制码;
取值:
SERVICE_ACCEPT_NETBINDCHANGE:该服务是一个网络成分,可在不停止和重启的情况下接受绑定的变化;
SERVICE_ACCEPT_PARAMCHANGE:该服务可以在不停止和重启的情况下重新读取其启动参数;
SERVICE_ACCEPT_PAUSE_CONTINUE:该服务可以被暂停和继续;
SERVICE_ACCEPT_PRESHUTDOWN:该服务可执行预关闭任务;
SERVICE_ACCEPT_SHUTDOWN:当系统关闭时通知该服务;
SERVICE_ACCEPT_STOP:该服务可以被停止;
SERVICE_ACCEPT_HARDWAREPROFILECHANGE:
SERVICE_ACCEPT_POWEREVENT:
SERVICE_ACCEPT_SESSIONCHANGE:
SERVICE_ACCEPT_TIMECHANGE:
SERVICE_ACCEPT_TRIGGEREVENT:
SERVICE_ACCEPT_USERMODEREBOOT:

dwWin32ExitCode:错误码,用于在服务启动或关闭时报错;
dwServiceSpecificExitCode:特定于服务的错误码,用于在服务启动或关闭时报错;
dwCheckPoint:检测点值,服务周期性的增加该值,以报告服务在一段长期时间内所做的启动,停止,暂停,继续的处理;The check-point value the service increments periodically to report its progress during a lengthy start, stop, pause, or continue operation.
dwWaitHint:以毫秒为单位的估计时间,用于估计启动、停止、暂停、继续操作的时间;The estimated time required for a pending start, stop, pause, or continue operation, in milliseconds.

RegisterServiceCtrlHandler:服务用该方程注册一个函数处理服务的控制请求;
SERVICE_STATUS_HANDLE RegisterServiceCtrlHandler(
LPCTSTR ,
LPHANDLER_FUNCTION
);
LPCTSTR:指针指向一个空字符结尾的字符串,该字符串代表由线程运行的服务的名称;
LPHANDLER_FUNCTION:指向被注册的处理函数;

SetServiceStatus:为调用的服务更新服务状态管理器的状态信息;
BOOL SetServiceStatus(
SERVICE_STATUS_HANDLE hServiceStatus,
LPSERVICE_STATUS lpServiceStatus
);
hServiceStatus:当前服务状态信息结构的句柄,该句柄由RegisterServiceCtrlHandler函数返回;
lpServiceStatus:指向包含被调用的服务的最新的状态信息的服务状态结构体;

MEMORYSTATUS:此结构包含了物理内存和虚拟内存的细节;
dwLength:MEMORYSTATUS结构的大小,在调GlobalMemoryStatus函数前用sizeof()函数求得,用来供函数检测结构的版本。
dwMemoryLoad:返回一个介于0~100之间的值,用来指示当前系统内存的使用率。
dwTotalPhys:返回总的物理内存大小,以字节(byte)为单位。
dwAvailPhys:返回可用的物理内存大小,以字节(byte)为单位。 
dwTotalPageFile:显示可以存在页面文件中的字节数。注意这个数值并不表示在页面文件在磁盘上的真实物理大小。   
dwAvailPageFile:返回可用的页面文件大小,以字节(byte)为单位。   
dwTotalVirtual:返回调用进程的用户模式部分的全部可用虚拟地址空间,以字节(byte)为单位。   
dwAvailVirtual:返回调用进程的用户模式部分的实际自由可用的虚拟地址空间,以字节(byte)为单位。

SERVICE_TABLE_ENTRY:
typedef struct _SERVICE_TABLE_ENTRYA {
LPSTR lpServiceName;
LPSERVICE_MAIN_FUNCTIONA lpServiceProc;
} SERVICE_TABLE_ENTRYA, *LPSERVICE_TABLE_ENTRYA;

lpServiceName:在服务处理中运行的服务名称;
lpServiceProc:指向ServiceMain函数的指针;

StartServiceCtrlDispatcher:绑定
BOOL StartServiceCtrlDispatcher(
CONST SERVICE_TABLE_ENTRYA *lpServiceStartTable
);

【2】服务的组成部分:
(1)Service Control Manager(SCM),每个Windows系统都有一个SCM,SCM存在于Service.exe中,在Windows启动的时候会自动运行,伴随着操作系统的启动和关闭而产生和终止。这个进程以系统特权运行,并且提供一个统一的、安全的手段去控制服务。SCM包含一个储存着已安装的服务和驱动程序的信息的数据库,通过SCM可以统一的、安全的管理这些信息,因此一个服务程序的安装过程就是将自身的信息写入这个数据库。
(2)服务本身,一个服务拥有能从SCM收到信号和命令所必需的的特殊代码,并且能够在处理后将它的状态回传给SCM。
(3)Service Control Dispatcher(SCP),它是一个拥有用户界面,允许用户开始、停止、暂停、继续,并且控制一个或多个安装在计算机上服务的Win32应用程序。SCP的作用是与SCM通讯,Windows管理工具中的“服务”就是一个典型的SCP。

【3】设计服务的几个重要函数:
(1)WinMain:入口函数,负责初始化整个进程,由这个进程中的主线程来执行。这意味着它应用于这个可执行文件中的所有服务。要知道,一个可执行文件中能够包含多个服务以使得执行更加有效。主进程通知SCM在可执行文件中含有几个服务,并且给出每一个服务的ServiceMain回调(Call Back)函数的地址。一旦在可执行文件内的所有服务都已经停止运行,主线程就在进程终止前对整个进程进行清除。
(2)ServiceMain:这个函数由操作系统调用,并执行能完成服务的代码。一个专用的线程执行每一个服务的ServiceMain函数,每个服务也都拥有与自己唯一对应的ServiceMain函数。当主线程调用Win32函数StartServiceCtrlDispatcher的时候,SCM为这个进程中的每一个服务产生一个线程。这些线程中的每一个都和它的相应的服务的ServiceMain函数一起执行,这就是服务总是多线程的原因——一个仅有一个服务的可执行文件将有一个主线程,其它的线程执行服务本身。
(3)CtrlHandler:一个回调函数,用户必须为它的服务程序中每一个服务写一个单独的CtrlHandler函数,因此如果有一个程序含有两个服务,那么它至少要拥有5个不同的函数:作为入口点的main()或WinMain(),用于第一个服务的ServiceMain函数和CtrlHandler函数,以及用于第二个服务的ServiceMain函数和CtrlHandler函数。
SCM调用一个服务的CtrlHandler函数去改变这个服务的状态。例如,当某个管理员用管理工具里的“服务”尝试停止你的服务的时候,你的服务的CtrlHandler函数将收到一个SERVICE_CONTROL_STOP通知。CtrlHandler函数负责执行停止服务所需的一切代码。由于是进程的主线程执行所有的CtrlHandler函数,因而必须尽量优化你的CtrlHandler函数的代码,使它运行起来足够快,以便相同进程中的其它服务的CtrlHandler函数能在适当的时间内收到属于它们的通知。而且基于上述原因,你的CtrlHandler函数必须要能够将想要传达的状态送到服务线程,这个传递过程没有固定的方法,完全取决于你的服务的用途。
【4】服务设计的说明:
【4.1】入口函数的说明
(1)在入口函数里面要完成ServiceMain的初始化,准确点说是初始化一个SERVICE_TABLE_ENTRY结构数组,这个结构记录了这个服务程序里面所包含的所有服务的名称和服务的进入点函数;
(2)数组的地址被传递到StartServiceCtrlDispatcher函数,StartServiceCtrlDispatcher为每一个传递到它的数组中的非空元素产生一个新的线程,每一个进程开始执行由数组元素中的lpServiceStartTable指明的ServiceMain函数。
SCM启动一个服务程序之后,它会等待该程序的主线程去调StartServiceCtrlDispatcher。如果那个函数在两分钟内没有被调用,SCM将会认为这个服务有问题,并调用TerminateProcess去杀死这个进程。这就要求你的主线程要尽可能快的调用StartServiceCtrlDispatcher。StartServiceCtrlDispatcher函数则并不立即返回,相反它会驻留在一个循环内。当在该循环内时,StartServiceCtrlDispatcher悬挂起自己,等待下面两个事件中的一个发生。
第一,如果SCM要去送一个控制通知给运行在这个进程内一个服务的时候,这个线程就会激活。当控制通知到达后,线程激活并调用相应服务的CtrlHandler函数。CtrlHandler函数处理这个服务控制通知,并返回到StartServiceCtrlDispatcher。StartServiceCtrlDispatcher循环回去后再一次悬挂自己。
第二,如果服务线程中的一个服务中止,这个线程也将激活。在这种情况下,该进程将运行在它里面的服务数减一。如果服务数为零,StartServiceCtrlDispatcher就会返回到入口点函数,以便能够执行任何与进程有关的清除工作并结束进程。如果还有服务在运行,哪怕只是一个服务,StartServiceCtrlDispatcher也会继续循环下去,继续等待其它的控制通知或者剩下的服务线程中止。
【4.2】ServiceMain函数说明:
(1)函数设置一个服务最好的方法就是设置注册表,一般服务在
HKEY_LOCAL_MACHINESYSTEM CurrentControlSetService ServiceName Parameters
子键下存放自己的设置,这里的ServiceName是服务的名字。事实上,可能要写一个客户应用程序去进行服务的背景设置,这个客户应用程序将这些信息存在注册表中,以便服务读取。当一个外部应用程序已经改变了某个正在运行中的服务的设置数据的时候,这个服务能够用RegNotifyChangeKeyValue函数去接受一个通知,这样就允许服务快速的重新设置自己。
(2)ServiceMain首先是必不可少的两项工作,一、调用RegisterServiceCtrlHandler函数去通知SCM它的CtrlHandler回调函数的地址;二、在RegisterServiceCtrlHandler函数返回后,ServiceMain线程要立即告诉SCM服务正在继续初始化。具体的方法是通过调用SetServiceStatus函数传递SERVICE_STATUS数据结构。该函数要求传递给它指明服务的句柄(刚刚通过调用RegisterServiceCtrlHandler得到),和一个初始化的SERVICE_STATUS结构的地址;三、在服务的所有初始化都完成之后,服务调用SetServiceStatus指明SERVICE_RUNNING,在那一刻服务已经开始运行。通常一个服务是把自己放在一个循环之中来运行的,在循环的内部这个服务进程悬挂自己,等待指明它下一步是应该暂停、继续或停止之类的网络请求或通知;当一个请求到达的时候,服务线程激活并处理这个请求,然后再循环回去等待下一个请求/通知。如果一个服务由于一个通知而激活,它会先处理这个通知,除非这个服务得到的是停止或关闭的通知。如果真的是停止或关闭的通知,服务线程将退出循环,执行必要的清除操作,然后从这个线程返回。当ServiceMain线程返回并中止时,引起在StartServiceCtrlDispatcher内睡眠的线程激活,并像在前面解释过的那样,减少它运行的服务的计数。
【4.3】CtrlHandler函数说明:
(1)当调用RegisterServiceCtrlHandler函数时,SCM得到并保存回调函数CtrlHandler的地址。当CtrlHandler函数收到一个SERVICE_CONTROL_STOP、SERVICE_CONTROL_PAUSE、 SERVICE_CONTROL_CONTINUE控制代码的时候,SetServiceStatus必须被调用去确认这个代码,并指定你认为服务处理这个状态变化所需要的时间。
例如:你的服务收到了停止请求,首先要把SERVICE_STATUS结构的dwCurrentState成员设置成SERVICE_STOP_PENDING,这样可以使SCM确定你已经收到了控制代码。当一个服务的暂停或停止操作正在执行的时候,必须指定你认为这种操作所需要的时间:这是因为一个服务也许不能立即改变它的状态,它可能必须等待一个网络请求被完成或者数据被刷新到一个驱动器上。指定时间的方法就像我上一章说的那样,用成员dwCheckPoint和dwWaitHint来指明它完成状态改变所需要的时间。如果需要,可以用增加dwCheckPoint成员的值和设置dwWaitHint成员的值去指明你期待的服务到达下一步的时间的方式周期性的报告进展情况。当整个启动的过程完成之后,要再一次调用SetServiceStatus。这时就要把SERVICE_STATUS结构的dwCurrentState成员设置成SERVICE_STOPPED,当报告状态代码的同时,一定要把成员dwCheckPoint和dwWaitHint设置为0,因为服务已经完成了它的状态变化。暂停或继续服务的时候方法也一样。
当CtrlHandler函数收到一个SERVICE_CONTROL_INTERROGATE控制代码的时候,服务将简单的将dwCurrentState成员设置成服务当前的状态,同时,把成员dwCheckPoint和dwWaitHint设置为0,然后再调用SetServiceStatus就可以了。
在操作系统关闭的时候,CtrlHandler函数收到一个SERVICE_CONTROL_SHUTDOWN控制代码。服务根本无须回应这个代码,因为系统即将关闭。它将执行保存数据所需要的最小行动集,这是为了确定机器能及时关闭。缺省时系统只给很少的时间去关闭所有的服务,你可以手动的修改这个数值,它被记录在HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControl子键里面的WaitToKillServiceTimeout,单位是毫秒。
当CtrlHandler函数收到任何用户定义的代码时,它应该执行期望的用户自定义行动。除非用户自定义的行动要强制服务去暂停、继续或停止,否则不调SetServiceStatus函数。如果用户定义的行动强迫服务的状态发生变化,SetServiceStatus将被调用去设置dwCurrentState、dwCheckPoint和dwWaitHint,具体控制代码和前面说的一样。
如果你的CtrlHandler函数需要很长的时间执行操作的话,千万要注意:假如CtrlHandler函数在30秒内没有返回的话,SCM将返回一个错误,这不是我们所期望的。所以如果出现上述情况,最好的办法是再建立一个线程,让它去继续执行操作,以便使得CtrlHandler函数能够迅速的返回。
例如,当收到一个SERVICE_CONTROL_STOP请求的时候,服务可能正在等待一个网络请求被完成或者数据被刷新到一个驱动器上,而这些操作所需要的时间是你不能估计的,那么就要建立一个新的线程等待操作完成后执行停止命令,CtrlHandler函数在返回之前仍然要报告SERVICE_STOP_PENDING状态,当新的线程执行完操作之后,再由它将服务的状态设置成SERVICE_STOPPED。如果当前操作的时间可以估计的到就不要这样做,仍然使用前面交待的方法处理。
【5】示例代码

#include "stdafx.h"
#include<stdio.h>
#include<iostream>
#include<windows.h>

using namespace std;

#define SLEEP_TIME 5000                         //定义写LOG的间隔时间

#define FILE_PATH "E:\\log.txt"                 //LOG文件的存放路径

bool brun=false;

SERVICE_STATUS servicestatus;                   //定义服务结构体变量,该结构体包含该服务的所有信息

SERVICE_STATUS_HANDLE hstatus;                  //服务处理函数的句柄

int WriteToLog(char* str);                      //定义一个写日志的函数

void WINAPI ServiceMain(int argc, char** argv);     //服务的主函数,服务的主要操作在该函数下进行;

void WINAPI CtrlHandler(DWORD request);         //服务处理函数*

int WriteToLog(char* str)                       //自定义的写日志函数
{
    FILE* pfile;
    fopen_s(&pfile,FILE_PATH,"a+");             //已追加的方式打开文件;fopen_s函数的安全性更高;
    if (pfile==NULL)     
    {
        return -1; 
    }
    fprintf_s(pfile,"%s\n",str);                    //向文件中写入字符串
    fclose(pfile);                              //关闭打开的文件
    return 0;
}

void WINAPI ServiceMain(int argc, char** argv)  //服务的主函数
{
    servicestatus.dwServiceType = SERVICE_WIN32; //服务类型
    servicestatus.dwCurrentState = SERVICE_START_PENDING; //服务当前的状态
    servicestatus.dwControlsAccepted = SERVICE_ACCEPT_SHUTDOWN|SERVICE_ACCEPT_STOP; //服务接受以及在处理函数中处理的控制码
    servicestatus.dwWin32ExitCode = 0; //错误码
    servicestatus.dwServiceSpecificExitCode = 0; //特定于服务的错误码,用于在服务启动或关闭时报错;
    servicestatus.dwCheckPoint = 0; //检测点值
    servicestatus.dwWaitHint = 0; //估计时间
    hstatus = ::RegisterServiceCtrlHandler(L"MyService", CtrlHandler); //指定服务于处理函数

    if (hstatus==0) //根据绑定的情况写日志
    {
        WriteToLog("RegisterServiceCtrlHandler failed");
        return;
    }
    WriteToLog("RegisterServiceCtrlHandler success");

    servicestatus.dwCurrentState = SERVICE_RUNNING; //更改服务的当前状态
    SetServiceStatus (hstatus, &servicestatus); // 为调用的服务更新服务状态管理器的状态信息

    brun=true;

    MEMORYSTATUS memstatus; //内存相关的结构体

    char str[100];

    memset(str,'\0',100); //初始化内存

    while (brun)
    {
        GlobalMemoryStatus(&memstatus); //获取内存状态
        int availmb=memstatus.dwAvailPhys/1024/1024; //转换内存表现方式
        sprintf_s(str,100,"available memory is %dMB",availmb);
        WriteToLog(str);
        Sleep(SLEEP_TIME);
    }
    WriteToLog("service stopped");
}



void WINAPI CtrlHandler(DWORD request)
{
    switch (request)
    {
        case SERVICE_CONTROL_STOP:
            brun=false;
            servicestatus.dwCurrentState = SERVICE_STOPPED;
        break;

        case SERVICE_CONTROL_SHUTDOWN:
            brun=false;
            servicestatus.dwCurrentState = SERVICE_STOPPED;
        break;

        default:

        break;
    }

    SetServiceStatus (hstatus, &servicestatus); //更新服务状态

}

int _tmain(int argc, _TCHAR* argv[])
{
    SERVICE_TABLE_ENTRY entrytable[1];
    entrytable[0].lpServiceName= L"MyService";
    entrytable[0].lpServiceProc=(LPSERVICE_MAIN_FUNCTION)ServiceMain;
    StartServiceCtrlDispatcher(entrytable); //绑定服务
    return 0;
}

【5】服务的安装
(1)编译程序,得到可执行文件,例如本例中的My_Service.exe;
(2)以管理源身份运行cmd;
(3)安装服务:sc create MyService binPath= “E\My_Service.exe”;注意“=”后应该有一个空格,服务名与可执行文件名不能相同;
(4)启动服务:sc start MyService;
(5)停止服务:sc stop MyService;
(6)卸载服务:sc delete MyService;

致谢
本博客参考网上各家博主的博客整合而成,在此感谢各位博主的辛勤工作与分享;

猜你喜欢

转载自blog.csdn.net/qq_27788177/article/details/82224116