Windows服务编写原理及探讨

来源:互联网 发布:matlab mac 2016 破解 编辑:程序博客网 时间:2024/05/16 13:48

有那么一类应用程序,是能够为各种用户(包括本地用户和远程用户)所用的,拥有用户授权级进行管理的能力,并且不论用户是否物理的与正在运行该应用程序的计算机相连都能正常执行,这就是所谓的服务了。

(一)服务的基础知识

Question 1. 什么是服务?它的特征是什么?

  在NT/2000中,服务是一类受到操作系统优待的程序。一个服务首先是一个Win32可执行程序,如果要写一个功能完备且强大的服务,需要熟悉动态连接库(Dlls)、结构异常处理、内存映射文件、虚拟内存、设备I/O、线程及其同步、Unicode以及其他的由WinAPI函数提供的应用接口。当然本文讨论的只是建立一个可以安装、运行、启动、停止的没有任何其他功能的服务,所以无需上述知识仍可以继续看下去,我会在过程中将理解本文所需要的知识逐一讲解。

  第二要知道的是一个服务决不需要用户界面。大多数的服务将运行在那些被锁在某些黑暗的,冬暖夏凉的小屋子里的强大的服务器上面,即使有用户界面一般也没有人可以看到。如果服务提供任何用户界面如消息框,那么用户错过这些消息的可能性就极高了,所以服务程序通常以控制台程序的形式被编写,进入点函数是main()而不是WinMain()。

  也许有人有疑问:没有用户界面的话,要怎样设置、管理一个服务?怎样开始、停止它?服务如何发出警告或错误信息、如何报告关于它的执行情况的统计数据?这些问题的答案就是服务能够被远程管理,Windows NT/2000提供了大量的管理工具,这些工具允许通过网络上的其它计算机对某台机器上面的服务进行管理。比如Windows 2000里面的“控制台”程序(mmc.exe),用它添加“管理单元”就可以管理本机或其他机器上的服务。

Question 2. 服务的安全性...

  想要写一个服务,就必须熟悉Win NT/2000的安全机制,在上述操作系统之中,所有安全都是基于用户的。换句话说——进程、线程、文件、注册表键、信号、事件等等等等都属于一个用户。当一个进程被产生的时候,它都是执行在一个用户的上下文(context),这个用户帐号可能在本机,也可能在网络中的其他机器上,或者是在一个特殊的账号:System Account——即系统帐号的上下文

  如果一个进程正在一个用户帐号下执行,那么这个进程就同时拥有这个用户所能拥有的一切访问权限,不论是在本机还是网络。系统帐号则是一个特殊的账号,它用来标识系统本身,而且运行在这个帐号下的任何进程都拥有系统上的所有访问权限,但是系统帐号不能在域上使用,无法访问网络资源...

  服务也是Win32可执行程序,它也需要执行在一个context,通常服务都是在系统账号下运行,但是也可以根据情况选择让它运行在一个用户账号下,也就会因此获得相应的访问资源的权限。

Question 3. 服务的三个组成部分

  一个服务由三部分组成,第一部分是Service Control Manager(SCM)。每个Windows NT/2000系统都有一个SCM,SCM存在于Service.exe中,在Windows启动的时候会自动运行,伴随着操作系统的启动和关闭而产生和终止。这个进程以系统特权运行,并且提供一个统一的、安全的手段去控制服务。它其实是一个RPC Server,因此我们可以远程安装和管理服务,不过这不在本文讨论的范围之内。SCM包含一个储存着已安装的服务和驱动程序的信息的数据库,通过SCM可以统一的、安全的管理这些信息,因此一个服务程序的安装过程就是将自身的信息写入这个数据库。

  第二部分就是服务本身。一个服务拥有能从SCM收到信号和命令所必需的的特殊代码,并且能够在处理后将它的状态回传给SCM。

  第三部分也就是最后一部分,是一个Service Control Dispatcher(SCP)。它是一个拥有用户界面,允许用户开始、停止、暂停、继续,并且控制一个或多个安装在计算机上服务的Win32应用程序。SCP的作用是与SCM通讯,Windows 2000管理工具中的“服务”就是一个典型的SCP。

  在这三个组成部分中,用户最可能去写服务本身,同时也可能不得不写一个与其伴随的客户端程序作为一个SCP去和SCM通讯,本文只讨论去设计和实现一个服务,关于如何去实现一个SCP则在以后的其它文章中介绍。

Question 4. 怎样开始设计服务

  还记得前面我提到服务程序的入口点函数一般都是main()吗?一个服务拥有很重要的三个函数,第一个就是入口点函数,其实用WinMain()作为入口点函数也不是不可以,虽然说服务不应该有用户界面,但是其实存在很少的几个例外,这就是下面图中的选项存在的原因。

  由于要和用户桌面进行信息交互,服务程序有时会以WinMain()作为入口点函数。

  入口函数负责初始化整个进程,由这个进程中的主线程来执行。这意味着它应用于这个可执行文件中的所有服务。要知道,一个可执行文件中能够包含多个服务以使得执行更加有效。主进程通知SCM在可执行文件中含有几个服务,并且给出每一个服务的ServiceMain回调(Call Back)函数的地址。一旦在可执行文件内的所有服务都已经停止运行,主线程就在进程终止前对整个进程进行清除。

  第二个很重要的函数就是ServiceMain,我看过一些例子程序里面对自己的服务的进入点函数都固定命名为ServiceMain,其实并没有规定过一定要那样命名,任何的函数只要符合下列的形式都可以作为服务的进入点函数。

VOID WINAPI ServiceMain(
  DWORD dwArgc, // 参数个数

  LPTSTR *lpszArgv // 参数串
);

  这个函数由操作系统调用,并执行能完成服务的代码。一个专用的线程执行每一个服务的ServiceMain函数,注意是服务而不是服务程序,这是因为每个服务也都拥有与自己唯一对应的ServiceMain函数,关于这一点可以用“管理工具”里的“服务”去察看Win2000里面自带的服务,就会发现其实很多服务都是由service.exe单独提供的。当主线程调用Win32函数StartServiceCtrlDispatcher的时候,SCM为这个进程中的每一个服务产生一个线程。这些线程中的每一个都和它的相应的服务的ServiceMain函数一起执行,这就是服务总是多线程的原因——一个仅有一个服务的可执行文件将有一个主线程,其它的线程执行服务本身。

  第三个也就是最后的一个重要函数是CtrlHandler,它必须拥有下面的原型:

VOID WINAPI CtrlHandler(
DWORD fdwControl //控制命令
)

  像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_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 shouldimmediately call the RegisterServiceCtrlHandler function to specify aHandler 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内睡眠的线程激活,并像在前面解释过的那样,减少它运行的服务的计数。
现在我们还剩下一个函数可以在细节上讨论,那就是服务的CtrlHandler函数。

  当调用RegisterServiceCtrlHandler函数时,SCM得到并保存这个回调函数的地址。一个SCP调一个告诉SCM如何去控制服务的Win32函数,现在已经有10个预定义的控制请求:

Control code
Meaning
SERVICE_CONTROL_STOPRequests the service to stop. ThehService handle must have SERVICE_STOP access.SERVICE_CONTROL_PAUSERequests the service to pause. ThehService handle must have SERVICE_PAUSE_CONTINUE access.SERVICE_CONTROL_CONTINUERequests the paused service to resume. ThehService handle must have SERVICE_PAUSE_CONTINUE access.SERVICE_CONTROL_INTERROGATERequests the service to update immediately its current status information to the service control manager. ThehService handle must have SERVICE_INTERROGATE access.SERVICE_CONTROL_SHUTDOWNRequests the service to perform cleanup tasks, because the system is shutting down.
For more information, see Remarks.
SERVICE_CONTROL_PARAMCHANGEWindows 2000: Requests the service to reread its startup parameters. ThehService handle must have SERVICE_PAUSE_CONTINUE access.SERVICE_CONTROL_NETBINDCHANGEWindows 2000: Requests the service to update its network binding. ThehService handle must have SERVICE_PAUSE_CONTINUE access.SERVICE_CONTROL_NETBINDREMOVEWindows 2000: Notifies a network service that a component for binding has been removed. The service should reread its binding information and unbind from the removed component. SERVICE_CONTROL_NETBINDENABLEWindows 2000: Notifies a network service that a disabled binding has been enabled. The service should reread its binding information and add the new binding.SERVICE_CONTROL_NETBINDDISABLEWindows 2000: Notifies a network service that one of its bindings has been disabled. The service should reread its binding information and remove the binding.

  上表中标有Windows 2000字样的就是2000中新添加的控制代码。除了这些代码之外,服务也可以接受用户定义的,范围在128-255之间的代码。

  当CtrlHandler函数收到一个SERVICE_CONTROL_STOP、SERVICE_CONTROL_PAUSE、
SERVICE_CONTROL_CONTINUE控制代码的时候,SetServiceStatus必须被调用去确认这个代码,并指定你认为服务处理这个状态变化所需要的时间。

  例如:你的服务收到了停止请求,首先要把SERVICE_STATUS结构的dwCurrentState成员设置成SERVICE_STOP_PENDING,这样可以使SCM确定你已经收到了控制代码。当一个服务的暂停或停止操作正在执行的时候,必须指定你认为这种操作所需要的时间:这是因为一个服务也许不能立即改变它的状态,它可能必须等待一个网络请求被完成或者数据被刷新到一个驱动器上。指定时间的方法就像我上一章说的那样,用成员dwCheckPoint和dwWaitHint来指明它完成状态改变所需要的时间。如果需要,可以用增加dwCheckPoint成员的值和设置dwWaitHint成员的值去指明你期待的服务到达下一步的时间的方式周期性的报告进展情况。

  当整个启动的过程完成之后,要再一次调用SetServiceStatus。这时就要把SERVICE_STATUS结构的dwCurrentState成员设置成SERVICE_STOPPED,当报告状态代码的同时,一定要把成员dwCheckPoint和dwWaitHint设置为0,因为服务已经完成了它的状态变化。暂停或继续服务的时候方法也一样。

  当CtrlHandler函数收到一个SERVICE_CONTROL_INTERROGATE控制代码的时候,服务将简单的将dwCurrentState成员设置成服务当前的状态,同时,把成员dwCheckPoint和dwWaitHint设置为0,然后再调用SetServiceStatus就可以了。

  在操作系统关闭的时候,CtrlHandler函数收到一个SERVICE_CONTROL_SHUTDOWN控制代码。服务根本无须回应这个代码,因为系统即将关闭。它将执行保存数据所需要的最小行动集,这是为了确定机器能及时关闭。缺省时系统只给很少的时间去关闭所有的服务,MSDN里面说大概是20秒的时间,不过那可能是Windows NT 4的设置,在我的Windows 2000 Server里这个时间是10秒,你可以手动的修改这个数值,它被记录在HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control子键里面的WaitToKillServiceTimeout,单位是毫秒。

  当CtrlHandler函数收到任何用户定义的代码时,它应该执行期望的用户自定义行动。除非用户自定义的行动要强制服务去暂停、继续或停止,否则不调SetServiceStatus函数。如果用户定义的行动强迫服务的状态发生变化,SetServiceStatus将被调用去设置dwCurrentState、dwCheckPoint和dwWaitHint,具体控制代码和前面说的一样。

  如果你的CtrlHandler函数需要很长的时间执行操作的话,千万要注意:假如CtrlHandler函数在30秒内没有返回的话,SCM将返回一个错误,这不是我们所期望的。所以如果出现上述情况,最好的办法是再建立一个线程,让它去继续执行操作,以便使得CtrlHandler函数能够迅速的返回。例如,当收到一个SERVICE_CONTROL_STOP请求的时候,就像上面说的一样,服务可能正在等待一个网络请求被完成或者数据被刷新到一个驱动器上,而这些操作所需要的时间是你不能估计的,那么就要建立一个新的线程等待操作完成后执行停止命令,CtrlHandler函数在返回之前仍然要报告SERVICE_STOP_PENDING状态,当新的线程执行完操作之后,再由它将服务的状态设置成SERVICE_STOPPED。如果当前操作的时间可以估计的到就不要这样做,仍然使用前面交待的方法处理。

  CtrlHandler函数我就先讲这些,下面说说服务怎么安装。一个服务程序可以使用CreateService函数将服务的信息添加到SCM的数据库。

SC_HANDLE CreateService(
SC_HANDLE hSCManager, // handle to SCM database
LPCTSTR lpServiceName, // name of service to start
LPCTSTR lpDisplayName, // display name
DWORD dwDesiredAccess, // type of access to service
DWORD dwServiceType, // type of service
DWORD dwStartType, // when to start service
DWORD dwErrorControl, // severity of service failure
LPCTSTR lpBinaryPathName, // name of binary file
LPCTSTR lpLoadOrderGroup, // name of load ordering group
LPDWORD lpdwTagId, // tag identifier
LPCTSTR lpDependencies, // array of dependency names
LPCTSTR lpServiceStartName, // account name
LPCTSTR lpPassword // account password
);

  hSCManager是一个标示SCM数据库的句柄,可以简单的通过调用OpenSCManager得到。

SC_HANDLE OpenSCManager(
LPCTSTR lpMachineName, // computer name
LPCTSTR lpDatabaseName, // SCM database name
DWORD dwDesiredAccess // access type
);

  lpMachineName是目标机器的名字,还记得我在第一章里说过可以在其它的机器上面安装服务吗?这就是实现的方法。对方机器名字必须以“\\”开始。如果传递NULL或者一个空的字符串的话就默认是本机。

  lpDatabaseName是目标机器上面SCM数据库的名字,但MSDN里面说这个参数要默认的设置成SERVICES_ACTIVE_DATABASE,如果传递NULL,就默认的打开SERVICES_ACTIVE_DATABASE。所以我还没有真的搞明白这个参数的存在意义,总之使用的时候传递NULL就行了。

  dwDesiredAccess是SCM数据库的访问权限,具体值见下表:

Object access
Description
SC_MANAGER_ALL_ACCESSIncludes STANDARD_RIGHTS_REQUIRED, in addition to all of the access types listed in this table.SC_MANAGER_CONNECTEnables connecting to the service control manager.SC_MANAGER_CREATE_SERVICEEnables calling of theCreateService function to create a service object and add it to the database.SC_MANAGER_ENUMERATE_SERVICEEnables calling of theEnumServicesStatus function to list the services that are in the database.SC_MANAGER_LOCKEnables calling of theLockServiceDatabase function to acquire a lock on the database.SC_MANAGER_QUERY_LOCK_STATUSEnables calling of theQueryServiceLockStatus function to retrieve the lock status information for the database.

  想要获得访问权限的话,似乎没那么复杂。MSDN里面说所有进程都被允许获得对所有SCM数据库的SC_MANAGER_CONNECT, SC_MANAGER_ENUMERATE_SERVICE, and SC_MANAGER_QUERY_LOCK_STATUS权限,这些权限使得你可以连接SCM数据库,枚举目标机器上安装的服务和查询目标数据库是否已被锁住。但如果要创建服务,首先你需要拥有目标机器的管理员权限,一般的传递SC_MANAGER_ALL_ACCESS就可以了。这个函数返回的句柄可以被CloseServiceHandle函数关闭。

  lpServiceName是服务的名字,lpDisplayName是服务在“服务”管理工具里显示的名字。

  dwDesiredAccess也是访问的权限,有一个比上面的还长的多的一个表,各位自己查MSDN吧。我们要安装服务,仍然简单的传递SC_MANAGER_ALL_ACCESS。

  dwServiceType是指你的服务是否和其它的进程相关联,一般是SERVICE_WIN32_OWN_PROCESS,表示不和任何进程相关联。如果你确认你的服务需要和某些进程相关联,就设置成SERVICE_WIN32_SHARE_PROCESS。当你的服务要和桌面相关联的时候,需要设置成SERVICE_INTERACTIVE_PROCESS。

  dwStartType是服务的启动方式。服务有三种启动方式,分别是“自动(SERVICE_AUTO_START)”“手动(SERVICE_DEMAND_START)”和“禁用(SERVICE_DISABLED)”。在MSDN里还有另外的两种方式,不过是专为驱动程序设置的。

  dwErrorControl决定服务如果在系统启动的时候启动失败的话要怎么办。

意义
SERVICE_ERROR_IGNORE启动程序记录错误发生,但继续启动。SERVICE_ERROR_NORMAL启动程序记录错误发生,并弹出一个消息框,但仍继续启动SERVICE_ERROR_SEVERE启动程序记录错误发生,如果是以last-known-good configuration启动的话,启动会继续。否则会以last-known-good configuration重新启动计算机。SERVICE_ERROR_CRITICAL启动程序记录错误发生,如果可能的话。如果是以last-known-good configuration启动的话,启动会失败。否则会以last-known-good configuration重新启动计算机。好严重的错误啊。

  lpBinaryPathName是服务程序的路径。MSDN里面特别提到如果服务路径里面有空格的话一定要将路径用引号引起来。例如"d:\\my share\\myservice.exe"就一定要指定为"\"d:\\my share\\myservice.exe\""。

  lpLoadOrderGroup的意义在于,如果有一组服务要按照一定的顺序启动的话,这个参数用于指定一个组名用于标志这个启动顺序组,不过我还没有用过这个参数。你的服务如果不属于任何启动顺序组,只要传递NULL或者一个空的字符串就行了。

  lpdwTagId是应用了上面的参数之后要指定的值,专用于驱动程序,与本文内容无关。传递NULL。

  lpDependencies标示一个字符串数组,用于指明一串服务的名字或者一个启动顺序组。当与一个启动顺序组建立关联的时候,这个参数的含义就是只有你指定的启动顺序组里有至少一个经过对整个组里所有的成员已经全部尝试过启动后,有至少一个成员成功启动,你的服务才能启动。不需要建立依存关系的话,仍是传递NULL或者一个空的字符串。但如果你要指定启动顺序组的话,必须为组名加上SC_GROUP_IDENTIFIER前缀,因为组名和服务名是共享一个命名空间的。

  lpServiceStartName是服务的启动账号,如果你设置你的服务的关联类型是SERVICE_WIN32_OWN_PROCESS的话,你需要以DomainName\UserName的格式指定用户名,如果这个账户在你本机的话,用.\UserName就可以指定。如果传递NULL的话,会以本地的系统账户登陆。如果是Win NT 4.0或更早的版本的话,如果你指定了SERVICE_WIN32_SHARE_PROCESS,就必须传递.\System指定服务使用本地的系统账户。最后,如果你指定了SERVICE_INTERACTIVE_PROCESS,你必须使服务运行在本机系统账户。

  看名字就知道了,lpPassword是账户的密码。如果指定系统账户的话,传递NULL。如果账户没有密码的话,传递空字符串。

四)一些问题的讨论

  前面的内容都是服务的一些通用的编写原理,但里面隐含着一些问题,编写简单的服务时看不出来,但遇到复杂的应用就会出现一些问题,所以本章就是用来分析、解决这些问题的,适用于高级应用的开发人员。我这一章的内容都是经过实验得到的,很有实际意义。

  前面说过,是由一个服务的主线程执行CtrlHandler函数,它将收到各种控制命令,但是真正处理命令,执行操作的是ServiceMain的线程。现在,当一个SERVICE_CONTROL_STOP到达之后,你作为一个开发者,要怎样停止这个服务?在我看过的一些源代码里,大部分只是简单的调用TerminateThread函数去强行杀掉服务进程。但应该稍稍有点线程编程的常识就应该知道TerminateThread函数是可用的调用中最为糟糕的一个,服务线程将得不到任何机会去做应该的清理工作,诸如清除内存、释放核心对象,Dlls也得不到任何线程已经被毁的通知。

  所以停止服务的适当方法是以某种方式激活服务线程,让它停止继续提供服务功能,然后执行完当前操作和清除工作后返回。这就表示你必须在CtrlHandler线程和ServiceMain线程之间执行适当的线程通信。现在已知的最好的内部线程通信机制是I/O Completion Port(I/O 完成端口),假如你编写的是一个大型的服务,需要同时处理为数众多的请求,并且运行在多处理器系统上面,这个模型就可以提供最佳的系统性能。但也正因为它的复杂性较高,在小规模的应用上面不值得花费很多的时间和精力,这时作为开发者可以适当的选取其它的通信方式,诸如异步过程调用队列、套接字和窗口消息,以适应实际情况。

  开发服务时的另外一个重要问题就是调用SetServiceStatus函数时的所有状态报告问题。很多的服务开发者为了在什么时候调用SetServiceStatus的问题而常常产生争论,一般推荐的方法就是:先调用SetServiceStatus函数,报告SERVICE_STOP_PENDING状态,然后将控制代码传给服务线程或者再建立一个新的线程,让它去继续执行操作,当该线程即将执行完操作之前,再由它将服务的状态设置成SERVICE_STOPPED,然后服务正好停止。

  上面的主意从两个方面来讲还是很不错的。首先服务可以立即确认收到了控制代码,并将在它认为适当的时候进行处理;然后就是因为前面说过的,执行CtrlHandler函数的是主线程,如果按照这种工作方法,CtrlHandler函数可以迅速的返回,不会影响到其它服务可能收到的控制请求,对含有多个服务的程序来说,响应各个服务的控制代码的速度会大大的提高。可是,随之而来的是问题——race condition 即“竞争条件”的产生。

  摆在下面的就是一个竞争条件的例子,我花了一点时间来修改我的基本服务的代码,意图故意引发“竞争条件”的发生。我添加了一个线程,CtrlHandler函数的线程在收到请求后立刻作出反应,将当前的服务状态设置成“请求正在被处理”即..._PENDING,然后由我添加的线程在睡眠了5秒之后再将服务状态设置成“请求已完成”状态——以模拟服务正在处理一些不可中止的事件,只有处理完成后才会更改服务的状态。一切就绪之后,我尝试在短时间内连续发送两个“暂停”请求,如果“竞争条件”不存在的话应该只有先发送的那个请求能够到达SCM,而另一个则应该返回请求发送失败的信息,天下太平。

  事实上很不幸的,我成功了。当我在两个不同的“命令提示符”窗口分别同样的输入下面的命令:

net pause kservice

  之后在“事件查看器”里面,我找到了我的服务在“应用程序日志”里添加的事件记录,结果是我得到了这样的事件列表:

SERVICE_PAUSE_PENDING
SERVICE_PAUSE_PENDING
SERVICE_PAUSED
SERVICE_PAUSED

  看上去很奇怪是不是?因为服务处于正在暂停状态的时候,它不应该被再次暂停的。但事实摆在眼前,很多服务都曾明确的报告过上面的顺序状态。我曾经认为这时SCM应该说些什么或做些什么,以阻止“竞争状态”的出现,但实验结果告诉我SCM似乎对此无能为力,因为它不能控制状态代码在什么时候被发送。当用户使用“管理工具”里面的“服务”工具来管理服务的状态的时候,在一个“暂停”请求已经发出之后不能再次用这个工具向它发出“暂停”请求,如果正在暂停服务,会有一个对话框出现,阻止你按下它后面的“服务”工具的工具栏上的任何按钮,如果已经暂停,“暂停“按钮将变成灰色。但是这时用命令行工具 net.exe 就可以很顺利地将暂停请求再次送到服务。证据就是我添加的其他事件记录里面记下了SetServiceStatus的调用全都成功了,这更进一步的说明了我提交的两个暂停请求都经过SCM,然后到达了我的服务。

  接下来我又进行了其它的测试,例如先发送“暂停”请求,后发送“停止”请求,和先发送“停止”请求,再发送“暂停”或“停止”请求。前一种情况更加糟糕,先发送的“暂停”请求和后发送的“停止”请求都没有得到什么好下场,虽然SCM老老实实的先暂停了服务,后停止了服务,但 net.exe 的两个实例的调用均告失败。不过在测试先发送停止“请求”的时候,所有的现象都表示这两个请求只有先发送的“停止”到达了SCM,这还算是个好消息...

  为了解决这个问题,当服务得到一个“停止”“暂停”或“继续”请求的时候,应该首先检查服务是否已经在处理另外的一个请求,如果是,就依情况而定:是不调用SetServiceStatus直接返回还是暂时忍耐直到前一个请求动作完成再调用SetServiceStatus,这是你作为一个开发者要自己决定的。

  如果说前面的问题已经足够麻烦了,下面的问题会令你觉得更加怪异。它其实是一种可以解决上面的问题的方法:当CtrlHandler函数的线程收到SERVICE_PAUSE_PENDING请求之后,它调用SetServiceStatus报告服务正在暂停,然后由它自己调用SuspendThread来暂停服务的线程,然后再由它自己调用SetServiceStatus报告服务已经被暂停。这样做的确避免了“竞争条件”的出现,因为所有的工作都是由一个函数来做的。现在需要注意的不是“竞争条件”而是服务本身,挂起服务的线程会不会暂停服务呢?答案是会的。但是暂停服务意味着什么呢?

  假如我的服务是用来处理网络客户的请求,那么暂停对于我的服务来说应该是停止接受新的请求。如果我现在正处在处理请求的过程中,那么我应该怎么办?也许我应该结束它,使客户不至于无限期悬挂。但如果我只是简单的调用SuspendThread,那么不排除服务线程正处于孤立的中间状态的可能,或者正在调用malloc函数去尝试分配内存,如果运行在同一个进程中的另一个服务也调内存分配函数,那么它也会被挂起,这肯定不是我期望的结果。

  还有一个问题:用户认为自己可以被允许去停止一个已经被暂停了的服务吗?我认为是这样的,而且很明显的,微软也这么认为。因为当我们在“服务”管理工具里面选中一个已暂停的服务之后,“停止”按钮是可以被按下的。但我要怎样停止一个由于线程被挂起才处于暂停状态的服务呢?不,不要TerminateThread,请别跟我提起它。

  解决这所有的混乱的最好方法,就是有一个能够把所有事做好的线程,而且它应该是服务线程,而不是CtrlHandler线程。当CtrlHandler函数得到控制代码之后,它要迅速的将控制代码通过线程内部通讯手段送到服务线程中排队,然后CtrlHandler函数就应该返回,它决不应该调SetServiceStatus。这样,服务可以随心所欲的控制每件事情,因为没有什么比它更有发言权的了,没有“竞争条件”。服务决定暂停意味着什么,服务能够允许自己在已经暂停的情况下停止,服务决定什么内部通讯机制是最好的——并且CtrlHandler函数必须简单的与这种机制相一致。

  事情没有完美的,上面的方法也不例外,它仅有一个小缺陷:就是假定当服务收到控制代码后,在较短的时间内就能做出应有的响应。如果服务线程正在忙于处理一个客户的请求,控制代码可能进入等待队列,而且SetServiceStatus可能也无法迅速的被调用。如果真是这样的话,负责发送通知的SCP可能会认为你的服务已经失败,并向用户报告一个消息框。事实上服务并没有失败,而且也不会被终止。

  这种情况够糟糕了,没有用户会去责怪SCP——虽然SCP将他们引导到了错误的状态,他们只会责怪服务的作者——就是我或你...因此,在服务中怎么做才能防止这种问题发生呢?很简单,使服务快速有效的运行,并且总保持一个活动线程等待去处理控制代码。

  说起来好像很容易,但实际做起来就被那么简单了,这也不是我能够向各位解释的了,只有认真的调试自己的服务,才能找出最为适合处理方法。所以我的文章也真的到了该结束的时候了,感谢各位的浏览。如果我有什么地方说的不对,请不吝赐教,谢谢。

  下面是我写的一个服务的源代码,没什么功能,只能启动、停止和安装。

#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <tchar.h>

#define SZAPPNAME "basicservice"
#define SZSERVICENAME "KService"
#define SZSERVICEDISPLAYNAME "KService"
#define SZDEPENDENCIES ""

void WINAPI KServiceMain(DWORD argc, LPTSTR * argv);
void InstallService(const char * szServiceName);
void LogEvent(LPCTSTR pFormat, ...);
void Start();
void Stop();

SERVICE_STATUS ssStatus;
SERVICE_STATUS_HANDLE sshStatusHandle;


int main(int argc, char * argv[])
{
  if ((argc==2) && (::strcmp(argv[1]+1, "install")==0))
  {
    InstallService("KService");
    return 0;
  }

SERVICE_TABLE_ENTRY   service_table_entry[] =
  {
    { "KService", KServiceMain },
    { NULL, NULL }
  };
  ::StartServiceCtrlDispatcher(service_table_entry);
  return 0;
}

void InstallService(const char * szServiceName)
{
  SC_HANDLE handle = ::OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
  char szFilename[256];
  ::GetModuleFileName(NULL, szFilename, 255);
  SC_HANDLE hService = ::CreateService(handle, szServiceName,
  szServiceName, SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS,
  SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE, szFilename, NULL,
  NULL, NULL, NULL, NULL);
  ::CloseServiceHandle(hService);
  ::CloseServiceHandle(handle);
}

SERVICE_STATUS servicestatus;
SERVICE_STATUS_HANDLE servicestatushandle;

void WINAPI ServiceCtrlHandler(DWORD dwControl)
{
  switch (dwControl)
  {

//下面虽然添加了暂停、继续等请求的处理代码,但没有实际作用
  //这是为什么呢?到了下面的KServiceMain函数里面就明白了...

case SERVICE_CONTROL_PAUSE:
    servicestatus.dwCurrentState = SERVICE_PAUSE_PENDING;
    // TODO: add code to set dwCheckPoint & dwWaitHint
    // This value need to try a lot to confirm
    // ...
    ::SetServiceStatus(servicestatushandle, &servicestatus);
    // TODO: add code to pause the service
    // not called in this service
    // ...
    servicestatus.dwCurrentState = SERVICE_PAUSED;
    // TODO: add code to set dwCheckPoint & dwWaitHint to 0
    break;

  case SERVICE_CONTROL_CONTINUE:
    servicestatus.dwCurrentState = SERVICE_CONTINUE_PENDING;
    // TODO: add code to set dwCheckPoint & dwWaitHint

    ::SetServiceStatus(servicestatushandle, &servicestatus);
    // TODO: add code to unpause the service
    // not called in this service
    // ...
    servicestatus.dwCurrentState = SERVICE_RUNNING;
    // TODO: add code to set dwCheckPoint &
dwWaitHint
to 0
    break;

  case SERVICE_CONTROL_STOP:
    servicestatus.dwCurrentState = SERVICE_STOP_PENDING;
    // TODO: add code to set dwCheckPoint & dwWaitHint

    
::SetServiceStatus(servicestatushandle, &servicestatus);
    // TODO: add code to stop the service
    Stop();
    servicestatus.dwCurrentState = SERVICE_STOPPED;
    // TODO: add code to set dwCheckPoint & dwWaitHint
to 0
    break;

  case SERVICE_CONTROL_SHUTDOWN:
    // TODO: add code for system shutdown
    // as quick as possible
    break;

  case SERVICE_CONTROL_INTERROGATE:
    // TODO: add code to set the service status
    // ...
    servicestatus.dwCurrentState = SERVICE_RUNNING;
    break;
  }
  ::SetServiceStatus(servicestatushandle, &servicestatus);
}

void WINAPI KServiceMain(DWORD argc, LPTSTR * argv)
{
  servicestatus.dwServiceType = SERVICE_WIN32;
  servicestatus.dwCurrentState = SERVICE_START_PENDING;
  servicestatus.dwControlsAccepted = SERVICE_ACCEPT_STOP;//上面的问题的答案就在这里

  servicestatus.dwWin32ExitCode = 0;
  servicestatus.dwServiceSpecificExitCode = 0;
  servicestatus.dwCheckPoint = 0;
  servicestatus.dwWaitHint = 0;

  servicestatushandle =
  ::RegisterServiceCtrlHandler("KService", ServiceCtrlHandler);
  if (servicestatushandle == (SERVICE_STATUS_HANDLE)0)
  {
    return;
  }

  bool bInitialized = false;
  // Initialize the service
  // ...
  Start();

  bInitialized = true;

  servicestatus.dwCheckPoint = 0;
  servicestatus.dwWaitHint = 0;
  if (!bInitialized)
  {
    servicestatus.dwCurrentState = SERVICE_STOPPED;
    servicestatus.dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR;
    servicestatus.dwServiceSpecificExitCode = 1;
  }
  else
  {
    servicestatus.dwCurrentState = SERVICE_RUNNING;
  }
  ::SetServiceStatus(servicestatushandle, &servicestatus);
  return;
}


void Start()
{
  LogEvent("Service Starting...");
}

void LogEvent(LPCTSTR pFormat, ...)
{
  TCHAR chMsg[256];
  HANDLE hEventSource;
  LPTSTR lpszStrings[1];
  va_list pArg;

  va_start(pArg, pFormat);
  _vstprintf(chMsg, pFormat, pArg);
  va_end(pArg);

  lpszStrings[0] = chMsg;

  if (1)
  {
    // Get a handle to use with ReportEvent().
    hEventSource = RegisterEventSource(NULL, "KService");
    if (hEventSource != NULL)
    {
    // Write to event log.
      ReportEvent(hEventSource, EVENTLOG_INFORMATION_TYPE, 0, 0, NULL, 1, 0, (LPCTSTR*) &lpszStrings[0], NULL);
      DeregisterEventSource(hEventSource);
    }
  }
  else
  {
    // As we are not running as a service, just write the error to the console.
    _putts(chMsg);
  }
}

void Stop()
{
  LogEvent("Service Stoped.");
}

原创粉丝点击