中文:使用服务 - Win32 apps | Microsoft Learn
英文:Using Services - Win32 apps | Microsoft Learn
1. 服务主程序
一个服务程序可以包含一个或多个服务的可执行代码
- 类型为SERVICE_WIN32_OWN_PROCESS创建的服务程序仅包含一个服务的代码
- 类型SERVICE_WIN32_SHARE_PROCESS创建的服务程序可以包含多个服务的代码,它们能够共享代码。例如:Svchost.exe。
服务控制管理器 (SCM) 要求服务程序必须包括:
- 服务入口点main函数
- ServiceMain 函数
- 服务控制处理程序函数
以上这三点不适用于驱动程序服务。
服务作为后台进程运行,可能会影响系统性能、响应能力、节能和安全性。 有关服务优化指南,请参考:Introduction to Power Management - Windows drivers | Microsoft Learn
服务程序还需注意:
- 服务状态转换
- 在服务中接收控制请求
- 多线程服务
- 服务和注册表
- 服务和重定向驱动器
- 服务触发器事件
注意:如果服务程序充当 RPC 服务器,则应使用动态终结点(dynamic endpoints)和相互身份验证(mutual authentication)。
1.1 服务入口点main函数
服务通常以控制台应用程序的形式编写,控制台应用程序的入口点一般是main函数。服务的注册表项ImagePath可以指定传给服务main函数的参数。
当 SCM 启动服务程序时,SCM会等待服务程序调用 StartServiceCtrlDispatcher 函数:
- SERVICE_WIN32_OWN_PROCESS类型的服务程序应立即在main函数调用 StartServiceCtrlDispatcher 函数,然后可以在服务启动后执行初始化工作
- 如果服务类型为SERVICE_WIN32_SHARE_PROCESS,且服务程序中对所有服务有统一的初始化,则可以在调用 StartServiceCtrlDispatcher 之前在主线程中执行初始化,注意初始化不要超过30秒。否则,必须创建另一个线程来执行通用初始化,而主线程调用 StartServiceCtrlDispatcher。 在服务启动后,仍需要执行特定于某个服务的初始化。
StartServiceCtrlDispatcher 函数为进程中包含的每个服务维护一个SERVICE_TABLE_ENTRY结构,每个SERVICE_TABLE_ENTRY结构指定服务名称和服务入口点。
如果 StartServiceCtrlDispatcher 执行成功,则调用线程不会返回,直到进程中的所有正在运行的服务都进入SERVICE_STOPPED状态。
SCM 通过命名管道向此线程发送控制请求。该线程充当控制调度程序,执行以下任务:
- 创建新线程来调用服务入口点函数。
- 调用对应的服务控制处理程序函数来处理服务控制请求。
1.2 ServiceMain函数
每一个服务都有一个入口点回调函数,服务main函数中通过调用StartServiceCtrlDispatcher将服务的主线程连接到服务控制管理器SCM,从而使该线程成为服务的控制调度线程。
当服务控制管理器SCM启动服务时,服务控制调度程序创建一个新线程,用于为服务执行 ServiceMain 函数。
ServiceMain 函数应执行以下任务:
- 初始化全局变量。
- 调用 RegisterServiceCtrlHandler 函数注册服务控制处理函数来处理服务的控制请求。RegisterServiceCtrlHandler 的返回值是服务状态句柄,用于通知 SCM 服务当前的状态。
- 执行初始化。
- 如果初始化代码的执行时间预计很短(不到一秒),则可以在 ServiceMain 中直接执行初始化。
- 如果初始化时间应超过一秒,则服务应使用以下初始化方法之一:
- 调用 SetServiceStatus 函数设置服务状态为SERVICE_RUNNING,但在初始化完成之前不再接受其他控制。即:服务调用 SetServiceStatus 函数时,将 dwCurrentState 设置为 SERVICE_RUNNING,并在 SERVICE_STATUS 结构中将dwControlsAccepted 设置为 0。这可以保证 SCM 在服务准备就绪并释放 SCM 来管理其他服务之前不会向服务发送任何控制请求。建议使用此方法进行初始化来提升性能,尤其是对于自动启动的服务。
- 向SCM报告服务状态为SERVICE_START_PENDING,且不接受任何控制,并指定等待时间。如果服务初始化执行所需时间超过初始等待时间,则代码中必须定期调用 SetServiceStatus 函数(可能带有修订的等待提示) 来说明服务正在进行。 请勿在单独的线程调用 SetServiceStatus ,除非确定执行初始化的线程确实正在进行。使用此方法的服务还可以指定check-point,并在长时间的初始化过程中定期递增此值。 启动该服务的程序可以调用 QueryServiceStatus 或 QueryServiceStatusEx ,以便从 SCM 获取最新的检查点值,并使用该值向用户报告增量进度。
- 初始化完成后,调用 SetServiceStatus 将服务状态设置为SERVICE_RUNNING并指定服务接收的控制。
- 执行服务任务,或者,如果没有挂起的任务,则返回对调用方的控制。 服务状态的任何更改都要调用 SetServiceStatus 来报告新的状态信息。
- 如果服务正在初始化或运行时发生错误,则服务应调用 SetServiceStatus 来将服务状态设置为SERVICE_STOP_PENDING(如果清理时间较长)。 清理完成后,调用 SetServiceStatus 将服务状态设置为SERVICE_STOPPED。 确保设置SERVICE_STATUS结构的 dwServiceSpecificExitCode 和 dwWin32ExitCode 成员来标识错误。
1.3 服务控制处理函数
1.3.1 服务控制处理函数介绍
每个服务都有一个控制处理函数,服务在ServiceMain函数中通过调用 RegisterServiceCtrlHandler 或 RegisterServiceCtrlHandlerEx 函数来注册其服务控制处理函数。
在服务控制处理函数中,如果处理控制码导致服务状态发生变化,服务必须调用 SetServiceStatus 函数将其新状态报告给 SCM,同时可以启用或禁用接受其他控制码
服务控制处理函数必须在 30 秒内返回,否则 SCM 认为发生错误。如果服务执行控制处理函数时间较长,则应创建一个线程来执行,然后从控制处理函数返回。例如:处理服务停止请求时,可以创建一个线程来处理停止服务需要执行的内容,然后服务控制处理函数使用SERVICE_STOP_PENDING调用 SetServiceStatus并返回服务状态。
服务控制程序可以使用 ControlService 函数发送控制请求。
1.3.2 处理控制码
① SERVICE_CONTROL_INTERROGATE:通知服务将其当前状态报告给服务控制管理器SCM。
所有服务都必须接受和处理 SERVICE_CONTROL_INTERROGATE 控制码
② SERVICE_CONTROL_STOP:通知服务停止
如果服务收到 SERVICE_CONTROL_STOP 控制码,则必须在收到后停止,转到 SERVICE_STOP_PENDING 或 SERVICE_STOPPED 状态。 SCM 发送此控制码后给服务后,不会发送其他控制码。
如果服务需要更多时间来停止,服务应该发送 STOP_PENDING 状态消息以及等待时间,这样服务控制管理器SCM知道在向系统报告服务关闭已完成之前等待多长时间。 但是,为了防止服务停止关闭,服务控制管理器等待的时间有限制。 如果通过服务控制面板关闭服务,则限制为 125 秒。 如果操作系统正在重新启动,则 WaitToKillServiceTimeout 值(毫秒)指定此时间限制。注册表路径为:
KEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control
③ SERVICE_ACCEPT_PRESHUTDOWN:通知服务系统即将关闭
当用户关闭系统时,如果服务可以接受SERVICE_CONTROL_PRESHUTDOWN控制码,即:使用SERVICE_ACCEPT_PRESHUTDOWN控制码调用 SetServiceStatus 函数告诉过SCM,那么服务控制处理函数就会收到此控制码。服务控制管理器SCM会等待服务停止或ChangeServiceConfig2 函数指定的超时时间过期。注意:此控制码仅在特殊情况下使用,因为处理此通知的服务会阻止系统关闭,直到服务停止或超时时间过期。
④ SERVICE_CONTROL_SHUTDOWN控制码:通知服务系统正在关闭
完成preshutdown通知后,如果服务可以接受SERVICE_CONTROL_SHUTDOWN控制码,即:使用RVICE_ACCEPT_SHUTDOWN控制码调用 SetServiceStatus 函数告诉过SCM,那么服务控制处理函数就会收到此控制码。 默认情况下,在系统关闭之前,服务大约有 20 秒来执行清理任务。 此时间过后,无论服务关闭是否完成,系统关闭都会继续。
2. 注意事项
2.1 服务和注册表
服务不应访问 HKEY_CURRENT_USER 或 HKEY_CLASSES_ROOT。 改用 RegOpenCurrentUser 或 RegOpenUserClassesRoot 函数。
如果尝试从服务访问 HKEY_CURRENT_USER 或 HKEY_CLASSES_ROOT ,它可能会失败,或者它似乎起作用,但存在可能导致加载或卸载用户配置文件的问题的潜在泄漏。
2.2 服务状态转换
① SERVICE_STOPPED
服务的初始状态为SERVICE_STOPPED
② SERVICE_START_PENDING和SERVICE_RUNNING
当 SCM 启动服务时,将服务状态设置为SERVICE_START_PENDING并调用服务的 ServiceMain 函数。 然后,该服务使用 ServiceMain 函数中所述的方法之一完成其初始化。服务完成初始化并准备好开始接收控制请求后,服务调用 SetServiceStatus 来报告SERVICE_RUNNING并指定服务准备接受的控制请求。
从SERVICE_START_PENDING转换到SERVICE_RUNNING表示服务已成功启动。 如果服务报告SERVICE_RUNNING以外的状态,SCM 可能会将服务标记为启动失败。
③ 服务状态改变
服务状态通常因处理控制请求而更改。 控制导致服务状态更改的请求包括SERVICE_CONTROL_STOP、SERVICE_CONTROL_PAUSE、SERVICE_CONTROL_CONTINUE
下图详细地显示了服务的状态转换,包括服务控制程序启动服务,服务调用 SetServiceStatus 向 SCM 报告状态。 注意,SCM 只会发送服务指定接受的控制请求,因此服务可能不会接收图中显示的所有请求。
3. Demo程序
GitHub - duanxiaodeng/SomethingAboutWindows: windows下C++开发一些分享