3.2.1 服务应用程序.htm
来自「Windows2000后台服务程序开发手册」· HTM 代码 · 共 499 行 · 第 1/4 页
HTM
499 行
<DIV align=center>[字体: <INPUT title=把正文字体缩小 style="HEIGHT: 16px" onclick="fontSize('m','ArticleBody')" type=button value=小>
<INPUT title=把正文字体扩大 style="HEIGHT: 16px" onclick="fontSize('b','ArticleBody')" type=button value=大>
<INPUT title=转为简体中文模式 style="HEIGHT: 16px" onclick="bodytojt('ArticleBody')" type=button value=简>
<INPUT title=转为繁体中文模式 style="HEIGHT: 16px" onclick="bodytoft('ArticleBody')" type=button value=繁>
<A href="javascript:fontColor('ArticleBody')"><IMG alt=字体颜色
src="3.2.1 服务应用程序.files/fgcolor.gif" align=absMiddle
border=0></A>]</DIV></TD></TR></TBODY></TABLE>
<TABLE cellSpacing=5 cellPadding=0 width="100%" align=center border=0>
<TBODY>
<TR>
<TD vAlign=top>
<TABLE cellSpacing=0 cellPadding=10 align=left border=0>
<TBODY>
<TR>
<TD></TD></TR></TBODY></TABLE>
<DIV class=content id=ArticleBody
style="PADDING-RIGHT: 10px; DISPLAY: block; PADDING-LEFT: 10px; PADDING-BOTTOM: 0px; PADDING-TOP: 0px">
<P class=content></P>
<DIV
style="WIDTH: 650px; WORD-BREAK: break-all; LINE-HEIGHT: 25px"><BR>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#3e72d7
size=4><B style="LINE-HEIGHT: 25px">处理线程内部通讯议题<BR
style="LINE-HEIGHT: 25px"> </B></FONT></P><BR>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>尽管主要线程执行了收到动作要求的HandlerEx函数,一个服务还是很难编写的,通常服务线程需要去做现实的工作以处理要求。例如,您也许想编写一个处理从命名管道上传来之客户端要求的服务。您的服务线程会自己暂停以等待一个客户端连结。如果您的HandlerEx线程收到一个SERVICE_CONTROL_STOP控制码,您该如何停止该服务?我曾经看过许多开发者只是简单地从HandlerEx函数中呼叫TerminateThread来强迫删除该服务线程。现在,您应该知道TerminateThread是您可能呼叫的函数中最糟的一个,因为线程不能取得一个清除的机会:线程的堆叠没有被删除、线程无法释放任何必须被等待的核心物件、DLLs没有收到线程已被删除的通知等等。</FONT></P><BR>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>使服务停止之适当方法是以某种方式醒来、察看哪个服务应被停止、正确地清除以及从它的ServiceMain函数回传。为了建立一个服务执行它,您必须实作一些在您的HandlerEx与您的ServiceMain函数间的内部线程通讯。您可以使用任何您喜欢的要求内部线程通讯机制,包括APC伫列、套接字以及视窗讯息。我总是使用I/O完成端口。</FONT></P><BR>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>为了更新目前的状态,一个服务必须频繁地呼叫SetServiceStatus。所有的这一切状态回报在服务编码方面可能是很困难的。服务的实作通当会思考在哪里放置呼叫SetServiceStatus的函数。以下是一些可能的方式:</FONT></P><FONT
style="LINE-HEIGHT: 25px" face=arial color=#000000 size=2><BR>
<UL style="LINE-HEIGHT: 25px"><BR>
<LI
style="LINE-HEIGHT: 25px">使HandlerEx函数建立一个初始呼叫SetServiceStatus以回报未完成的动作,然后使用线程内部通讯去取得ServiceMain线程的控制码。ServiceMain线程会做该工作并且使用线程内部通讯以使HandlerEx函数得知该动作已完成。由此得知,HandlerEx再次呼叫SetServiceStatus以回报服务的新执行状态。<BR
style="LINE-HEIGHT: 25px"> <BR>
<LI
style="LINE-HEIGHT: 25px">使HandlerEx函数使用线程内部通讯以取得ServiceMain线程的控制码。ServiceMain线程会建立初始的呼叫SetServiceStatus以回报未完成的动作、执行这个工作,然后再次呼叫SetServiceStatus以回执服务的新执行状态。<BR
style="LINE-HEIGHT: 25px"> <BR>
<LI
style="LINE-HEIGHT: 25px">使HandlerEx函数建立初始呼叫至SetServiceStatus以回报未完成动作,然后使用线程内部通讯去取得ServiceMain线程的控制码。ServiceMain线程会执行这个工作而且也会再次呼叫SetServiceStatus以回报服务的新执行状态。<BR
style="LINE-HEIGHT: 25px"> </LI></UL></FONT><BR>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>所有以上所述的情形有正面的也有反面的。我曾对这些可能的方案做过实验并且大胆的建议使用最后的选项,理由是:</FONT></P><BR>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>第一,SCP呼叫一个函数以控制一个服务,而且SCM传递此控制给服务。由于这点,SCP会暂停执行,等待服务呼叫SetServiceStatus以指示服务已经接收该控制码。假如服务的HandlerEx函数没有在30秒内回传,SCM会允许SCP醒来并且SCP的函数会呼叫去控制服务回传失败。</FONT></P><BR>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>第二,HandlerEx函数经由服务处理程序的主要线程而被执行(所有在一个单一处理程序中的服务皆拥有它们的HandlerEx函数,而且它们被主要线程执行)。如果HandlerEx在等候ServiceMain于回传之前完成该动作,任何在同一个处理程序中的其他服务皆不能接收动作要求或通知。这可以使所有其他的服务出现没有回应的情形,这是不被接受的。</FONT></P><BR>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>所以我优先选择第叁种方法—HandlerEx函数建立初始呼叫至SetServiceStatus,线程内部通讯被用来取得ServiceMain线程的控制码,而且由ServiceMain线程完成工作与呼叫SetServiceStatus以回报新的执行状态。然而,这个方法也有一个问题:存在着一个潜在的?赛条件(race
condition)。想像一个服务的HandlerEx函数接收了一个SERVICE_CONTROL_PAUSE控制码,并以SERVICE_PAUSE_PENDING回答,然后传递控制码至ServiceMain线程。当ServiceMain线程开始处理该控制码时,突然间,HandlerEx线程先取得ServiceMain线程并且接收一个SERVICE_CONTROL_STOP控制码。HandlerEx函数现在回应一个SERVICE_STOP_PENDING控制码并且新的控制码伫列至ServiceMain线程中。当ServiceMain绪行绪再次取得CPU时间,它会完成自己的SERVICE_CONTROL_PAUSE控制码过程并且回报SERVICE_PAUSED。然后线程会察看被伫列之SERVICE_CONTROL_STOP控制码、停止服务以及回报SERVICE_STOPPED。在这些以后,SCM会接收以下的更新状态:</FONT></P><BR>
<DIV style="LINE-HEIGHT: 25px; BACKGROUND-COLOR: #d7d7d7"><FONT
style="LINE-HEIGHT: 25px" face=Arial size=3><PRE style="LINE-HEIGHT: 25px">SERVICE_PAUSE_PENDING <BR><BR>SERVICE_STOP_PENDING <BR><BR>SERVICE_PAUSED <BR><BR>SERVICE_STOPPED</PRE></FONT></DIV><BR>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>就像您看到的,这些更新毫无意义,只会让管理者相当困惑。请注意,不管怎样,服务还是执行得很好。您会对我曾经见过可以实际地回执这个序列的数量感到惊讶。这些服务的开发者从未修复这些问题,它是不太可能会发生的,因为一个管理者会快速地发布动作要求至服务中—但是它还是可能发生。为了解决这个序列问题您必须使用一个同步线程机制。在本章后面的TimeService应用程序范例中使用了一个CGAte的C++
类别来有效地解决这个问题。</FONT></P><BR>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>当我开始使用服务时,认为SCM可能是预防发生竞赛条件的原因。但是经验告诉我SCM对确定一个适当地接受控制码的服务来说是没有帮助的。事实上,它真的没有什么帮助。意思是说:当一个服务已被暂停时,试着传送给服务一个SERVICE_CONTROL_PAUSE控制码。因为嵌入式管理单元一旦知道服务已被暂停即会使暂停按钮失效,所您不能在服务嵌入式管理单元中使用它。但是如果您使用SC.exe命令列工具程序,任何传送一个暂停控制码至已经被暂停的服务并不会被停止。我曾预期SCM回报失败至SC.exe工具程序中,但是SCM只会呼叫服务的HandlerEx函数,并传送SERVICE_CONTROL_PAUSE控制码。您的服务必须能够小心地处理这些不正确的控制码。</FONT></P><BR>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>我曾经见过许多没有对存在于一个资料列中被多次传送至服务之相同控制码的可能性做处理。例如,我知道当服务被闲置时,它关闭了命名管道的handle。这个服务接下来会建立另一个核心物件,碰巧的是,它取得了与原始命名管道同样的handle值。然后服务会接收另一个暂停控制码并且呼叫CloseHandle与传递旧管道的handle值。由于这个值刚好和另一个核心物件的handle相同,所以新的核心物件会被删除,而其馀的服务则由于奇怪且不可思议的方法而失败。我无法告诉您该如何愉快的调整这个混乱的情形。</FONT></P><BR>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>为了要修复这个多重的停止、暂停或继续执行控制码的问题,第一件是即是察看您的哪一些服务已经位于需求状态。如果它是的话,不要呼叫SetServiceStatus,也不要执行您的改变状态之程序代码—只要回传即可。这里有一些我常用在服务的逻辑。当HandlerEx函数接收一个SERVICE_CONTROL_PAUSE控制码时,HandlerEx函数会呼叫SetServiceStatus以回报SERVICE_PAUSE_
PENDING,呼叫SuspendThread以使服务的线程暂停执行,然后再次呼叫SetServiceStatus以回报SERVICE_PAUSED控制码。这一系列的呼叫是为了避免竞赛条件的产生,因为所有的工作被一个线程完成,但是这个控制码做了什么?闲置服务线程会使该服务暂停执行吗?对于这些,我想必须回答「是的」。然而,对于服务来说,暂停它代表什么意思?答案是依据服务而定。</FONT></P><BR>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>如果我正在编写一个处理网路上之客户端要求的服务,对我来说,暂停代表停止接受新的要求,但是要如何处理正在处理中的要求呢?也许我应该完成它以使客户端不会被无限期地悬置。如果我的HandlerEx函数简单地呼叫了SuspendThread,该服务线程可能会在任何的状态中。也许该线程正呼叫至malloc,并试着去配置一些内存。假如另一个服务在也呼叫malloc的同一个处理程序中执行,这个服务也会被悬置(直到该存取动作被序列化为止)。这必定是我们不想要产生的情形。</FONT></P><BR>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>看看这个如何:您认为您应该被允许去停止的一个已被暂停的服务吗?我想是的,而且显然Microsoft也是这样想的,因为即使该服务已被暂停,服务嵌入式管理单元还是允许我去按下停止按钮来停止它的执行。但是我要如何停止一个因为它的线程已被悬置之已被暂停的服务呢?请不要回答TerminateThread。</FONT></P><BR>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>这些是关于向建立服务开发挑战的议题。</FONT></P><A style="LINE-HEIGHT: 25px"
name=203005><BR>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#3e70d7
size=5><B style="LINE-HEIGHT: 25px">关于服务的议题<BR
style="LINE-HEIGHT: 25px"> </B></FONT></P><BR>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>当您第一次开发服务程序时,您将会注意到有一些程序并没有照您的预期的状况执行。服务是一个在一个特殊的作业环境中执行的野兽。本节将会讨论一些您可能会遭遇的情形。然而,并不会花太多时间在它们上面,因为本书的各个章节中会对它们做更详细的说明。在此只是先给您一个大略的概念而已。</FONT></P><BR>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#3e72d7
size=4><B style="LINE-HEIGHT: 25px">本机帐户与特定的使用者帐户<BR
style="LINE-HEIGHT: 25px"> </B></FONT></P><BR>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>本节会开始解释在本机帐户中与在一个特定的使用者帐户中执行服务有什么不同。本机帐户是一个被作业系统给予的帐户,作业系统不会对它限制资源的存取权限。在本机帐户中执行一个服务时,可以存取任何的目录或文件、改变系统时间、启动或停止任何的服务、关闭机器以及没有任何的障碍即可以执行所有其他被限制的正常动作。一个本机服务被认为是系统之Trusted
Computing Base(TCB)的一部份。</FONT></P><BR>
<HR style="LINE-HEIGHT: 25px">
<BR>
<P><FONT style="LINE-HEIGHT: 25px" face=Arial color=#3e77d7 size=3
Black><B style="LINE-HEIGHT: 25px">说明</B></FONT> </P><BR>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>这里是我在<A
href="http://nsi.org/Library/Compsec/compglos.txt找到对Trusted"
target=_blank
Base(TCB)的定义内容:「一个电脑系统中的总体保护机制」—包括了硬体、韧体以及软件—负责加强一个安全策略的结合。一个TCB由一个或多个在一个产品或系统上实施统一的安全策略之元件所组成。一个TCB的能力为正确地执行那些依据TCB以及由系统管理者所输入与安全策略有关之正确参数的安全策略。(例如,清除一个使用者帐户)。」
Computing>http://nsi.org/Library/Compsec/compglos.txt找到对Trusted</A></FONT></P><BR>
<HR style="LINE-HEIGHT: 25px">
<BR>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>当然,所有的核心模式程序代码—硬体设备、内存管理、文件系统、安全控管、线程的工作排程等等,皆是系统之TCB的一部份。在TCB中执行的服务具有非常大的权力,这就是为什么只有机器的管理者拥有安装服务权利的缘故。</FONT></P><BR>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>所以如果本机帐户拥有很大的权力,为何您还会想让服务在一个特定的使用者帐户中执行呢?的确,本机服务拥有在本机上的所有权力,在预设的情形下,它们不能在整个网路中被使用。例如,因为本地端机器的本机帐户无法被远端机器验证,所以本机服务不能存取另一台机器上被分享的目录、文件或打印机。在Windows
2000中,Microsoft对这个情况做了改善:当一台电脑位于网域中,您可以将它视为一个使用者帐户并取得它的存取权限。</FONT></P><BR>
<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><BR>
<UL style="LINE-HEIGHT: 25px"><BR>
<LI
style="LINE-HEIGHT: 25px">在一个已取得网路资源存取权之特定使用者帐户下执行服务。注意如此做将会限制该服务所能在本机上做的工作。<BR
style="LINE-HEIGHT: 25px"> <BR>
<LI
style="LINE-HEIGHT: 25px">使用一个不要求验证的通讯协定来存取资源。例如,一个本机服务可以经由套接字、命名管道或邮件通讯协定。当然,要使这个通讯执行成功需要远端机器支援这些通讯协定。这些连结被称为NULL工作阶段,并且可以经由设定位于以下所列之登录机码中的NullSessionPipes与NullSessionShares的值以加以控制:<BR
style="LINE-HEIGHT: 25px"> <BR>
<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 <BR><BR> \LanmanServer\Parameters</PRE></FONT></DIV><BR>
<P>您也能够允许由所有NULL工作阶段的连结而把资料值设置成0(位于相同的子机码下)以存取机器上的所有管道与被分享的资料。虽然您可以这么做,但您却不应该做它,因为它会使系统上产生一个巨大的安全漏洞。
</P><BR>
<LI
style="LINE-HEIGHT: 25px">模拟一个特定使用者以存取资源。为了达到这个目的,您可以呼叫许多Windows所提供的模拟函数(我们将在本书第四篇的章节中讨论模拟的部份)。一个服务也能经由使本机服务呼叫LogonUser函数来模拟一个特定的使用者,并传递一个网域、使用者名称与密码以被验证。注意LogonUser函数需要经过TCB授权(也被视为作业系统特权的一部份),在预设的情形下,只有本机帐户—多么方便啊!<BR
style="LINE-HEIGHT: 25px"> </LI></UL></FONT><BR>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#3e72d7
size=4><B style="LINE-HEIGHT: 25px">本机与特定使用者之登录子机码<BR
style="LINE-HEIGHT: 25px"> </B></FONT></P><BR>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>登录被分为二个主要的机码。第一个为HKEY_LOCAL_MACHINE,它被用来储存所有的系统设定。一个服务或是一个应用程序可以经常读取任何位于此机码中的设定值。</FONT></P><BR>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>第二个机码为HKEY_USERS,它被用来储存每一个使用者之特定设定值。这个机码会进一步分成二个类型之子机码。第一个类型是一个特定的使用者子机码。机器上的每一个使用者帐户皆有一个在HKEY_USERS之下对应至子机码之登录设定集合。当特定的使用者登入并且成为一个互动的使用者时,常见的HKEY_CURRENT_USER机码会对应至位于HKEY_USERS下的特定使用者之子机码。</FONT></P><BR>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
size=2>第二个位于HKEY_USERS中的子机码称为
.DEFAULT,它包含了一个使用者的内定设定值。当一个新的使用者帐户在系统中被建立时,在HKEY_USERS之中也会建立一个新的子机码而且该子机码的设定值会与目前存放在
.DEFAULT子机码中的设定相同。</FONT></P><BR>
<P><FONT style="LINE-HEIGHT: 25px" face=arial color=#000000
⌨️ 快捷键说明
复制代码Ctrl + C
搜索代码Ctrl + F
全屏模式F11
增大字号Ctrl + =
减小字号Ctrl + -
显示快捷键?