3.1.2 服务应用程序.htm
来自「Windows2000后台服务程序开发手册」· HTM 代码 · 共 559 行 · 第 1/4 页
HTM
559 行
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>在可执行之处理程序中呼叫这个函数与传递服务阵列表内的位址可以指出哪些服务包含在该处理程序内。如此,SCM会知道哪些服务曾经尝试启动以及哪些服务会反覆地经由阵列来寻找它。一旦服务被发现,则会建立一个线程并执行该服务的ServiceMain函数(由SERVICE_TABLE_ENTRY阵列获得位址者)。</FONT></P>
<HR style="LINE-HEIGHT: 25px">
<P><FONT style="LINE-HEIGHT: 25px" face=Arial color=#3e77d7 size=3
Black><B style="LINE-HEIGHT: 25px">说明</B></FONT> </P>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>SCM保持着关闭的状态页签,以表示服务正以何种方式执行。例如,当SCM产生一个可执行的服务,SCM会等待可执行之主要线程去呼叫StartServiceCtrlDispatcher,如果StartServiceCtrlDispatcher在30秒内没有被呼叫,则SCM会认为该服务发生故障并且会呼叫TerminateProcess来强迫删除该处理程序。由于此原因,如果您的处理程序需要超过30秒的时间来初始化,您必须产生另一个线程去处理该初始化动作,所以主要的线程可以快速地呼叫StartServiceCtrlDispatcher。注意此处我所提出的是于全程序范围的初始化动作。个别的服务应该自己使用它们的ServiceMain函数来做初始化。</FONT></P>
<HR style="LINE-HEIGHT: 25px">
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>StartServiceCtrlDispatcher在所有服务执行结束前并不会返回。当至少一个服务正在执行时,SCM会控制该处理程序的主要线程。通常这个线程并没有做什么动作而且只是处于静止状态,所以不会浪费CPU时间。如果管理者试图去启动另一个实作在同一个执行档内的服务时,SCM不会产生另一个可执行档实例。反之,SCM会与可执行档之主要线程通讯并且再次重复服务的清单,并寻找此服务。一旦找到,一个新的线程会被产生,它会执行适当服务的ServiceMain函数。</FONT></P>
<HR style="LINE-HEIGHT: 25px">
<P><FONT style="LINE-HEIGHT: 25px" face=Arial color=#3e77d7 size=3
Black><B style="LINE-HEIGHT: 25px">说明</B></FONT> </P>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>当决定是否要产生一个新的服务程序或是一个已存在于服务处理程序的线程时,SCM会与一个服务执行路径字串做详细的比较。例如,在同一个可执行档MyServices.exe内执行二个服务。使用一个可执行的路径名称「%windir%\System32\MyService.exe」将第一个服务加到SCM资料库内,而第二个服务则是使用「C:\WinNT\System32\MyService.exe」而加到资料库内。如果这二个服务被启动,则SCM会产生二个分开的处理程序,二者皆执行同一个MyService.exe服务应用程序。在服务被加到SCM资料库时使用相同的路径名称字串是为了确定SCM将所有在单一可执行档内的服务用于一个处理程序上。</FONT></P>
<HR style="LINE-HEIGHT: 25px">
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>在系统内不会记录在处理程序中的哪些服务正在执行。当每个服务离开时(通常是因为ServiceMain返回的缘故),系统会检查并察看哪些服务仍然在执行。如果没有服务正在执行,则只有当时执行进入点函数呼叫者会被返回。因为处理程序中断执行的缘故,您的程序代码必须在全程序范围内执行清除的动作,并将进入点函数回传。注意您必须在30秒内或是在SCM删除该处理程序前完成您的清除程序代码。</FONT></P>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#3e72d7
size=4><B style="LINE-HEIGHT: 25px">ServiceMain函数<BR
style="LINE-HEIGHT: 25px"> </B></FONT></P>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>每一个在执行档内的服务必须拥有它们自己的ServiceMain函数:</FONT></P>
<DIV style="LINE-HEIGHT: 25px; BACKGROUND-COLOR: #d7d7d7"><FONT
style="LINE-HEIGHT: 25px" face=Arial size=3><PRE style="LINE-HEIGHT: 25px">VOID WINAPI ServiceMain( <BR> DWORD dwArgc, <BR> PTSTR* pszArgv);</PRE></FONT></DIV>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>SCM经由建立一个新的线程来启动一个服务,此线程使用它的ServiceMain函数执行。像之前所提到的,呼叫这个特定的ServiceMain函数,却可以将此函数命名为任何您想要的。您将此函数命名成任何名称皆不重要,因为您传递的是存在SERVICE_TABLE_ENTRY内的lpServiceProc成员。然而您不能在一个单一执行档内使用二个相同的ServiceMain函数;如果这样的话,当您试着建立您的专案时,编译器或连结器会产生错误。</FONT></P>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>ServiceMain函数需要传递二个参数,这些参数建立了一个允许管理者经由StartService函数使用一些命令列工具来启动服务的机制(在下一章中讨论)。就个人而言,我不知道有任何服务参考到这些参数并且建议您忽略它们。一个可经由读取下列所示之登录子机码内容来自我设定的服务比使用传递参数到ServiceMain函数的方法要好。</FONT></P>
<DIV style="LINE-HEIGHT: 25px; BACKGROUND-COLOR: #d7d7d7"><FONT
style="LINE-HEIGHT: 25px" face=Arial size=3><PRE style="LINE-HEIGHT: 25px">HKEY_LOCAL_MACHINE \SYSTEM \CurrentControlSet \Services \ServiceName \Parameters</PRE></FONT></DIV>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>许多服务包含在允许管理者修改服务设定的客户端应用程序中。该客户端简单地储存了那些存在登录子机码中的设定。当服务启动时,它会至登录中检查设定。</FONT></P>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>假如改变了一个正在执行之服务的架构时,可使用以下叁个选项:</FONT></P><FONT
style="LINE-HEIGHT: 25px" face=arial color=#000000 size=2>
<UL style="LINE-HEIGHT: 25px">
<LI
style="LINE-HEIGHT: 25px">服务会忽略此架构之修改内容,直到下一次服务启动为止。这是一个简单的选择,很多今天存在的服务皆会采取这个方法。<BR
style="LINE-HEIGHT: 25px">
<LI
style="LINE-HEIGHT: 25px">服务明确地被告知自己被重新设定。一个SCP会经由呼叫ServiceControl函数来传递SERVICE_CONTROL_PARAMCHANGE值。在第四章中我们会讨论如何达到这个功能的内容。<BR
style="LINE-HEIGHT: 25px">
<LI
style="LINE-HEIGHT: 25px">当一个外部应用程序改变了它的登录设定时,服务可以呼叫RegNotifyChangeKeyValue函数以取得通知。这允许一个服务在执行时重新设定它自己。第五章的RegNotify范例应用程序说明了如何完成这个任务。<BR
style="LINE-HEIGHT: 25px"> </LI></UL></FONT>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>ServiceMain必须完成的第一个任务是将服务的HandlerEx回呼函数的位址告诉SCM。它利用呼叫RegisterServiceCtrlHandlerEx来完成这个任务:</FONT></P>
<DIV style="LINE-HEIGHT: 25px; BACKGROUND-COLOR: #d7d7d7"><FONT
style="LINE-HEIGHT: 25px" face=Arial size=3><PRE style="LINE-HEIGHT: 25px">SERVICE_STATUS_HANDLE RegisterServiceCtrlHandlerEx( <BR> PCTSTR pszServiceName, // 服务的内部名称 <BR> LPHANDLER_FUNCTION_EX pfnHandler, // 服务的HandlerEx函数 <BR> PVOID pvContext); // 使用者定义值</PRE></FONT></DIV>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>第一个参数表示您放置了一个HandlerEx函数的服务,而第二个参数是HandlerEx函数的位址。当SERVICE_TABLE_ENTRY阵列被初始化并被传递到StartService-CtrlDispatcher时,pszServiceName参数必须与符合所使用的名称。最后一个参数pvContext是一个使用者定义的值,它会被传递到服务的HandlerEx函数中。下一节我们将会讨论HandlerEx函数的内容。</FONT></P>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>RegisterServiceCtrlHandlerEx返回了一个唯一可以从服务到SCM识别的SERVICE_STATUS_HANDLE值。所有将来从服务到SCM的通讯皆会要求以这个Handle来取代服务的内部字串名称。</FONT></P>
<HR style="LINE-HEIGHT: 25px">
<P><FONT style="LINE-HEIGHT: 25px" face=Arial color=#3e77d7 size=3
Black><B style="LINE-HEIGHT: 25px">说明</B></FONT> </P>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>不像大部份在系统中的Handle一般,从RegisterServiceCtrlHandlerEx回传的Handle不会为了您而关闭。</FONT></P>
<HR style="LINE-HEIGHT: 25px">
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>在RegisterServiceCtrlHandlerEx回传后,ServiceMain线程应该立即地告知SCM该服务已开始执行初始化。这个工作由呼叫SetServiceStatus函数来完成:</FONT></P>
<DIV style="LINE-HEIGHT: 25px; BACKGROUND-COLOR: #d7d7d7"><FONT
style="LINE-HEIGHT: 25px" face=Arial size=3><PRE style="LINE-HEIGHT: 25px">BOOL SetServiceStatus( <BR> SERVICE_STATUS_HANDLE hService, <BR> LPSERVICE_STATUS pServiceStatus);</PRE></FONT></DIV>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>这个函数要求您传递该识别您的服务之Handle(那些已经从呼叫RegisterServiceCtrlHandlerEx返回者)以及一个已初始化之SERVICE_STATUS之结构的位址:</FONT></P>
<DIV style="LINE-HEIGHT: 25px; BACKGROUND-COLOR: #d7d7d7"><FONT
style="LINE-HEIGHT: 25px" face=Arial size=3><PRE style="LINE-HEIGHT: 25px">typedef struct _SERVICE_STATUS { <BR> DWORD dwServiceType; <BR> DWORD dwCurrentState; <BR> DWORD dwControlsAccepted; <BR> DWORD dwWin32ExitCode; <BR> DWORD dwServiceSpecificExitCode; <BR> DWORD dwCheckPoint; <BR> DWORD dwWaitHint; <BR>} SERVICE_STATUS, *LPSERVICE_STATUS;</PRE></FONT></DIV>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>SERVICE_STATUS结构包含了影响服务之现行状态的七个成员。这些成员定义在如下所示清单,在您传递此结构到SetServiceStatus前,它们必须被正确地设定。</FONT></P><FONT
style="LINE-HEIGHT: 25px" face=arial color=#000000 size=2>
<UL style="LINE-HEIGHT: 25px">
<LI style="LINE-HEIGHT: 25px"><FONT style="LINE-HEIGHT: 25px"
face=arial color=#3e80d7 size=2><B
style="LINE-HEIGHT: 25px"> dwServiceType </B></FONT>这个成员表示您所执行的可执行服务类型。当您的可执行档中存在一个单一的服务,可以将它设定为SERVICE_WIN32_OWN_
PROCESS,而当您的可执行档包含了二个或多个服务时,则将它设定为SERVICE_WIN32_SHARE_PROCESS。除了这二个标记之外,当您的服务需要与桌面互动时,您可以在SERVICE_INTERACTIVE_PROCESS标记内做OR的动作(您应该尽量避免执行一个相互作用的服务)。在您的服务执行期间,绝对不要更改dwServiceType值。<BR
style="LINE-HEIGHT: 25px">
<LI style="LINE-HEIGHT: 25px"><FONT style="LINE-HEIGHT: 25px"
face=arial color=#3e80d7 size=2><B
style="LINE-HEIGHT: 25px"> dwCurrentState </B></FONT>这个成员是SERVICE_STATUS结构内最重要的。它将您的服务之现行状态告知SCM。若要在您的服务初始化期间报告现行状态,则您应该将此成员设定成SERVICE_START_PENDING。〈要求描述状态之程序代码〉一节会讨论HandlerEx并解释其他可能的值。<BR
style="LINE-HEIGHT: 25px">
<LI style="LINE-HEIGHT: 25px"><FONT style="LINE-HEIGHT: 25px"
face=arial color=#3e80d7 size=2><B
style="LINE-HEIGHT: 25px"> dwControlsAccepted </B></FONT>这个成员表示哪些控制通执服务是可接受的。假如您允许一个服务控制程序去暂停与继续执行您的服务,指定SERVICE_ACCEPT_PAUSE_CONTINUE即可。许多服务并不支援暂停与继续执行的功能,然而若这个功能可用在您的服务上,您便必须去解决它。假如您允许一个服务控制程序停止您的服务,必须指定SERVICE_ACCEPT_STOP。如果您想让您的服务在关机时收作业系统的通知,必须指定SERVICE_ACCEPT_SHUTDOWN。您也可以指定SERVICE_ACCEPT_
PARAMCHANGE、SERVICE_ACCEPT_HARDWAREPROFILECHANGE或SERVICE_ACCEPT_POWEREVENT,以决定您想要在参数被改变、硬体设定档改变以及收到电源事件通知时接收它。<BR
style="LINE-HEIGHT: 25px">使用OR运算子去结合被要求之标记组。注意当您的服务执行时可改变它接受的控制。例如,我写了一个只要没有客户端连接至它时,便允许它们自己暂停的服务。
<LI style="LINE-HEIGHT: 25px"><FONT style="LINE-HEIGHT: 25px"
face=arial color=#3e80d7 size=2><B
style="LINE-HEIGHT: 25px"> dwWin32ExitCode与dwServiceSpecificExitCode </B></FONT>这二个成员允许服务回报错误码。如果一个服务想要回报告一个Win32的错误码(预设是在WinError.h中),则它设定dwWin32ExitCode成员到所需的程序代码中。服务也可以回报一个被指定到服务而且没有对应到事先已定义之Win32错误码。要使服务执行这个动作,您必须设定dwWin32ExitCode成员为ERROR_SERVICE_
SPECIFIC_ERROR,然后设定dwServiceSpecificExitCode为服务器特性的错误码。注意一个经自订的SCP可能会要求回报这些错误码。当服务已经正常地执行并且没有回报错误时,请将dwWin32ExitCode成员设定为NO_ERROR。<BR
style="LINE-HEIGHT: 25px">
<LI style="LINE-HEIGHT: 25px"><FONT style="LINE-HEIGHT: 25px"
face=arial color=#3e80d7 size=2><B
style="LINE-HEIGHT: 25px"> dwCheckPoint与dwWaitHint </B></FONT>这二个成员允许一个服务回报它的进度。当您将dwCurrentState设定为SERVICE_START_PENDING时,您应该将dwCheckPoint设定为1,并将dwWaitHint设定为服务需抵达下一个SetServiceStatus呼叫时所需达到的毫秒数。一旦该服务已完全地被初始化后,您应该对SERVICE_STATUS结构的成员重新初始化,所以dwCurrentState成员设为SERVICE_RUNNING,然后将dwCheckPoint与dwWaitHint二者设定为0。<BR
style="LINE-HEIGHT: 25px"> </LI></UL></FONT>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>由于dwCheckPoint成员的存在而使您拥有优势。它允许一个服务回报它已经处理了多少进度。每一次您呼叫SetServiceStatus时,您应该增加dwCheckPoint到一个数字,以指示您的服务已经执行了什么「步骤」。要多常去回报服务执行进度完全取决于您。如果您决定在您的服务初始化的每个?骤皆回报执行进度,则dwWaitHint成员应该被设定为指示您认为到达下一个步骤(检查点(Checkpoint))所花的毫秒数,而不是指定到完成服务时所需的毫秒数。</FONT></P>
<HR style="LINE-HEIGHT: 25px">
<P><FONT style="LINE-HEIGHT: 25px" face=Arial color=#3e77d7 size=3
Black><B style="LINE-HEIGHT: 25px">说明</B></FONT> </P>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>ServiceMain函数必须在启动后80秒内或SCM认为该服务启动失败时呼叫SetServiceStatus。假如没有其他服务正在服务处理程序内执行,则SCM会删除该处理程序。</FONT></P>
<HR style="LINE-HEIGHT: 25px">
<HR style="LINE-HEIGHT: 25px">
<P><FONT style="LINE-HEIGHT: 25px" face=Arial color=#3e77d7 size=3
Black><B style="LINE-HEIGHT: 25px">说明</B></FONT> </P>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>在建立您的服务线程前,SCM会设定您的服务状态到显示一个START_PENDING的现行状态、一个为0的检查点以及一个等待2000毫秒的提示。如果您的ServiceMain函数要求超过2000毫秒的时间来做初始化的动作,则在您第一次呼叫SetServiceStatus时,应该指示一个SERVICE_START_PENDING之现行状态、一个为1的检查点以及一个被要求等待的提示。注意检查点应该被设定为1,一个程序开发者常犯的一个非常普通的错误是在第一次呼叫SetServiceStatus时将检查点设定为0,这会使管理者的SCP程序混淆,认为服务没有正确地回应。如果您的服务要求更多的初始化动作,您可以继续回报一个增加中的检查点,并依需要设定等待提示时间之SERVICE_START_PENDING状态。</FONT></P>
<HR style="LINE-HEIGHT: 25px">
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>在您的服务初始化完成后,您的服务会呼叫SetServiceStatus以指定为SERVICE_RUNNING(检查点与等待提示时间皆设定为0)。现在您的服务正在执行。通常一个服务会将它自己放在一个回圈内执行。在回圈内,该服务线程会自己暂停执行,并等待一个网路要求或一个指定服务应该暂停的通知、继续停止、关机等等。如果一个网路要求到来,该服务线程会醒来、处理该要求以及回到回圈中等待下一个要求或通知。</FONT></P>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>假如服务被一个通知唤醒,则它会处理该通知。如果服务收到一个停止或关机的通知,则该回圈会结束以及该服务的ServiceMain函数会返回、并删除线程。若服务为处理程序中最后一个被执行,则该处理程序也会停止执行。</FONT></P>
<HR style="LINE-HEIGHT: 25px">
<P><FONT style="LINE-HEIGHT: 25px" face=Arial color=#3e77d7 size=3
Black><B style="LINE-HEIGHT: 25px">说明</B></FONT> </P>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>SetServiceStatus函数会检查SERVICE_STATUS结构的+成员。若此成员被设定为SERVICE_STOPPED,则SetServiceStatus会关闭该服务的Handle(第一个传递至SetServiceStatus的参数)。这就是为什么您从来不必很明确地关闭从RegisterServiceCtrlHandlerEx回传的Handle,以及更重要的是为什么在呼叫它与一个SERVICE_STOPPED的现行状态后不用呼叫SetServiceStatus的答案。当在除错器之下执行服务时,试图那么做会引起一个无效的Handle例外。</FONT></P>
<HR style="LINE-HEIGHT: 25px">
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#3e72d7
size=4><B style="LINE-HEIGHT: 25px">HandlerEx函数<BR
style="LINE-HEIGHT: 25px"> </B></FONT></P>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>每一个在可执行文件中的服务必须与一个HandlerEx关联:</FONT></P>
<DIV style="LINE-HEIGHT: 25px; BACKGROUND-COLOR: #d7d7d7"><FONT
style="LINE-HEIGHT: 25px" face=Arial size=3><PRE style="LINE-HEIGHT: 25px">DWORD WINAPI HandlerEx( <BR> DWORD dwControl, <BR> DWORD dwEventType, <BR> PVOID pvEventData, <BR> PVOID pvContext);</PRE></FONT></DIV>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
⌨️ 快捷键说明
复制代码Ctrl + C
搜索代码Ctrl + F
全屏模式F11
增大字号Ctrl + =
减小字号Ctrl + -
显示快捷键?