荔园在线

荔园之美,在春之萌芽,在夏之绽放,在秋之收获,在冬之沉淀

[回到开始] [上一篇][下一篇]


发信人: oopilix (。。), 信区: Visual
标  题: [zz]Windows服务编写原理及探讨(2)
发信站: 荔园晨风BBS站 (Sat Oct  4 18:19:10 2003), 站内信件

二)对服务的深入讨论之上

  上一章其实只是概括性的介绍,下面开始才是真正的细节所在。在进入点函数
里面要完成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\Paramete
rs
子键下存放自己的设置,这里的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内睡眠的线程激活,并像在前面解释过的那样,
减少它运行的服务的计数。



[回到开始] [上一篇][下一篇]

荔园在线首页 友情链接:深圳大学 深大招生 荔园晨风BBS S-Term软件 网络书店