关于 Service 设计初步(MSDN节选翻译)

发表于:2007-07-01来源:作者:点击数: 标签:
以下内容是我这几天学写 Service 的笔记,共享一下,刚好让想写 Service 的朋友一起探讨(当然,这翻译很烂请不要笑话): · 一个 Service 程序包括三个部分 第一个部分是控制模块,主要是与服务管理程序沟通,进行服务程序的安装和删除。 第二个模块是主模块

  以下内容是我这几天学写 Service 的笔记,共享一下,刚好让想写 Service 的朋友一起探讨(当然,这翻译很烂请不要笑话):

 · 一个 Service 程序包括三个部分

     第一个部分是控制模块,主要是与服务管理程序沟通,进行服务程序的安装和删除。

     第二个模块是主模块,也就是服务程序运行过程中要做的工作,应该是一个循环(如果退出了该循环,是否需要通知操作系统?)。

     第三个模块是与服务管理程序控制处理模块,主要处理 Service启动,Service停止等事件的。并通知服务管理程序当前的状态。

 

了解过程

  · 如何安装一个 Service 程序,以及如何删除一个 Service 程序。

 

  · 如何处理系统的启动 Service 和停止 Service 的事件

 

  · Service 程序运行的主模块应该放在那里?以及如何组织?

 

 

MSDN 节选内容

 

  Service Control Manager 简称 SCM (或 SCMgr),是当系统启动的时候自动运行的远程调用

 

 ·维护已安装 Service 程序的数据库(List)

 

 ·在系统启动时或当需要时启动 Service 或 驱动型 Services 。

 

 ·列举已经安装的 Service 和 Service 驱动

 

 ·获取运行中的 Service 和 Service 驱动的状态信息

 

 ·给运行中的 Service 发送控制要求

 

 ·锁定 (或解除) Service 数据库。

 

 

The main Function

 

  Service 程序 通常会被写成 控制台程序, main 函数将会是控制台程序的入口,得到来自注册表内预定的参数。

  当 SCM 启动 Service 程序,它会等待 调用 StartServiceCtrlDispatcher 函数

  SERVICE_WIN32_OWN_PROCESS 类型的 Service 会立即从它的主线程里调用 StartServiceCtrlDispatcher 。你可以在 ServiceMain 函数内执行一些初始化操作,当 Service 启动以后。

  StartServiceCtrlDispatcher 函数有一个 SERVICE_TABLE_ENTRY 作为参数,里面指定了 Service 的名字和入口点。

  如果 StartServiceCtrlDispatcher 函数调用成功,被调用线程不会在 Service 进程结束以前返回。

   ·当新的 Service 启动时,创建一个新的线程并调用对应的入口点

   ·调用适当的处理函数去处理 Service 控制要求

 

 

The ServiceMain Function

 

ServiceMain 函数是 Service 的入口点。

当 Service 控制程序需要一个新的 Service 运行。 SCM 开始 Service 并发送启动要求给控制传送者,然后由它创建新的线程以运行 Service 的 ServiceMain 函数。

ServiceMain 函数需要执行一下任务:

马上调用 RegisterServiceCtrlHandlerEx 去注册一个 处理函数(HandlerEx) 去处理 Service 的 控制要求。返回值是通知 SCM 的 Service 状态处理的 handle 。

执行初始化单元,如果初始化代码的执行时间少于1秒或很短,可以在 ServiceMain 里执行。

如果 初始化时间太长了,最好先调用 SetServiceStatus 函数,指定状态为 SERVICE_START_PENDING (启动中),最好是经常作 SetServiceStatus 报告现在的进度,这样可以比较好的进行 Debug 。

当初始化完成后,调用 SetServiceStatus 并指定 SERVICE_RUNNING 标示现在的状态。

执行 Service 的任务,或者返回(如果没有任务的)

如果 初始化 或 运行 过程中出现了错误,应该调用 SetServiceStatus 并指定 SERVICE_STOP_PENDING(正在停止) 。完成清场工作后,再指定现在状态为 SERVICE_STOPPED, 记得在 SERVICE_STATUS 结构中指定成员 dwServiceSpecificExitCode 和 dwWin32ExitCode 的值,用来指定错误的类型。

 

 

The Control Handler Function

  每一个 Service 都有控制处理 (HandlerEx) 函数,它会被控制发送机制调用 当 Service 进程从Service 控制程序那里获得控制要求,因此,这个函数是在 control dispatcher 的线程中执行的。

  当处理函数被调用,Service 必须要调用 SetServiceStatus 函数来向 SCM 汇报当前状态,不管当前的状态是否又改变了。

  Service 控制程序可以使用 ControlService 函数来发送控制要求,所有的 Service 都必须接受并处理 SERVICE_CONTROL_INTERROGATE (控制询问),你可以打开或者禁止接受其他的控制代码,使用 SetServiceStatus 来完成。如果要接收 SERVICE_CONTROL_DEVICEEVENT (控制事件驱动) 代码,你必须调用 RegisterDeviceNotification 函数,Service 还可以处理其他的用户自定义的代码。

  控制处理函数必须在30秒内返回,否则 SCM 会返回错误, 如果 Service 需要做更多的工作来处理,那么最好是创建一个新的线程,比方说要处理 停止服务的 要求,可以创建另外一个线程去做处理,然后在处理函数里面调用 SetServiceStatus 设置 SERVICE_STOP_PENDING 来返回。

  当使用者关闭操作系统时,所有的控制处理过程都会因为被调用 SetServiceStatus 设置了 SERVICE_ACCEPT_SHUTDOWN 而 接收到 SERVICE_CONTROL_SHUTDOWN 控制信号,它们会按照安装顺序而被排队通知该信号,按照默认,每个 Service 可以有大约20秒的清场时间赶在系统关闭之前完成,你可以设置注册表的  WaitToKillServiceTimeout 键值去改变系统等待 Service 关闭的计时,该键值在以下地方设置。

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control

 

如何写 Service 程序的 main 函数

  Servcie 的 main 函数调用 StartServiceCtrlDispatcher 函数去连接 SCM 并开始控制发送器的线程,该线程开始并等待控制请求的到来(当然,是经过过滤的),这个函数是不会返回的的,直到有错误发生或者进程内所有的服务都完成了。 SCM 会发送控制请求以告诉关闭线程,才会从 StartServiceCtrlDispatcher 函数里返回,然后进程就关闭了。

  以下的例子是一个服务进程里只包含一个服务的情况。(省略。。。)

 

SERVICE_STATUS          MyServiceStatus;
SERVICE_STATUS_HANDLE   MyServiceStatusHandle;
 
VOID  MyServiceStart (DWORD argc, LPTSTR *argv);
VOID  MyServiceCtrlHandler (DWORD opcode);
DWORD MyServiceInitialization (DWORD argc, LPTSTR *argv,  DWORD *specificError);
 
void main( )
{
    SERVICE_TABLE_ENTRY   DispatchTable[] =
    {
        { "MyService", MyServiceStart      },
        { NULL,              NULL          }
    };
 
    if (!StartServiceCtrlDispatcher( DispatchTable))
    {
        SvcDebugOut(" [MY_SERVICE] StartServiceCtrlDispatcher error = %d\n", GetLastError());
    }
}
 
VOID SvcDebugOut(LPSTR String, DWORD Status)
{
    CHAR  Buffer[1024];
    if (strlen(String) < 1000)
    {
        sprintf(Buffer, String, Status);
        OutputDebugStringA(Buffer);
    }
}
 

如果你的 Service 程序支持多个服务的, 那么 main 函数将会有轻微的不同, 更多的服务名称将会被加入 dispatch table 以可以被 dispatcher 线程监控。

 

 

如何写 ServiceMain 函数

   以下的那个例子里面 MyServiceStart 函数是 Service 的入口, MyServiceStart 使用 命令行参数,就好像 Console 程序的 main 函数一样。 第一个参数包含共有几个参数被传送到 Service 。所以,那总是至少有一个参数,第二个参数是一个指针的,指向字串指针的数组。数组第一个项一定是 Service 的名字。

   MyServiceStart 首先填充 SERVICE_STATUS 结构,包含了控制代号可以被接受的,尽管这个 Service 可以接受 SERVICE_CONTROL_PAUSE (暂停) 和 SERVICE_CONTROL_CONTINUE (继续) 的,被告知 暂停 是没有意义的,SERVICE_ACCEPT_PAUSE_CONTINUE 标志的包含只是为了说明为目的。如果 暂停 在你的 Service 里面并没有意义,那么就不要支持它。

   MyServiceStart 函数然后调用 RegisterServiceCtrlHandler 函数去注册 MyService 当成 Service 的处理函数,并开始初始化的工作。 下列的初始化例子程序  MyServiceInitialization 只是用来说明,并没有任何实际的初始化代码,和创建任何的线程。如果你的初始化代码可能会很长,那么你的代码最好周期性的调用 SetServiceStatus 来发送等待信息和初始化的进度信息。

   当初始化完成后,例子会调用 SetSercviceStatus 设置 SERVICE_RUNNING 并继续它的工作,如果在初始化期间出了错, MyServiceStart 报告 SERVICE_STOPPED 后返回。

   因为这个例子并没有完成什么真正的任务,MyServiceStart 简单的返回控制给调用者。 无论如何,你的 Service 应该使用这个线程去完成本来想要设计做的东西。如果你的 Service 并不需要运行什么东西(只是操作 RPC 请求而已), ServiceMain 函数应当返回到调用者那里。这个比调用 ExitThread 好的多,因为我们要给机会调用程序做清理现场的工作,比方申请的内存和数组。

   如果要输出 debug 信息, 可以调用 SvcDebugOut ,该函数的源代码已经在《如何写 Servcie 程序的 main 函数》那里给出

 

SERVICE_STATUS          MyServiceStatus;

SERVICE_STATUS_HANDLE   MyServiceStatusHandle;

 

void MyServiceStart (DWORD argc, LPTSTR *argv)

{

    DWORD status;

    DWORD specificError;

 

    MyServiceStatus.dwServiceType       = SERVICE_WIN32;

    MyServiceStatus.dwCurrentState       = SERVICE_START_PENDING;

MyServiceStatus.dwControlsAclearcase/" target="_blank" >ccepted   = SERVICE_ACCEPT_STOP |

                                    SERVICE_ACCEPT_PAUSE_CONTINUE;

    MyServiceStatus.dwWin32ExitCode     = 0;

    MyServiceStatus.dwServiceSpecificExitCode = 0;

    MyServiceStatus.dwCheckPoint         = 0;

    MyServiceStatus.dwWaitHint           = 0;

 

    MyServiceStatusHandle = RegisterServiceCtrlHandler(

        "MyService",

        MyServiceCtrlHandler);

 

    if (MyServiceStatusHandle == (SERVICE_STATUS_HANDLE)0)

    {

      SvcDebugOut(" [MY_SERVICE] RegisterServiceCtrlHandler failed %d\n", GetLastError());

        return;

    }

 

    // Initialization code goes here.

    status = MyServiceInitialization(argc,argv, &specificError);

 

    // Handle error condition

    if (status != NO_ERROR)

    {

        MyServiceStatus.dwCurrentState       = SERVICE_STOPPED;

        MyServiceStatus.dwCheckPoint         = 0;

        MyServiceStatus.dwWaitHint           = 0;

        MyServiceStatus.dwWin32ExitCode      = status;

        MyServiceStatus.dwServiceSpecificExitCode = specificError;

 

        SetServiceStatus (MyServiceStatusHandle, &MyServiceStatus);

        return;

    }

 

    // Initialization complete - report running status.

    MyServiceStatus.dwCurrentState       = SERVICE_RUNNING;

    MyServiceStatus.dwCheckPoint         = 0;

    MyServiceStatus.dwWaitHint           = 0;

 

    if (!SetServiceStatus (MyServiceStatusHandle, &MyServiceStatus))

    {

        status = GetLastError();

        SvcDebugOut(" [MY_SERVICE] SetServiceStatus error %ld\n",status);

    }

 

    // This is where the service does its work.

    SvcDebugOut(" [MY_SERVICE] Returning the Main Thread \n",0);

 

    return;

}

 

// Stub initialization function.

DWORD MyServiceInitialization(DWORD   argc, LPTSTR  *argv, DWORD *specificError)

{

    argv;

    argc;

    specificError;

    return(0);

}

 

如何写 控制处理 函数

  在下面的例子里 MyServiceCtrlHandler 是处理函数,当这个函数被 dispatcher 线程调用,它会处理来自 OPcode 参数控制代码的然后调用 SetServiceStatus 更新当前的 Service 状态。 每次当处理函数接收到控制信号,它会适当的处理并返回状态,而不管 Service 是否还在处置控制信息。

  当接收到暂停的控制信号。 MyServiceCtrlHandler 简单的设置当前 SERVICE_STATUS 里的 dwCurrentState 为失败于 SERVICE_PAUSED 里。同样的,如果接收到继续的信息,则设置 SERVICE_RUNNING 。所以,该例子并不是一个好的用来处理 暂停和继续 的例子程序。其实如果你的 Service 曾经声明不支持 暂停和继续,那么 SCM 是不会发送这些信息到处理函数的。

 

SERVICE_STATUS          MyServiceStatus;

SERVICE_STATUS_HANDLE   MyServiceStatusHandle;

 

VOID MyServiceCtrlHandler (DWORD Opcode)

{

    DWORD status;

    switch(Opcode)

    {

        case SERVICE_CONTROL_PAUSE:

        // Do whatever it takes to pause here.

            MyServiceStatus.dwCurrentState = SERVICE_PAUSED;

            break;

 

        case SERVICE_CONTROL_CONTINUE:

        // Do whatever it takes to continue here.

            MyServiceStatus.dwCurrentState = SERVICE_RUNNING;

            break;

 

        case SERVICE_CONTROL_STOP:

        // Do whatever it takes to stop here.

            MyServiceStatus.dwWin32ExitCode = 0;

            MyServiceStatus.dwCurrentState  = SERVICE_STOPPED;

            MyServiceStatus.dwCheckPoint    = 0;

            MyServiceStatus.dwWaitHint      = 0;

             if (!SetServiceStatus (MyServiceStatusHandle,   &MyServiceStatus))

            {

                status = GetLastError();

                SvcDebugOut(" [MY_SERVICE] SetServiceStatus error  %ld\n",status);

            }

             SvcDebugOut(" [MY_SERVICE] Leaving MyService \n",0);     return;

 

        case SERVICE_CONTROL_INTERROGATE:

        // Fall through to send current status.

            break;

 

        default:

            SvcDebugOut(" [MY_SERVICE] Unrecognized opcode %ld\n",  Opcode);

    }

 

    // Send current status.

    if (!SetServiceStatus (MyServiceStatusHandle,  &MyServiceStatus))

    {

        status = GetLastError();

        SvcDebugOut(" [MY_SERVICE] SetServiceStatus error %ld\n",status);

    }

    return;

}

  完毕,如果我写了什么可以参考的服务程序例子,我再拿到这里来,以权作参考。


原文转自:http://www.ltesting.net