65.9K
CodeProject 正在变化。 阅读更多。
Home

NT 服务编写、安装、启动和停止的初学者入门指南

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (89投票s)

2001 年 12 月 30 日

CPOL

10分钟阅读

viewsIcon

624292

downloadIcon

16081

包含一个简单 NT 服务的通用骨架。解释如何以编程方式安装、启动和停止服务。

NT 服务简介

系统启动时,Windows NT/2K 会启动一个 RPC 服务器,称为 **服务控制管理器 (SCM)**。NT 服务基本上是一个由 SCM 加载的 win32 程序。它们在任何用户登录系统之前加载。服务有时也可以手动启动,而不是在启动时自动启动。最近我第一次尝试编写 NT 服务,我沮丧地发现,对于服务新手来说,可用的信息很少。即使在 Code Project 上,我也只能找到封装类,这不是我想要的。 

本文为您提供了一个通用的服务骨架,您可以将其用作编写第一个服务的起点。该服务基本上不执行任何操作。我在网上找到了几个示例,它们都被称为蜂鸣器服务,因为它们就是这样做的。它们会以固定的时间间隔使系统扬声器发出蜂鸣声。我想为我的骨架服务做同样的事情,因为这似乎是表示服务已启动并正在运行的最简单方法。

服务骨架

主函数

我将我的服务编写为控制台应用程序,因此是 main 函数。但我认为没有什么能阻止您将服务编写为带有 WinMain 的 GUI 应用程序,但由于我还没有尝试过,所以我不会深入探讨。main 函数所做的只是调用 StartServiceCtrlDispatcher 来将我们服务的主线程连接到 SCM。我们只需填充 SERVICE_TABLE_ENTRY 结构并调用 StartServiceCtrlDispatcher,将 SERVICE_TABLE_ENTRY 结构作为参数传递。

SERVICE_TABLE_ENTRY servicetable[]=
{
	{strServiceName,(LPSERVICE_MAIN_FUNCTION)ServiceMain},
	{NULL,NULL}
};

strServiceName 是我们服务的名称。我们还传递了一个指向 ServiceMain 函数的指针。我使用 ServiceMain 这个名称,以为它是强制性的,但后来我意识到您可以使用任何您想要的名称。我想我有点傻。表中最后一项的成员必须具有 NULL 值,表示表已结束。

StartServiceCtrlDispatcher(servicetable);

如您所见,调用 StartServiceCtrlDispatcher 是一件简单的事情。只需传递一个指向 SERVICE_TABLE_ENTRY 数组的指针。如果 StartServiceCtrlDispatcher 失败,它会立即返回 false,否则只有在我们的服务终止后才会返回。最近我了解到同一个可执行文件可以有多个服务,但由于我没有真正尝试过,所以我将避免做出任何大胆的声明。无论如何,我认为每个 exe 一个服务是一种遵循“保持简单”范式的好方法。

ServiceMain 函数

ServiceMain 是我们服务的入口点函数。当 **SCM** 启动我们的服务时,它会创建一个新线程来执行我们的 ServiceMain 函数。ServiceMain 所做的第一件事是调用 RegisterServiceCtrlHandler 来注册一个处理程序函数。服务使用此处理程序函数作为其控制处理程序函数,该函数接收控制代码,包括启动、停止、暂停和继续服务的代码。

RegisterServiceCtrlHandler(strServiceName,
	(LPHANDLER_FUNCTION)ServiceCtrlHandler);

注册服务控制处理程序后,我们需要向 SCM 更新我们服务的状态。我们可以使用 SetServiceStatus API 调用来完成此操作。在程序运行过程中,我们需要多次执行此操作,每次都涉及填充 SERVICE_STATUS 结构。因此,我编写了一个名为 UpdateServiceStatus 的函数来为我们自动执行此操作。我将在本文后面讨论此函数。基本上,我们在注册处理程序后所做的是向 SCM 更新我们服务的 SERVICE_START_PENDING 状态,这意味着我们的服务正在启动。

UpdateServiceStatus(SERVICE_START_PENDING,NO_ERROR,0,1,3000);

3000 是 SERVICE_STATUS 结构的 dwWaitHint 参数,以毫秒为单位。如果此时间已过但服务状态尚未更改,SCM 会假定发生了错误。一旦我们向 SCM 更新了状态,我们就会创建一个事件。我们这样做是为了可以在此事件上使用 WaitForSingleObject。然后我们可以在程序的其他地方设置事件来终止我们的服务。

killServiceEvent=CreateEvent(0,TRUE,FALSE,0);

执行此操作后,我们再次调用 UpdateServiceStatus,并将状态设为 SERVICE_START_PENDING,只是这次我们增加了 SERVICE_STATUS 结构的 dwCheckPoint 参数。此参数用于在长时间的启动或停止操作期间跟踪服务的进度。现在我们启动服务执行线程。

StartServiceThread();

我稍后会讨论这个函数,但总而言之,它只是使用 CreateThread 启动一个新线程,我们将实际功能放在其中。现在我们再次调用 UpdateServiceStatus,传递 SERVICE_RUNNING 作为我们的参数。

UpdateServiceStatus(SERVICE_RUNNING,NO_ERROR,0,0,0);

好的,现在我们的服务已启动并运行,我们需要对我们之前创建的事件调用 WaitForSingleObject。因为 ServiceMain 应该在我们的服务终止之前不结束。同样,这是一个非常简单的步骤,如下所示。

WaitForSingleObject(killServiceEvent,INFINITE);

UpdateServiceStatus 函数

正如我之前提到的,我编写了这个函数来封装 SetServiceStatus API 调用。这绝不是一个创新的想法。我看到的几乎每个服务示例都使用了某种形式的封装函数,因为在服务程序的运行过程中,我们需要多次更改服务状态。基本上,我们在这个函数中所做的是填充一个 SERVICE_STATUS 结构。我将提及这个结构中对我们很重要的一些成员。我强烈建议您在 MSDN 中查找这个结构。

**dwCurrentState**:- 这表示服务的当前状态。我们使用的一些值是 SERVICE_STOPPEDSERVICE_RUNNINGSERVICE_START_PENDING。您可以在 MSDN 上查找其他允许的值。

**dwControlsAccepted**:- 这用于指示将由我们的服务处理程序处理的控制代码。对于我们的骨架服务,我使用了 SERVICE_ACCEPT_STOPSERVICE_ACCEPT_SHUTDOWN。这些是我们的骨架服务将处理的仅有的两个控制代码。当我们的服务处于 SERVICE_START_PENDING 状态时,我们必须将此参数设置为零。

**dwCheckPoint**:- 我之前提到过这个参数。服务在漫长的启动或停止操作期间增加此值。任何在服务上调用操作的程序都可以使用此值来跟踪各种操作的进度。如果您想知道程序如何做到这一点,请查看 QueryServiceStatus API 调用。

**dwWaitHint**:- 这指定了服务状态再次更改之前的毫秒间隔。如果服务状态在此之前没有更改,**SCM** 会假定发生了错误。当我们设置服务状态为 SERVICE_RUNNING 时,将此参数设为零。

SetServiceStatus(nServiceStatusHandle,&nServiceStatus);

我们传递的第一个参数是服务状态句柄,由 RegisterServiceCtrlHandler 函数返回。我已将其保存在一个全局变量中。第二个参数是指向我们已填充的 SERVICE_STATUS 结构的指针。

StartServiceThread 函数

好的,这个函数只是使用 CreateThread API 调用启动我们的服务执行线程。如果线程创建成功,我还会将全局变量 nServiceRunning 设置为 true。我使用了 CreateThread,但如果您计划在线程中使用一些 CRT 函数,您可能需要使用 _beginthreadex

ServiceExecutionThread 函数

此函数是我们的主要服务执行线程。在我们的骨架服务中,我只是使用我的全局 nServiceRunning 布尔变量作为 while 循环的评估表达式,放置了一个 **while** 循环。因此,直到 nServiceRunning 变为 false,**while** 循环将无限循环。请记住,无限 while 循环将无限期地占用您的 CPU,直到您的机器陷入悲惨的冻结状态。我通过使用 Sleep 来避免这种情况,但您可能希望在程序中使用某种阻塞调用或等待调用。

while(nServiceRunning)
{		
	Beep(450,150);
	Sleep(4000);
}

好的,这就是服务的主体。我想不会再简单了。当然,功能是无用的,所以我们称之为骨架服务。

ServiceCtrlHandler 函数

这是我们服务的控制处理程序函数。所有服务控制请求,例如启动服务、停止服务等,都由控制处理程序处理。MSDN 中此函数的原型如下。

VOID WINAPI Handler(
  DWORD fdwControl   // requested control code
);

基本上,我们对 fdwControl 变量使用 switch 语句,并且为我们打算处理的每个控制代码设置了 case 块。

switch(nControlCode)
{	
case SERVICE_CONTROL_SHUTDOWN:
case SERVICE_CONTROL_STOP:
	nServiceCurrentStatus=SERVICE_STOP_PENDING;				
	success=UpdateServiceStatus(SERVICE_STOP_PENDING,NO_ERROR,0,1,3000);
	KillService();		
	return;
default:
	break;
}

如您所见,我们的骨架程序的 switch 结构只处理两个控制代码,SERVICE_CONTROL_SHUTDOWNSERVICE_CONTROL_STOP。如果您向上滚动,您会看到当我将服务状态设置为 SERVICE_RUNNING 时,我将 SERVICE_STATUS 结构的 **dwControlsAccepted** 成员设置为 SERVICE_ACCEPT_STOP|SERVICE_ACCEPT_SHUTDOWN。因此,这些是 **SCM** 将发送到我们的控制处理程序函数的仅有的两个控制代码。如您所见,对于这两种情况,我们使用的是相同的代码。我们所做的只是将我们的全局服务状态变量更改为 SERVICE_STOP_PENDING,然后我们调用 UpdateServiceStatus,传递 SERVICE_STOP_PENDING。然后我们调用我们自己的 KillService 函数(我将在下面解释)并返回。

KillService 函数

好的,我们使用 KillService 函数来终止我们的服务。我们首先将 nServiceRunning 设置为 false,以便我们的服务执行线程退出。然后我们设置我们的阻塞事件,以便 ServiceMain 将退出。

nServiceRunning=false;
SetEvent(killServiceEvent);

完成此操作后,我们需要通知 **SCM** 我们的服务已终止。因此,我们调用 UpdateServiceStatus,将 SERVICE_STOPPED 作为 **dwCurrentState** 参数。

UpdateServiceStatus(SERVICE_STOPPED,NO_ERROR,0,0,0);

安装我们的服务

首先,我们使用 API 调用 OpenSCManager 来获取 **SCM** 数据库的句柄。

scm=OpenSCManager(0,0,SC_MANAGER_CREATE_SERVICE);

我们对 lpMachineName 和 lpDatabaseName 都传递 0,因为我们需要打开本地计算机上的 SCM 数据库。我们将 SC_MANAGER_CREATE_SERVICE 作为我们的 **dwDesiredAccess** 传递,以便我们可以使用 CreateService API 调用来创建我们的新服务并将其添加到 **SCM** 数据库。

CreateService(scm,"NishService",
	"Buster's first NT service",
	SERVICE_ALL_ACCESS,SERVICE_WIN32_OWN_PROCESS,SERVICE_DEMAND_START,
	SERVICE_ERROR_NORMAL,
	"D:\\nish\\FirstService\\Debug\\FirstService.exe",
	0,0,0,0,0);

您必须在 MSDN 上查找 CreateService。我已将 SERVICE_ALL_ACCESS 用作我的 **dwDesiredAccess** 参数。这赋予了我全部权限,我可以随心所欲地操作。我已将 SERVICE_DEMAND_START 用作 **dwStartType** 参数。这意味着服务不会在系统启动时自动启动。它需要手动启动,可以通过控制面板的 **组件服务** 小程序,或者以编程方式使用 StartService。在本文后面,我将向您展示如何以编程方式启动和停止我们的服务。您需要指定服务可执行文件的完整路径。最后 5 个参数暂时可以忽略。坦率地说,当我发现它们都可以为 NULL 时,我就没有费心去解释它们的用途。但我建议您继续弄清楚如何正确使用它们。

以编程方式启动我们的服务

我们首先需要做的是使用 OpenSCManager 获取 **SCM** 的句柄。现在我们使用 OpenService 获取我们骨架服务的句柄。

NishService=OpenService(scm,"NishService",SERVICE_ALL_ACCESS);

如果 OpenService 成功返回(我们可以通过检查它是否返回 NULL 来判断,如果是 NULL 则调用失败),我们可以继续调用 StartService,使用 OpenService 返回的句柄。

StartService(NishService,0,NULL);

由于我们没有向 ServiceMain 传递任何参数,我将 0 和 NULL 作为第 2 个和第 3 个参数。如果 StartService 成功,返回值为非零,我们可以假设我们的服务已成功启动。这很快就会变得显而易见,当 PC 扬声器每 4 秒开始发出一次蜂鸣声时,您可能会收到同事粗鲁的目光。在这种情况下,您可能需要停止服务。

以编程方式停止我们的服务

停止我们服务的前两个步骤与启动我们服务的步骤相同。我们调用 OpenSCManager 获取 **SCM** 的句柄,然后调用 OpenService 获取我们服务的句柄。现在我们可以使用 ControlService API 调用向我们的服务处理程序发送控制代码。

ControlService(NishService,SERVICE_CONTROL_STOP,&m_SERVICE_STATUS);

m_SERVICE_STATUS 是一个 SERVICE_STATUS 结构,它将接收我们服务的状态信息。如您所见,我已将 SERVICE_CONTROL_STOP 作为控制代码传递。我们知道我们是如何在服务处理程序中处理此控制代码的。现在一切都开始符合模式了,嗯?到这时,4 秒钟的蜂鸣声将停止,粗鲁的目光将慢慢消失。

谢谢 [请记住,我也是服务新手]

© . All rights reserved.