VC++ Service编程
编写main函数
最近由于工作需要,一直在学习MSDN里面有关于服务编程的东西。整天看英文文档真是觉得无聊,所以我决定边看边翻译一下,也算是给学习留下一点成果吧。
编写服务程序的main函数
服务程序的main函数调用StartServiceCtrlDispatcher函数,将它与SCM(服务控制管理器)联系起来,并开启一个控制调度线程。控制调度线程不停的循环,等待接收在调度表中已经定义好的控制请求。除非程序中的所有服务都中止或者遇到错误,要不这个线程不会返回。当程序中的所有服务都中止的时候,SCM给调度线程发送一个控制请求,让它关闭。这时,线程从调用StartServiceCtrlDispatcher函数的地方返回,程序中止。
下面,是一个只支持一个服务的程序。SvcDebugOut函数负责向调试器输出指示信息和错误。它有两个参数:一个格式化的输出字符串,和这个字符串的长度值。
#include <windows.h>
SERVICE_STATUS MyServiceStatus;
SERVICE_STATUS_HANDLE MyServiceStatusHandle;
VOID SvcDebugOut(LPSTR String, DWORD Status);
VOID WINAPI MyServiceCtrlHandler (DWORD opcode);
VOID MyServiceStart (DWORD argc, LPTSTR *argv);
DWORD MyServiceInitialization (DWORD argc, LPTSTR *argv, DWORD *specificError);
void main()
{
SERVICE_TABLE_ENTRY DispatchTable[] =
{
{ "MyService", MyServiceStart},
{ NULL, NULL}
};
if (!StartServiceCtrlDispatcher( DispatchTable))
{
SvcDebugOut(" [MY_SERVICE] StartServiceCtrlDispatcher (%d)/n", GetLastError());
}
}
VOID SvcDebugOut(LPSTR String, DWORD Status)
{
CHAR Buffer[1024];
if (strlen(String) < 1000)
{
sprintf(Buffer, String, Status);
OutputDebugStringA(Buffer);
}
}
如果想要服务程序支持多服务,main函数的实现会有一些同。将所有服务的名称都按照"MyService"的格式加入到调度表中,使他们可以被调度线程监视。
编写ServiceMain函数
下面例子中的MyServiceStart函数是这个服务的ServiceMain函数。MyServiceStart函数可以使用命令行参数,就像控制台程序中main函数那样的方式。第一个参数是传递给服务的值的个数,并且这个个数的最小值总是1。第二个参数是一个字符数组指针。数组的第一项总是当前服务的名称。
MyServiceStart函数首先填充一个SERVICE_STATUS结构,其中包含它所要接受的控制请求代码。虽然这个服务接受SERVICE_CONTROL_PAUSE 和 SERVICE_CONTROL_CONTINUE,但在被要求停止的时候,并没有做什么重要的操作。包含SERVICE_ACCEPT_PAUSE_CONTINUE仅仅是为了举一个例子。如果服务暂停时并没有传递进来的参数,不要支持暂停。
MyServiceStart函数马上调用RegisterServiceCtrlHandler函数,将MyService函数注册成为服务的控制请求处理函数,并且开始作初始化的工作。下面例子中的初始化函数——MyServiceInitialization,只是为了举个例;它没有做任何初始化的工作,比如说创建一个额外的线程。如果服务的初始化工作要花比一秒更长的时间,应该在代码里周期性的调用SetServiceStatus函数建立等待时间和检查点,来指示初始化工作正在进行。
当初始化成功完成时,例子以SERVICE_RUNNING为参数调用SetServiceStatus函数,并且继续它自己的工作。如果在初始化的过程中遇到错误,则以SERVICE_STOPPED为参数调用SetServiceStatus函数,然后返回。
因为例子并没有完成什么真的工作,所以它只是简单的将控制权返回给调用函数。让这个函数自己返回,而不是调用ExitThread函数很重要,因为返回时,可以清理为变量开辟的内存。
见源代码。
服务入口
通常,我们把服务做成控制台程序。控制台程序的入口是main函数。main函数从注册表中的ImagePath值来获取参数,提供给服务。
当SCM(服务控制管理器)开启一个服务,它就开始等待这个服务调用StartServiceCtrlDispatcher函数。使用下面的原则:
1、SERVICE_WIN32_OWN_PROCESS类型的服务必须在主线程中马上调用StartServiceCtrlDispatcher函数。在服务启动后,你可以像“服务ServiceMain函数”中描述的那样,做任何初始化。
2、 如果服务的类型是SERVICE_WIN32_SHARE_PROCESS,并且在这个服务中要为其它服务提供通用初始化。如果这些通用初始化可以在30秒内完成,你可以在主线程中调用StartServiceCtrlDispatcher函数之前做这些事情。否则,就必须创建另外一个线程来做这些通用的初始化。同时,在主线程中调用StartServiceCtrlDispatcher。
StartServiceCtrlDispatcher为程序中的所有服务提供一个SERVICE_TABLE_ENTRY结构。每一个结构都指名服务的名称和入口。
如果StartServiceCtrlDispatcher成功,调用它的线程要等到程序中所有服务都中止后才会返回。SCM利用一个已命名的管道来向这个线程发送控制请求。这个线程扮演一个控制调度员的角色,执行下面的任务:
1、当新服务开始运行时,创建一个新的线程来调用相关的入口。
2、调用相关的handler函数来处理服务控制请求。
服务ServiceMain函数
当一个服务控制程序请求开启一个新的服务时,SCM(服务控制管理器)开启服务的同时,向控制调度器发送一个开始请求。控制调度器为服务创建一个新的线程来运行ServiceMain函数。
ServiceMain函数应该执行下面的这些任务:
1、立刻为服务调用RegisterServiceCtrlHandlerEx函数,用来注册一个处理控制请求的HandlerEx函数。RegisterServiceCtrlHandlerEx函数的返回值是一个service status handle(服务状态句柄),稍后向SCM通知当前服务状态会用到。
2、执行初始化。如果初始化的时间非常短(小于一秒),可以直接在ServiceMain函数中完成。
如果预期初始化的时间会比一秒长,调用SetServiceStatus函数,在SERVICE_STATUS结构中指明等待间隔,以及当前的状态是SERVICE_START_PENDING。当初始化继续时,服务应该再调用SetServiceStatus函数来报告当前状态。多次调用SetServiceStatus函数对调试服务很有用。
3、初始化结束后,调用SetServiceStatus函数,在SERVICE_STATUS结构中指明SERVICE_RUNNING状态。
4、执行服务的任务,或者已经没有未完成的任务,那就返回。服务状态有任何改变,都要保证调用SetServiceStatus函数来报告新的状态。
5、如果在初始化或者运行的过程中遇到错误并且清理的过程会很长,服务应该调用SetServiceStatus函数,在SERVICE_STATUS结构中指定SERVICE_STOP_PENDING状态。一旦清理结束,在最后中止的线程中调用SetServiceStatus函数,在SERVICE_STATUS结构中指定SERVICE_STOPPED状态。确保向SERVICE_STATUS结构里的dwServiceSpecificExitCode和dwWin32ExitCode赋值,来标志错误。
编写Control Handler函数
下面例子中的MyServiceCtrlHandler函数是一个Handler函数。当被调度线程调用时,它处理Opcode参数传进来的控制码,然后调用SetServiceStatus函数更新服务的状态。Handler函数收到一个控制码后,不管做什么操作,服务都应该调用SetServiceStatus函数。
当接收到暂停请求,MyServiceCtrlHandler将SERVICE_STATUS结构中的dwCurrentState设置为SERVICE_PAUSED。同样的,当接收到继续请求时,状态被设置为SERVICE_RUNNING。所以,对于处理暂停和继续请求,MyServiceCtrlHandler函数并不是一个好例子。因为,MyServiceCtrlHandler函数是一个Handler函数的模版,暂停和继续控制的代码并不完整。支持暂停和继续控制的服务应该用一个有意义的方法来处理这些控制。很多服务都不支持暂停或者继续控制。如果服务利用dwControlsAccepted参数来指明它不支持暂停或继续,那么,SCM(服务控制管理器)就不会向服务的Handler函数发送暂停或继续控制信息。
为了输出调试信息,MyServiceCtrlHandler调用SvcDebugOut。SvcDebugOut的代码已经在“编写main函数”中列出。并且,注意一下,MyServiceStatus变量是一个全局变量,应该像在“编写ServiceMain函数”中描述的那样进行初始化。
Control Handler函数
每个服务都有一个控制处理器,HandlerEx函数。当服务程序从一个服务控制程序那里收到一个控制请求时,HandlerEx函数就会被控制调度器调用。因此,这个函数运行在控制调度器的环境中。服务调用RegisterServiceCtrlHandler或者RegisterServiceCtrlHandlerEx函数来注册它的Control Handler(控制处理器)函数。
任何时间调用HandlerEx,服务必须调用SetServiceStatus函数,向SCM(服务控制管理器)报告当前状态。不管状态改变与否,都必须这么做。
服务控制程序可以利用ControlService来发送控制请求。所有的服务都必须接收并处理SERVICE_CONTROL_INTERROGATE控制码。你可以调用SetServiceStatus来设定接不接收其它的控制代码。要接收SERVICE_CONTROL_DEVICEEVENT控制码,你必须调用RegisterDeviceNotification函数。服务亦可以处理额外的用户定义的控制码。
控制处理器必须在30秒内返回,否则SCM会返回一个错误。如果控制处理器在运行的同时,服务需要做一个较长的处理,就应该创建一个新的线程来做这个处理,然后返回。举个例子,长时间处理一个停止请求,创建一个新的线程来处理这个停止操作。控制处理器应该调用SetServiceStatus函数发送SERVICE_STOP_PENDING消息,然后返回。
当用户关闭系统的时候,所有用SERVICE_ACCEPT_SHUTDOWN控制码调用过SetServiceStatus的控制处理器,都会收到SERVICE_CONTROL_SHUTDOWN控制码。所有的这些服务都会按照他们在已安装服务数据库中的顺序被通知到。在系统关闭之前,一个服务有大概20秒的默认时间来进行清理工作。这段时间过后,不管服务是否已经完全关闭,系统都会关闭。注意,如果系统进入关闭状态(不是重新启动或者电源关闭),服务继续运行。
如果服务需要更多时间用来清理,它发送一个STOP_PENDING消息和一个等待时间,这样,服务控制器就知道要等多长时间,然后再向系统报告服务关闭完成。尽管这样,服务控制器等待的时间也是有限度的。可以更改注册表项HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Control中的WaitToKillServiceTimeout值来改变这个时间限制。
顾客要求操作系统快速关闭。举个例子,使用UPS电源运转的计算机如果不在UPS能源耗尽之前关闭完,数据就会丢失。所以,服务应该尽可能快地完成清理工作。将为保存数据减到最少,在关闭时至保存为保存的数据是个好习惯。因为计算机正在关闭,千万不要在释放内存或别的系统资源上浪费时间。如果你要向服务器通知这在退出,把等待返回的时间减到最小,因为网络问题也有可能耽误你的服务关闭。
注意,服务关闭过程中,SCM不会考虑从属关系。SCM注意清点正在运行的服务,并向它们发送SERVICE_CONTROL_SHUTDOWN命令。因此,一个服务可能会因另一个服务已经中止而运行失败。利用SetProcessShutdownParameters函数,来设置从属服务的关闭顺序。SCM通过这个函数给它的处理器赋予0x1E0优先级;当它的控制处理器被调用,SCM就发送SERVICE_CONTROL_SHUTDOWN通知,同时等待这些服务退出,然后从控制处理器里返回。
创建多线程服务
下面的例子论述了,一个简单的服务怎么样创建工作进程、响应SCM(服务控制管理器)、通知线程退出、不断地向SCM通知当前状态和处理过程、然后向SCM报告服务停止。要安装这个服务,请把它创建成为以给控制台程序,并使用Platform SDK中的SC工具。使用控制面板中的服务控制工具来开启关闭这个服务。
////////////
多线程服务
SCM(服务控制管理器)向服务的控制处理器发送服务控制事件来控制一个服务。服务必须在短时间内响应控制请求,使得SCM可以明确服务的当前状态。还有,服务的当前状态必须和它向SCM报告的状态相一致。
由于服务和SCM之间的这种通信机制,使用多线程服务必须小心。当从SCM那里收到停止指示,在向SCM报告服务已经停止之前,必须等待服务中的所有线程全部退出。否则,SCM可能不能正确关闭服务。
服务响应停止控制事件并开始停止服务的工作,这时需要通知SCM。如果服务在之前调用的SetServiceStatus中设置的等待时间内做出响应,并且检查点比之前调用的SetServiceStatus中设置的检查点大,SCM就假定服务已经开始停止处理。
如果在所有的线程退出之前,服务向SCM报告已经停止,SCM可能会认为这是矛盾的。这可能导致服务进入一个不能停止也不能重新启动的状态。
PS:可运行的源代码:
#include <windows.h>
#include <stdio.h>
SERVICE_STATUS MyServiceStatus;
SERVICE_STATUS_HANDLE MyServiceStatusHandle;
VOID SvcDebugOut(LPSTR String, DWORD Status);
VOID WINAPI MyServiceCtrlHandler(DWORD opcode);
void WINAPI MyServiceStart(DWORD argc, LPTSTR *argv);
DWORD MyServiceInitialization (DWORD argc, LPTSTR *argv, DWORD *specificError);
void main()
{
SERVICE_TABLE_ENTRY DispatchTable[] =
{
{ "MyService", (LPSERVICE_MAIN_FUNCTION)MyServiceStart},
{ NULL, NULL}
};
if (!StartServiceCtrlDispatcher( DispatchTable))
{
SvcDebugOut(" [MY_SERVICE] StartServiceCtrlDispatcher (%d)/n", GetLastError());
}
}
VOID SvcDebugOut(LPSTR String, DWORD Status)
{
FILE* fp = NULL;
fp = fopen("c:/Redirect/andytest.txt", "a+");
if (fp == NULL)
{
MessageBox(NULL, TEXT("the file can't open"), TEXT("file open"), MB_OK);
return;
}
CHAR Buffer[1024];
if (strlen(String) < 1000)
{
sprintf(Buffer, String, Status);
OutputDebugStringA(Buffer);
fprintf(fp, Buffer);
}
fclose(fp);
}
void WINAPI MyServiceStart(DWORD argc, LPTSTR *argv)
{
DWORD status;
DWORD specificError;
MyServiceStatus.dwServiceType = SERVICE_WIN32;
MyServiceStatus.dwCurrentState = SERVICE_START_PENDING;
MyServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PAUSE_CONTINUE;
MyServiceStatus.dwWin32ExitCode = 0;
MyServiceStatus.dwServiceSpecificExitCode = 0;
MyServiceStatus.dwCheckPoint = 0;
MyServiceStatus.dwWaitHint = 0;
MyServiceStatusHandle = RegisterServiceCtrlHandler("MyService", MyServiceCtrlHandler);
if (MyServiceStatusHandle == (SERVICE_STATUS_HANDLE)0)
{
SvcDebugOut(" [MY_SERVICE] RegisterServiceCtrlHandler failed %d/n", GetLastError());
return;
}
// Initialization code goes here.
status = MyServiceInitialization(argc,argv, &specificError);
// Handle error condition
if (status != NO_ERROR)
{
MyServiceStatus.dwCurrentState = SERVICE_STOPPED;
MyServiceStatus.dwCheckPoint = 0;
MyServiceStatus.dwWaitHint = 0;
MyServiceStatus.dwWin32ExitCode = status;
MyServiceStatus.dwServiceSpecificExitCode = specificError;
SetServiceStatus (MyServiceStatusHandle, &MyServiceStatus);
return;
}
// Initialization complete - report running status.
MyServiceStatus.dwCurrentState = SERVICE_RUNNING;
MyServiceStatus.dwCheckPoint = 0;
MyServiceStatus.dwWaitHint = 0;
if (!SetServiceStatus (MyServiceStatusHandle, &MyServiceStatus))
{
status = GetLastError();
SvcDebugOut(" [MY_SERVICE] SetServiceStatus error %ld/n",status);
}
// This is where the service does its work.
SvcDebugOut(" [MY_SERVICE] Returning the Main Thread /n",0);
return;
}
// Stub initialization function.
DWORD MyServiceInitialization(DWORD argc, LPTSTR *argv, DWORD *specificError)
{
argv;
argc;
specificError;
return 0;
}
VOID WINAPI MyServiceCtrlHandler (DWORD Opcode)
{
DWORD status;
switch(Opcode)
{
case SERVICE_CONTROL_PAUSE:
// Do whatever it takes to pause here.
MyServiceStatus.dwCurrentState = SERVICE_PAUSED;
break;
case SERVICE_CONTROL_CONTINUE:
// Do whatever it takes to continue here.
MyServiceStatus.dwCurrentState = SERVICE_RUNNING;
break;
case SERVICE_CONTROL_STOP:
// Do whatever it takes to stop here.
MyServiceStatus.dwWin32ExitCode = 0;
MyServiceStatus.dwCurrentState = SERVICE_STOPPED;
MyServiceStatus.dwCheckPoint = 0;
MyServiceStatus.dwWaitHint = 0;
if (!SetServiceStatus (MyServiceStatusHandle, &MyServiceStatus))
{
status = GetLastError();
SvcDebugOut(" [MY_SERVICE] SetServiceStatus error %ld/n", status);
}
SvcDebugOut(" [MY_SERVICE] Leaving MyService /n",0);
return;
case SERVICE_CONTROL_INTERROGATE:
// Fall through to send current status.
break;
default:
SvcDebugOut(" [MY_SERVICE] Unrecognized opcode %ld/n", Opcode);
}
// Send current status.
if (!SetServiceStatus (MyServiceStatusHandle, &MyServiceStatus))
{
status = GetLastError();
SvcDebugOut(" [MY_SERVICE] SetServiceStatus error %ld/n", status);
}
return;
}