Windows服务程序.docx
- 文档编号:26610601
- 上传时间:2023-06-20
- 格式:DOCX
- 页数:34
- 大小:34.03KB
Windows服务程序.docx
《Windows服务程序.docx》由会员分享,可在线阅读,更多相关《Windows服务程序.docx(34页珍藏版)》请在冰豆网上搜索。
Windows服务程序
有那么一类应用程序,是能够为各种用户(包括本地用户和远程用户)所用的,
拥有用户授权级进行管理的能力,并且不论用户是否物理的与正在运行该应用程
序的计算机相连都能正常执行,这就是所谓的服务了。
(一)服务的基础知识
Question1.什么是服务?
它的特征是什么?
在NT/2000中,服务是一类受到操作系统优待的程序。
一个服务首先是一个
Win32可执行程序,如果要写一个功能完备且强大的服务,需要熟悉动态连接库
(Dlls)、结构异常处理、内存映射文件、虚拟内存、设备I/O、线程及其同步、
Unicode以及其他的由WinAPI函数提供的应用接口。
当然本文讨论的只是建立一
个可以安装、运行、启动、停止的没有任何其他功能的服务,所以无需上述知识
仍可以继续看下去,我会在过程中将理解本文所需要的知识逐一讲解。
第二要知道的是一个服务决不需要用户界面。
大多数的服务将运行在那些被
锁在某些黑暗的,冬暖夏凉的小屋子里的强大的服务器上面,即使有用户界面一
般也没有人可以看到。
如果服务提供任何用户界面如消息框,那么用户错过这些
消息的可能性就极高了,所以服务程序通常以控制台程序的形式被编写,进入点
函数是main()而不是WinMain()。
也许有人有疑问:
没有用户界面的话,要怎样设置、管理一个服务?
怎样开
始、停止它?
服务如何发出警告或错误信息、如何报告关于它的执行情况的统计
数据?
这些问题的答案就是服务能够被远程管理,WindowsNT/2000提供了大量
的管理工具,这些工具允许通过网络上的其它计算机对某台机器上面的服务进行
管理。
比如Windows2000里面的“控制台”程序(mmc.exe),用它添加“管理单
元”就可以管理本机或其他机器上的服务。
Question2.服务的安全性…
想要写一个服务,就必须熟悉WinNT/2000的安全机制,在上述操作系统之
中,所有安全都是基于用户的。
换句话说——进程、线程、文件、注册表键、信
号、事件等等等等都属于一个用户。
当一个进程被产生的时候,它都是执行在一
个用户的上下文(context),这个用户帐号可能在本机,也可能在网络中的其他
机器上,或者是在一个特殊的账号:
SystemAccount——即系统帐号的上下文
如果一个进程正在一个用户帐号下执行,那么这个进程就同时拥有这个用户
所能拥有的一切访问权限,不论是在本机还是网络。
系统帐号则是一个特殊的账
号,它用来标识系统本身,而且运行在这个帐号下的任何进程都拥有系统上的所
有访问权限,但是系统帐号不能在域上使用,无法访问网络资源…
服务也是Win32可执行程序,它也需要执行在一个context,通常服务都是在
系统账号下运行,但是也可以根据情况选择让它运行在一个用户账号下,也就会
因此获得相应的访问资源的权限。
Question3.服务的三个组成部分
一个服务由三部分组成,第一部分是ServiceControlManager(SCM)。
每个
WindowsNT/2000系统都有一个SCM,SCM存在于Service.exe中,在Windows启动
的时候会自动运行,伴随着操作系统的启动和关闭而产生和终止。
这个进程以系
统特权运行,并且提供一个统一的、安全的手段去控制服务。
它其实是一个RPC
Server,因此我们可以远程安装和管理服务,不过这不在本文讨论的范围之内。
SCM包含一个储存着已安装的服务和驱动程序的信息的数据库,通过SCM可以统一
的、安全的管理这些信息,因此一个服务程序的安装过程就是将自身的信息写入
这个数据库。
第二部分就是服务本身。
一个服务拥有能从SCM收到信号和命令所必需的的
特殊代码,并且能够在处理后将它的状态回传给SCM。
第三部分也就是最后一部分,是一个ServiceControlDispatcher(SCP)。
它是一个拥有用户界面,允许用户开始、停止、暂停、继续,并且控制一个或多
个安装在计算机上服务的Win32应用程序。
SCP的作用是与SCM通讯,Windows
2000管理工具中的“服务”就是一个典型的SCP。
在这三个组成部分中,用户最可能去写服务本身,同时也可能不得不写一个
与其伴随的客户端程序作为一个SCP去和SCM通讯,本文只讨论去设计和实现一个
服务,关于如何去实现一个SCP则在以后的其它文章中介绍。
Question4.怎样开始设计服务
还记得前面我提到服务程序的入口点函数一般都是main()吗?
一个服务拥有
很重要的三个函数,第一个就是入口点函数,其实用WinMain()作为入口点函数
也不是不可以,虽然说服务不应该有用户界面,但是其实存在很少的几个例外,
这就是下面图中的选项存在的原因。
由于要和用户桌面进行信息交互,服务程序有时会以WinMain()作为入口点
函数。
入口函数负责初始化整个进程,由这个进程中的主线程来执行。
这意味着它
应用于这个可执行文件中的所有服务。
要知道,一个可执行文件中能够包含多个
服务以使得执行更加有效。
主进程通知SCM在可执行文件中含有几个服务,并且
给出每一个服务的ServiceMain回调(CallBack)函数的地址。
一旦在可执行文件
内的所有服务都已经停止运行,主线程就在进程终止前对整个进程进行清除。
第二个很重要的函数就是ServiceMain,我看过一些例子程序里面对自己的
服务的进入点函数都固定命名为ServiceMain,其实并没有规定过一定要那样命
名,任何的函数只要符合下列的形式都可以作为服务的进入点函数。
VOIDWINAPIServiceMain(
DWORDdwArgc,//参数个数
LPTSTR*lpszArgv//参数串
);
这个函数由操作系统调用,并执行能完成服务的代码。
一个专用的线程执行
每一个服务的ServiceMain函数,注意是服务而不是服务程序,这是因为每个服
务也都拥有与自己唯一对应的ServiceMain函数,关于这一点可以用“管理工具
”里的“服务”去察看Win2000里面自带的服务,就会发现其实很多服务都是由
service.exe单独提供的。
当主线程调用Win32函数StartServiceCtrlDispatcher
的时候,SCM为这个进程中的每一个服务产生一个线程。
这些线程中的每一个都
和它的相应的服务的ServiceMain函数一起执行,这就是服务总是多线程的原因
——一个仅有一个服务的可执行文件将有一个主线程,其它的线程执行服务本身
。
第三个也就是最后的一个重要函数是CtrlHandler,它必须拥有下面的原型
:
VOIDWINAPICtrlHandler(
DWORDfdwControl//控制命令
)
像ServiceMain一样,CtrlHandler也是一个回调函数,用户必须为它的服务
程序中每一个服务写一个单独的CtrlHandler函数,因此如果有一个程序含有两
个服务,那么它至少要拥有5个不同的函数:
作为入口点的main()或WinMain(),
用于第一个服务的ServiceMain函数和CtrlHandler函数,以及用于第二个服务的
ServiceMain函数和CtrlHandler函数。
SCM调用一个服务的CtrlHandler函数去改变这个服务的状态。
例如,当某个
管理员用管理工具里的“服务”尝试停止你的服务的时候,你的服务的
CtrlHandler函数将收到一个SERVICE_CONTROL_STOP通知。
CtrlHandler函数负责
执行停止服务所需的一切代码。
由于是进程的主线程执行所有的CtrlHandler函
数,因而必须尽量优化你的CtrlHandler函数的代码,使它运行起来足够快,以
便相同进程中的其它服务的CtrlHandler函数能在适当的时间内收到属于它们的
通知。
而且基于上述原因,你的CtrlHandler函数必须要能够将想要传达的状态
送到服务线程,这个传递过程没有固定的方法,完全取决于你的服务的用途。
(二)对服务的深入讨论之上
上一章其实只是概括性的介绍,下面开始才是真正的细节所在。
在进入点函
数里面要完成ServiceMain的初始化,准确点说是初始化一个
SERVICE_TABLE_ENTRY结构数组,这个结构记录了这个服务程序里面所包含的所
有服务的名称和服务的进入点函数,下面是一个SERVICE_TABLE_ENTRY的例子:
SERVICE_TABLE_ENTRYservice_table_entry[]=
{
{"MyFTPd",FtpdMain},
{"MyHttpd",Httpserv},
{NULL,NULL},
};
第一个成员代表服务的名字,第二个成员是ServiceMain回调函数的地址,
上面的服务程序因为拥有两个服务,所以有三个SERVICE_TABLE_ENTRY元素,前
两个用于服务,最后的NULL指明数组的结束。
接下来这个数组的地址被传递到StartServiceCtrlDispatcher函数:
BOOLStartServiceCtrlDispatcher(
LPSERVICE_TABLE_ENTRYlpServiceStartTable
)
这个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/Parame
ters
子键下存放自己的设置,这里的ServiceName是服务的名字。
事实上,可能要写
一个客户应用程序去进行服务的背景设置,这个客户应用程序将这些信息存在注
册表中,以便服务读取。
当一个外部应用程序已经改变了某个正在运行中的服务
的设置数据的时候,这个服务能够用RegNotifyChangeKeyValue函数去接受一个
通知,这样就允许服务快速的重新设置自己。
前面讲到StartServiceCtrlDispatcher为每一个传递到它的数组中的非空元
素产生一个新的线程。
接下来,一个ServiceMain要做些什么呢?
MSDN里面的原
文是这样说的:
TheServiceMainfunctionshouldimmediatelycallthe
RegisterServiceCtrlHandlerfunctiontospecifyaHandlerfunctionto
handlecontrolrequests.Next,itshouldcalltheSetServiceStatus
functiontosendstatusinformationtotheservicecontrolmanager.为
什么呢?
因为发出启动服务请求之后,如果在一定时间之内无法完成服务的初始
化,SCM会认为服务的启动已经失败了,这个时间的长度在WinNT4.0中是80秒
,Win2000中不详…
基于上面的理由,ServiceMain要迅速完成自身工作,首先是必不可少的两
项工作,第一项是调用RegisterServiceCtrlHandler函数去通知SCM它的
CtrlHandler回调函数的地址:
SERVICE_STATUS_HANDLERegisterServiceCtrlHandler(
LPCTSTRlpServiceName,//服务的名字
LPHANDLER_FUNCTIONlpHandlerProc//CtrlHandler函数地址
)
第一个参数指明你正在建立的CtrlHandler是为哪一个服务所用,第二个参
数是CtrlHandler函数的地址。
lpServiceName必须和在SERVICE_TABLE_ENTRY里
面被初始化的服务的名字相匹配。
RegisterServiceCtrlHandler返回一个
SERVICE_STATUS_HANDLE,这是一个32位的句柄。
SCM用它来唯一确定这个服务。
当这个服务需要把它当时的状态报告给SCM的时候,就必须把这个句柄传给需要
它的Win32函数。
注意:
这个句柄和其他大多数的句柄不同,你无需关闭它。
SCM要求ServiceMain函数的线程在一秒钟内调用
RegisterServiceCtrlHandler函数,否则SCM会认为服务已经失败。
但在这种情
况下,SCM不会终止服务,不过在NT4中将无法启动这个服务,同时会返回一个
不正确的错误信息,这一点在Windows2000中得到了修正。
在RegisterServiceCtrlHandler函数返回后,ServiceMain线程要立即告诉
SCM服务正在继续初始化。
具体的方法是通过调用SetServiceStatus函数传递
SERVICE_STATUS数据结构。
BOOLSetServiceStatus(
SERVICE_STATUS_HANDLEhService,//服务的句柄
SERVICE_STATUSlpServiceStatus//SERVICE_STATUS结构的地址
)
这个函数要求传递给它指明服务的句柄(刚刚通过调用
RegisterServiceCtrlHandler得到),和一个初始化的SERVICE_STATUS结构的地
址:
typedefstruct_SERVICE_STATUS
{
DWORDdwServiceType;
DWORDdwCurrentState;
DWORDdwControlsAccepted;
DWORDdwWin32ExitCode;
DWORDdwServiceSpecificExitCode;
DWORDdwCheckPoint;
DWORDdwWaitHint;
}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内睡眠的线程激活,并像在前面解释过
的那样,减少它运行的服务的计数。
(三)对服务的深入讨论之下
现在我们还剩下一个函数可以在细节上讨论,那就是服务的CtrlHandler函
数。
当调用RegisterServiceCtrlHandler函数时,SCM得到并保存这个回调函数
的地址。
一个SCP调一个告诉SCM如何去控制服务的Win32函数,现在已经有10个
预定义的控制请求:
Controlcode Meaning
SERVICE_CONTROL_STOPRequeststheservicetostop.ThehServicehandle
musthaveSERVICE_STOPaccess.
SERVICE_CONTROL_PAUSERequeststheservicetopause.ThehService
handlemusthaveSERVICE_PAUSE_CONTINUEaccess.
SERVICE_CONTROL_CONTINUERequeststhepausedservicetoresume.The
hServicehandlemusthaveSERVICE_PAUSE_CONTINUEaccess.
SERVICE_CONTROL_INTERROGATERequeststheservicetoupdateimmediately
itscurrentstatusinformationtotheservicecontrolmanager.The
hServicehandlemusthaveSERVICE_INTERROGATEaccess.
SERVICE_CONTROL_SHUTDOWNRequeststheservicetoperformcleanup
tasks,becausethesystemisshuttingdown.Formoreinformation,see
Remarks.
SERVICE_CONTROL_PARAMCHANGEWindows2000:
Requeststheserviceto
rereaditsstartupparameters.ThehServicehandlemusthave
SERVICE_PAUSE_CONTINUEaccess.
SERVICE_CONTROL_NETBINDCHANGEWindows2000:
Reques
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Windows 服务 程序