当您编写了一个服务时,同时也会建立一个应用程序以让管理员用来控制该服务。这个管理用的应用程序应该经由使用在本章中所提之各种函数来控制服务。这个应用程序也应该帮助管理者处理其他方面的服务,例如设定它(在第五章中叙述)与在Active Directory中发布它。在观念上,此管理应用程序实现了一个像在Microsoft Management Console(MMC)中的嵌入式管理单元或是一个以网路为基础的主控台。
与一个机器的SCM通讯之第一个步骤是呼叫OpenSCManager:
SC_HANDLE OpenSCManager( PCTSTR pszMachineName, PCTSTR pszDatabaseName, DWORD dwDesiredAccess);
这个函数建立了一个与机器上之SCM通讯的通道,它经由pszMachineName参数指定,并传递NULL去开启在本端机器上的SCM。pszDatabaseName参数会确认应该开始哪一个资料库;您应该只是传递SERVICES_ACTIVE_DATABASE或NULL二者之一给此参数。dwDesiredAccess参数告诉函数您想要处理的SCM资料库。表4-1说明了有哪些存取权限可供使用。
表4-1 用来指定存取SCM之OpenSCManager的dwDesiredAccess参数存取权限值 |
存取权限 | 说明 |
---|---|
SC_MANAGER_ALL_ACCESS | 除了STANDARD_RIGHTS_REQUIRED外,还包括了所有列在本表中的存取类型。 |
SC_MANAGER_CONNECT | 允许连接至SCM。即使没有明白指定,然而这个存取经常是被隐含在内的。 |
SC_MANAGER_CREATE_ SERVICE | 呼叫CreateService,以将一个服务加入SCM资料库中。 |
SC_MANAGER_ENUMERATE_SERVICE | 呼叫EnumServicesStatus,以取得在SCM资料库中的服务清单与每一个服务的状态。 |
SC_MANAGER_LOCK | 呼叫LockServiceDatabase,以停止从SCM启动的任何更多的服务。 |
SC_MANAGER_QUERY_ LOCK_STATUS | 呼叫QueryServiceLockStatus,以找出哪一些使用者被锁定在SCM资料库中。 |
Windows提供了下列所示之预设存取,以使SCM变得较安全:
OpenSCManager回传了一个SC_HANDLE让您将它传递至另一个函数中,以便您可以操作SCM的资料库。当您完成了对SCM资料库的存取时,您必须将它传递给CloseServiceHandle,以关闭此handle:
BOOL CloseServiceHandle(SC_HANDLE hSCManager);
增加一个服务至SCM资料库
大部份要操作SCM资料库的原因之一即是增加一个服务。为了要增加一个服务,您必须呼叫OpenSCManager,并指定SC_MANAGER_CREATE_SERVICE之存取,然后再呼叫CreateService:
SC_HANDLE CreateService( SC_HANDLE hSCManager, PCTSTR pszServiceName, //内部的、计划性的字串名称 PCTSTR pszDisplayName, DWORD dwDesiredAccess, DWORD dwServiceType, DWORD dwStartType, DWORD dwErrorControl, PCTSTR pszPathName, PCTSTR pszLoadOrderGroup, PDWORD pdwTagId, // 若为服务的话,通常为0 PCTSTR pszDependencies, // 使0终止的字串增加一倍 PCTSTR pszUserName, PCTSTR pszUserPswd);
如您所见,CreateService需要相当多的参数(正确数目为13个)。hSCManager参数为由OpenSCManager回传的handle。接下来的二个参数pszServiceName和pszDisplayName指示了服务的名称。服务拥有一个内部的名称供程序设计者使用,以及一个给使用者看的显示名称。经由pszServiceName可以确定内部名称,它被SCM用来储存在登录内部的服务资讯。举例来说,Logical Disk Manager服务拥有一个内部名称「dmserver」,而且它的服务资讯可以在以下所示的登录机码中找到:
HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/dmserver
CreateService的dwDesiredAccess参数是有用的,因为它会告诉SCM在CreateService返回一个handle给它后,您打算如何处理刚被安装的服务(所以您可以正确的操作该服务)。如果您只安装了一个服务并且在它被安装后不打算操作它,那么只需简单地传递0给dwDesiredAccess,然后立即关闭经由呼叫CloseServiceHandle而被CreateService回传的handle即可。表4-2中显示了当您使用CreateService时可指定给dwDesiredAccess的存取权限。
CreateService 函数一个参数可接受一个SECURITY_ATTRIBUTES结构的指标。所以当一个新的服务被安装至SCM资料库时,SCM会对该服务设定预设之安全性。您可以使用QueryServiceObjectSecurity与SetServiceObjectSecurity函数来改变这些安全性设定。它们为SCM为服务设定的预设安全性:
Administrators与System Operators拥有SERVICE_CHANGE_CONFIG、SERVICE_ENUMERATE_DEPENDENTS、SERVICE_INTERROGATE、SERVICE_ PAUSE_CONTINUE、SERVICE_QUERY_CONFIG、SERVICE_QUERY_STATUS、SERVICE_START、SERVICE_STOP、SERVICE_USER_DEFINED_CONTROL、READ_CONTROL、WRITE_OWNER、WRITE_DAC与DELETE以存取服务。dwServiceType参数告诉系统哪一些可执行档中包含了一个或多个服务。当可执行档中实行单一的服务时,传递SERVICE_WIN32_OWN_PROCESS;而当可执行档中实行了二或多个服务时,则传递SERVICE_WIN32_ SHARE_PROCESS。如果您想要让服务在一个处理程序中与使用者的桌面互动时,您也可以结合SERVICE_INTERACTIVE_PROCESS标记与SERVICE_WIN32_ OWN_PROCESS或SERVICE_WIN32_SHARE_PROCESS二者之一。
说明
在SERVICE_WIN32_SHARE_PROCESS服务之可执行档中,如果该服务要求使用SERVICE_INTERACTIVE_PROCESS标记时,所有的服务即必须使用这个标记。当系统第一次启动该服务时,服务的设定会决定哪一个处理程序被允许与桌面互动。
dwStartType参数告诉系统服务应该在何时被启动。当机器开机时,会有一个SERVICE_AUTO_START值指示SCM启动服务以及SERVICE_DEMAND_START值会指示系统不启动服务。管理员则可以手动地启动服务。此外,SERVICE_ DEMAND_START可指定一个服务为要求启动的服务,它告诉SCM如果管理员试图去启动一个服务时,便自动地启动服务。接下来会讨论更多有关服务的依存关系。一个SERVICE_DISABLED值可防止由系统完全地启动服务。
服务在系统中是一个非常重要的部份,所以系统需要知道如果服务启动失败时,它应该做什么。这个指令是dwErrorControl参数的工作。传递一个SERVICE_ERROR_IGNORE或SERVICE_ERROR_NORMAL以告诉系统在系统之事件记录中记录服务的错误以及继续启动系统。这二个控制码的不同之处是设定为SERVICE_ERROR_NORMAL时系统会显示一个讯息方块以通知使用者服务启动失败。设定为要求启动的服务应该指定为SERVICE_ERROR_IGNORE。
当服务启动失败时,SERVICE_ERROR_SEVERE与SERVICE_ERROR_CRITICAL会告诉系统中止启动它。当一个服务启动失败并且被指定为这些控制码之一时,系统会在系统之事件记录中记录此错误情形,并且自动地使用已知上次为良好的设定重新开机。如果系统以已知上次为良好的设定开机后,有一个服务在启动失败时的错误控制码为SERVICE_ERROR_SEVERE,则系统会继续开机。若有一个服务之启动失败的错误控制码为SERVICE_ERROR_CRITICAL,则系统也会中止已知上次为良好设定的开机动作。
CreateService的pszPathName参数指示了包含一个或多个服务之可执行档的完整路径。许多服务文件被安装在 /WINNT/System32目录中,但是您可以将服务之可执行档放置在文件系统的任何一个地方。
现在我们到达服务之依存关系的议题。简单地说,一个服务就像作业系统的一部份,而且除非他们知道另一个系统的一部份已经先执行,否则有很多服务不会正确地工作。当系统开机时,它会依照一个规定服务之启动顺序的演算法来执行。Microsoft将系统服务划分为一个事先已定义的群组中,列示如下:
系统保留的您也可以在以下所示之登录子机码中找到这个清单:
HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Control/ServiceGroupOrder
就像系统开机一样,它会反覆地执行这个清单、载入任何的设备驱动程序以及属于每一个部份的服务。例如,在载入属于SCSI miniport群组之设备驱动程序与服务前即载入包含在系统保留群组中的所有设备驱动程序与服务。
当您加入一个服务至SCM资料库中时,可以经由在CreateService的pszLoadOrderGroup参数中传递群组的名称,指定它为上述之已定义群组清单中的一项。通常,您的服务不需要在系统开机周期即载入,而是应该在所有群组设备与服务开始启动并执行后才载入执行。为了确定您的服务在所有的系统之关键设备驱动程序与服务之后才载入执行,您只需简单地在pszLoadOrderGroup参数中传递NULL即可。
如果您增加了一个设备驱动程序至SCM中(相对于一个服务),则可以在经由指定一个标签ID,在建立驱动程序的启动时间取得更多的资料点(Granularity)。服务无法利用这个额外的资料点,并且必须总是传递NULL给CreateService的pdwTagId参数。若您对设备驱动程序有与趣,可参阅在Platform SDK文件与DDK文件中讨论pdwTagId参数以及二个附加的选项(SERVICE_BOOT_ START与SERVICE_SYSTEM_START)。
除了告诉SCM您的服务属于一个特定载入顺序的群组外,您还可以告诉SCM您的服务要求某些其他的服务与群组需要在您的服务可以执行前即已执行。例如,Computer Browser服务要求Workstation与Server服务必须在它可以正确地执行前即已执行,而ClipBook服务则要求Network DDE服务被执行。
指定您的服务依存于哪些服务,比表明您服务为某个群组的一部份更为有用。您会使用CreateService的pszDependencies参数来告知SCM资料库您的服务依存于哪一些服务。如果您的服务没有存在依存关系,则只需传递NULL给此参数即可。
因为您必须传递一个透过零分离的名称之双倍终止位址阵列,所以pszDependencies是一个非常奇特的参数。以另一种方式来说,pszDependencies参数必须指向一个包含终止字串与一个缓冲器尾端之额外空字元的内存区块。
所以为了建立一个依存于Workstation服务的服务(像Alerter服务即是),在传递它给CreateService前,您应先设定pszDependencies(程序代码如下所示):
// 在下面的缓冲器中以二个空字元结束 PCTSTR pszDependencies = TEXT("LanmanWorkstation/0"); CreateService(..., pszDependencies, ...);
而建立一个依存于Workstation服务与Remote Procedure Call(RPC)服务(如Messenger服务)的服务时,您将以如下的方式设定pszDependencies:
// 在下面的缓冲器中分开一个空字元的字串并以二个空字元结束 PCTSTR pszDependencies = TEXT("LanmanWorkstation/0RpcSs/0"); CreateService(..., pszDependencies, ...);
一个服务也能被一个群组而非一个单一的服务依存,但是这是非常不寻常的情形。载入次序群组上的从属意味着在一个尝试启动群组中所有成员的动作被完成后,该群组中至少有一个成员为执行状态。为了在一个pszDependencies缓冲器中指定一个群组,您必须在群组名称前加上一个特定的SC_GROUP_IDENTIFIER字元,它被定义在WinSvc.h中,如下所示:
#define SC_GROUP_IDENTIFIERW L'+' #define SC_GROUP_IDENTIFIERA '+' #ifdef UNICODE #define SC_GROUP_IDENTIFIER SC_GROUP_IDENTIFIERW #else #define SC_GROUP_IDENTIFIER SC_GROUP_IDENTIFIERA #endif
因此,为了要建立一个依存于Workstation服务与TDI群组的服务,您应如以下的方式来设定pszDependencies:
// 在下面的缓冲器中指定二个依存关系:Workstation服务与TDI群组(一个「+」在TDI之前的群组) PCTSTR pszDependencies = TEXT("LanmanWorkstation/0+TDI/0"); CreateService(..., pszDependencies, ...);
当设定pszDependencies值时,您可以依自己的方式指定许多服务与群组。只要记得在每一个服务或群组间放置一个空字元与在所有群组名称前放入一个加号,以及在结尾引用前加入一个终止之空字元。
我们现在可以开始说明CreateService之最终二个参数:pszUserName与pszUserPswd。这二个参数允许您在使用者帐户下指定哪一些服务可被执行。为了使服务在本机帐户下执行(大部份的情形),只需传递NULL给此二个参数即可。如果您想让服务在一个特定的使用者帐户下执行,则以DomainName\UserName形式来传送一个帐户名称给pszUserName参数,并传递使用者帐户的密码至pszUserPswd参数中。
说明
互动式的服务必须被设定在一个本机帐户下执行。若您试图在非本机帐户下加入一个互动式的服务,则CreateService无法增加服务至SCM资料库中。
如果CreateService成功的将一个服务加入至SCM资料库中,此时会回传一个非NULL的handle。由另一个函数来要求这个handle,以操作服务。当您要结束使用它时,请确定您已将这个handle传递给CloseServiceHandle函数。如果CreateService执行失败,它会回传一个NULL值,并且一个呼叫至GetLastError的函数会回传一个值,以指示执行失败的原因。以下为CreateService可能执行失败的大部份原因:
从OpenSCManager回传的handle中,没有SC_MANAGER_CREATE_SERVICE的存取。我经常将服务编写成可执行的服务程序,所以它们能够自行安装。在我的(w)main或(w)WinMain函数中,若以命令列传递一个「-install」参数,则我会呼叫一个ServiceInstall函数(显示在以下的程序片段中)。在上一章中展示的TimeService范例服务程序即说明了这个技术。
void ServiceInstall(PCTSTR pszInternalName, PCTSTR pszDisplayName, DWORD dwServiceType, DWORD dwStartType, DWORD dwErrorControl) { // 开启SCM资料库以增加一个服务 SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE); // 取得我们的服务之可执行完整路径 char szModulePathname [_MAX_PATH]; GetModuleFileName(NULL, szModulePathname, sizeof(szModulePathname)); // 增加此服务至SCM资料库 SC_HANDLE hService = CreateService( hSCM, pszInternalName, pszDisplayName, 0, dwServiceType, dwStartType, dwErrorControl, szModulePathname, NULL, NULL, NULL, NULL, NULL); // 关闭新建立之服务与SCM CloseServiceHandle(hService); CloseServiceHandle(hSCM); }
说明
为了要更清楚地说明,所以前面所示之程序代码并没有处理任何的错误检查。所以OpenSCManager与CreateService可能会因为许多原因而执行失败。当您在增加类似上述程序代码至您的应用程序中时,请适当的增加错误检查机制。
从SCM资料库中删除一个服务
一个好的软件套件需要支援解除安装的功能,就像它支援安装功能一样。所以您也要了解如何移除一个服务。为了要将一个服务移除,您必须先开启它:
SC_HANDLE OpenService( SC_HANDLE hSCManager, PCTSTR pszInternalName, DWORD dwDesiredAccess);
在OpenService函数中,您经由服务的内部名称(与您在CreateService的pszServiceName参数中传递的值相同)而将被OpenSCManager回传的handle传递出去,然后将被要求的存取删除。现在您已经拥有了可指定服务的handle,那么您便可传递由OpenService回传的handle值,以经由呼叫DeleteService的方式来将服务删除:
BOOL DeleteService(SC_HANDLE hService);
DeleteService并没有真正地将服务删除,它只是被删除的服务标记起来而已。只有当服务停止执行以及在所有开启该服务的handle被关闭时,SCM才会删除该服务。
我还将我的服务以可执行的方式编写,所以它们可以自行从SCM资料库中删除。若从命令列中传递「-remove」参数时,会呼叫像是ServiceRemove的函数,程序代码如下:
void ServiceRemove(PCTSTR pszInternalName) { // 开启SCM资料库 SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT); // 开启服务以做删除动作 SC_HANDLE hService = OpenService(hSCM, pszInternalName, DELETE); //将被删除的服务做标记 //注意:除非所有被开启的handle已被关闭以及服务停止执行,否则该服务 //不会被删除 DeleteService(hService); // 关闭服务与SCM CloseServiceHandle(hService); CloseServiceHandle(hSCM); }
说明
为了使您更清楚的缘故,上述的程序代码中并不包含任何的错误检查。OpenSCManager、OpenService与DeleteService可能会因为任何的原因而执行失败。当您在应用程序中加入类似的程序代码时,请加入适当的错误检查机制。
启动与控制一个服务
如前所述,许多服务皆包含在一个客户端应用程序中,以允许管理员可以去启动、停止、暂停、继续执行以及使用其他的方式来控制一个服务。编写一个服务控制程序是非常容易的。在此为您说明它如何工作:程序使用SC_MANAGER_ CONNECT存取权限,并经由呼叫OpenSCManager函数,在被要求的机器上第一次开启SCM。然后程序会呼叫OpenService以开启您想要经由结合SERVICE_ START, SERVICE_STOP、SERVICE_PAUSE_CONTINUE、SERVICE_USER_DEFINED_ CONTROL、与SERVICE_INTERROGATE存取权限控制的服务。当服务被开启后,呼叫StartService即可启动它:
BOOL StartService( SC_HANDLE hService, DWORD dwArgc, PCTSTR* pszArgv);
hService参数指示了被开启的服务,而dwArgc与pszArgv参数则指示了您想要传递至服务的ServiceMain函数中之一组参数。大部份的服务不使用这些参数,所以通常会传递0与NULL给最后二个参数。请记得如果您启动的服务依存于其他服务或群组时,若启动一个服务即会导致许多服务一起启动。以下为一些StartService执行失败的主要原因:
从OpenService回传的handle没有拥有SERVICE_START的存取权。注意一旦服务的主要线程被建立起来后,StartService函数便会立即返回,所以服务不能准备去处理这些经由StartService返回的客户端要求之控制码或handle。并且,当它正在初始化或发生死结时(持续80秒),服务不能呼叫StartService。这个问题是因为当启动一个服务时,SCM会将SCM资料库锁定以预防另一个服务开始执行。
一旦服务开始执行,您便可以呼叫ControlService传送控制给它:
BOOL ControlService( SC_HANDLE hService, DWORD dwControl, SERVICE_STATUS* pss);
再一次说明,hService p参数指示了您希望控制之被开启的服务。dwControl参数指示了您希望服务做什么而且它可为以下所列值之一:
SERVICE_CONTROL_STOP注意这些控制码与您的HandlerEx函数所接收的值相同(如第叁章中所讨论的)。除了这些值之外,您可以传送一个范围为128至255的使用者定义控制码。注意如果您传递一个SERVICE_CONTROL_SHUTDOWN的值,则ControlService会执行失败,只有系统可以传送这个控制码至一个服务的控制函数中。
ControlService的最后一个参数pss必须指向一个SERVICE_STATUS结构。该函数会初始化这个结构的成员并回报服务之最后被回报的状态资料。您可以在ControlService返回后检查这个资讯,以察看服务如何工作。这里有一些ControlService可能会执行失败的主要原因:
从OpenService回传的handle没有适当的存取权。无疑地,如果服务的控制函数处理了这个呼叫,则您会预期SERVICE_STATUS结构已被适当地初始化并且回传。然而如果您试图去传送一个SERVICE_CONTROL_INTERROGATE控制至一个被停止执行的服务时,您认为SERVICE_STATUS结构的内容会是什么呢?嗯,您将会很乐意知道Microsoft已经加强了ControlService函数,所以如果函数执行失败并跟随着一个ERROR_INVALID_SERVICE_CONTROL、ERROR_SERVICE_CANNOT_ACCEPT_CTRL或ERROR_SERVICE_NOT_ACTIVE的错误码时,它会回传一个有效的SERVICE_STATUS结构。以下的程序代码说明了如何停止一个服务的方法:
void StopService(PCTSTR pszInternalName) { // 开启SCM与被要求的服务 SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT); SC_HANDLE hService = OpenService(hSCM, pszInternalName, SERVICE_STOP | SERVICE_QUERY_STATUS); // 告诉服务停止执行 SERVICE_STATUS ss; ControlService(hService, SERVICE_CONTROL_STOP, &ss); // 等待至15秒以让服务停止执行 WaitForServiceState(hService, SERVICE_STOPPED, &ss, 15000); // 关闭服务与SCM CloseServiceHandle(hService); CloseServiceHandle(hSCM); }
说明
为了更清楚地说明,上述的程序代码中并不包含任何的错误控制处理。OpenSCManager、OpenService与ControlService可能会因为许多的理由而执行失败。当您在应用程序中加入类似的程序代码时,请适当地加入错误控制机制。
您应该注意到StopService呼叫了WaitForServiceState函数。WaitForServiceState函数并不是一个Windows函数,但也不是我所编写的函数,而它说明了如何适当地去处理一个服务的状态轮询。考虑以下的情形:使用服务嵌入式管理单元,您初始化了一个服务的停止要求,使得SCM去通知被选择的服务应该停止执行。该服务应该经由呼叫SetServiceStatus与在SERVICE_STATUS结构中,被设定为SERVICE_STOP_PENDING的dwCurrentState成员而回应。然而,服务还是没有被停止,所以服务嵌入式管理单元没有更新它的使用者介面,以反映服务已被停止的情形。不幸地,系统没有提供一个当服务状态改变时通知应用程序的方法,所以一个SCP必须周期性地轮询服务以决定何时改变它的状态。WaitForServiceState函数处理了这个轮询的动作。
BOOL WaitForServiceState(SC_HANDLE hService, DWORD dwDesiredState, SERVICE_STATUS* pss, DWORD dwMilliseconds) { BOOL fServiceOk = TRUE; BOOL fFirstTime = TRUE; // 第一次不要比较状态/检查点 DWORD dwLastState = 0, dwLastCheckPoint = 0; DWORD dwTimeout = GetTickCount() + dwMilliseconds; // 递回至服务到达了被要求的状态、错误发生或逾时为止 for (;;) { // 取得目前服务的状态 fServiceOk = ::QueryServiceStatus(hService, pss); // 如果我们不能询问服务,我们会做 if (!fServiceOk)break; // 如果服务到达了被要求的状态,我们会做 if (pss->dwCurrentState == dwDesiredState) break; // 如果逾时,我们会做 if ((dwMilliseconds != INFINITE) && (dwTimeout < GetTickCount())) { fServiceOk = FALSE; SetLastError(ERROR_TIMEOUT); break; } // 如果第一次储存服务的状态/检查点 if (fFirstTime) { dwLastState = pss->dwCurrentState; dwLastCheckPoint = pss->dwCheckPoint; fFirstTime = FALSE; } else { // 如果不是第一次且状态被改变,则储存状态/检查点 if (dwLastState ! = pss->dwCurrentState) { dwLastState = pss->dwCurrentState; dwLastCheckPoint = pss->dwCheckPoint; } else { // 状态无法被改变;确定检查点没有被减少 if (pss->dwCheckPoint >= dwLastCheckPoint) { // 好的检查点;储存它 dwLastCheckPoint = pss->dwCheckPoint; } else { // 坏的检查点、服务执行失败,我们会做 fServiceOk = FALSE; break; } } } // 我们没有完成;等待被指定的一段时间 // 等待提示的1/10轮询 DWORD dwWaitHint = pss->dwWaitHint / 10; // 最多一次一秒 if (dwWaitHint < 1000) dwWaitHint = 1000; // 至少每个10秒 if (dwWaitHint > 10000)dwWaitHint = 10000; Sleep(dwWaitHint); } // 注意:最后一个SERVICE_STATUS被回传至呼叫者处,所以呼叫者可以检 // 查服务状态与错误码 return(fServiceOk); }
我们知道去做轮询的动作是一件麻烦的事,因为它浪费了宝贵的CPU循环时间,但是在这个情形下,我们真的没有其他选择。幸运地,情况并不像您所想的严重,因为SERVICE_STATUS结构包含了dwWaitHint成员。当一个服务呼叫SetServiceStatus时,dwWaitHint成员必须指示在执行下次轮询服务状态的动作前程序在传送控制码时应该等待多少个毫秒。
服务控制程序也应该检查在确认它不会被减少的轮询处理程序期间,从服务被回传的检查点。如果一个服务回传了一个小于检查点的值,那么该服务控制程序应该假设服务已经执行失败。
您将会通知WaitForServiceState去呼叫QueryServiceStatus:
BOOL QueryServiceStatus( SC_HANDLE hService, SERVICE_STATUS* pss);
QueryServiceStatus询问SCM以回传最后被快取的服务状态资讯(当服务最后被SetServiceStatus呼叫时设定)。呼叫QueryServiceStatus的方法就像透过传递SERVICE_CONTROL_INTERROGATE控制码呼叫ControlService一样,但是使用SERVICE_CONTROL_INTERROGATE呼叫ControlService时,会传送一个动作要求给服务以更新目前之状态资讯。另一个在呼叫QueryServiceStatus与ControlService的不同处是QueryServiceStatus总是会在一定的时间内返回,反之,如果服务已经停止回应,则ControlService可能会回传失败。当您传送一个询问控制码给它时,若该服务的控制函数正处于忙碌的状态,则该服务可能会有一段会无法回应,这些会导致您的程序呼叫ControlService并开始等待(也许为30秒)。当然,减少使用QueryServiceStatus函数可能会使SCM被快取的资料不精确并且会影响服务之最新状态。现在您已经知道折衷点在哪里了,所以您可以依照您的实际情况来决定在询问服务使用状态时应该使用何种方式。
除了QueryServiceStatus之外,Microsoft最近加入了新的QueryServiceStatusEx函数:
BOOL QueryServiceStatusEx( SC_HANDLE hService, SC_STATUS_TYPE InfoLevel, PBYTE pbBuffer, DWORD cbBufSize, PDWORD pdwBytesNeeded);
这个函数要求一个服务的状态并将新的SERVICE_STATUS_PROCESS结构初始化:
typedef struct _SERVICE_STATUS_PROCESS { DWORD dwServiceType; DWORD dwCurrentState; DWORD dwControlsAccepted; DWORD dwWin32ExitCode; DWORD dwServiceSpecificExitCode; DWORD dwCheckPoint; DWORD dwWaitHint; DWORD dwProcessId; DWORD dwServiceFlags; } SERVICE_STATUS_PROCESS, *LPSERVICE_STATUS_PROCESS;
此结构与SERVICE_STATUS结构完全相同,只是它多出了二个成员:dwProcessId与dwServiceFlags。dwProcessId成员指示了包含服务处理程序的ID,而dwServiceFlags则指出一些关于服务之额外资讯。如果dwServiceFlags包含了SERVICE_RUNS_IN_SYSTEM_PROCESS(目前唯一被定义的标记),则表示该服务正在系统处理程序中执行,例如Services.exe或LSASS.exe。因为处理程序为作业系统之必要元件,所以您不该试图删除一个正在系统处理程序中执行的服务。
重新设定一个服务
CreateService函数会将一个新服务的控制项加入至SCM资料库中。它通常不会如此做,然而偶尔您也许会想改变资料库中的资讯。例如,与控制项联系的使用者帐户也许需要已修改的密码,或者您可能想要服务的启动方式从手动改成自动启动。Windows提供您四个函数,以帮助您重新设定一个服务。第一个函数为QueryServiceConfig,它会从SCM资料库中取回服务的控制项:
BOOL QueryServiceConfig( SC_HANDLE hService, QUERY_SERVICE_CONFIG* pqsc, DWORD dwBufSize, PDWORD pdwBytesNeeded);
当您呼叫这个函数时,hService参数会确认您想要询问的服务。而它的handle必须经由SERVICE_QUERY_CONFIG的存取而被开启。您也必须配置一个足够大的内存缓冲区以保存QUERY_SERVICE_CONFIG结构与服务的所有字串资料。一个QUERY_SERVICE_CONFIG结构看起来就像这样:
typedef struct _QUERY_SERVICE_CONFIG { DWORD dwServiceType; DWORD dwStartType; DWORD dwErrorControl; PTSTR lpBinaryPathName; PTSTR lpLoadOrderGroup; DWORD dwTagId; PTSTR lpDependencies; PTSTR lpServiceStartName; PTSTR lpDisplayName; } QUERY_SERVICE_CONFIG, *LPQUERY_SERVICE_CONFIG;
QueryServiceConfig的dwBufSize参数会告诉函数您需要多大的缓冲区,而被pdwBytesNeeded参数所指的DWORD会被函数填满,并告诉您缓冲区需要多大。因为该函数在复制固定长度的资料结构后,会立即复制所有服务的字串资料到缓冲区中,所以您传递至QueryServiceConfig的缓冲区永远会大于QUERY_SERVICE_ CONFIG结构。PTSTR成员会指向缓冲区内部的内存位址。
一旦您拥有了服务的现行设定,您便可以经由呼叫以下的函数来改变它:
BOOL ChangeServiceConfig( SC_HANDLE hService, DWORD dwServiceType, DWORD dwStartType, DWORD dwErrorControl, PCTSTR pszPathName, PCTSTR pszLoadOrderGroup, PDWORD pdwTagId, PCTSTR pszDependencies, PCTSTR pszUserName, PCTSTR pszUserPswd, PCTSTR pszDisplayName);
如您所见,这些参数几乎与那些被传递至CreateService的一样。其不同之处是您不能改变服务的内部名称,还有它的显示名称为最后一个参数。当您使用ChangeServiceConfig时,除非停止服务,否则任何的改变都不会生效。
除了提供QueryServiceConfig与ChangeServiceConfig外,Windows还提供了QueryServiceConfig2与ChangeServiceConfig2函数,它们允许您取得与设定一个服务的依存关系与执行失败时所做动作。这二个特色在很久以前即已被要求,现在看到Microsoft在Windows 2000加入它们,真的很好。
一个服务的依存关系是一个简单地描述服务的字串(限制在1024个字元)。这个字串显示在服务嵌入式管理单元中,它也使管理员了解每一个在系统中被安装与执行的服务之目的。当您加入一个服务至SCM资料库时,强烈地建议您一并加入对该服务的说明。
使用失败动作时,管理员可以告诉系统当服务执行失败时应该做什么(如果在它的处理程序死亡时没有把它的状态设定为SERVICE_STOPPED时,服务会失败)。一个管理者可以使SCM自动地重新启动服务、执行一个应用程序或将电脑重新开机。有关QueryServiceConfig2与ChangeServiceConfig2的更多资讯,请参阅《Platform SDK》文件。
将SCM资料库锁定
如果您发现您自己在SCM资料库中做了许多的修改时,您可能会想要经由取得SCM的锁定而从任何已启动的服务中暂时停止SCM的执行。获得锁定的好时机是当您想要询问一个服务的设定并改变它的「atomically」时。当服务依存于其他的服务时,锁定SCM可能也会有用。为了预防SCM再启动更多的服务,可以呼叫LockServiceDatabase函数,并将经由使用SC_MANAGER_LOCK存取而呼叫至OpenSCManager所回传的handle传递给它:
SC_LOCK LockServiceDatabase(SC_HANDLE hSCManager);
LockServiceDatabase函数会回传一个指示为锁定的值。因为当您想要释放该锁定时,您将会需要将它传递至UnlockServiceDatabase中,所以要保存这个值:
BOOL UnlockServiceDatabase(SC_LOCK scLock);
同一时间内只有一个处理程序可以拥有SCM的锁定,当然,您应该尽量将拥有此锁定的时间缩短。如果拥有锁定之处理程序终止了,SCM会自动地回收它,以使服务可以再次启动。
注意如果您至SCM中关闭了handle,则该锁定并不会自动地被释放。考虑这个范例:
SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_LOCK); // 锁定SCM资料库 SC_LOCK scLock = LockServiceDatabase(hSCM); CloseServiceHandle(hSCM); // 注意:资料库仍然被锁定 . . . UnlockServiceDatabase(scLock); // 资料库现在被解除锁定
QueryServiceLockStatus函数会回传资讯以允许您察看SCM的锁定状态:
BOOL QueryServiceLockStatus( SC_HANDLE hSCManager, QUERY_SERVICE_LOCK_STATUS* pqsls, DWORD dwBufSize, PDWORD pdwBytesNeeded);
如果SCM已经被锁定,则该函数也会将哪一个使用者帐户拥有该锁定以及被锁定的时间回传。所有的讯息会经由一个QUERY_SERVICE_LOCK_STATUS结构而被回传:
typedef struct _QUERY_SERVICE_LOCK_STATUS { DWORD fIsLocked; PTSTR lpLockOwner; DWORD dwLockDuration; } QUERY_SERVICE_LOCK_STATUS, *LPQUERY_SERVICE_LOCK_STATUS;
就像QueryServiceConfig函数一样,您传递至QueryServiceLockStatus的缓冲区必须大于该结构的大小。再一次说明,这是因为结构包含了一个在复制了固定尺寸的结构后便会被立即地复制至缓冲区的字串值。
各式各样的服务控制程序函数
Windows提供了许多的服务控制函数。在此我想简单地提出来说明,以完成我们的讨论内容。
一个函数从它的内部名称来查询一个服务的显示名称:
BOOL GetServiceDisplayName( SC_HANDLE hSCManager, PCTSTR pszServiceName, PTSTR pszDisplayName, PDWORD pdwChars);
另一个相反动作的函数:
BOOL GetServiceKeyName( SC_HANDLE hSCManager, PCTSTR pszDisplayName, PTSTR pszServiceName, PDWORD pdwChars);
传递给这些函数的参数应该非常明显了,所以在此我不讨论它们。请参阅《Platform SDK》文件以得到更多的资讯。
EnumServicesStatusEx函数会请求SCM将资料库内所包含的服务(及其状态)列举出来:
BOOL EnumServicesStatusEx( SC_HANDLE hSCManager, SC_ENUM_TYPE InfoLevel, DWORD dwServiceType, DWORD dwServiceState, PBYTE pbServices, DWORD dwBufSize, PDWORD pdwBytesNeeded, PDWORD pdwServicesReturned, PDWORD pdwResumeHandle, PCTSTR pszGroupName);
服务嵌入式管理单元会呼叫此函数以列出已被安装的服务清单。它的第一个参数hSCManager指出您想要SCM列举的服务。而第二个参数InfoLevel必须为SC_ENUM_PROCESS_INFO,以告诉函数您想要取回的每一个服务名称与服务状态。目前没有其他有效值可供InfoLevel参数使用。
第叁个参数dwServiceType告诉函数该列举服务或是设备驱动程序。若为服务,则传递SERVICE_WIN32。第四个参数dwServiceState允许您去调整您的要求。您可以传递SERVICE_ACTIVE、SERVICE_INACTIVE或SERVICE_STATE_ALL以列举正在执行中的服务、已停止执行的服务或二者皆列举。最后一个参数为pszGroupName,允许您调整被回传的服务或甚至更多其他的设备驱动程序。如果pszGroupName指示了一个群组,则只有当服务属于被指定群组之一时才会被列举。若pszGroupName指示了一个空字串(""),则会将不属于任何群组的服务列举出来。最后,如果pszGroupName为NULL,则所有的服务皆会被列举。
剩下的其他所有参数皆与取得被回传资料之缓冲区有关。当您呼叫EnumServicesStatusEx时,您会传递一个被ENUM_SERVICE_STATUS_PROCESS结构填满之阵列缓冲区给它,看起来像这样:
typedef struct _ENUM_SERVICE_STATUS_PROCESS { LPTSTR lpServiceName; LPTSTR lpDisplayName; SERVICE_STATUS_PROCESS ServiceStatusProcess; } ENUM_SERVICE_STATUS_PROCESS, *LPENUM_SERVICE_STATUS_PROCESS;
因为每一个服务皆拥有与它相关联的字串资料,该字串资料会被复制至缓冲区的结尾。而固定尺寸的ENUM_SERVICE_STATUS_PROCESS结构在缓冲区的开始是邻接的,所以您可以很容易地透过被回传的资料结构而重覆它。当函数返回时,经由pdwServicesReturned所指的DWORD会包含被安装至缓冲区中的ENUM_SERVICE_STATUS_PROCESS结构。
您在第一次呼叫EnumServicesStatusEx时,请确定经由pdwResumeHandle所指的DWORD已经被初始化为0。这个pdwResumeHandle被使用在那些超过您的缓冲区所能储存的情形下。如果缓冲区太小,则EnumServicesStatusEx会将这个DWORD以一个特别的值填满。这个值使用在下一次您呼叫EnumServicesSta时,所以它会知道这个列举在哪里继续。以下的程序代码说明了如何配置一个足以储存所有服务资料的缓冲区,所以许多的EnumServicesStatusEx呼叫会变得不需要:
DWORD dwBytesNeeded, dwServicesReturned, dwResumeHandle = 0; EnumServicesStatusEx(hSCManager, SC_ENUM_PROCESS_INFO, SERVICE_WIN32, SERVICE_STATE_ALL, NULL, 0, &dwBytesNeeded, &dwServicesReturned, &dwResumeHandle, NULL); ENUM_SERVICE_STATUS_PROCESS* pessp = (ENUM_SERVICE_STATUS_PROCESS*) _alloca(dwBytesNeeded); EnumServicesStatusEx(hSCManager, SC_ENUM_PROCESS_INFO, SERVICE_WIN32, SERVICE_STATE_ALL, (PBYTE) pessp, dwBytesNeeded, &dwBytesNeeded, &dwServicesReturned, &dwResumeHandle, NULL); for (DWORD dw = 0; dw < dwServicesReturned; dw++) { // 提到pessp里面的成员,例如 _tprintf(TEXT("%s/n"), pessp[dw].lpDisplayName); }
说明
为了更清楚的说明,前述的程序代码并没有包含任何的错误检查。特别是在呼叫_alloca时可能会产生一个堆叠溢位例外的情形。结构化的例外处理(Structured exception handling,SEH)要求从这个例外中优雅地而且在服务没有终止的情形下恢复。由于有一些正在执行Windows 2000 Advanced Server以及Windows 2000 Data Center的机器中可能已经安装了超过200个的服务,所以在这个程序代码中使用SHE是特别重要的。
下一个要讨论的函数能让您去决定哪一个服务依存于另一个服务:
BOOL EnumDependentServices( SC_HANDLE hService, DWORD dwServiceState, ENUM_SERVICE_STATUS* pess, DWORD dwBufSize, PDWORD pdwBytesNeeded, PDWORD pdwServicesReturned);
这个函数类似于EnumServicesStatusEx,所以它的所有参数涵义对您来说应该已经很清楚。如果您试着停止一个被另一个服务所依存的服务,则服务嵌入式管理单元会呼叫此函数。例如,若我尝试停止Workstation服务,我取得显示于图4-1中的对话方块—即EnumDependentServices会被用来填入依存于它的服务清单。
许多开发者为了取得每一个服务的依存关系,会以递回的方式呼叫EnumDependentServices。其实并不需要,因为在处理EnumDependentServices时,SCM便会替您完成这个递回动作。由这个函数传回的服务设备即是完整的设备,若需要的话,您的SCP应用程序应该只要重覆经由这个设备停止每一个服务的动作。
最后我们到达了最后二个服务控制函数—QueryServiceObjectSecurity与SetServiceObjectSecurity:
BOOL QueryServiceObjectSecurity( SC_HANDLE hService, SECURITY_INFORMATION dwSecurityInformation, PSECURITY_DESCRIPTOR psd, DWORD dwBufSize, PDWORD pdwBytesNeeded); BOOL SetServiceObjectSecurity( SC_HANDLE hService, SECURITY_INFORMATION dwSecurityInformation, PSECURITY_DESCRIPTOR psd);
这二个函数允许您询问与改变一个与服务有关的安全描述项(Security Descriptor)。它们很少被呼叫,因为当CreateService被呼叫时,放在服务上的预设安全性在大多数情形下已经足够。请参阅
SuperSCP范例应用程序
SuperSCP范例应用程序(「04 SuperSCP.exe」)是一个关于您能想像服务会做的任何事之SCP。此应用程序的原始码与资源文件放置在附赠光碟上的04-SuperSCP目录中。当您启动程序时,会显示如图4-2所示的视窗。
此应用程序经由本地端机器的电脑名称而初始化,而且它会把这个名称放至Machine栏位中。然后应用程序会与机器的SCM沟通,列举所有被安装的服务,并把每一个服务的内部名称放入Internal Name栏位的下拉式方块中。任何时间内,您皆可以在一个远端机器上经由在Machine栏位中输入远端机器的名称以及按下Refresh按钮来管理服务。
只要您从Internal Name栏位中选择一个项目,所有其他的栏位皆会被更新。您可以改变任何一个服务的设定组合,然后按下Reconfigure按钮以使改变生效。按下Security按钮可以允许您去改变所选择服务的安全性。您也可以按下Remove按钮而将一个被选择的服务从SCM资料库中标记起来,表示此服务已被删除。若您将要使用SCM来完成一些事情,则您可以使用Lock SCM按钮来将它锁定。
若要将一个新的服务加到SCM资料库中,只要在Internal Name栏位中键入所需的内部名称即可。如果内部名称栏位中包含了一个与现存服务之内部名称不相配的值时,SuperSCP会假设该栏位描述了一个新的服务,所以您可以将它加至SCM资料库中。在键入新服务的内部名称后,即可以任何您想要的方式设定该服务。使用Browse按钮可以帮助您找出服务的可执行文件。而且,如果您将一个可执行文件从一个资料夹中拖放至SuperSCP视窗时,它的路径名称会显示在Pathname栏位中。一旦您完成了服务的设定,即可按下Create按钮以将新的服务加入SCM资料库中。
在视窗底部的按钮能让您改变服务执行方式的执行控制部分。您应该能从字面上了解此部份所有按钮之意义。右边的清单方块每秒会接收一个新的项目。每一个项目皆会指示一个项目编号以及服务的现行状态:已停止(Stopped)、启动搁置中(Start Pending)、停止搁置中(Stop Pending)、执行中(Running)、持续搁置中(Continue Pending)、暂停搁置中(Pause Pending)或已暂停(Paused)。在显示这些状态后,最近的检查点会显示并等待由服务报告的提示。最后的二个Werr与Serr栏位会经由服务而显示最后的Win32错误码与特殊的服务错误码。
依照程序代码而执行下去,没有什么事是难处理的。它仅仅是做着在正确时间内呼叫正确的SCP函数的事。然而,一些可重覆使用的C++ 类别使得这个动作更容易。二个主要的C++ 类别是CSCMCtrl和CserviceCtrl。CSCMCtrl类别是在SCP函数上面的一个小的包装函数(Wrapper),它会直接与SCM沟通。CSCMCtrl包含了Open、LockDatabase、QueryLockOwner、GetInternalName以及GetDisplayName等方法。另外,还有建立一个SCM的服务快照(Snapshot)以及经由它们而列举的方法。C++ 类别在此处真的很有用,因为它处理了所有内部内存管理的问题。
CserviceCtrl类别是在SCP函数上直接与服务沟通的一个小包装函数(Wrapper)。它包括了InstallAndOpen、Open、Delete、Start、Control、WaitForState、QueryStatus、QueryConfig、QueryDescription、ChangeConfig、QueryFailureActions、ChangeFailureActions以及EditSecurity等方法。它也存在建立一个服务的依存关系快照的方法。再一次说明,因为有许多关于这些方法的内部内存管理问题要处理,所以会用得着C++ 类别。
第十章 以了解更多关于安全描述项与其相关的主题。