转:http://blog.163.com/lgjmfyx/blog/static/46955077200851111348812/
在2000/XP等基于NT 的操作系统中,有一个服务管理器,它管理的后台进程被称为 service
服务是一种应用程序类型,它在后台运行,与 UNIX 后台应用程序类似服务应用程序通常可以
在本地和通过网络为用户提供一些功能,例如客户端/服务器应用程序Web 服务器数据库服
务器以及其他基于服务器的应用程序
后台服务 程序是在后台悄悄运行的我们通过将自己的程序登记为服务,可以使自己的程序不出现
在任务管理器中,并且随系统启动而最先运行,随系统关闭而最后停止
服务控制管理器是一个RPC 服务器,它显露了一组应用编程接口,程序员可以方便的编写程序来配置
服务和控制远程服务器中服务程序
服务程序通常编写成控制台类型的应用程序,总的来说,一个遵守服务控制管理程序接口要求的程序
包含下面三个函数:
1服务程序主函数(main):调用系统函数 StartServiceCtrlDispatcher 连接程序主线程到服务控制管理程序
2服务入口点函数(ServiceMain):执行服务初始化任务,同时执行多个服务的服务进程有多个服务入口函数
3控制服务处理程序函数(Handler):在服务程序收到控制请求时由控制分发线程引用(此处是Service_Ctrl)
另外在系统运行此服务之前需要安装登记服务程序:installService 函数删除服务程序则需要先删除服务安装登记:removeService 函数
服务类型:
类型 | 说明 |
SERVICE_FILE_SYSTEM_DRIVER=2 | 文件系统驱动服务 |
SERVICE_KERNEL_DRIVER=1 | 驱动服务 |
SERVICE_WIN32_OWN_PROCESS=16 | 独占一个进程的服务 |
SERVICE_WIN32_SHARE_PROCESS=32 | 与其他服务共享一个进程的服务 |
新建WIN32控制台程序, 其源文件名为service.cpp 我用的开发工具是VC++.NET
1.服务程序主函数
服 务控制管理程序启动服务程序后,等待服务程序主函数调用系统函StartServiceCtrlDispatcher一个 SERVICE_WIN32_OWN_PROCESS 类型的服务应该立即调用 StartServiceCtrlDispatcher 函数,可以在服务启动后让服务入口点函数完成初始化工作对于 SERVICE_WIN32_OWN_PROCESS 类型的服务和程序中所有服务共同的初始化工作可以在主函数中完成,但不要超过30秒否则必须建立另外的线程完成这些共同的初始化工作,从而保证服务程序 主函数能及时地调用 StartServiceCtrlDispatcher 函数
主函数处理了三中命令行参数:- install,- remove,- debug,分别用于安装,删除和调试服务程序如果不带参数运行,则认为是服务控制管理出现启动该服务程序参数不正确则给出提示信息
StartServiceCtrlDispatcher 函数负责把程序主线程连接到服务控制管理程序具体描述如下:
BOOL StartServiceCtrlDispatcher(
const LPSERVICE_TABLE_ENTRY lpServiceTable);
lpServiceStartTable 指向 SERVICE_TABLE_ENTRY 结构类型的数组,他包含了调用进程所提供的每个服务的入口函数和字符串名表中的最后一个元素必须为 NULL,指明入口表结束SERVICE_TABLE_ENTRY 结构具体描述如下:
typedef struct _SERVICE_TABLE_ENTRY { LPTSTR lpServiceName; LPSERVICE_MAIN_FUNCTION lpServiceProc;
} SERVICE_TABLE_ENTRY, *LPSERVICE_TABLE_ENTRY;
lpServiceName 是一个以 NULL 结尾的字符串,标识服务名如果是 SERVICE_WIN32_OWN_PROCESS 类型的服务,这个字符串会被忽略
lpServiceProc 指向服务入口点函数
//服务程序主函数
#include "stdafx.h"
#include "Windows.h"
#define SZAPPNAME "serverSample" //服务程序名
#define SZSERVICENAME "serviceSample" //标识服务的内部名
//内部变量
bool bDebugServer=false;
SERVICE_STATUS ssStatus;
SERVICE_STATUS_HANDLE sshStatusHandle;
DWORD dwErr=0;
TCHAR szErr[256];
//下面的函数由程序实现
void WINAPI Service_Main(DWORD dwArgc, LPTSTR *lpszArgv);
void WINAPI Service_Ctrl(DWORD dwCtrlCode);
void installService();
void removeService();
void debugService(int argc,char** argv);
bool ReportStatusToSCMgr(DWORD dwCurrentState,DWORD dwWin32ExitCode,DWORD dwWaitHint);
void AddToMessageLog(LPTSTR lpszMsg);
int _tmain(int argc, _TCHAR* argv[])
{
SERVICE_TABLE_ENTRY dispatchTable[]=
{
{TEXT(SZSERVICENAME),(LPSERVICE_MAIN_FUNCTION)Service_Main},
{ NULL,NULL}
};
if((argc>1)&&((*argv[1]=='-')||(argv[1]=="/")))
{
if(_stricmp("install",argv[1]+1)==0)
{
installService();
}
else if(_stricmp("remove",argv[1]+1)==0)
{
removeService();
}
else if(_stricmp("debug",argv[1]+1)==0)
{
bDebugServer=true;
debugService(argc,argv);
}
else
{ //如果未能和上面的如何参数匹配,则可能是服务控制管理程序来启动该程序立即调用
//StartServiceCtrlDispatcher 函数
printf("%s - install to install the service \n",SZAPPNAME);
printf("%s - remove to remove the service \n",SZAPPNAME);
printf("%s - debug to debug the service \n",SZAPPNAME);
printf("\n StartServiceCtrlDispatcher being called.\n");
printf("This may take several seconds.Please wait.\n");
if(!StartServiceCtrlDispatcher(dispatchTable))
AddToMessageLog(TEXT("StartServiceCtrlDispatcher failed."));
else
AddToMessageLog(TEXT("StartServiceCtrlDispatcher OK."));
}
exit(0);
}
return 0;
}
上一章其实只是概括性的介绍,下面开始才是真正的细节所在。在进入点函数里面要完成ServiceMain的初始化,准确点说是初始化一个SERVICE_TABLE_ENTRY结构数组,这个结构记录了这个服务程序里面所包含的所有服务的名称和服务的进入点函数,下面是一个SERVICE_TABLE_ENTRY的例子:
SERVICE_TABLE_ENTRY service_table_entry[] =
{
{ "MyFTPd" , FtpdMain },
{ "MyHttpd", Httpserv},
{ NULL, NULL },
};
第一个成员代表服务的名字,第二个成员是ServiceMain回调函数的地址,上面的服务程序因为拥有两个服务,所以有三个SERVICE_TABLE_ENTRY元素,前两个用于服务,最后的NULL指明数组的结束。
接下来这个数组的地址被传递到StartServiceCtrlDispatcher函数:
BOOL StartServiceCtrlDispatcher(
LPSERVICE_TABLE_ENTRY lpServiceStartTable
)
这个Win32函数表明可执行文件的进程怎样通知SCM包含在这个进程中的服务。就像上一章中讲的那样,StartServiceCtrlDispatcher为每一个传递到它的数组中的非空元素产生一个新的线程,每一个进程开始执行由数组元素中的lpServiceStartTable指明的ServiceMain函数。
SCM启动一个服务程序之后,它会等待该程序的主线程去调StartServiceCtrlDispatcher。如果那个函数在两分钟内没有被调用,SCM将会认为这个服务有问题,并调用TerminateProcess去杀死这个进程。这就要求你的主线程要尽可能快的调用StartServiceCtrlDispatcher。
StartServiceCtrlDispatcher函数则并不立即返回,相反它会驻留在一个循环内。当在该循环内时,StartServiceCtrlDispatcher悬挂起自己,等待下面两个事件中的一个发生。第一,如果SCM要去送一个控制通知给运行在这个进程内一个服务的时候,这个线程就会激活。当控制通知到达后,线程激活并调用相应服务的CtrlHandler函数。CtrlHandler函数处理这个服务控制通知,并返回到StartServiceCtrlDispatcher。StartServiceCtrlDispatcher循环回去后再一次悬挂自己。
第二,如果服务线程中的一个服务中止,这个线程也将激活。在这种情况下,该进程将运行在它里面的服务数减一。如果服务数为零,StartServiceCtrlDispatcher就会返回到入口点函数,以便能够执行任何与进程有关的清除工作并结束进程。如果还有服务在运行,哪怕只是一个服务,StartServiceCtrlDispatcher也会继续循环下去,继续等待其它的控制通知或者剩下的服务线程中止。
上面的内容是关于入口点函数的,下面的内容则是关于ServiceMain函数的。还记得以前讲过的ServiceMain函数的的原型吗?但实际上一个ServiceMain函数通常忽略传递给它的两个参数,因为服务一般不怎么传递参数。设置一个服务最好的方法就是设置注册表,一般服务在
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Service\ServiceName\Parameters
子键下存放自己的设置,这里的ServiceName是服务的名字。事实上,可能要写一个客户应用程序去进行服务的背景设置,这个客户应用程序将这些信息存在注册表中,以便服务读取。当一个外部应用程序已经改变了某个正在运行中的服务的设置数据的时候,这个服务能够用RegNotifyChangeKeyValue函数去接受一个通知,这样就允许服务快速的重新设置自己。
前面讲到StartServiceCtrlDispatcher为每一个传递到它的数组中的非空元素产生一个新的线程。接下来,一个ServiceMain要做些什么呢?MSDN里面的原文是这样说的:The ServiceMain function should immediately call the RegisterServiceCtrlHandler function to specify a Handler function to handle control requests. Next, it should call the SetServiceStatus function to send status information to the service control manager. 为什么呢?因为发出启动服务请求之后,如果在一定时间之内无法完成服务的初始化,SCM会认为服务的启动已经失败了,这个时间的长度在Win NT 4.0中是80秒,Win2000中不详...
基于上面的理由,ServiceMain要迅速完成自身工作,首先是必不可少的两项工作,第一项是调用RegisterServiceCtrlHandler函数去通知SCM它的CtrlHandler回调函数的地址:
SERVICE_STATUS_HANDLE RegisterServiceCtrlHandler(
LPCTSTR lpServiceName, //服务的名字
LPHANDLER_FUNCTION lpHandlerProc //CtrlHandler函数地址
)
第一个参数指明你正在建立的CtrlHandler是为哪一个服务所用,第二个参数是CtrlHandler函数的地址。lpServiceName必须和在SERVICE_TABLE_ENTRY里面被初始化的服务的名字相匹配。RegisterServiceCtrlHandler返回一个SERVICE_STATUS_HANDLE,这是一个32位的句柄。SCM用它来唯一确定这个服务。当这个服务需要把它当时的状态报告给SCM的时候,就必须把这个句柄传给需要它的Win32函数。注意:这个句柄和其他大多数的句柄不同,你无需关闭它。
SCM要求ServiceMain函数的线程在一秒钟内调用RegisterServiceCtrlHandler函数,否则SCM会认为服务已经失败。但在这种情况下,SCM不会终止服务,不过在NT 4中将无法启动这个服务,同时会返回一个不正确的错误信息,这一点在Windows 2000中得到了修正。
在RegisterServiceCtrlHandler函数返回后,ServiceMain线程要立即告诉SCM服务正在继续初始化。具体的方法是通过调用SetServiceStatus函数传递SERVICE_STATUS数据结构。
BOOL SetServiceStatus(
SERVICE_STATUS_HANDLE hService, //服务的句柄
SERVICE_STATUS lpServiceStatus //SERVICE_STATUS结构的地址
)
这个函数要求传递给它指明服务的句柄(刚刚通过调用RegisterServiceCtrlHandler得到),和一个初始化的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_STATUS结构含有七个成员,它们反映服务的现行状态。所有这些成员必须在这个结构被传递到SetServiceStatus之前正确的设置。
成员dwServiceType指明服务可执行文件的类型。如果你的可执行文件中只有一个单独的服务,就把这个成员设置成SERVICE_WIN32_OWN_PROCESS;如果拥有多个服务的话,就设置成SERVICE_WIN32_SHARE_PROCESS。除了这两个标志之外,如果你的服务需要和桌面发生交互(当然不推荐这样做),就要用“OR”运算符附加上SERVICE_INTERACTIVE_PROCESS。这个成员的值在你的服务的生存期内绝对不应该改变。
成员dwCurrentState是这个结构中最重要的成员,它将告诉SCM你的服务的现行状态。为了报告服务仍在初始化,应该把这个成员设置成SERVICE_START_PENDING。在以后具体讲述CtrlHandler函数的时候具体解释其它可能的值。
成员dwControlsAccepted指明服务愿意接受什么样的控制通知。如果你允许一个SCP去暂停/继续服务,就把它设成SERVICE_ACCEPT_PAUSE_CONTINUE。很多服务不支持暂停或继续,就必须自己决定在服务中它是否可用。如果你允许一个SCP去停止服务,就要设置它为SERVICE_ACCEPT_STOP。如果服务要在操作系统关闭的时候得到通知,设置它为SERVICE_ACCEPT_SHUTDOWN可以收到预期的结果。这些标志可以用“OR”运算符组合。
成员dwWin32ExitCode和dwServiceSpecificExitCode是允许服务报告错误的关键,如果希望服务去报告一个Win32错误代码(预定义在WinError.h中),它就设置dwWin32ExitCode为需要的代码。一个服务也可以报告它本身特有的、没有映射到一个预定义的Win32错误代码中的错误。为了这一点,要把dwWin32ExitCode设置为ERROR_SERVICE_SPECIFIC_ERROR,然后还要设置成员dwServiceSpecificExitCode为服务特有的错误代码。当服务运行正常,没有错误可以报告的时候,就设置成员dwWin32ExitCode为NO_ERROR。
最后的两个成员dwCheckPoint和dwWaitHint是一个服务用来报告它当前的事件进展情况的。当成员dwCurrentState被设置成SERVICE_START_PENDING的时候,应该把dwCheckPoint设成0,dwWaitHint设成一个经过多次尝试后确定比较合适的数,这样服务才能高效运行。一旦服务被完全初始化,就应该重新初始化SERVICE_STATUS结构的成员,更改dwCurrentState为SERVICE_RUNNING,然后把dwCheckPoint和dwWaitHint都改为0。
dwCheckPoint成员的存在对用户是有益的,它允许一个服务报告它处于进程的哪一步。每一次调用SetServiceStatus时,可以增加它到一个能指明服务已经执行到哪一步的数字,它可以帮助用户决定多长时间报告一次服务的进展情况。如果决定要报告服务的初始化进程的每一步,就应该设置dwWaitHint为你认为到达下一步所需的毫秒数,而不是服务完成它的进程所需的毫秒数。
在服务的所有初始化都完成之后,服务调用SetServiceStatus指明SERVICE_RUNNING,在那一刻服务已经开始运行。通常一个服务是把自己放在一个循环之中来运行的。在循环的内部这个服务进程悬挂自己,等待指明它下一步是应该暂停、继续或停止之类的网络请求或通知。当一个请求到达的时候,服务线程激活并处理这个请求,然后再循环回去等待下一个请求/通知。
如果一个服务由于一个通知而激活,它会先处理这个通知,除非这个服务得到的是停止或关闭的通知。如果真的是停止或关闭的通知,服务线程将退出循环,执行必要的清除操作,然后从这个线程返回。当ServiceMain线程返回并中止时,引起在StartServiceCtrlDispatcher内睡眠的线程激活,并像在前面解释过的那样,减少它运行的服务的计数。