Windows 服务增强

来源:互联网 发布:unity3d开源项目2016 编辑:程序博客网 时间:2024/05/18 03:43
 
Windows 服务增强
Kenny Kerr

延迟自动启动服务
改进关机可预见性
失败操作和受控停止
减少权限
保护服务数据
用受限令牌保护其他项目
接收服务通知
后续内容

Windows®服务开发的状态自从在 Windows NT® 中出现服务以来一直没有较大的改变,但是 Windows Vista® 和 Windows Server® 2008 打破了这一僵局。这其中的许多功能主要是为了以更简捷的方式生成更安全的服务,但是在与安全性不相关的服务功能中,有些功能是为了提高 Windows 的整体响应能力和可靠性。

延迟自动启动服务
可通过 StartService 函数将服务配置为只在得到特定请求时启动。此服务称为按需启动服务。或者,也可将它们配置为在操作系统的启动进程中自动启动。这些就是所称的自动启动服务,对许多功能在所有时间都可用的服务或其他服务所必需的服务,此模式很有意义。但是,有些自动启动服务在用户登录到计算机时,并非绝对需要运行,因为可能不会立即使用它们。
Windows Vista 采取另一个步骤来减少 Windows 的启动时间,方法是:为只在 Windows 完成自身启动进程后要启动的自动启动服务提供工具。具体而言,就是在正常自动启动服务完成两分钟后,激活延迟的自动启动服务。服务控制管理器 (SCM) 还可创建优先级较低的主服务线程,从而确保任何登录的用户都不受延迟自动启动服务的显著影响。优先级在服务更新其正在运行的状态后重新设置为“正常”。如果发现延迟的自动启动服务在应用程序需要使用它的时候没有启动,可通过使用 StartService 函数强制其迅速启动。
以下函数显示如何控制此选项(它应由服务的安装程序调用):
复制代码
bool ChangeDelayedAutoStart(    SC_HANDLE service, bool delayed){    ASSERT(0 != service);    SERVICE_DELAYED_AUTO_START_INFO info = { delayed };    return 0 != ::ChangeServiceConfig2(        service,         SERVICE_CONFIG_DELAYED_AUTO_START_INFO,         &info);}
服务句柄确定要配置的服务。此句柄可通过调用 OpenService 或 CreateService 函数来获得。ChangeServiceConfig2 函数能让您更改许多更高级的服务配置选项。在本例中,我使用 SERVICE_CONFIG_DELAYED_AUTO_START_INFO 标记告知函数将SERVICE_DELAYED_AUTO_START_INFO 指针作为其第三个参数。如果失败,ChangeServiceConfig2 将返回零值,可通过调用 GetLastError 函数获取详细的错误信息。

改进关机可预见性
Windows Vista 以前的版本,通常不可能在计算机关机时始终确保服务能正常停止。系统通知 SCM 要关机时,只有大约 20 秒的时间指示所有运行中的服务停止。如果时间太长,系统最终将粗略中止 SCM 进程。由于没有正常关闭的服务在再次重新启动时通常需要处理不一致的状态,因此这会引起许多问题,包括增加启动时间。
Windows Vista 引进了服务可以请求接收的新预关机通知。如果要开发必须正常关闭但不需要很快关闭的服务,则可以向 SCM 请求预关机通知。SCM 在执行其传统服务的关机进程前,将等待(可能无限期)所有预关机服务都停止。尽管它对服务有好处,但是对于希望计算机很快关机的用户而言作用不大。因此,应限制将此功能用于非常关键的服务,最好是只用于一个服务器方案。
服务通过将 SERVICE_ACCEPT_PRESHUTDOWN 标记包含在其服务状态中,指示其希望接收预关机通知。服务的处理程序函数随后将通过 SERVICE_CONTROL_PRESHUTDOWN 控制代码得到通知。尽管 Windows 愿意无限期地等待预关机服务停止,但是服务仍必须对来自 SCM 的查询给出响应,并通过增加检查点值来更新服务的状态。如果 SCM 确定服务没有响应,则会最终放弃,并且系统将继续关机。注意,如果服务处理 SERVICE_CONTROL_PRESHUTDOWN 控制代码,则不会接收传统的 SERVICE_CONTROL_SHUTDOWN 控制代码。在大多数情况下,对所有与停止相关的控制代码,最好使用单个关机程序。
SCM 一个最显著的行为特征是一次只与一个服务进行通信。它具有特定的含意,如表露出来意思十分明显:如果两个应用程序(或线程)调用服务控制函数(如 StartService 或 ControlService),则 SCM 将对它们进行排队,并且一次只为一个请求服务。另一个存在更多问题的情况是在 Window 关机时。通过在 SCM 调用 StartServiceCtrlDispatcher 函数时,SCM 和服务之间建立的命名管道来进行通信。SCM 向服务发送控制代码时,通过命名管道与服务中的 StartServiceCtrlDispatcher 函数进行通信。它将请求转发到特殊服务(共享的进程服务)的处理程序,并使处理程序在返回请求前对其进行处理。问题是不能阻止处理函数阻塞很长时间。如果服务不能在 30 秒之内作出响应或虽然作出了响应,但同时没有其他服务可以控制,则 SCM 发送的请求将最终超时。
作为服务开发人员,需要记住这一点,并且切勿阻塞服务的处理程序函数。一个性能良好的服务应该能接受请求,设置标记或启动某些工作程序线程以处理请求并立即返回。这样,在您的服务有时间处理请求时,SCM 就能继续与其他服务进行通信。图 1 显示了如何执行停止和关闭控制代码。
复制代码
DWORD WINAPI StopThread(PVOID){  // perform any length shutdown operation   m_status.dwCurrentState = SERVICE_STOPPED;  VERIFY(::SetServiceStatus(m_handle,                            &m_status));  return 0;}...switch (control){  case SERVICE_CONTROL_STOP:  case SERVICE_CONTROL_SHUTDOWN:  case SERVICE_CONTROL_PRESHUTDOWN:  {    m_status.dwCurrentState = SERVICE_STOP_PENDING;    VERIFY(::SetServiceStatus(m_handle,                              &m_status));    CHandle thread(::CreateThread(0, // default security                                  0, // default stack size                                  StopThread,                                  this, // context                                  0, // no flags                                  0)); // ignore thread id    break;  }  ...}
处理 SERVICE_CONTROL_STOP、SERVICE_CONTROL_SHUTDOWN 和 SERVICE_CONTROL_PRESHUTDOWN 控制代码的控制处理程序将服务的状态设置为 SERVICE_STOP_PENDING,创建一个工作程序线程进行工作,然后返回,而不做进一步操作。StopThread 函数可在需要时阻塞函数,然后将服务的状态设置为 SERVICE_STOPPED,通知 SCM,它随后可让 StartServiceCtrlDispatcher 函数返回并中止进程(假定它是共享的进程服务的最后一个服务)。只需记住,如果服务处于待定状态,则需要定期用递增的检查点值更新服务的状态,以确保 SCM 不挂起。

失败操作和受控停止
失败操作长期以来一直用于帮助突然中止的服务以某种方式恢复。Windows Vista 以前的版本中,仅在没有先将服务的状态设置为 SERVICE_STOPPED 服务进程就中止的情况下,才执行失败操作。在 Windows Vista 中,服务可以请求在其状态设置为 SERVICE_STOPPED,但 SERVICE_STATUS::dwWin32ExitCode 成员提供的服务退出代码被设置为 ERROR_SUCCESS 以外的内容时,也执行失败操作。这样,您就可以在服务失败时使用失败操作,同时又能正常停止服务。
以下函数显示如何控制此选项来停止服务(它还是应由服务的启动程序调用):
复制代码
bool ChangeFailureActionsOnNonCrashFailures(  SC_HANDLE service,  bool failureActionsOnNonCrashFailures){  ASSERT(0 != service);  SERVICE_FAILURE_ACTIONS_FLAG flag = {    failureActionsOnNonCrashFailures };  return 0 != ::ChangeServiceConfig2(service,    SERVICE_CONFIG_FAILURE_ACTIONS_FLAG,    &flag);}
对向后兼容,此选项的默认值为 false,因此保留了较早版本 Windows 的行为。

减少权限
大多数管理员希望服务在内置的 Windows 服务帐户下运行。这样就不必处理服务帐户密码管理固有的麻烦问题了。当然,内置的服务帐户有很多,并且它们提供的权利和权限可能多于服务的需求。尽管 Windows Vista 不再添加服务帐户(原因就是不实用),但是引进了一些强大的新配置选项,允许使用受限令牌精确锁定服务。
第一个这样的配置选项可确切地指定服务要求的权限。SCM 将确保服务的进程令牌只包含需要的权限。图 2 显示了如何控制此选项。应由服务的启动程序对其进行调用。
复制代码
bool ChangeRequiredPrivileges(    SC_HANDLE service,    const CAtlArray<PCWSTR>& requiredPrivileges){  ASSERT(0 != service);  size_t bufferSize = 1;  for (size_t index = 0; index < requiredPrivileges.GetCount(); ++index)  {    bufferSize += wcslen(requiredPrivileges[index]) + 1;  }  CAtlArray<WCHAR> buffer;  if (!buffer.SetCount(bufferSize))  {    ::SetLastError(ERROR_OUTOFMEMORY);    return false;  }  PWSTR position = &buffer[0];  for (size_t index = 0; index < requiredPrivileges.GetCount(); ++index)  {    PCWSTR name = requiredPrivileges[index];    const size_t nameSize = wcslen(name);    wcscpy_s(position,             nameSize + 1,             name);    position += nameSize + 1;  }  SERVICE_REQUIRED_PRIVILEGES_INFO info = { &buffer[0] };  return 0 != ::ChangeServiceConfig2(    service,    SERVICE_CONFIG_REQUIRED_PRIVILEGES_INFO,    &info);}
此函数看起来有点复杂,但只是因为 ChangeServiceConfig2 函数要求权限名称列表以一系列以 null 值结尾的字符串方式提供,并且以空值结尾。但不管怎样,使用 ChangeRequiredPrivileges 函数并不费力:
复制代码
CAtlArray<PCWSTR> requiredPrivileges;requiredPrivileges.Add(SE_CHANGE_NOTIFY_NAME);requiredPrivileges.Add(SE_IMPERSONATE_NAME);if (!ChangeRequiredPrivileges(service, requiredPrivileges)){    // call GetLastError for more info}
实际指定的权限需要授予给配置使用的服务帐户,请牢记这一点。例如,如果配置服务要求 SE_TCB_NAME 权限,但是服务被配置为在没有此权限的 LocalService 帐户下运行,则 SCM 将不能启动服务,因为它假定该服务没有所需的权限就不能正常工作。
要记住的另一件事与共享的进程服务有关。因为共享进程的所有服务也共享相同的安全上下文,因此 SCM 将合并有可能共享进程的所有服务所需的权限。这可能影响决定哪些服务来共享地址空间和安全上下文。

保护服务数据
尽管减少可用权限在限制所用服务的可执行操作方面非常有用(到一定程度),但是它不会限制服务可访问的数据。就其所有管理优点而言,内置的服务帐户从来不是最理想的,因为所有使用相同服务帐户的服务都有权访问相同的安全资源。Windows Vista 以前的版本,不能说以 LocalService 运行的“服务 A”有权访问某些资源,而“服务 B”却没有。这就很难确保特定服务的数据不被其他服务攻击。
Windows Vista 通过引进服务安全标识符 (SID) 修正了此问题,在访问控件列表 (ACL) 中使用该标识符为特殊的服务帐户和服务设置权限。Windows Vista 和 Windows Server 2008 中的每个服务都具有标识它的 SID ,在访问控制编辑器和绝大部分指定安全描述符的位置中均可使用该标识符。图 3 提供了一个文件夹示例,具有专门向文件夹授予服务权限的访问控制条目。
图 3 授予服务 A 的权限 
可使用“NT 服务”域在访问控制编辑器中指定服务名称。在图 3 所示的示例中,帐户名称为 NT Service\ServiceA。如果需要以编程方式使用服务 SID,还要使用 LookupAccountName 函数。
默认情况下,为服务创建的进程令牌不包括服务 SID。更改此默认选项是 ChangeServiceConfig2 函数的工作。以下函数显示如何控制此选项(是的,它应由服务的安装程序调用):
复制代码
bool ChangeSidType(SC_HANDLE service,  DWORD sidType){  ASSERT(0 != service);  SERVICE_SID_INFO info = { sidType };  return 0 != ::ChangeServiceConfig2(service,    SERVICE_CONFIG_SERVICE_SID_INFO,    &info);}
SID 类型的默认值为 SERVICE_SID_TYPE_NONE 。这是出于应用程序兼容性考虑,并且生成的安全上下文与 Windows 早期版本服务所在的上下文类似。指定 SERVICE_SID_TYPE_UNRESTRICTED 作为 SID 类型,指示 SCM 将服务 SID 添加到服务的进程令牌中,从而允许服务获得访问资源的权限,该资源可能配置为只允许访问特定的服务。

用受限令牌保护其他项目
用服务 SID 保护服务的资源非常适合阻止其他用户和服务访问资源的数据或设置(或至少能防止进行修改),但不能阻止服务访问其无需访问的其他资源。我们为什么要关心这个问题呢?如果服务受损,则可能被用作攻击媒介。
Windows 使用 SERVICE_SID_TYPE_RESTRICTED 这一 SID 类型为 SERVICE_CONFIG_SERVICE_SID_INFO 标记提供了第三个选项。此 SID 类型产生了包含在服务进程令牌中的服务 SID,并指示 SCM 将限制 SID 的列表添加到令牌中,以进一步约束服务可访问的资源。
限制 SID 以前一般很难应用,因为它很难产生 SID 的约束列表,而该列表将有效减少令牌的到达,并且不会过分约束它使其变得无用。为解决此问题,Windows Vista 引入了一个新的令牌类型,称为限制写入令牌。它基本上是放宽了管理限制 SID 的规则。限制写入令牌不是简单地限制访问自主访问控制列表 (DACL) 以外的内容,它只在评估写入访问检查时使用。限制 SID 的适用范围因此大为提高。
如采用 SERVICE_SID_TYPE_RESTRICTED 这一 SID 类型,SCM 会使用下列限制 SID 为服务进程创建一个受限令牌:每个人 SID、登录 SID、服务 SID 和限制写入 SID。这样您就能根据特定的服务 SID 或任何限制写入令牌,充分自由地授予对资源的写入访问权限。更重要的是,它严格约束服务可写入的地方,因而限制了在出现受损情况时它可能产生的影响。

接收服务通知
控制和查询服务状态从前是一项很困难的任务。例如,您可以调用 StartService 函数请求启动特定的服务。但是不能立即得到该服务是否成功启动的通知,因为可能要花一些时间来完成启动过程。而是要等到服务启动后,调用 QueryServiceStatus 函数来定期检查其进度。不用说,这不是一个理想的解决方案。其中一个原因是轮询会导致性能降低,这是由于为检查服务的状态对调用线程进行了不必要的计划安排。幸运的是,Windows Vista 最终通过引入服务状态通知解决了此问题。
新的 NotifyServiceStatusChange 函数可使调用方以异步过程调用 (APCs) 方式接收通知每个对 NotifyServiceStatusChange 的调用至多会产生一个列队的 APC,因此如果需要得到多个更改的通知,将需要反复调用 NotifyServiceStatusChange。如果不再对接收通知感兴趣,只需关闭服务句柄即可,系统将出列任何未完成的 APC。
NotifyServiceStatusChange 能够为特定的服务或 SCM 本身提供通知。图 4 显示了启动 Windows 搜索服务后如何接收通知。
复制代码
void CALLBACK ServiceNotifyCallback(void* param){  SERVICE_NOTIFY* serviceNotify = static_cast<SERVICE_NOTIFY*>(param);  // Check the notification results}int main(){  SC_HANDLE scm = ::OpenSCManager(    0,                              // local SCM    SERVICES_ACTIVE_DATABASE,    SC_MANAGER_ENUMERATE_SERVICE);  ASSERT(0 != scm); // Call GetLastError for more info.  SC_HANDLE service = ::OpenService(    scm,    L"wsearch",    SERVICE_QUERY_STATUS);  ASSERT(0 != service); // Call GetLastError for more info.  SERVICE_NOTIFY serviceNotify =     { SERVICE_NOTIFY_STATUS_CHANGE,       ServiceNotifyCallback };  const DWORD result = ::NotifyServiceStatusChange(    service,    SERVICE_NOTIFY_RUNNING,    &serviceNotify);  ASSERT(ERROR_SUCCESS == result);  ::SleepEx(INFINITE, TRUE); // Wait for the notification  ::CloseServiceHandle(service);  ::CloseServiceHandle(scm);}
如果想接收 SCM 通知,只需使用 SCM 句柄代替服务句柄即可。注意:所使用的通知标记不同。只有 SERVICE_NOTIFY_CREATED 和 SERVICE_NOTIFY_DELETED 标记应用于 SCM。SERVICE_NOTIFY_RUNNING, SERVICE_NOTIFY_STOPPED 和其他所有标记都需要服务句柄。有关标记的完整列表,请查看 NotifyServiceStatusChange 文档。
同一 SERVICE_NOTIFY 结构的地址会传递给通知回调函数。然后可以确定通知的详情。
最后,如果不熟悉进程,将 APC 用于通知可能有点感到非常头疼,但是一旦习惯使用后就会得心应手。有关 APC 的详细信息,请访问weblogs.asp.net/kennykerr,查看我的系列文章“使用 C++ 并行编程”。

后续内容
随后我们将探讨 Windows Vista 引入的 Windows 服务增强。由于支持限制服务对网络的访问,新的 Windows 防火墙 API 已得到了增强。尽管 Windows 防火墙 API 不属本刊的范围,但是我希望在将来的专栏中专门对其进行讨论。

请将您想向 Kenny 咨询的问题和提出的意见发送至  mmwincpp@microsoft.com.


Kenny Kerr 是一名专门从事 Windows 软件开发的软件专家。他热衷于撰写有关编程和软件设计的文章,并向开发人员讲授与此有关的知识。您可通过weblogs.asp.net/kennykerr 与 Kenny 联系。