SilverRoe / Windows

About windows programming
0 stars 0 forks source link

Windows service 编程 #1

Open SilverRoe opened 4 years ago

SilverRoe commented 4 years ago

MSDN上关于windows services的文档很齐全,这里翻译部分方便查阅: 服务程序 一个服务程序包含了一个或多个服务的代码。 SERVICE_WIN32_OWN_PROCESS 类型的服务只包含一个服务的代码; SERVICE_WIN32_SHARE_PROCESS 类型包含多个服务的代码;

服务程序可以在build-in(local), primary 或者 trusted domain 账户下运行。也可以被配置在指定账户下运行

一个服务程序必须包含以下部分:

服务入口点 服务一般被写为console应用。一个console应用的入口点是main函数。服务的main函数接受记录在注册表键里的ImagePath作为参数。 当SCM启动一个服务程序,它将等待服务程序调用 StartServiceCtrlDispatcher 函数;请注意以下这些点:

StartServiceCtrlDispatcher 函数为每一个进程中的服务保存一个 SERVICE_TABLE_ENTRY 结构。每个结构都指定了服务名和对应的入口点(entry point) 如果 StartServiceCtrlDispatcher 成功了,调用的线程会直到所有运行的服务都进入 SERVICE_STOPPED 状态。SCM通过一个命名管道向这个线程发送请求。这个线程作为控制分发者(control dispatcher),将执行以下任务:

服务ServiceMain函数 当一个服务控制程序请求一个新服务运行时,SCM 启动服务并且向control dispatcher 发送一个开始请求.control dispatcher 创建一个新的线程并且执行该服务的 ServiceMain函数。 ServiceMain 函数将执行以下任务:

  1. 初始化全局变量
  2. 直接调用 RegisterServiceCtrlHandler 函数 注册一个对服务请求的回调函数。RegisterServiceCtrlHandler 的返回值是一个服务状态,这个状态将被用于通知SCM服务的当前状态
  3. 执行初始化。如果初始化时间特别短(少于1s),可以直接在ServiceMain中执行;如果大于1s,服务可以使用以下任一初始化机制:
    • 调用 SetServiceStatus 报告状态为 SERVICE_RUNNING ,但是直到初始化完成才接收控制。通过调用 SetServiceStatus 将 当前状态设置为 SERVICE_RUNNING 并且 dwControlsAccepted 设置为0 实现。这保证了SCM将不再发送任何control请求直到它准备好了。这种初始化方式 被推荐给那些考虑性能的服务,特别是autostart的服务。
    • 报告 SERVICE_START_PENDING ,不接受控制,并且定义一个超时时间。如果服务的初始化时间超过了预定义的等待时间,那么必须周期性的调用SetServiceStatus API重新定义超时时间,但要确保初始化确实是有进度的,因为此时SCM认为初始化是有进度的,它会阻塞其他服务的开启。如果超时了,而等待的进度信息未改变,SCM或者服务控制程序会认为发生了错误而需要终止服务(但是如果改服务与其他服务共享进程,则不会结束该服务所在进程)。使用这种方法初始化,可以根据实际进度周期性的调用SetServiceStatus API增加“检查点”的值,服务启动者可以通过QueryServiceStatus 或者 QueryServiceStatusEx API 去获得检查点的值,也就获得了服务初始化进度。
  4. 当初始化结束后,调用 SetServiceStatus 将服务状态设置为 SERVICE_RUNNING 并且 指定这个服务可以接受的control code
  5. 执行服务的任务,如果没有任务,把control交还给调用者(指的是StartServiceCtrlDispatcher)
  6. 如果服务初始化或者运行过程中出错了,应该调用SetServiceStatus 将服务状态设置为 SERVICE_STOP_PENDDING。cleanup完成后, 在最后一个结束的线程调用 SetServiceStatus 将服务状态设置为 SERVICE_STOPPED

服务控制回调Control Handler 函数 每个服务都有一个控制回调;当服务进程接收到一个控制请求时,这个回调将被control dispatcher执行。因此,这个函数在control dispatcher的上下文环境中被执行。 服务调用RegisterServiceCtrlHandler 或者 RegisterServiceCtrlHandlerEx 函数注册服务控制回调函数。 当一个服务控制回调被执行,服务只有在控制code造成服务状态改变时才 必须调用 SetServiceStatus 函数来给SCM报告状态。如果控制码没有导致服务状态改变,那么就不需要调用SetServiceStatus。 服务控制程序可以通过 ControlService API 发送控制请求。所有服务都必须接收和处理SERVICE_CONTROL_INTERROGATE 控制码。 如果一个服务接收了SERVICE_CONTROL_STOP 控制码,它必须接收时马上停下来,变成 SERVICE_STOP_PENDDING 或 SERVICE_STOPPED 状态。当SCM发送了这个控制码,将不会再发送其他控制码。 控制回调必须在30s内返回,否则SCM将返回一个错误。如果一个服务需要在控制回调中做一个耗时动作,它应该创建一个线程,并且直接从控制回调中返回。举个例子,当处理停止请求时,如果服务需要花费较长时间,那么创建另外一个线程来停止,控制回调应该只是简单调用SetServiceStatus 返回SERVICE_STOP_PENDING 消息并且返回。 当用户关机时,所有调用SetServiceStatus并且设置SERVICE_ACCEPT_PRESHUTDOWN 控制码的回调将收到SERVICE_CONTROL_PRESHUTDOWN 控制码。这个控制码只应该在特殊场景使用,因为这个服务回调将阻塞系统关机 直到服务停止 或者超时。(这里只需要大概了解下,服务在关机前能收到通知)

其他相关的topics:

一个完整的服务例子

SilverRoe commented 4 years ago

我的总结: 服务里的线程关系: SvcMain 并不是主线程;StartServiceCtrlDispatcher会阻塞主线程直到所有运行的服务都进入 SERVICE_STOPPED 状态; SvcCtrlHandler 控制回调会在主线程里执行;

多服务共享进程实例: 多个服务共享一个进程,必须在CreateService 设置 SERVICE_WIN32_SHARE_PROCESS标志 备注:不同服务可以设置为相同的执行程序,服务启动时会启动多个进程实例;

服务提权: 从服务中启动进程可以解决UAC弹窗的问题。主要需要使用CreateProcessAsUser API

服务相关命令: sc create SvcTest binpath= e:\CodeForFun\ServiceTest.exe sc delete SvcTest sc stop SvcTest sc start SvcTest

调试信息输出: 可以用OutputDebugString 输出调试信息,并用DbgView查看