长时间使用电脑,导致眼睛胀痛,去医院检查,眼压升高,严重可能导致青光眼,进而导致失明。
所以准备开发一个程序来提醒自己保护眼睛。
程序在后台运行,所以准备开发为服务,开机自启动。
我对C++更熟,对C#不熟,所以准备使用C++。
服务检测开机与解锁,检测到之后开始计时,比如40分钟,这个可以自己定,之后提示已用时40分钟,需要休息眼睛。
windows服务开发文档
Services (Services) - Win32 apps | Microsoft Learn
服务
一个服务应用程序符合服务控制管理器(SCM)的接口规则。它可以在系统启动时自动启动,通过服务控制面板小程序由用户启动,或者由使用服务函数的应用程序启动。即使没有用户登录到系统,服务也可以执行。
驱动程序服务符合设备驱动程序协议。它类似于服务应用程序,但不与SCM交互。为简单起见,在这个概述中,术语"服务"指的是服务应用程序。
触发器现在可以用来控制服务启动。有关更多信息,请参阅服务配置。
关于服务
服务控制管理器(SCM)维护一个安装的服务和驱动程序服务的数据库,并提供了一种统一且安全的方法来控制它们。数据库包含每个服务或驱动程序服务应如何启动的信息。它还使系统管理员能够自定义每个服务的安全要求,从而控制对该服务的访问。
下面是使用SCM提供的功能的程序类型:
类型 描述
服务程序:提供一个或多个服务所需的可执行代码的程序。服务程序使用与SCM连接并向SCM发送状态信息的函数。
服务配置程序:查询或修改服务数据库的程序。服务配置程序使用打开数据库的函数,在数据库中安装或删除服务,并查询或修改已安装服务的配置和安全参数。服务配置程序管理服务和驱动程序服务。
服务控制程序:启动和控制服务和驱动程序服务的程序。服务控制程序使用向SCM发送请求的函数,而SCM执行这些请求。
服务控制管理器
.服务控制管理器
服务控制管理器(SCM)在系统启动时启动。它是一个远程过程调用(RPC)服务器,因此服务配置和服务控制程序可以操作远程机器上的服务。
服务函数提供了SCM执行以下任务的接口:
维护安装的服务数据库。 在系统启动或需求时启动服务和驱动程序服务。 枚举安装的服务和驱动程序服务。 维护正在运行的服务和驱动程序服务的状态信息。 向正在运行的服务发送控制请求。 锁定和解锁服务数据库。
.已安装服务的数据库
SCM在注册表中维护着一个已安装服务的数据库。该数据库由SCM和添加、修改或配置服务的程序使用。以下是该数据库的注册表键:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services。
该键包含每个已安装服务和驱动程序服务的子键。子键的名称是服务的名称,由服务配置程序在安装服务时通过CreateService函数指定。
在系统安装时创建了数据库的初始副本。数据库包含系统启动期间所需的设备驱动程序的条目。数据库包括每个已安装服务和驱动程序服务的以下信息:
服务类型。这指示服务是在自己的进程中执行还是与其他服务共享进程。对于驱动程序服务,这指示服务是内核驱动程序还是文件系统驱动程序。 启动类型。这指示服务或驱动程序服务是否在系统启动时自动启动(自动启动服务),或者在服务控制程序请求时由SCM启动它(按需启动服务)。启动类型还可以指示服务或驱动程序服务被禁用,此时无法启动它。 错误控制级别。如果服务或驱动程序服务在系统启动期间无法启动,这指定错误的严重性,并确定启动程序将采取的操作。 可执行文件的完整路径。服务的文件扩展名为.EXE,驱动程序服务的文件扩展名为.SYS。 用于确定启动服务或驱动程序服务的正确顺序的可选依赖信息。对于服务,此信息可以包括SCM必须在启动指定服务之前启动的服务列表、服务所属的加载顺序组的名称,以及指示服务在其加载顺序组中的启动顺序的标识符。对于驱动程序服务,此信息包括必须在指定驱动程序之前启动的驱动程序的列表。 对于服务,还有一个可选的账户名和密码。服务程序在该账户的上下文中运行。如果未指定账户,则服务在LocalSystem账户的上下文中执行。 对于驱动程序服务,还有一个可选的驱动程序对象名称(例如,\FileSystem\Rdr或\Driver\Xns),由I/O系统用于加载设备驱动程序。如果未指定名称,则I/O系统将根据驱动程序服务名称创建一个默认名称。
.自动启动服务
在系统启动过程中,SCM会启动所有自动启动的服务以及它们所依赖的服务。例如,如果一个自动启动的服务依赖于一个按需启动的服务,那么按需启动的服务也会被自动启动。
加载顺序由以下因素决定:
加载顺序组列表中组的顺序。这些信息存储在以下注册表键中的ServiceGroupOrder值中:
HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control
要为一个服务指定加载顺序组,可以使用CreateService或ChangeServiceConfig函数的lpLoadOrderGroup参数。
标签顺序向量中每个组内服务的顺序。这些信息存储在以下注册表键中的GroupOrderList值中:
HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control
每个服务所列出的依赖关系。
在引导完成后,系统执行由以下注册表键的BootVerificationProgram值指定的引导验证程序:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control
默认情况下,此值没有设置。在第一个用户登录后,系统只是报告引导成功。您可以提供一个引导验证程序,使用NotifyBootConfigStatus函数检查系统是否存在问题,并向SCM报告引导状态。
成功启动后,系统会将数据库的副本保存在最后已知的良好(LKG)配置中。如果对活动数据库进行的更改导致系统重新启动失败,系统可以恢复此数据库副本。以下是此数据库的注册表键:
HKEY_LOCAL_MACHINE\SYSTEM\ControlSetXXX\Services
其中XXX是保存在以下注册表值中的值:HKEY_LOCAL_MACHINE\System\Select\LastKnownGood。
如果一个具有SERVICE_ERROR_CRITICAL错误控制级别的自动启动服务启动失败,SCM会使用LKG配置重新启动计算机。如果已经使用LKG配置,则引导失败。
可以通过调用ChangeServiceConfig2函数并使用SERVICE_CONFIG_DELAYED_AUTO_START_INFO将自动启动服务配置为延迟自动启动服务。此更改将在下次系统启动后生效。有关更多信息,请参阅SERVICE_DELAYED_AUTO_START_INFO。
.按需启动服务
用户可以使用服务控制面板实用程序启动服务。用户可以在“启动参数”字段中指定服务的参数。服务控制程序可以使用StartService函数启动服务并指定其参数。
当服务启动时,SCM执行以下步骤:
检索存储在数据库中的帐户信息。 登录服务帐户。 加载用户配置文件。 创建挂起状态的服务。 将登录令牌分配给进程。 允许进程执行。
.服务记录列表
在从已安装服务的数据库中读取每个服务条目时,SCM会为该服务创建一个服务记录。服务记录包括以下内容:
- 服务名称
- 启动类型(自动启动或按需启动)
- 服务状态(参见 SERVICE_STATUS 结构)
- 类型
- 当前状态
- 可接受的控制代码
- 退出码
- 等待提示
- 依赖列表指针
在安装服务时,会指定帐户的用户名和密码。SCM将用户名存储在注册表中,将密码存储在本地安全机构(LSA)的安全部分中。系统管理员可以创建永不过期的带密码的帐户。或者,系统管理员可以创建具有过期密码的帐户,并定期更改密码来管理这些帐户。
SCM保留用户帐户密码的两个副本,一个是当前密码,一个是备份密码。第一次安装服务时指定的密码被存储为当前密码,并且备份密码未初始化。当SCM尝试以用户帐户的安全上下文运行服务时,它使用当前密码。如果当前密码成功使用,它也会保存为备份密码。如果使用ChangeServiceConfig函数或服务控制面板实用程序修改密码,则新密码存储为当前密码,先前的密码存储为备份密码。如果SCM尝试启动服务并且当前密码失败,则使用备份密码。如果备份密码成功使用,则保存为当前密码。
当服务使用SetServiceStatus函数向SCM发送状态通知时,SCM会更新服务状态。SCM通过查询I/O系统来维护驱动程序服务的状态,而不是像从服务中接收状态通知那样。
服务可以通过调用SetServiceBits函数注册附加类型信息。NetServerGetInfo和NetServerEnum函数获取支持的服务类型。
.SCM(Service Control Manager)句柄
SCM支持句柄类型,以允许访问以下对象:
- 已安装服务的数据库。
- 一个服务。
- 数据库锁。
SCManager对象代表已安装服务的数据库。它是一个容器对象,保存着服务对象。OpenSCManager函数返回一个句柄,用于表示指定计算机上的SCManager对象。在安装、删除、打开和枚举服务以及锁定服务数据库时,使用该句柄。
服务对象代表一个已安装的服务。CreateService和OpenService函数返回已安装服务的句柄。
OpenSCManager、CreateService和OpenService函数可以请求对SCManager和服务对象的不同类型的访问权限。所请求的访问权限将根据调用进程的访问令牌和与SCManager或服务对象相关联的安全描述符而被授予或拒绝。
CloseServiceHandle函数用于关闭对SCManager和服务对象的句柄。当您不再需要这些句柄时,请确保关闭它们。
服务程序
.服务程序
一个服务程序包含一个或多个服务的可执行代码。使用类型为SERVICE_WIN32_OWN_PROCESS创建的服务程序仅包含一个服务的代码。使用类型为SERVICE_WIN32_SHARE_PROCESS创建的服务程序包含多个服务的代码,使它们可以共享代码。一个实现这一功能的服务程序的示例是通用服务主机进程Svchost.exe,它托管内部的Windows服务。请注意,Svchost.exe被操作系统保留使用,非Windows服务不应使用它。开发人员应该实现自己的服务托管程序。
一个服务程序可以配置为在内置(本地)、主要或受信任的域的用户帐户上下文中执行。它还可以配置为在特殊的服务用户帐户中运行。
以下主题描述了服务程序必须包含的服务控制管理器(SCM)接口要求:
服务入口点 服务ServiceMain函数 服务控制处理程序函数 这些主题不适用于驱动程序服务。有关驱动程序服务的接口要求,请参阅Windows驱动程序开发工具包(WDK)。
服务作为后台进程运行,可能会影响系统的性能、响应性、能效和安全性。有关服务优化指南,请参阅为Windows开发高效的后台进程。以下主题描述了其他编程注意事项:
服务状态转换 在服务中接收事件 多线程服务 服务和注册表 服务和重定向驱动器 服务触发事件 请注意,如果服务程序充当RPC服务器,应使用动态端点和相互认证。
.服务入口点
服务通常以控制台应用程序的形式编写。控制台应用程序的入口点是它的主函数(main function)。主函数从注册表键的ImagePath值中接收服务的参数。有关详细信息,请参阅CreateService函数的备注部分。
当服务控制管理器(SCM)启动一个服务程序时,它会等待该程序调用StartServiceCtrlDispatcher函数。请按照以下准则操作:
对于类型为SERVICE_WIN32_OWN_PROCESS的服务,应立即从主线程中调用StartServiceCtrlDispatcher。在服务启动后,您可以执行任何初始化操作,如ServiceMain函数中所述。 如果服务类型为SERVICE_WIN32_SHARE_PROCESS,并且程序中所有服务都具有共同的初始化操作,您可以在调用StartServiceCtrlDispatcher之前,在主线程中执行初始化操作,前提是该操作耗时不超过30秒。否则,您必须创建另一个线程来执行共同的初始化操作,而主线程调用StartServiceCtrlDispatcher。在服务启动后,仍然应执行任何特定于服务的初始化操作。 StartServiceCtrlDispatcher函数对于进程中包含的每个服务,都需要一个SERVICE_TABLE_ENTRY结构。每个结构指定了服务名称和服务的入口点。有关示例,请参阅编写服务程序的main函数。
如果StartServiceCtrlDispatcher成功,调用线程将一直等待,直到进程中的所有正在运行的服务都进入了SERVICE_STOPPED状态。SCM通过命名管道向此线程发送控制请求。该线程充当控制分派程序,执行以下任务:
当启动新服务时,创建一个新线程来调用适当的入口点。
调用适当的处理程序函数来处理服务控制请求。
.服务ServiceMain函数
服务控制程序请求运行一个新的服务时,服务控制管理器(SCM)会启动该服务并发送启动请求给控制分派程序。控制分派程序创建一个新线程来执行服务的ServiceMain函数。有关示例,请参阅编写ServiceMain函数。
ServiceMain函数应执行以下任务:
-
初始化所有全局变量。
-
立即调用RegisterServiceCtrlHandler函数注册处理程序函数,用于处理服务的控制请求。RegisterServiceCtrlHandler的返回值是一个服务状态句柄,将在向SCM通知服务状态的调用中使用。
-
执行初始化操作。如果初始化代码的执行时间预计非常短(少于一秒),可以直接在ServiceMain中进行初始化。
-
如果初始化时间预计超过一秒钟,服务应使用以下初始化技术之一:
- 调用SetServiceStatus函数报告SERVICE_RUNNING状态,但在初始化完成之前不接受任何控制请求。服务通过在SERVICE_STATUS结构中将dwCurrentState设置为SERVICE_RUNNING,dwControlsAccepted设置为0,调用SetServiceStatus来实现此目的。这确保SCM在服务准备就绪之前不会发送任何控制请求,并使SCM能够管理其他服务。特别是对于自动启动服务,推荐使用此初始化方法以提高性能。
- 报告SERVICE_START_PENDING状态,不接受任何控制请求,并指定一个等待提示。如果服务的初始化代码执行的任务预计需要比初始等待提示值更长的时间,则代码必须定期调用SetServiceStatus函数(可能使用修订后的等待提示值)来指示正在进行进度。确保仅在初始化正在取得进展时调用SetServiceStatus。否则,SCM会等待服务进入SERVICE_RUNNING状态,假定服务正在取得进展,并阻止其他服务启动。除非您确定执行初始化的线程确实取得进展,否则不要从单独的线程调用SetServiceStatus。
- 使用此方法的服务还可以指定一个检查点值,并在长时间初始化期间定期递增该值。启动服务的程序可以调用QueryServiceStatus或QueryServiceStatusEx从SCM获取最新的检查点值,并将该值用于向用户报告增量进度。
-
初始化完成后,调用SetServiceStatus将服务状态设置为SERVICE_RUNNING,并指定服务准备接受的控制请求。有关控制请求的列表,请参见SERVICE_STATUS结构。
-
执行服务任务,或者如果没有待处理任务,则将控制返回给调用者。任何服务状态的更改都需要调用SetServiceStatus来报告新的状态信息。
-
如果服务在初始化或运行期间发生错误,服务应调用SetServiceStatus将服务状态设置为SERVICE_STOP_PENDING,如果清理工作需要很长时间。清理完成后,调用SetServiceStatus从最后一个终止的线程将服务状态设置为SERVICE_STOPPED。确保将SERVICE_STATUS结构的dwServiceSpecificExitCode和dwWin32ExitCode成员设置为标识错误的值。
.服务控制句柄函数
每个服务都有一个控制处理程序,即Handler函数,当服务进程接收到来自服务控制程序的控制请求时,它将由控制调度程序调用。因此,该函数在控制调度程序的上下文中执行。例如,要查看编写控制处理程序函数的示例。
服务调用RegisterServiceCtrlHandler或RegisterServiceCtrlHandlerEx函数来注册其服务控制处理程序函数。
当服务控制处理程序被调用时,服务必须调用SetServiceStatus函数向SCM报告其状态,仅当处理控制码导致服务状态发生更改时。如果处理控制码不会导致服务状态发生更改,则无需调用SetServiceStatus。
服务控制程序可以使用ControlService函数发送控制请求。所有服务必须接受和处理SERVICE_CONTROL_INTERROGATE控制码。您可以通过调用SetServiceStatus启用或禁用其他控制代码的接受。要接收SERVICE_CONTROL_DEVICEEVENT控制代码,您必须调用RegisterDeviceNotification函数。服务还可以处理其他用户定义的控制代码。
如果服务接受了SERVICE_CONTROL_STOP控制代码,则必须在接收时停止,转到SERVICE_STOP_PENDING或SERVICE_STOPPED状态。在SCM发送此控制代码之后,它将不发送其他控制代码。
Windows XP:如果服务返回NO_ERROR并继续运行,则它将继续接收控制代码。自Windows Server 2003和Windows XP Service Pack 2(SP2)开始,此行为已更改。
控制处理程序必须在30秒内返回,否则SCM将返回错误。如果服务在执行控制处理程序时必须进行长时间的处理,则应创建一个辅助线程来执行长时间的处理,然后从控制处理程序返回。这可以防止服务绑定控制调度程序。例如,在处理需要很长时间的服务停止请求时,创建另一个线程来处理停止进程。控制处理程序只需使用SERVICE_STOP_PENDING消息调用SetServiceStatus并返回即可。
当用户关闭系统时,所有调用SetServiceStatus并使用SERVICE_ACCEPT_PRESHUTDOWN控制代码的控制处理程序都会收到SERVICE_CONTROL_PRESHUTDOWN控制代码。服务控制管理器等待服务停止或指定的预关闭超时值到期(可以使用ChangeServiceConfig2函数设置此值)。仅在特殊情况下应使用此控制代码,因为处理此通知的服务将阻止系统关闭,直到服务停止或预关闭超时间隔到期。
完成预关闭通知后,所有调用SetServiceStatus并使用SERVICE_ACCEPT_SHUTDOWN控制代码的控制处理程序都会收到SERVICE_CONTROL_SHUTDOWN控制代码。它们按安装的服务数据库中出现的顺序接收通知。默认情况下,服务具有大约20秒的时间来执行清理任务,然后系统关闭。此时间到期后,无论服务关闭是否完成,系统关闭都会继续。请注意,如果系统处于关闭状态(未重新启动或关机),则服务将继续运行。
如果服务需要更多时间来进行清理,则发送STOP_PENDING状态消息以及等待提示,以便服务控制器知道在报告服务关闭完成之前等待的时间长度。但是,为了防止服务停止关闭,服务控制器等待的时间有限。如果通过“服务”选项卡关闭服务,则限制为125秒或125,000毫秒。如果操作系统正在重新启动,则时间限制在以下注册表键的WaitToKillServiceTimeout值(以毫秒为单位)中指定:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control
重要提示:
服务不应尝试通过修改此值来增加时间限制。如果确实需要手动设置WaitToKillServiceTimeout,则该值应以毫秒为单位。
客户需要快速关闭操作系统。例如,如果在UPS电源上运行的计算机在UPS耗尽电力之前无法完成关闭,则可能会丢失数据。因此,服务应尽快完成其清理任务。最好的做法是通过定期保存数据、跟踪保存到磁盘的数据并仅在关闭时保存未保存的数据来最小化未保存的数据。因为正在关闭计算机,所以不要花时间释放已分配的内存或其他系统资源。如果需要通知服务器您正在退出,请最小化等待回复的时间,因为网络问题可能会延迟您的服务的关闭。
请注意,在服务关闭期间,默认情况下,SCM不考虑依赖关系。SCM枚举运行中的服务列表并发送SERVICE_CONTROL_SHUTDOWN命令。因此,由于其依赖的另一个服务已经停止,服务可能会失败。
要手动设置服务的关闭顺序,请创建一个包含按应关闭的顺序列出的服务名称的多字符串注册表值,并将其分配给Control键的PreshutdownOrder值,如下所示:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\PreshutdownOrder="Shutdown Order"
要从应用程序中设置依赖服务的关闭顺序,请使用SetProcessShutdownParameters函数。SCM使用此函数为其处理程序分配0x1E0优先级。当其控制处理程序被调用时,SCM发送SERVICE_CONTROL_SHUTDOWN通知并等待服务退出,然后才从其控制处理程序返回。
.服务状态转换
服务负责将其状态的更改报告给服务控制管理器(SCM)。服务控制程序和系统只能从SCM获取服务的状态,因此服务正确报告其状态非常重要。服务通过调用SetServiceStatus函数并提供完整初始化的SERVICE_STATUS结构的指针来报告其状态。结构中的dwCurrentState成员包含要报告的服务状态。
服务的初始状态是SERVICE_STOPPED。当SCM启动服务时,它将服务状态设置为SERVICE_START_PENDING,并调用服务的ServiceMain函数。然后,服务使用Service ServiceMain Function中描述的技术之一完成初始化。在服务完成初始化并准备好开始接收控制请求后,服务调用SetServiceStatus报告SERVICE_RUNNING,并指定服务准备接受的控制请求。从SERVICE_START_PENDING到SERVICE_RUNNING的转换告诉SCM和服务监控工具服务已成功启动。如果服务报告的状态与SERVICE_RUNNING不同,SCM或服务监控工具可能会将服务标记为启动失败。
SCM仅向服务发送指定的控制请求(除了始终发送的SERVICE_CONTROL_INTERROGATE请求)。有关服务可以接受的控制请求列表,请参见SERVICE_STATUS结构的dwControlsAccepted成员。有关注册以接收设备事件的信息,请参见RegisterDeviceNotification函数。
服务状态通常在处理控制请求时更改。导致服务状态更改的控制请求包括SERVICE_CONTROL_STOP、SERVICE_CONTROL_PAUSE和SERVICE_CONTROL_CONTINUE。如果服务需要执行较长时间的处理来处理这些请求之一,应创建一个辅助线程来执行较长时间的处理,并向SCM报告相应的等待状态。(对于Windows Vista和更高版本的Windows,服务应使用线程池中的工作线程来获得最佳性能。)然后,当完成较长时间的处理时,服务应报告已完成的状态转换。有关处理控制请求的更多信息,请参见Service Control Handler Function。
只有某些服务状态转换是有效的。下图显示了有效的转换过程。
请注意,只有部分状态转换是允许的,具体取决于服务的当前状态。
服务报告给SCM的状态确定了SCM与服务交互的方式。例如,如果一个服务报告了SERVICE_STOP_PENDING状态,那么SCM不会向服务传递更多的控制请求,因为这种状态表示服务正在关闭。服务报告的下一个状态应该是SERVICE_STOPPED,因为这是SERVICE_STOP_PENDING之后唯一有效的状态。然而,如果服务报告了一个无效的转换,SCM不会失败调用。
下图更详细地显示了服务状态转换,包括由服务控制程序(服务客户端)发起的控制请求和服务调用SetServiceStatus报告状态更改给SCM。如前所述,SCM仅发送服务已指定接受的控制请求,因此服务可能无法接收图中显示的所有请求。
.在服务里接受事件
作为控制台应用程序的服务可以注册一个控制台控制处理程序,以便在用户注销时接收通知。然而,在交互式用户登录时,没有发送控制台事件。如果想要接收用户登录时的通知,请参阅创建Winlogon通知包。
系统向所有服务广播设备更改事件。这些事件可以由服务在窗口过程或服务控制处理程序中接收。要指定服务应该接收哪些事件,请使用RegisterDeviceNotification函数。
请确保尽快处理即插即用设备事件。否则,系统可能会变得无响应。如果事件处理程序要执行可能阻塞执行的操作(如I/O操作),最好启动另一个线程以异步执行操作。
当一个服务调用RegisterDeviceNotification时,服务同时需要指定一个窗口句柄或服务状态句柄。如果服务指定了一个窗口句柄,窗口过程将接收通知事件。如果服务指定了自己的服务状态句柄,则其服务控制处理程序将接收通知事件。有关更多信息,请参阅HandlerEx。
通过RegisterDeviceNotification返回的设备通知句柄在不再需要时必须通过调用UnregisterDeviceNotification函数关闭。
.多线程服务
服务控制管理器(SCM)通过向服务的控制处理程序例程发送服务控制事件来控制服务。服务必须及时响应控制事件,以便SCM可以跟踪服务的状态。此外,服务的状态必须与SCM接收到的状态描述相匹配。
由于服务和SCM之间的这种通信机制,使用多个线程时必须小心。当SCM要求服务停止时,在向SCM报告服务已停止之前,服务必须等待所有线程退出。否则,SCM可能会对服务的状态感到困惑,并可能无法正确关闭服务。
SCM需要被通知服务正在响应停止控制事件,并且停止服务取得了进展。如果服务在上一次调用SetServiceStatus指定的时间(等待提示)内响应(通过SetServiceStatus),并且检查点更新为大于上一次调用SetServiceStatus指定的检查点,则SCM将假定服务正在取得进展。
如果服务在所有线程退出之前向SCM报告服务已停止,可能会导致SCM发生矛盾。这可能导致服务无法停止或重启。
.服务和注册表
一个服务不应该访问HKEY_CURRENT_USER或HKEY_CLASSES_ROOT,尤其是在模拟用户身份时。相反,应该使用RegOpenCurrentUser或RegOpenUserClassesRoot函数。
如果你尝试从服务中访问HKEY_CURRENT_USER或HKEY_CLASSES_ROOT,可能会失败,或者看起来可以工作,但存在潜在的泄漏问题,可能导致加载或卸载用户配置文件时出现问题。
.服务和重定向的驱动器
如果一个服务(或任何在不同安全上下文中运行的进程)必须访问远程资源,应该使用通用命名约定(UNC)名称来访问该资源。服务必须具有适当的权限才能访问该资源。如果服务器端的服务使用RPC连接,则远程服务器上必须启用委派。
驱动器字母在系统中不是全局的。每个登录会话都会从A到Z获得自己的一组驱动器字母。因此,不能在不同用户帐户下运行的进程之间共享重定向的驱动器。此外,一个服务(或任何在其自己的登录会话中运行的进程)无法访问在不同登录会话中建立的驱动器字母。
一个服务不应直接通过映射的驱动器字母访问本地或网络资源,也不应在运行时调用net use命令来映射驱动器字母。不推荐使用net use命令的原因有:
驱动器映射存在于登录上下文中,因此如果应用程序在LocalService帐户的上下文中运行,则在该上下文中运行的任何其他服务都可以访问映射的驱动器。 如果服务向net use命令提供了显式凭据,那么这些凭据可能会无意中在服务边界之外共享。相反,服务应使用客户端模拟来模拟用户。 在相同上下文中运行的多个服务可能会相互干扰。如果两个服务都执行显式的net use并同时提供相同的凭据,那么一个服务将失败,并显示“已连接”错误。 此外,服务不应使用Windows网络功能来管理映射的驱动器字母。虽然WNet函数可能会成功返回,但结果行为与预期不符。当系统建立重定向的驱动器时,它是基于每个用户存储的。只有用户能够管理重定向的驱动器。系统根据用户的登录安全标识符(SID)来跟踪重定向的驱动器。登录SID是用户登录会话的唯一标识符。一个用户可以在系统上有多个同时登录会话。
如果一个服务被配置为在用户帐户下运行,系统总是为用户创建一个新的登录会话,并在该新的登录会话中启动服务。因此,一个服务无法管理用户的其他会话中建立的驱动器映射。
Windows Server 2003:在具有多个网络接口(即多宿主计算机)的计算机上,当使用UNC路径访问存储在远程服务器信息块(SMB)服务器上的文件时,可能会出现长达60秒的延迟。
.服务与RPC/TCP
从Windows Vista开始,服务控制管理器(SCM)支持在传输控制协议(RPC/TCP)和命名管道(RPC/NP)上进行远程过程调用。客户端SCM函数默认使用RPC/TCP。
RPC/TCP适用于大多数远程管理或监视工具等需要远程使用SCM函数的应用程序。但是,为了兼容性和性能,一些应用程序可能需要通过设置本主题中描述的注册表值来禁用RPC/TCP。
当服务调用远程SCM函数时,客户端SCM首先尝试使用RPC/TCP与服务器端SCM通信。如果服务器运行支持RPC/TCP并允许RPC/TCP流量的Windows版本,则RPC/TCP连接将成功。如果服务器运行不支持RPC/TCP的Windows版本,或者支持RPC/TCP但位于只允许命名管道流量的防火墙后,则RPC/TCP连接超时,SCM会重试使用RPC/NP进行连接。这最终会成功,但可能需要一些时间(通常超过20秒),导致OpenSCManager函数看起来被阻塞。
TCP不携带使用net use命令指定的用户凭据。因此,如果启用了RPC/TCP并使用sc.exe尝试访问指定的服务,则命令可能会因访问被拒绝而失败。在客户端禁用RPC/TCP可使sc.exe命令使用携带用户凭据的命名管道,从而使命令成功。有关sc.exe的信息,请参见使用SC控制服务。
服务配置程序
.服务配置程序
程序员和系统管理员使用服务配置程序来修改或查询已安装服务的数据库。这个数据库也可以通过使用注册表函数来访问。然而,你应该只使用SCM配置函数,这些函数确保服务被正确安装和配置。
SCM配置函数要求获取一个SCManager对象的句柄或一个服务对象的句柄。为了获得这些句柄,服务配置程序必须:
使用OpenSCManager函数在指定的计算机上获取一个SCM数据库的句柄。 使用OpenService或CreateService函数获取一个服务对象的句柄。 有关更多信息,请参阅以下主题:
服务的安装、删除和枚举 服务配置 使用SC进行服务配置
.服务的安装、删除和枚举
配置程序使用CreateService函数在SCM(服务控制管理器)数据库中安装新的服务。该函数指定服务的名称并提供存储在数据库中的配置信息。有关每个服务在数据库中存储的信息的描述,请参阅已安装服务的数据库。有关示例代码,请参阅安装服务。
配置程序使用DeleteService函数从数据库中删除已安装的服务。有关更多信息,请参阅删除服务。
要获取服务名称,请调用GetServiceKeyName函数。通过调用GetServiceDisplayName函数,可以获取在服务控制面板应用程序中使用的服务显示名称。
服务配置程序可以使用EnumServicesStatusEx函数枚举所有服务及其状态。它还可以使用EnumDependentServices函数枚举哪些服务依赖于指定的服务对象。
.服务配置
系统使用配置信息来确定如何启动服务。配置信息还包括服务的显示名称和描述。例如,对于DHCP服务,可以使用显示名称"Dynamic Host Configuration Protocol Service"和描述"为您网络上的计算机提供互联网地址"。
要修改服务对象的配置信息,配置程序使用ChangeServiceConfig或ChangeServiceConfig2函数。有关示例,请参阅更改服务配置。
要检索服务对象的配置信息,配置程序使用QueryServiceConfig或QueryServiceConfig2函数。有关示例,请参阅查询服务的配置。
ChangeServiceConfig2和QueryServiceConfig2服务配置函数支持使用触发器来控制服务启动。
要修改SCManager对象或服务对象的安全描述符,配置程序使用SetServiceObjectSecurity函数。要检索安全描述符的副本,配置程序使用QueryServiceObjectSecurity函数。
.使用SC配置一个服务
Windows SDK中包含一个命令行实用程序Sc.exe,可用于查询或修改已安装服务的数据库。其命令对应于SCM提供的函数。其语法如下:
sc.exe [<servername>] [<command>] [<servicename>] [<option1>] [<option2>] ...
To see the available commands, type:
sc
To see the syntax for a specific command, include a command, e.g.
sc pause
参数 描述 <servername> 指定服务所在的远程服务器的名称。名称必须使用通用命名约定(UNC)格式(例如,\myserver)。要在本地运行SC.exe,请不要使用此参数。 <command> 以下命令之一: boot config create delete description EnumDepend failure failureflag GetDisplayName GetKeyName Lock qc qdescription qfailure qfailureflag qprivs qsidtype query queryex privs QueryLock sdset sdshow showsid sidtype <servicename> 安装时指定的服务名称。 <option1> 可选参数。 <option2> 可选参数。
服务控制程序
.服务控制程序
服务控制程序启动并控制服务。它执行以下操作:
- 如果启动类型为SERVICE_DEMAND_START,则启动服务或驱动程序服务。
- 向正在运行的服务发送控制请求。
- 查询正在运行的服务的当前状态。
这些操作需要一个打开的服务对象句柄。为了获取句柄,服务控制程序必须:
- 使用OpenSCManager函数获取指定计算机上SCM数据库的句柄。
- 使用OpenService或CreateService函数获取服务对象的句柄。
.服务启动
为了启动一个服务或驱动程序服务,服务控制程序使用StartService函数。如果数据库被锁定,StartService函数会失败。如果发生这种情况,服务控制程序应等待几秒钟,然后再次调用StartService。它可以通过调用QueryServiceLockStatus函数来检查数据库的当前锁定状态。
如果服务控制程序正在启动一个服务,它可以使用StartService函数来指定要传递给服务的ServiceMain函数的参数数组。在创建一个新线程来执行ServiceMain函数后,StartService函数将返回。服务控制程序可以通过调用QueryServiceStatus函数来获取新启动的服务的状态,该函数将其保存在一个SERVICE_STATUS结构中。在初始化过程中,dwCurrentState成员应为SERVICE_START_PENDING。dwWaitHint成员是一个以毫秒为单位的时间间隔,表示服务控制程序应在再次调用QueryServiceStatus之前等待多长时间。当初始化完成时,服务将把dwCurrentState更改为SERVICE_RUNNING。
服务控制管理器不支持在启动时向服务传递自定义环境变量。而且服务控制管理器在服务运行时不会检测和传递环境变量的更改。与其使服务依赖于环境变量,不如使用注册表值或ServiceMain的参数。
以下是典型服务由服务控制管理器启动时发生的简化概述:
- SCM从注册表读取服务路径并准备启动服务。这包括获取服务锁。在持有服务锁的情况下尝试启动另一个服务将被阻塞,直到释放服务锁。
- SCM启动进程,并等待子进程退出(表示失败)或报告SERVICE_RUNNING状态。
- 应用程序进行非常简单的初始化,并调用StartServiceCtrlDispatcher函数。
- StartServiceCtrlDispatcher连接到服务控制管理器,并启动一个调用服务的ServiceMain函数的第二个线程。ServiceMain应尽快报告SERVICE_RUNNING。
- 当服务控制管理器被通知服务正在运行时,它会释放服务锁。
- 如果服务在80秒内没有更新其状态,再加上最后的等待提示,服务控制管理器将判断服务已停止响应。服务控制管理器会记录一个事件并停止服务。
.服务控制请求
要向正在运行的服务发送控制请求,服务控制程序使用ControlService函数。该函数指定一个控制值,该值传递给指定服务的HandlerEx函数。这个控制值可以是用户定义的代码,也可以是启用调用程序执行以下操作的标准代码之一:
停止服务(SERVICE_CONTROL_STOP)。 暂停服务(SERVICE_CONTROL_PAUSE)。 恢复执行已暂停的服务(SERVICE_CONTROL_CONTINUE)。 从服务检索更新的状态信息(SERVICE_CONTROL_INTERROGATE)。 每个服务都指定了它将接受和处理的控制值。要确定哪些标准控制值被服务接受,请使用QueryServiceStatusEx函数,或在对ControlService函数的调用中指定SERVICE_CONTROL_INTERROGATE控制值。这些函数返回的SERVICE_STATUS结构体的dwControlsAccepted成员指示服务是否可以停止、暂停或恢复。所有服务都接受SERVICE_CONTROL_INTERROGATE控制值。
QueryServiceStatusEx函数报告指定服务的最新状态,但不从服务本身获取更新的状态。在对ControlService的调用中使用SERVICE_CONTROL_INTERROGATE控制值可以确保返回的状态信息是当前的。
.使用SC控制一个服务
Windows SDK包含一个命令行实用程序Sc.exe,可用于控制服务。它的命令对应于SCM提供的功能。其语法如下所示。
语法
sc.exe [<servername>] [<command>] [<servicename>] [<option1>] [<option2>] ...
要查看可用的命令,请输入:
sc
要查看特定命令的语法,请包括一个命令。以下是一个例子:
sc start 参数 参数 描述 <servername> 指定服务所在的远程服务器的名称。名称必须使用通用命名约定(UNC)格式(例如,\myserver)。若要在本地运行SC.exe,请不要使用此参数。 <command> 以下命令之一: continue control interrogate pause start stop <servicename> 安装服务时指定的服务名称。 <option1> 可选参数。 <option2> 可选参数。
服务用户账户
.服务用户账户
每个服务在一个用户账户的安全上下文中执行。用户账户的用户名和密码在安装服务时由CreateService函数指定。可以使用ChangeServiceConfig函数来更改用户账户的用户名和密码。可以使用QueryServiceConfig函数获取与服务对象关联的用户名(但不包括密码)。服务控制管理器(SCM)会自动加载用户配置文件。
在启动服务时,SCM会使用与服务关联的账户进行登录。如果登录成功,系统将生成一个访问令牌,并将其附加到新的服务进程上。此令牌将在后续与可安全对象(具有安全描述符的对象)进行交互时用于标识服务进程。例如,如果服务尝试打开一个管道句柄,在授予访问权限之前,系统会将服务的访问令牌与管道的安全描述符进行比较。
SCM不会保存服务用户账户的密码。如果密码已过期,则登录失败,服务无法启动。为服务分配账户的系统管理员可以创建密码永不过期的账户。管理员还可以通过使用服务配置程序定期更改密码来管理密码过期的账户。
如果一个服务需要在共享信息之前识别另一个服务,第二个服务可以使用与第一个服务相同的账户,或者可以在属于第一个服务认可的别名的账户中运行。需要在网络上以分布方式运行的服务应该使用跨域账户。
您可以指定以下特殊账户之一,而不是为服务指定一个用户账户:
.本地服务账户
LocalService账户是由服务控制管理器使用的预定义本地账户。它在本地计算机上具有最低权限,并在网络上提供匿名凭据。
可以在CreateService和ChangeServiceConfig函数的调用中指定此账户。请注意,此账户没有密码,因此在此调用中提供的任何密码信息都将被忽略。尽管安全子系统本地化了此账户名,但SCM不支持本地化名称。因此,您将从LookupAccountSid函数中收到此账户的本地化名称,但是在调用CreateService或ChangeServiceConfig时,无论区域设置如何,账户的名称都必须是NT AUTHORITY\LocalService,否则可能会出现意外结果。
用户SID是从SECURITY_LOCAL_SERVICE_RID值创建的。
LocalService账户在HKEY_USERS注册表键下有自己的子键。因此,HKEY_CURRENT_USER注册表键与LocalService账户关联。
LocalService账户具有以下特权:
SE_ASSIGNPRIMARYTOKEN_NAME(已禁用) SE_AUDIT_NAME(已禁用) SE_CHANGE_NOTIFY_NAME(已启用) SE_CREATE_GLOBAL_NAME(已启用) SE_IMPERSONATE_NAME(已启用) SE_INCREASE_QUOTA_NAME(已禁用) SE_SHUTDOWN_NAME(已禁用) SE_UNDOCK_NAME(已禁用) 任何分配给用户和经过身份验证的用户的特权
.网络服务账户
NetworkService账户是由服务控制管理器使用的预定义本地账户。该账户在安全子系统中不被识别,因此无法在调用LookupAccountName函数时指定其名称。它在本地计算机上具有最低权限并充当网络上的计算机。
可以在调用CreateService和ChangeServiceConfig函数时指定该账户。请注意,该账户没有密码,因此在调用中提供的任何密码信息都会被忽略。虽然安全子系统会本地化此账户名称,但服务控制管理器不支持本地化名称。因此,您将从LookupAccountSid函数接收到本地化的账户名称,但在调用CreateService或ChangeServiceConfig时,账户的名称必须为NT AUTHORITY\NetworkService,而与区域设置无关,否则可能会导致意外结果。
以NetworkService账户的上下文运行的服务向远程服务器呈现计算机的凭据。默认情况下,远程令牌包含Everyone和Authenticated Users组的SID。用户SID是根据SECURITY_NETWORK_SERVICE_RID值创建的。
NetworkService账户在HKEY_USERS注册表键下有自己的子键。因此,HKEY_CURRENT_USER注册表键与NetworkService账户相关联。
NetworkService账户具有以下特权:
- SE_ASSIGNPRIMARYTOKEN_NAME(已禁用)
- SE_AUDIT_NAME(已禁用)
- SE_CHANGE_NOTIFY_NAME(已启用)
- SE_CREATE_GLOBAL_NAME(已启用)
- SE_IMPERSONATE_NAME(已启用)
- SE_INCREASE_QUOTA_NAME(已禁用)
- SE_SHUTDOWN_NAME(已禁用)
- SE_UNDOCK_NAME(已禁用)
任何分配给用户和经过身份验证的用户的特权
.本地系统账户
LocalSystem账户是由服务控制管理器使用的预定义本地账户。该账户在安全子系统中不被识别,因此无法在调用LookupAccountName函数时指定其名称。它在本地计算机上具有广泛的权限,并且在网络上充当计算机。它的令牌包括NT AUTHORITY\SYSTEM和BUILTIN\Administrators的SID;这些账户可以访问大多数系统对象。账户在所有区域设置中的名称都为.\LocalSystem。也可以使用名称LocalSystem或ComputerName\LocalSystem。该账户没有密码。如果您在调用CreateService或ChangeServiceConfig函数时指定LocalSystem账户,那么提供的任何密码信息都会被忽略。
在LocalSystem账户的上下文中运行的服务继承了服务控制管理器的安全上下文。用户SID是根据SECURITY_LOCAL_SYSTEM_RID值创建的。该账户与任何已登录用户账户都没有关联。这有一些影响:
- 注册表键HKEY_CURRENT_USER与默认用户相关联,而不是当前用户。要访问其他用户的配置文件,请模拟该用户,然后访问HKEY_CURRENT_USER。
- 服务可以打开注册表键HKEY_LOCAL_MACHINE\SECURITY。
- 服务向远程服务器呈现计算机的凭据。
- 如果服务打开一个命令窗口并运行批处理文件,用户可以按下CTRL+C来终止批处理文件,并获得具有LocalSystem权限的命令窗口。
LocalSystem账户具有以下特权:
- SE_ASSIGNPRIMARYTOKEN_NAME(已禁用)
- SE_AUDIT_NAME(已启用)
- SE_BACKUP_NAME(已禁用)
- SE_CHANGE_NOTIFY_NAME(已启用)
- SE_CREATE_GLOBAL_NAME(已启用)
- SE_CREATE_PAGEFILE_NAME(已启用)
- SE_CREATE_PERMANENT_NAME(已启用)
- SE_CREATE_TOKEN_NAME(已禁用)
- SE_DEBUG_NAME(已启用)
- SE_IMPERSONATE_NAME(已启用)
- SE_INC_BASE_PRIORITY_NAME(已启用)
- SE_INCREASE_QUOTA_NAME(已禁用)
- SE_LOAD_DRIVER_NAME(已禁用)
- SE_LOCK_MEMORY_NAME(已启用)
- SE_MANAGE_VOLUME_NAME(已禁用)
- SE_PROF_SINGLE_PROCESS_NAME(已启用)
- SE_RESTORE_NAME(已禁用)
- SE_SECURITY_NAME(已禁用)
- SE_SHUTDOWN_NAME(已禁用)
- SE_SYSTEM_ENVIRONMENT_NAME(已禁用)
- SE_SYSTEMTIME_NAME(已禁用)
- SE_TAKE_OWNERSHIP_NAME(已禁用)
- SE_TCB_NAME(已启用)
- SE_UNDOCK_NAME(已禁用)
大多数服务不需要如此高的特权级别。如果您的服务不需要这些特权,并且不是交互式服务,请考虑使用LocalService账户或NetworkService账户。有关更多信息,请参阅服务安全性和访问权限。
交互式服务
在这篇文章中,介绍了如何从一个服务间接地与用户进行交互的技术。通常,服务是没有图形用户界面(GUI)的控制台应用程序,旨在无人值守地运行。然而,有些服务可能需要偶尔与用户进行交互。本文讨论了从服务与用户进行交互的最佳方法。
重要提示:从Windows Vista开始,服务不能直接与用户进行交互。因此,不应在新代码中使用下文中提到的使用交互式服务的技术。
以下是可以在所有支持的Windows版本上使用的与用户间接交互的技术:
使用WTSSendMessage函数在用户会话中显示对话框。
创建一个独立的隐藏GUI应用程序,并使用CreateProcessAsUser函数以交互用户的上下文运行该应用程序。设计GUI应用程序通过某种进程间通信(IPC)方法(例如命名管道)与服务进行通信。服务与GUI应用程序进行通信,告诉它何时显示GUI。应用程序将用户交互的结果传回给服务,以便服务可以采取适当的操作。请注意,除非使用适当的访问控制列表(ACL),否则IPC可能会将您的服务接口暴露在网络上。
如果该服务运行在多用户系统上,请将应用程序添加到以下注册表项,以使其在每个会话中运行:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run。如果应用程序使用命名管道进行IPC,服务器可以通过根据会话ID为每个管道分配一个唯一名称来区分多个用户进程。
以下技术也适用于Windows Server 2003和Windows XP:
通过调用具有MB_SERVICE_NOTIFICATION参数的MessageBox函数显示消息框。这适用于显示简单的状态消息。除非从单独的线程调用它以便及时返回到SCM(服务控制管理器),否则不要在服务初始化或HandlerEx例程中调用MessageBox。
使用交互式服务 默认情况下,服务使用非交互窗口站,并且不能与用户进行交互。但是,交互式服务可以显示用户界面并接收用户输入。
注意:以LocalSystem帐户等提升的安全上下文运行的服务不应在交互式桌面上创建窗口,因为在交互式桌面上运行的任何其他应用程序都可以与该窗口进行交互。这会使服务暴露给任何被登录用户执行的应用程序。此外,作为LocalSystem运行的服务不应通过调用OpenWindowStation或GetThreadDesktop函数访问交互式桌面。
要创建交互式服务,请在调用CreateService函数时执行以下操作:
将lpServiceStartName参数指定为NULL,以在LocalSystem帐户的上下文中运行服务。 指定SERVICE_INTERACTIVE_PROCESS标志。 要确定服务是否作为交互式服务运行,请调用GetProcessWindowStation函数检索窗口站的句柄,并调用GetUserObjectInformation函数测试窗口站是否具有WSF_VISIBLE属性。
但是,请注意以下注册表键包含一个名为NoInteractiveServices的值,该值控制SERVICE_INTERACTIVE_PROCESS的效果:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Windows
NoInteractiveServices值默认为1,这意味着不允许任何服务交互式运行,无论是否具有SERVICE_INTERACTIVE_PROCESS。当NoInteractiveServices设置为0时,允许具有SERVICE_INTERACTIVE_PROCESS的服务交互式运行。
Windows 7、Windows Server 2008 R2、Windows XP和Windows Server 2003:NoInteractiveServices值默认为0,这意味着允许具有SERVICE_INTERACTIVE_PROCESS的服务交互式运行。当NoInteractiveServices设置为非零值时,以后启动的所有服务都不允许交互式运行,无论是否具有SERVICE_INTERACTIVE_PROCESS。
重要提示:所有服务在终端服务会话0中运行。因此,如果交互式服务显示用户界面,则仅对连接到会话0的用户可见。由于无法保证交互用户连接到会话0,因此不要配置服务以在终端服务下或支持快速用户切换的系统上作为交互式服务运行。
服务安全和访问权限
本文介绍了Windows操作系统中的服务控制管理器(SCM)和服务对象的访问权限。以下是文章中提到的具体内容:
服务控制管理器的访问权限:
- SC_MANAGER_ALL_ACCESS(0xF003F):包括STANDARD_RIGHTS_REQUIRED,以及表中列出的所有访问权限。
- SC_MANAGER_CREATE_SERVICE(0x0002):允许调用CreateService函数创建服务对象并将其添加到数据库中。
- SC_MANAGER_CONNECT(0x0001):允许连接到服务控制管理器。
- SC_MANAGER_ENUMERATE_SERVICE(0x0004):允许调用EnumServicesStatus或EnumServicesStatusEx函数列出数据库中的服务。还允许调用NotifyServiceStatusChange函数以接收创建或删除服务时的通知。
- SC_MANAGER_LOCK(0x0008):允许调用LockServiceDatabase函数对数据库进行加锁。
- SC_MANAGER_MODIFY_BOOT_CONFIG(0x0020):允许调用NotifyBootConfigStatus函数。
- SC_MANAGER_QUERY_LOCK_STATUS(0x0010):允许调用QueryServiceLockStatus函数检索数据库的锁定状态信息。
服务控制管理器的通用访问权限:
- GENERIC_READ:包括STANDARD_RIGHTS_READ,以及SC_MANAGER_ENUMERATE_SERVICE和SC_MANAGER_QUERY_LOCK_STATUS。
- GENERIC_WRITE:包括STANDARD_RIGHTS_WRITE,以及SC_MANAGER_CREATE_SERVICE和SC_MANAGER_MODIFY_BOOT_CONFIG。
- GENERIC_EXECUTE:包括STANDARD_RIGHTS_EXECUTE,以及SC_MANAGER_CONNECT和SC_MANAGER_LOCK。
- GENERIC_ALL:包括SC_MANAGER_ALL_ACCESS。
具有正确访问权限的进程可以打开一个指向SCM的句柄,该句柄可用于OpenService、EnumServicesStatusEx和QueryServiceLockStatus函数。只有具有管理员特权的进程才能打开可供CreateService和LockServiceDatabase函数使用的SCM句柄。
系统会为SCM创建安全描述符。要获取或设置SCM的安全描述符,可以使用QueryServiceObjectSecurity和SetServiceObjectSecurity函数,并提供SCManager对象的句柄。
Windows Server 2003和Windows XP:与大多数其他可安全对象不同,无法修改SCM的安全描述符。从Windows Server 2003 SP1开始,此行为发生了变化。
以下是授予的访问权限:
账户 访问权限
远程认证用户 SC_MANAGER_CONNECT
本地认证用户(包括LocalService和NetworkService) SC_MANAGER_CONNECT、SC_MANAGER_ENUMERATE_SERVICE、SC_MANAGER_QUERY_LOCK_STATUS、STANDARD_RIGHTS_READ
LocalSystem SC_MANAGER_CONNECT、SC_MANAGER_ENUMERATE_SERVICE、SC_MANAGER_MODIFY_BOOT_CONFIG、SC_MANAGER_QUERY_LOCK_STATUS、STANDARD_RIGHTS_READ 管理员 SC_MANAGER_ALL_ACCESS
请注意,通过网络进行远程身份验证但未进行交互登录的用户可以连接到SCM,但不能执行需要其他访问权限的操作。要执行这些操作,用户必须进行交互式登录或服务必须使用其中一个服务帐户。
Windows Server 2003和Windows XP:远程经过身份验证的用户被授予SC_MANAGER_CONNECT、SC_MANAGER_ENUMERATE_SERVICE、SC_MANAGER_QUERY_LOCK_STATUS和STANDARD_RIGHTS_READ访问权限。从Windows Server 2003 SP1开始,这些访问权限受到限制。
当进程使用OpenSCManager函数打开一个句柄用于已安装服务的数据库时,可以请求访问权限。系统会在授予请求的访问权限之前对SCM的安全描述符进行安全检查。
服务的访问权限:
- SERVICE_ALL_ACCESS(0xF01FF):包括STANDARD_RIGHTS_REQUIRED,以及表中列出的所有访问权限。
- SERVICE_CHANGE_CONFIG(0x0002):允许调用ChangeServiceConfig或ChangeServiceConfig2函数更改服务的配置。由于这授予了调用者更改系统运行的可执行文件的权利,因此应仅授予管理员。
- SERVICE_ENUMERATE_DEPENDENTS(0x0008):允许调用EnumDependentServices函数枚举依赖于该服务的所有服务。
- SERVICE_INTERROGATE(0x0080):允许调用ControlService函数立即向服务报告其状态。
- SERVICE_PAUSE_CONTINUE(0x0040):允许调用ControlService函数暂停或继续服务。
- SERVICE_QUERY_CONFIG(0x0001):允许调用QueryServiceConfig和QueryServiceConfig2函数查询服务的配置。
- SERVICE_QUERY_STATUS(0x0004):允许调用QueryServiceStatus或QueryServiceStatusEx函数查询服务控制管理器有关服务状态的信息。还允许调用NotifyServiceStatusChange函数以接收服务更改状态时的通知。
- SERVICE_START(0x0010):允许调用StartService函数启动服务。
- SERVICE_STOP(0x0020):允许调用ControlService函数停止服务。
- SERVICE_USER_DEFINED_CONTROL(0x0100):允许调用ControlService函数指定用户定义的控制代码。
服务的标准访问权限:
- ACCESS_SYSTEM_SECURITY:允许调用QueryServiceObjectSecurity或SetServiceObjectSecurity函数访问SACL。正确获取此访问权限的方式是在调用者的当前访问令牌中启用SE_SECURITY_NAME特权,以便以ACCESS_SYSTEM_SECURITY访问权限打开句柄,然后禁用特权。
- DELETE(0x10000):允许调用DeleteService函数删除服务。
- READ_CONTROL(0x20000):允许调用QueryServiceObjectSecurity函数查询服务对象的安全描述符。
- WRITE_DAC(0x40000):允许调用SetServiceObjectSecurity函数修改服务对象安全描述符的Dacl成员。
- WRITE_OWNER(0x80000):允许调用SetServiceObjectSecurity函数修改服务对象安全描述符的Owner和Group成员。
服务的通用访问权限:
- GENERIC_READ:包括STANDARD_RIGHTS_READ、SERVICE_QUERY_CONFIG、SERVICE_QUERY_STATUS、SERVICE_INTERROGATE和SERVICE_ENUMERATE_DEPENDENTS。
- GENERIC_WRITE:包括STANDARD_RIGHTS_WRITE和SERVICE_CHANGE_CONFIG。
- GENERIC_EXECUTE:包括STANDARD_RIGHTS_EXECUTE、SERVICE_START、SERVICE_STOP、SERVICE_PAUSE_CONTINUE和SERVICE_USER_DEFINED_CONTROL。
SCM在使用CreateService函数安装服务时创建服务对象的安全描述符。服务对象的默认安全描述符授予以下访问权限:
账户 访问权限 远程认证用户(默认未授予)
Windows Server 2003 SP1:SERVICE_USER_DEFINED_CONTROL
Windows Server 2003和Windows XP:远程经过身份验证的用户的访问权限与本地经过身份验证的用户相同。
本地认证用户(包括LocalService和NetworkService) READ_CONTROL、SERVICE_ENUMERATE_DEPENDENTS、SERVICE_INTERROGATE、SERVICE_QUERY_CONFIG、SERVICE_QUERY_STATUS、SERVICE_USER_DEFINED_CONTROL
LocalSystem READ_CONTROL、SERVICE_ENUMERATE_DEPENDENTS、SERVICE_INTERROGATE、SERVICE_PAUSE_CONTINUE、SERVICE_QUERY_CONFIG、SERVICE_QUERY_STATUS、SERVICE_START、SERVICE_STOP、SERVICE_USER_DEFINED_CONTROL
管理员 DELETE、READ_CONTROL、SERVICE_ALL_ACCESS、WRITE_DAC、WRITE_OWNER
要执行任何操作,用户必须进行交互式登录或服务必须使用其中一个服务帐户。
要获取或设置服务对象的安全描述符,请使用QueryServiceObjectSecurity和SetServiceObjectSecurity函数。有关更多信息,请参阅修改服务的DACL。
当进程使用OpenService函数时,系统会将请求的访问权限与服务对象的安全描述符进行比较。
授予某些访问权限给不受信任的用户(如SERVICE_CHANGE_CONFIG或SERVICE_STOP)可能会允许他们干扰您的服务的执行,并可能允许他们在LocalSystem帐户下运行应用程序。
当调用EnumServicesStatusEx函数时,如果调用者没有对服务具有SERVICE_QUERY_STATUS访问权限,则该服务将在返回给客户端的服务列表中被静默省略。
调试一个服务
您可以使用以下任何一种方法来调试您的服务。
-
在服务运行时使用调试器调试。首先,获取服务进程的进程标识符(PID)。在获取了PID之后,附加到正在运行的进程。有关语法信息,请参阅调试器附带的文档。
-
调用DebugBreak函数以调用调试器进行即时调试。
-
指定启动程序时要使用的调试器。为此,在以下注册表位置创建一个名为Image File Execution Options的键:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion
创建与您的服务相同名称的子键(例如,MYSERV.EXE)。为该子键添加一个REG_SZ类型的值,命名为Debugger。使用调试器的完整路径作为字符串值。在服务控制面板应用程序中,选择您的服务,单击启动,并选中允许服务与桌面交互。服务必须是交互式服务,否则调试器无法在默认桌面上运行。请注意,自Windows Vista起,不再支持此技术,因为所有服务都在专门为服务保留且不支持显示用户界面的会话中运行。
-
使用事件跟踪记录信息。
-
要调试自动启动服务的初始化代码,则必须临时将服务安装并运行为需求启动服务。
-
有时,出于调试目的,需要将服务作为控制台应用程序运行。在这种情况下,StartServiceCtrlDispatcher函数将返回ERROR_FAILED_SERVICE_CONTROLLER_CONNECT。因此,请确保构造您的代码,使服务特定的代码在返回此错误时不被调用。
服务触发事件
服务可以在触发事件发生时注册并启动或停止。这消除了服务需要在系统启动时启动,或者服务需要轮询或主动等待事件的需求;服务可以根据需要启动,而不是无论是否有工作要做都自动启动。预定义的触发事件示例包括指定设备接口类的设备到达或特定防火墙端口的可用性。服务还可以注册为由Windows事件跟踪(ETW)提供程序生成的自定义触发事件。
Windows Server 2008、Windows Vista、Windows Server 2003和Windows XP:直到Windows Server 2008 R2和Windows 7才支持服务触发事件。
触发器由触发事件类型、触发事件子类型、对触发事件的响应操作以及(对于某些触发事件类型)一个或多个特定于触发器的数据项组成。子类型和特定于触发器的数据项一起指定通知服务事件的条件。数据项的格式取决于触发事件类型;数据项可以是二进制数据、字符串或多字符串。字符串必须是Unicode;不支持ANSI字符串。
要注册触发事件,服务调用ChangeServiceConfig2,并提供一个SERVICE_TRIGGER_INFO结构,其中包含SERVICE_CONFIG_TRIGGER_INFO。SERVICE_TRIGGER_INFO结构指向SERVICE_TRIGGER结构的数组,每个结构指定一个触发器。
如果在系统启动时触发条件为真,或者在系统运行时触发条件变为真,则执行指定的触发器操作。例如,如果一个服务注册为在特定设备可用时启动,如果设备已经插入计算机,则服务在系统启动时启动;如果用户在系统运行时插入设备,则服务在设备到达时启动。
如果触发器具有特定于触发器的数据项,则仅当伴随触发事件的数据项与服务使用触发器指定的数据项之一匹配时,才执行触发器操作。二进制数据匹配通过按位比较进行。字符串匹配不区分大小写。如果数据项是多字符串,则所有多字符串中的字符串都必须匹配。
当服务在响应触发事件时启动时,在其ServiceMain回调函数中,服务将接收到argv[1]中的SERVICE_TRIGGER_STARTED_ARGUMENT。Argv[0]始终是服务的短名称。
以触发事件作为触发事件注册启动的服务,在没有工作要做时可能会在空闲超时后停止自身。停止自身的服务必须准备好处理在服务正在停止自身时到达的SERVICE_CONTROL_TRIGGEREVENT控制请求。每当服务处于运行状态时发生新的触发事件时,SCM会发送一个SERVICE_CONTROL_TRIGGEREVENT控制请求。为了避免丢失触发事件,服务应对到达的任何SERVICE_CONTROL_TRIGGEREVENT控制请求返回ERROR_SHUTDOWN_IN_PROGRESS。这告诉SCM将触发事件排队并等待服务进入停止状态。然后,SCM执行与排队的触发器事件关联的操作,例如启动服务。
当服务再次准备好处理触发事件时,它在调用SetServiceStatus时使用SERVICE_RUNNING设置SERVICE_ACCEPT_TRIGGEREVENT。通常情况下,这是当服务以SERVICE_RUNNING调用SetServiceStatus时完成的。然后,SCM针对每个排队的触发事件发出一个SERVICE_CONTROL_TRIGGEREVENT请求,直到队列为空为止。
一个服务拥有正在运行的依赖它的服务无法在响应触发事件时停止。
在低内存条件下,无法保证触发启动和触发停止请求。
使用QueryServiceConfig2函数检索服务的触发事件配置。
SC工具(sc.exe)可以在命令提示符下用于配置或查询服务的触发事件。使用triggerinfo选项来配置服务以响应触发事件的启动或停止。使用qtriggerinfo选项来查询服务的触发器配置。
以下示例查询配置了在计算机加入域时启动且在计算机离开域时停止的W32time服务的触发器配置。
语法
复制 C:>sc qtriggerinfo w32time [SC] QueryServiceConfig2 SUCCESS
SERVICE_NAME:w32time
启动服务
域加入状态:1ce20aba-9851-4421-9430-1ddeb766e809 [域已加入]
停止服务
域加入状态:ddaf516e-58c2-4866-9574-c3b615d42ea1 [未加入域]
以下示例查询配置了在带有GUID {4d1e55b2-f16f-11cf-88cb-001111000030}和任何指定HID设备ID的HID设备到达时启动的平板输入服务的触发器配置。
语法
复制 C:>sc qtriggerinfo tabletinputservice [SC] QueryServiceConfig2 SUCCESS
SERVICE_NAME:tabletinputservice
启动服务
设备接口到达:4d1e55b2-f16f-11cf-88cb-001111000030 [接口类GUID]
数据:HID_DEVICE_UP:000D_U:0001
数据:HID_DEVICE_UP:000D_U:0002
数据:HID_DEVICE_UP:000D_U:0003
数据:HID_DEVICE_UP:000D_U:0004
保护反恶意软件服务
...略
使用服务
服务程序的任务
编写服务程序的主函数
服务程序的主函数调用StartServiceCtrlDispatcher函数来连接到服务控制管理器(SCM)并启动控制分派器线程。分派器线程循环执行,等待来自在分派表中指定的服务的控制请求。当发生错误或者进程中的所有服务都终止时,该线程将返回。当进程中的所有服务都终止时,SCM会向分派器线程发送一个控制请求,告知其退出。然后,该线程从StartServiceCtrlDispatcher调用中返回,进程可以终止。
此示例中使用了以下全局定义。
#define SVCNAME TEXT("SvcName")
SERVICE_STATUS gSvcStatus;
SERVICE_STATUS_HANDLE gSvcStatusHandle;
HANDLE ghSvcStopEvent = NULL;
以下示例可以作为支持单个服务的服务程序的入口点。如果您的服务程序支持多个服务,请将附加服务的名称添加到分派表中,以便由分派器线程进行监控。
_tmain函数是入口点。SvcReportEvent函数将信息性消息和错误写入事件日志。关于编写SvcMain函数的信息,请参阅编写ServiceMain函数。有关SvcInstall函数的详细信息,请参阅安装服务。关于编写SvcCtrlHandler函数的信息,请参阅编写控制处理程序函数。有关完整示例服务(包括SvcReportEvent函数的源代码),请参阅Svc.cpp文件。
//
// Purpose:
// Entry point for the process
//
// Parameters:
// None
//
// Return value:
// None, defaults to 0 (zero)
//
int __cdecl _tmain(int argc, TCHAR *argv[])
{
// If command-line parameter is "install", install the service.
// Otherwise, the service is probably being started by the SCM.
if( lstrcmpi( argv[1], TEXT("install")) == 0 )
{
SvcInstall();
return;
}
// TO_DO: Add any additional services for the process to this table.
SERVICE_TABLE_ENTRY DispatchTable[] =
{
{ SVCNAME, (LPSERVICE_MAIN_FUNCTION) SvcMain },
{ NULL, NULL }
};
// This call returns when the service has stopped.
// The process should simply terminate when the call returns.
if (!StartServiceCtrlDispatcher( DispatchTable ))
{
SvcReportEvent(TEXT("StartServiceCtrlDispatcher"));
}
}
以下是由消息编译器生成的示例Sample.h文件。要了解更多信息,请参阅Sample.mc文件。
// The following are message definitions.
//
// Values are 32 bit values layed out as follows:
//
// 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1
// 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
// +---+-+-+-----------------------+-------------------------------+
// |Sev|C|R| Facility | Code |
// +---+-+-+-----------------------+-------------------------------+
//
// where
//
// Sev - is the severity code
//
// 00 - Success
// 01 - Informational
// 10 - Warning
// 11 - Error
//
// C - is the Customer code flag
//
// R - is a reserved bit
//
// Facility - is the facility code
//
// Code - is the facility's status code
//
//
// Define the facility codes
//
#define FACILITY_SYSTEM 0x0
#define FACILITY_STUBS 0x3
#define FACILITY_RUNTIME 0x2
#define FACILITY_IO_ERROR_CODE 0x4
//
// Define the severity codes
//
#define STATUS_SEVERITY_WARNING 0x2
#define STATUS_SEVERITY_SUCCESS 0x0
#define STATUS_SEVERITY_INFORMATIONAL 0x1
#define STATUS_SEVERITY_ERROR 0x3
//
// MessageId: SVC_ERROR
//
// MessageText:
//
// An error has occurred (%2).
//
//
#define SVC_ERROR ((DWORD)0xC0020001L)
写一个ServiceMain函数
以下示例中的SvcMain函数是示例服务的ServiceMain函数。SvcMain可以像控制台应用程序的main函数一样访问服务的命令行参数。第一个参数包含第二个参数中传递给服务的参数数量。至少会有一个参数。第二个参数是指向字符串指针数组的指针。数组中的第一项始终是服务名称。
SvcMain函数首先调用RegisterServiceCtrlHandler函数,将SvcCtrlHandler函数注册为服务的处理函数并开始初始化。RegisterServiceCtrlHandler应该是ServiceMain中的第一个非失败函数,这样服务可以使用此函数返回的状态句柄,在发生错误时调用SetServiceStatus并设置为SERVICE_STOPPED状态。
接下来,SvcMain函数调用ReportSvcStatus函数,指示其初始状态为SERVICE_START_PENDING。在服务处于此状态时,不接受任何控制。为了简化服务的逻辑,建议在执行初始化时,服务不接受任何控制。
最后,SvcMain函数调用SvcInit函数执行特定于服务的初始化,并开始服务要执行的工作。
示例初始化函数SvcInit是一个非常简单的示例;它不执行更复杂的初始化任务,比如创建额外的线程。它创建了一个事件,该事件可以由服务控制处理程序发出信号,表示服务应停止,然后调用ReportSvcStatus指示服务已进入SERVICE_RUNNING状态。此时,服务完成了初始化,并准备好接受控制。为了最佳系统性能,应用程序应在25-100毫秒内进入运行状态。
因为这个示例服务没有完成任何实际任务,所以SvcInit只是通过调用WaitForSingleObject函数等待服务停止事件发出信号,然后调用ReportSvcStatus指示服务已进入SERVICE_STOPPED状态,并返回。 (请注意,函数返回而不是调用ExitThread函数非常重要,因为返回允许清理为参数分配的内存。)您可以使用RegisterWaitForSingleObject函数而不是WaitForSingleObject来执行其他清理任务。运行ServiceMain函数的线程终止,但服务本身继续运行。当服务控制处理程序发出信号时,来自线程池的线程将执行您的回调函数,执行其他清理操作,包括将状态设置为SERVICE_STOPPED。
请注意,此示例使用SvcReportEvent将错误事件写入事件日志。有关SvcReportEvent的源代码,请参阅Svc.cpp。有关控制处理程序函数的示例,请参阅编写控制处理程序函数。
此示例中使用的以下全局定义:
#define SVCNAME TEXT("SvcName")
SERVICE_STATUS gSvcStatus;
SERVICE_STATUS_HANDLE gSvcStatusHandle;
HANDLE ghSvcStopEvent = NULL;
以下示例片段取自完整的服务示例。
//
// Purpose:
// Entry point for the service
//
// Parameters:
// dwArgc - Number of arguments in the lpszArgv array
// lpszArgv - Array of strings. The first string is the name of
// the service and subsequent strings are passed by the process
// that called the StartService function to start the service.
//
// Return value:
// None.
//
VOID WINAPI SvcMain( DWORD dwArgc, LPTSTR *lpszArgv )
{
// Register the handler function for the service
gSvcStatusHandle = RegisterServiceCtrlHandler(
SVCNAME,
SvcCtrlHandler);
if( !gSvcStatusHandle )
{
SvcReportEvent(TEXT("RegisterServiceCtrlHandler"));
return;
}
// These SERVICE_STATUS members remain as set here
gSvcStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
gSvcStatus.dwServiceSpecificExitCode = 0;
// Report initial status to the SCM
ReportSvcStatus( SERVICE_START_PENDING, NO_ERROR, 3000 );
// Perform service-specific initialization and work.
SvcInit( dwArgc, lpszArgv );
}
//
// Purpose:
// The service code
//
// Parameters:
// dwArgc - Number of arguments in the lpszArgv array
// lpszArgv - Array of strings. The first string is the name of
// the service and subsequent strings are passed by the process
// that called the StartService function to start the service.
//
// Return value:
// None
//
VOID SvcInit( DWORD dwArgc, LPTSTR *lpszArgv)
{
// TO_DO: Declare and set any required variables.
// Be sure to periodically call ReportSvcStatus() with
// SERVICE_START_PENDING. If initialization fails, call
// ReportSvcStatus with SERVICE_STOPPED.
// Create an event. The control handler function, SvcCtrlHandler,
// signals this event when it receives the stop control code.
ghSvcStopEvent = CreateEvent(
NULL, // default security attributes
TRUE, // manual reset event
FALSE, // not signaled
NULL); // no name
if ( ghSvcStopEvent == NULL)
{
ReportSvcStatus( SERVICE_STOPPED, NO_ERROR, 0 );
return;
}
// Report running status when initialization is complete.
ReportSvcStatus( SERVICE_RUNNING, NO_ERROR, 0 );
// TO_DO: Perform work until service stops.
while(1)
{
// Check whether to stop the service.
WaitForSingleObject(ghSvcStopEvent, INFINITE);
ReportSvcStatus( SERVICE_STOPPED, NO_ERROR, 0 );
return;
}
}
//
// Purpose:
// Sets the current service status and reports it to the SCM.
//
// Parameters:
// dwCurrentState - The current state (see SERVICE_STATUS)
// dwWin32ExitCode - The system error code
// dwWaitHint - Estimated time for pending operation,
// in milliseconds
//
// Return value:
// None
//
VOID ReportSvcStatus( DWORD dwCurrentState,
DWORD dwWin32ExitCode,
DWORD dwWaitHint)
{
static DWORD dwCheckPoint = 1;
// Fill in the SERVICE_STATUS structure.
gSvcStatus.dwCurrentState = dwCurrentState;
gSvcStatus.dwWin32ExitCode = dwWin32ExitCode;
gSvcStatus.dwWaitHint = dwWaitHint;
if (dwCurrentState == SERVICE_START_PENDING)
gSvcStatus.dwControlsAccepted = 0;
else gSvcStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP;
if ( (dwCurrentState == SERVICE_RUNNING) ||
(dwCurrentState == SERVICE_STOPPED) )
gSvcStatus.dwCheckPoint = 0;
else gSvcStatus.dwCheckPoint = dwCheckPoint++;
// Report the status of the service to the SCM.
SetServiceStatus( gSvcStatusHandle, &gSvcStatus );
}
写一个控制台操作函数
当调度线程调用处理程序函数时,它处理传递给Opcode参数的控制代码,然后调用ReportSvcStatus函数来更新服务状态。当处理程序函数接收到控制代码时,只有在处理控制代码导致服务状态发生变化时,才应报告服务状态给服务控制管理器。如果服务不对控制代码采取任何操作,就不应向服务控制管理器报告状态。有关ReportSvcStatus的源代码,请参阅编写ServiceMain函数。
在下面的示例中,SvcCtrlHandler函数是一个处理程序函数的示例。请注意,ghSvcStopEvent变量是一个全局变量,应该像在编写ServiceMain函数中演示的那样进行初始化和使用。
//
// Purpose:
// Called by SCM whenever a control code is sent to the service
// using the ControlService function.
//
// Parameters:
// dwCtrl - control code
//
// Return value:
// None
//
VOID WINAPI SvcCtrlHandler( DWORD dwCtrl )
{
// Handle the requested control code.
switch(dwCtrl)
{
case SERVICE_CONTROL_STOP:
ReportSvcStatus(SERVICE_STOP_PENDING, NO_ERROR, 0);
// Signal the service to stop.
SetEvent(ghSvcStopEvent);
ReportSvcStatus(gSvcStatus.dwCurrentState, NO_ERROR, 0);
return;
case SERVICE_CONTROL_INTERROGATE:
break;
default:
break;
}
}
服务配置程序任务
安装一个服务
一个服务配置程序使用CreateService函数将服务安装到SCM数据库中。
下面的示例中的SvcInstall函数展示了如何从服务程序本身安装服务。完整的示例请参见Svc.cpp。
//
// Purpose:
// Installs a service in the SCM database
//
// Parameters:
// None
//
// Return value:
// None
//
VOID SvcInstall()
{
SC_HANDLE schSCManager;
SC_HANDLE schService;
TCHAR szPath[MAX_PATH];
if( !GetModuleFileName( NULL, szPath, MAX_PATH ) )
{
printf("Cannot install service (%d)\n", GetLastError());
return;
}
// Get a handle to the SCM database.
schSCManager = OpenSCManager(
NULL, // local computer
NULL, // ServicesActive database
SC_MANAGER_ALL_ACCESS); // full access rights
if (NULL == schSCManager)
{
printf("OpenSCManager failed (%d)\n", GetLastError());
return;
}
// Create the service
schService = CreateService(
schSCManager, // SCM database
SVCNAME, // name of service
SVCNAME, // service name to display
SERVICE_ALL_ACCESS, // desired access
SERVICE_WIN32_OWN_PROCESS, // service type
SERVICE_DEMAND_START, // start type
SERVICE_ERROR_NORMAL, // error control type
szPath, // path to service's binary
NULL, // no load ordering group
NULL, // no tag identifier
NULL, // no dependencies
NULL, // LocalSystem account
NULL); // no password
if (schService == NULL)
{
printf("CreateService failed (%d)\n", GetLastError());
CloseServiceHandle(schSCManager);
return;
}
else printf("Service installed successfully\n");
CloseServiceHandle(schService);
CloseServiceHandle(schSCManager);
}
删除一个服务
一个服务配置程序使用OpenService函数获取一个已安装服务对象的句柄。程序可以使用该服务对象句柄在DeleteService函数中删除SCM数据库中的服务。
下面的示例中的DoDeleteSvc函数展示了如何从SCM数据库中删除服务。szSvcName变量是一个包含要删除的服务名称的全局变量。完整的示例在SvcConfig.cpp中实现。
//
// Purpose:
// Deletes a service from the SCM database
//
// Parameters:
// None
//
// Return value:
// None
//
VOID __stdcall DoDeleteSvc()
{
SC_HANDLE schSCManager;
SC_HANDLE schService;
SERVICE_STATUS ssStatus;
// Get a handle to the SCM database.
schSCManager = OpenSCManager(
NULL, // local computer
NULL, // ServicesActive database
SC_MANAGER_ALL_ACCESS); // full access rights
if (NULL == schSCManager)
{
printf("OpenSCManager failed (%d)\n", GetLastError());
return;
}
// Get a handle to the service.
schService = OpenService(
schSCManager, // SCM database
szSvcName, // name of service
DELETE); // need delete access
if (schService == NULL)
{
printf("OpenService failed (%d)\n", GetLastError());
CloseServiceHandle(schSCManager);
return;
}
// Delete the service.
if (! DeleteService(schService) )
{
printf("DeleteService failed (%d)\n", GetLastError());
}
else printf("Service deleted successfully\n");
CloseServiceHandle(schService);
CloseServiceHandle(schSCManager);
}
改变一个服务的配置
一个服务配置程序使用ChangeServiceConfig和ChangeServiceConfig2函数来更改已安装服务的配置参数。程序打开一个服务对象句柄,修改其配置,然后关闭服务对象句柄。
在下面的示例中,DoDisableSvc函数使用ChangeServiceConfig将服务的启动类型更改为“禁用”,DoEnableSvc函数使用ChangeServiceConfig将服务的启动类型更改为“启用”,DoUpdateSvcDesc函数使用ChangeServiceConfig2将服务的描述设置为“This is a test description”。szSvcName变量是一个包含服务名称的全局变量。完整的示例在SvcConfig.cpp中设置了这个变量。
//
// Purpose:
// Disables the service.
//
// Parameters:
// None
//
// Return value:
// None
//
VOID __stdcall DoDisableSvc()
{
SC_HANDLE schSCManager;
SC_HANDLE schService;
// Get a handle to the SCM database.
schSCManager = OpenSCManager(
NULL, // local computer
NULL, // ServicesActive database
SC_MANAGER_ALL_ACCESS); // full access rights
if (NULL == schSCManager)
{
printf("OpenSCManager failed (%d)\n", GetLastError());
return;
}
// Get a handle to the service.
schService = OpenService(
schSCManager, // SCM database
szSvcName, // name of service
SERVICE_CHANGE_CONFIG); // need change config access
if (schService == NULL)
{
printf("OpenService failed (%d)\n", GetLastError());
CloseServiceHandle(schSCManager);
return;
}
// Change the service start type.
if (! ChangeServiceConfig(
schService, // handle of service
SERVICE_NO_CHANGE, // service type: no change
SERVICE_DISABLED, // service start type
SERVICE_NO_CHANGE, // error control: no change
NULL, // binary path: no change
NULL, // load order group: no change
NULL, // tag ID: no change
NULL, // dependencies: no change
NULL, // account name: no change
NULL, // password: no change
NULL) ) // display name: no change
{
printf("ChangeServiceConfig failed (%d)\n", GetLastError());
}
else printf("Service disabled successfully.\n");
CloseServiceHandle(schService);
CloseServiceHandle(schSCManager);
}
//
// Purpose:
// Enables the service.
//
// Parameters:
// None
//
// Return value:
// None
//
VOID __stdcall DoEnableSvc()
{
SC_HANDLE schSCManager;
SC_HANDLE schService;
// Get a handle to the SCM database.
schSCManager = OpenSCManager(
NULL, // local computer
NULL, // ServicesActive database
SC_MANAGER_ALL_ACCESS); // full access rights
if (NULL == schSCManager)
{
printf("OpenSCManager failed (%d)\n", GetLastError());
return;
}
// Get a handle to the service.
schService = OpenService(
schSCManager, // SCM database
szSvcName, // name of service
SERVICE_CHANGE_CONFIG); // need change config access
if (schService == NULL)
{
printf("OpenService failed (%d)\n", GetLastError());
CloseServiceHandle(schSCManager);
return;
}
// Change the service start type.
if (! ChangeServiceConfig(
schService, // handle of service
SERVICE_NO_CHANGE, // service type: no change
SERVICE_DEMAND_START, // service start type
SERVICE_NO_CHANGE, // error control: no change
NULL, // binary path: no change
NULL, // load order group: no change
NULL, // tag ID: no change
NULL, // dependencies: no change
NULL, // account name: no change
NULL, // password: no change
NULL) ) // display name: no change
{
printf("ChangeServiceConfig failed (%d)\n", GetLastError());
}
else printf("Service enabled successfully.\n");
CloseServiceHandle(schService);
CloseServiceHandle(schSCManager);
}
//
// Purpose:
// Updates the service description to "This is a test description".
//
// Parameters:
// None
//
// Return value:
// None
//
VOID __stdcall DoUpdateSvcDesc()
{
SC_HANDLE schSCManager;
SC_HANDLE schService;
SERVICE_DESCRIPTION sd;
LPTSTR szDesc = TEXT("This is a test description");
// Get a handle to the SCM database.
schSCManager = OpenSCManager(
NULL, // local computer
NULL, // ServicesActive database
SC_MANAGER_ALL_ACCESS); // full access rights
if (NULL == schSCManager)
{
printf("OpenSCManager failed (%d)\n", GetLastError());
return;
}
// Get a handle to the service.
schService = OpenService(
schSCManager, // SCM database
szSvcName, // name of service
SERVICE_CHANGE_CONFIG); // need change config access
if (schService == NULL)
{
printf("OpenService failed (%d)\n", GetLastError());
CloseServiceHandle(schSCManager);
return;
}
// Change the service description.
sd.lpDescription = szDesc;
if( !ChangeServiceConfig2(
schService, // handle to service
SERVICE_CONFIG_DESCRIPTION, // change: description
&sd) ) // new description
{
printf("ChangeServiceConfig2 failed\n");
}
else printf("Service description updated successfully.\n");
CloseServiceHandle(schService);
CloseServiceHandle(schSCManager);
}
查询一个服务的配置
服务配置程序使用OpenService函数获取具有SERVICE_QUERY_CONFIG访问权限的已安装服务对象句柄。程序可以使用服务对象句柄在QueryServiceConfig和QueryServiceConfig2函数中检索服务的当前配置。
在下面的示例中,DoQuerySvc函数使用QueryServiceConfig和QueryServiceConfig2检索配置信息,然后将选定的信息写入控制台。szSvcName变量是一个包含服务名称的全局变量。完整的示例在SvcConfig.cpp中设置了这个变量。
//
// Purpose:
// Retrieves and displays the current service configuration.
//
// Parameters:
// None
//
// Return value:
// None
//
VOID __stdcall DoQuerySvc()
{
SC_HANDLE schSCManager;
SC_HANDLE schService;
LPQUERY_SERVICE_CONFIG lpsc;
LPSERVICE_DESCRIPTION lpsd;
DWORD dwBytesNeeded, cbBufSize, dwError;
// Get a handle to the SCM database.
schSCManager = OpenSCManager(
NULL, // local computer
NULL, // ServicesActive database
SC_MANAGER_ALL_ACCESS); // full access rights
if (NULL == schSCManager)
{
printf("OpenSCManager failed (%d)\n", GetLastError());
return;
}
// Get a handle to the service.
schService = OpenService(
schSCManager, // SCM database
szSvcName, // name of service
SERVICE_QUERY_CONFIG); // need query config access
if (schService == NULL)
{
printf("OpenService failed (%d)\n", GetLastError());
CloseServiceHandle(schSCManager);
return;
}
// Get the configuration information.
if( !QueryServiceConfig(
schService,
NULL,
0,
&dwBytesNeeded))
{
dwError = GetLastError();
if( ERROR_INSUFFICIENT_BUFFER == dwError )
{
cbBufSize = dwBytesNeeded;
lpsc = (LPQUERY_SERVICE_CONFIG) LocalAlloc(LMEM_FIXED, cbBufSize);
}
else
{
printf("QueryServiceConfig failed (%d)", dwError);
goto cleanup;
}
}
if( !QueryServiceConfig(
schService,
lpsc,
cbBufSize,
&dwBytesNeeded) )
{
printf("QueryServiceConfig failed (%d)", GetLastError());
goto cleanup;
}
if( !QueryServiceConfig2(
schService,
SERVICE_CONFIG_DESCRIPTION,
NULL,
0,
&dwBytesNeeded))
{
dwError = GetLastError();
if( ERROR_INSUFFICIENT_BUFFER == dwError )
{
cbBufSize = dwBytesNeeded;
lpsd = (LPSERVICE_DESCRIPTION) LocalAlloc(LMEM_FIXED, cbBufSize);
}
else
{
printf("QueryServiceConfig2 failed (%d)", dwError);
goto cleanup;
}
}
if (! QueryServiceConfig2(
schService,
SERVICE_CONFIG_DESCRIPTION,
(LPBYTE) lpsd,
cbBufSize,
&dwBytesNeeded) )
{
printf("QueryServiceConfig2 failed (%d)", GetLastError());
goto cleanup;
}
// Print the configuration information.
_tprintf(TEXT("%s configuration: \n"), szSvcName);
_tprintf(TEXT(" Type: 0x%x\n"), lpsc->dwServiceType);
_tprintf(TEXT(" Start Type: 0x%x\n"), lpsc->dwStartType);
_tprintf(TEXT(" Error Control: 0x%x\n"), lpsc->dwErrorControl);
_tprintf(TEXT(" Binary path: %s\n"), lpsc->lpBinaryPathName);
_tprintf(TEXT(" Account: %s\n"), lpsc->lpServiceStartName);
if (lpsd->lpDescription != NULL && lstrcmp(lpsd->lpDescription, TEXT("")) != 0)
_tprintf(TEXT(" Description: %s\n"), lpsd->lpDescription);
if (lpsc->lpLoadOrderGroup != NULL && lstrcmp(lpsc->lpLoadOrderGroup, TEXT("")) != 0)
_tprintf(TEXT(" Load order group: %s\n"), lpsc->lpLoadOrderGroup);
if (lpsc->dwTagId != 0)
_tprintf(TEXT(" Tag ID: %d\n"), lpsc->dwTagId);
if (lpsc->lpDependencies != NULL && lstrcmp(lpsc->lpDependencies, TEXT("")) != 0)
_tprintf(TEXT(" Dependencies: %s\n"), lpsc->lpDependencies);
LocalFree(lpsc);
LocalFree(lpsd);
cleanup:
CloseServiceHandle(schService);
CloseServiceHandle(schSCManager);
}