从NT内核开始,服务程序已经变为一种非常重要的系统进程,一般的驻守进程和普通的程序必须在桌面登录的情况下才能运行,而许多系统的基础程序必须在用户登录桌面之前就要运行起来,而利用服务,可以很方便的实现这种功能,而且服务程序一般不予用户进行交互,可以安静的在后台执行,合理的利用服务程序可以简化我们的系统设计,比如Windows系统的日志服务,IIS服务等等。 服务程序本身是依附在某一个可执行文件之中,系统将服务安装在注册表中的HKEY_LOCAL_MACHINESYSTEMCurrentControlSetservices位置,当需要执行服务程序时,由系统的服务控制管理器在注册表中对应的位置读取服务信息,并启动对应的程序。 下面从几个方面详细说明服务程序的基本框架
服务程序的框架
服务程序本身也是依附在exe或者dll文件中,一般一个普通的可执行文件中可以包含一个或者多个服务,但是为了代码的维护性,一般一个程序总是只包含一个服务。 服务程序是由服务管理器负责调度,控制的,所以我们在编写服务程序的时候必须满足服务控制管理器的调度,必须包含: 1. 立即调用StartServiceCtrlDispatchar函数把进程的主线程连接到ServiceControlManager的主函数 2. 在进程中运行的各个服务的入口点函数ServiceMain 3. 在进程中运行的各个服务的控制处理函数Handler ServiceControlManager函数的原型如下:
代码语言:javascript复制BOOL WINAPI StartServiceCtrlDispatcher(
__in const SERVICE_TABLE_ENTRY* lpServiceTable
);
函数参数是一个SERVICE_TABLE_ENTRY类型的指针,这个类型的定义如下:
代码语言:javascript复制typedef struct _SERVICE_TABLE_ENTRY
{
LPTSTR lpServiceName;
LPSERVICE_MAIN_FUNCTION lpServiceProc;
} SERVICE_TABLE_ENTRY, *LPSERVICE_TABLE_ENTRY;
这个结构是一个服务名称和对应入口函数指针的映射。在传入的时候必须给一个该类型的数组,数组的每一项都代表一个服务与其入口函数指针的映射,同时这个数组的最后一组必须为NULL 当启动服务的时候,系统会启动对应的进程,当进程代码执行到StartServiceCtrlDispatcher时,程序由服务控制管理器接管,服务控制管理器根据需要启动的服务名称,在传入的数组指针中,找到对应的入口函数,然后调用它,当对应的入口函数返回时结束服务,并将后续代码的控制权转交给对应主进程,由主进程接着执行后面的代码 在入口函数中我们必须给服务一个控制管理程序,这个程序主要是用来处理服务程序接受到的各种控制消息,比如启动服务,暂停服务,停止服务等,这个函数有点类似于Windows 窗口程序中的窗口过程。这个函数由我们自己编写,然后调用函数RegisterServiceCtrlHandler(Ex) 将服务名称与对应的控制函数绑定,每当有一个控制事件发生时都会调用我们注册的函数进行处理,RegisterServiceCtrlHandler函数会返回一个句柄,作为服务的控制句柄。当我们要自己向服务控制管理器报告服务的当前状态时需要这个句柄。
服务的启动过程
已经安装的服务,被系统存储在注册表的HKEY_LOCAL_MACHINESYSTEMCurrentControlSetservices 位置处,这个注册表项纪录了服务所依赖的exe或者dll文件,它的启动类型等信息,当我们尝试启动服务的时候,系统会在注册表的对应位置查找是否存在对应服务的表项,如果存在则启动对应的进程。当进程的代码执行到StartServiceCtrlDispatcher函数时,该进程将由服务控制管理器接管,服务控制管理器将会根据填入的SERVICE_TABLE_ENTRY,找到服务所对应的入口函数开启对应的服务线程并调用,在入口函数处会注册一个控制句柄,然后应该向服务控制管理程序报告当前状态为正在启动,然后执行服务的正式代码。(注意:由于服务的入口函数需要自己编写,所以这里提到的注册控制句柄,报告状态都应该是由程序员自己编写代码实现)
Handler函数
handler函数用来处理服务的控制请求,这个函数由RegisterServiceCtrlHandler(Ex)函数注册到系统,当服务控制请求到来时,由服务的主线程的控制分发线程来调用。综合上面的内容,可以看到一个服务程序应该是至少涉及到3个线程,进程的主线程,服务线程,控制分发线程,RegisterServiceCtrlHandler(Ex)的原型如下:
代码语言:javascript复制SERVICE_STATUS_HANDLE WINAPI RegisterServiceCtrlHandlerEx(
__in LPCTSTR lpServiceName,
__in LPHANDLER_FUNCTION_EX lpHandlerProc,
__in_opt LPVOID lpContext
);
不带Ex的版本只有前两个参数,带Ex版本的第3个参数是一个传入到对应的控制函数中的参数。对应提供的控制管理函数的原型如下:
代码语言:javascript复制DWORD WINAPI HandlerEx(
__in DWORD dwControl,
__in DWORD dwEventType,
__in LPVOID lpEventData,
__in LPVOID lpContext
);
第一个参数是一个控制码,类似于GUI程序中的消息,根据这个控制码就可以知道对应的控制消息,下面列举常见的控制码:
控制码 | 含义 |
---|---|
SERVICE_CONTROL_STOP | 请求服务停止 |
SERVICE_CONTROL_PAUSE | 请求暂停服务 |
SERVICE_CONTROL_CONTINUE | 请求恢复暂停的服务 |
SERVICE_CONTROL_INTERROGATE | 请求服务立即更新它的当前状态信息给服务控制管理程序 |
SERVICE_CONTROL_SHUTDOWN | 请求服务执行清理任务,因为系统正在关机.由于只有非常有限的时间用来关机,所以这个控制只应由绝对需要关机的服务使用.例如:事件登录服务需要清理维护的文件中的脏字节,或服务需要关机以便当系统在关机状态时网络连接不能进行. 如果服务关键要花时间,并发出STOP_PENDING状态信息,强烈建议这些消息包括一个等待提示使得服务控制程序知道在给系统指明服务关机完成之前要等多长时间.系统给服务控制管理器有限的时间(约20秒)完成服务关机,在这个时间后无论服务关机动作是否完成都进行系统关机 |
第二个参数是事件类型,对于有的控制码,它可能含有子控制类型来详细描述它,就好像WM_COMMAND消息中有子控件的相关消息 第三个参数是事件参数,这个参数是子控制码对应的参数 第四个参数是上面带Ex的函数第三个参数传进来的内容 每次Handler函数被调用时,服务必须调用SetServiceStatus函数把状态报告给服务管理器程序注意:即使状态无变化也要报告
服务控制管理器
在服务中一般有3类对象(在这并不是指Windows系统的内核对象,这里只是为了便于理解给出的一个分类): 1. 服务程序对象:服务本身的代码,一般是服务主要完成的功能代码 2. 服务控制对象:用来控制服务,向服务发送执行 3. 服务管理对象:用来响应对应的控制码,主要是指服务的handler函数 与GUI程序相类比,服务对象就好比GUI程序本身,服务控制对象就好像我们在操作GUI程序,比如点击鼠标,而服务控制对象就像窗口的窗口过程 服务管理器由SCManager对象代表。SCManager对象是持有服务对象的容器对象。SCManager对象和服务对象的句柄类型是SC_HANDLE。我们可以使用函数OpenService来在服务管理器中打开对应服务获取服务对象的句柄,或者使用函数CreateService在服务管理器中创建一个新服务并返回服务的句柄 后面关于服务的控制操作请参考本人之前写的一篇关于服务控制管理器的编写的博客点击这里 下面通过一个封装的Service库来说明服务程序的框架。这个简单的类的详细代码请点击这里下载 该项目中主要定义了三个类,其中CFSZService类是所有服务类的基类,CServiceCtrl是服务的控制类,该类用于控制服务,这个类中的所有函数都是静态函数。另外为了测试我从CFSZService类上派生了一个类——CTestService,用来编写服务的具体代码。如果以后想要使用这个项目中的代码,可以进行如下操作: 1. FSZService类中派生一个新类,并重载基类的RunService,在这个服务中编写具体的服务代码即可 2. 在相应位置调用DECLARE_SERVICE_TABLE_ENTRY宏,用来声明一个SERVICE_TABLE_ENTRY变量,用来绑定服务和对应的入口函数 3. 在相应位置添加代码:
代码语言:javascript复制IMPLAMENT_SERVICE_MAIN(GetSystemInfoService, CTestService)
BEGIN_SERVICE_MAP()
ON_SERVICE_MAP(GetSystemInfoService, CTestService)
END_SERIVCE_MAP()
第一个宏用来定义了一个函数,该函数是服务的入口函数,需要传入服务名称,服务的类名称。 第二个宏用来将服务名和它对应的入口函数进行绑定。 4. 在主函数处调用CFSZService::RegisterService(),在该函数里面会调用StartServiceCtrlDispatcher,一遍让服务控制管理程序来接管服务代码
代码的整体说明
服务基类的定义如下:
代码语言:javascript复制class CFSZService
{
public:
typedef CAtlMap<CString, CFSZService *> CFSZServiceMap; //服务名称和对应的服务对象
CFSZService(const CString& csSrvName);
~CFSZService(void);
virtual DWORD Run(DWORD dwArgc, LPTSTR* lpszArgv);
virtual BOOL OnInitService(DWORD dwArgc, LPTSTR* lpszArgv); //初始化服务
virtual DWORD RunService(); //运行服务
void SetServiceStatusHandle(SERVICE_STATUS_HANDLE);
static DWORD WINAPI HandlerEx(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext);
static BOOL RegisterService();
//服务命令处理函数
protected:
virtual DWORD OnStop();
virtual DWORD OnUserControl(DWORD dwControl);
virtual DWORD OnStart();
virtual DWORD OnContinue();
virtual DWORD OnPause();
virtual DWORD OnShutdown();
virtual DWORD OnInterrogate();
virtual DWORD OnShutDown();
protected://设备变更事件通知处理 SERVICE_CONTROL_DEVICEEVENT
virtual DWORD OnDeviceArrival(PDEV_BROADCAST_HDR pDbh){return 0;}
virtual DWORD OnDeviceRemoveComplete(PDEV_BROADCAST_HDR pDbh){return 0;}
virtual DWORD OnDeviceQueryRemove(PDEV_BROADCAST_HDR pDbh){return 0;}
virtual DWORD OnDeviceQueryRemoveFailed(PDEV_BROADCAST_HDR pDbh){return 0;}
virtual DWORD OnDeviceRemovePending(PDEV_BROADCAST_HDR pDbh){return 0;}
virtual DWORD OnCustomEvent(PDEV_BROADCAST_HDR pDbh){return 0;}
protected://硬件配置文件发生变动 SERVICE_CONTROL_HARDWAREPROFILECHANGE
virtual DWORD OnConfigChanged(){return 0;}
virtual DWORD OnQueryChangeConfig(){return 0;}
virtual DWORD OnConfigChangeCanceled(){return 0;}
protected://设备电源事件 SERVICE_CONTROL_POWEREVENT
virtual DWORD OnPowerSettingChange(PPOWERBROADCAST_SETTING pPs){return 0;}
protected://session 发生变化 SERVICE_CONTROL_SESSIONCHANGE
virtual DWORD OnWTSConsoleConnect(PWTSSESSION_NOTIFICATION pWn){return 0;}
virtual DWORD OnWTSConsoleDisconnect(PWTSSESSION_NOTIFICATION pWns){return 0;}
virtual DWORD OnWTSRemoteConnect(PWTSSESSION_NOTIFICATION pWns){return 0;}
virtual DWORD OnWTSRemoteDisconnect(PWTSSESSION_NOTIFICATION pWns){return 0;}
virtual DWORD OnWTSSessionLogon(PWTSSESSION_NOTIFICATION pWns){return 0;}
virtual DWORD OnWTSSessionLogoff(PWTSSESSION_NOTIFICATION pWns){return 0;}
virtual DWORD OnWTSSessionLock(PWTSSESSION_NOTIFICATION pWns){return 0;}
virtual DWORD OnWTSSessionUnLock(PWTSSESSION_NOTIFICATION pWns){return 0;}
virtual DWORD OnWTSSessionRemoteControl(PWTSSESSION_NOTIFICATION pWns){return 0;}
protected:
//内部的工具方法,设置服务为一个指定的状态
BOOL SetStatus(DWORD dwStatus,DWORD dwCheckPoint = 0,DWORD dwWaitHint = 0 ,DWORD dwExitCode = 0,DWORD dwAcceptStatus = SERVICE_CONTROL_INTERROGATE);
BOOL SetStartPending(DWORD dwCheckPoint = 0,DWORD dwWaitHint = 0); //设为正在启动状态
BOOL SetContinuePending(DWORD dwCheckPoint = 0,DWORD dwWaitHint = 0); //设为正在继续运行状态
BOOL SetPausePending(DWORD dwCheckPoint = 0,DWORD dwWaitHint = 0); //设为正在暂停状态
BOOL SetPause(); //设为暂停状态
BOOL SetRunning(); //设为以启动状态
BOOL SetStopPending(DWORD dwCheckPoint = 0,DWORD dwWaitHint = 0); //设为正在停止状态
BOOL SetStop(DWORD dwExitCode = 0); //设为以停止状态
BOOL ReportStatus(DWORD, DWORD, DWORD);//向服务管理器报告当前服务状态
protected:
CString m_csSrvName; //服务名称
DWORD m_dwCurrentStatus; //当前状态
SERVICE_STATUS_HANDLE m_hCtrl; //控制句柄
public:
static CFSZServiceMap ms_SrvMap;
};
在这个基类中主要定义了3类函数,分别是: 1. 服务本身的代码函数:用来处理服务的业务,实现服务的功能 2. 服务控制管理函数:包括各种控制消息的响应函数和服务控制句柄的管理函数 3. 服务状态设置函数:主要用来设置服务的状态
该项目使用Atl 和CString,一般在控制台程序中想要使用这二者只需要包含头文件:atlcoll.h、atlstr.h即可
CFSZServiceMap 成员
该成员是用来将服务名称和对应的类对象关联起来,这样以后根据服务名称就可以找到对应的服务类的对象指针,该类型定义如下:
代码语言:javascript复制typedef CAtlMap<CString, CFSZService *> CFSZServiceMap;
在每个类的构造函数中进行初始化:
代码语言:javascript复制CFSZService::CFSZService(const CString& csSrvName)
{
m_csSrvName = csSrvName;
ms_SrvMap.SetAt(m_csSrvName, this);
}
服务的入口函数
服务的入口函数是利用宏定义的一个函数,每当需要添加一个服务的时候都需要调用宏IMPLAMENT_SERVICE_MAIN来定义一个对应的服务入口ServiceMain,该函数的定义如下:
代码语言:javascript复制#define IMPLAMENT_SERVICE_MAIN(srvName, className)
VOID WINAPI _ServiceMain_##className(DWORD dwArgc, LPTSTR* lpszArgv)
{
CFSZService *pThis = NULL;
if(!CFSZService::ms_SrvMap.Lookup(_T(#srvName), pThis))
{
pThis = dynamic_cast<CFSZService*>( new className(_T(#srvName)) );
}
else
{
return;
}
assert(NULL != pThis);
SERVICE_STATUS_HANDLE hss = RegisterServiceCtrlHandlerEx(_T(#srvName), CFSZService::HandlerEx, reinterpret_cast<LPVOID>(pThis));
assert(NULL != hss);
pThis->SetServiceStatusHandle(hss);
pThis->Run(dwArgc, lpszArgv);
delete dynamic_cast<##className*>(pThis);
}
上面的代码首先根据传入的类名动态创建了一个服务类(由于这里服务对象都是动态创建和销毁的,所以在其他地方不需要创建服务对象),然后调用RegisterServiceCtrlHandlerEx构造了一个服务控制句柄,然后调用类的SetServiceStatusHandle函数来将对应的服务控制句柄保存起来最后调用Run函数来运行服务的正式代码,最后当Run函数执行完毕后,服务的相应工作也做完了,这个时候删除了这个类。Run函数的定义如下:
代码语言:javascript复制DWORD CFSZService::Run(DWORD dwArgc, LPTSTR* lpszArgv)
{
assert(NULL != this);
if (OnInitService(dwArgc, lpszArgv))
{
RunService();
}
return 0;
}
这个函数中使用了OnInitService函数来进一步初始化服务相关信息,该函数提供了一个服务初始化的时机。比如调用相关函数进行socket的初始化或者对com环境进行初始化等等。然后调用RunService执行服务正式的代码。
HandlerEx函数
在前面的宏IMPLAMENT_SERVICE_MAIN中调用了RegisterServiceCtrlHandlerEx将函数HandlerEx作为服务控制码的处理函数,调用的时候将服务类对象的指针通过第四个参数传入,这样在静态函数中就可以使用服务的类成员函数,函数HandlerEx的部分代码如下:
代码语言:javascript复制 DWORD dwRet = ERROR_SUCCESS;
if( NULL == lpContext )
{
return ERROR_INVALID_PARAMETER;
}
CFSZService*pService = reinterpret_cast<CFSZService*>(lpContext);
if( NULL == pService )
{
return ERROR_INVALID_PARAMETER;
}
switch(dwControl)
{
case SERVICE_CONTROL_STOP: //0x00000001 停止服务器
{
dwRet = pService -> OnStop();
}
break;
...
}
在该函数中,将所有的控制吗都列举出来,针对不同的控制吗都调用的对应的处理函数,并且这些函数都是虚函数,所以在派生类中需要处理某个控制消息就重写某个对应的函数即可。 最后再重新屡一下这个类在调用时的基本情况: 1. 在主函数中调用CFSZService::RegisterService();函数将之前我们通过一组BEGIN_SERVICE_MAP、ON_SERVICE_MAP、END_SERVICE_MAP组成的映射关系注册到系统的服务控制管理器中。这个函数单独调用了StartServiceCtrlDispatcher函数,一旦代码执行到这个地方,服务控制管理器会根据之前绑定的服务名称与入口函数的对应关系调用对应的入口函数 2. 入口函数是通过宏IMPLAMENT_SERVICE_MAIN定义的,在入口函数中首先动态创建了一个服务类,然后给这个服务注册服务控制句柄,并且服务控制函数为HandlerEx。 3. 接着,服务的入口函数调用对应服务的Run函数,在Run函数中调用OnInitService进行服务的初始化和调用RunService执行服务的正式代码,所以在重载类中可以重载这两个方法进行初始化和进行服务的相关操作 4. 当外部对服务进行控制时,服务控制管理器调用HandleEx函数进行相关的操作 5. 在HandleEx中会解析对应的控制事件,并调用对应的虚函数,所以如果想要处理某个消息,则重写对应的控制函数即可