Windows服务程序

https://blog.csdn.net/qq_37059136/article/details/114069779

基本概念

  1. 什么是服务应用程序?
    能够为各种用户(包括本地用户和远程用户)所用的,拥有用户授权级进行管理的能力,并且不论用户是否物理的与正在运行该应用程序的计算机相连都能正常执行,这就是所谓的服务了。
  2. 服务的特征
  • 服务是一类受到操作系统优待的程序。一个服务首先是一个Win32可执行程序;
  • 一个服务决不需要用户界面,进入点函数是main();
  • 服务能够被远程管理;
  • 如果一个进程正在一个用户帐号下执行,那么这个进程就同时拥有这个用户所能拥有的一切访问权限,不论是在本机还是网络。系统帐号则是一个特殊的账号,它用来标识系统本身,而且运行在这个帐号下的任何进程都拥有系统上的所有访问权限,但是系统帐号不能在域上使用,无法访问网络资源。服务也是Win32可执行程序,它也需要执行在一个context,通常服务都是在系统账号下运行,但是也可以根据情况选择让它运行在一个用户账号下,也就会因此获得相应的访问资源的权限。
  1. 服务的组成
  • 一个服务由三部分组成,第一部分是服务控制管理器 (SCM) 服务程序。
    (1)SCM必须包括的接口要求:服务入口点、 ServiceMain 函数、服务控制处理程序函数(不适用于驱动程序服务)。
    (2)每个Windows NT/2000系统都有一个SCM,SCM存在于Service.exe中,在Windows启动的时候会自动运行,伴随着操作系统的启动和关闭而产生和终止。这个进程以系统特权运行,并且提供一个统一的、安全的手段去控制服务。它其实是一个RPC Server,因此我们可以远程安装和管理服务。SCM包含一个储存着已安装的服务和驱动程序的信息的数据库,通过SCM可以统一的、安全的管理这些信息,因此一个服务程序的安装过程就是将自身的信息写入这个数据库。
  • 第二部分就是服务本身。一个服务拥有能从SCM收到信号和命令所必需的的特殊代码,并且能够在处理后将它的状态回传给SCM。
  • 第三部分也是一个Service Control Dispatcher(SCP)。它是一个拥有用户界面,允许用户开始、停止、暂停、继续,并且控制一个或多个安装在计算机上服务的Win32应用程序。SCP的作用是与SCM通讯,Windows 2000管理工具中的“服务”就是一个典型的SCP。

在这三个组成部分中,用户最可能去写服务本身,同时也可能不得不写一个与其伴随的客户端程序作为一个SCP去和SCM通讯。

  1. 服务的三个函数
  • 第一个就是入口点函数。其实用WinMain()作为入口点函数也不是不可以,虽然说服务不应该有用户界面,但是由于要和用户桌面进行信息交互,服务程序有时会以WinMain()作为入口点函数。入口函数负责初始化整个进程,由这个进程中的主线程来执行。这意味着它应用于这个可执行文件中的所有服务。要知道,一个可执行文件中能够包含多个服务以使得执行更加有效。主进程通知SCM在可执行文件中含有几个服务,并且给出每一个服务的ServiceMain回调(Call Back)函数的地址。一旦在可执行文件内的所有服务都已经停止运行,主线程就在进程终止前对整个进程进行清除。

  • 第二个函数就是ServiceMain。任何的函数只要符合下列的形式都可以作为服务的进入点函数。

    VOID WINAPI ServiceMain(
    DWORD dwArgc, // 参数个数
    LPTSTR *lpszArgv // 参数串
    );

    这个函数由操作系统调用,并执行能完成服务的代码。一个专用的线程执行每一个服务的ServiceMain函数,注意是服务而不是服务程序,这是因为每个服务也都拥有与自己唯一对应的ServiceMain函数,关于这一点可以用“管理工具”里的“服务”去察看Win2000里面自带的服务,就会发现其实很多服务都是由service.exe单独提供的。当主线程调用Win32函数StartServiceCtrlDispatcher的时候,SCM为这个进程中的每一个服务产生一个线程。这些线程中的每一个都和它的相应的服务的ServiceMain函数一起执行,这就是服务总是多线程的原因——一个仅有一个服务的可执行文件将有一个主线程,其它的线程执行服务本身。

  • 第三个也就是最后的一个重要函数是CtrlHandler,它必须拥有下面的原型:

    VOID WINAPI CtrlHandler(
    DWORD fdwControl //控制命令
    )

    像ServiceMain一样,CtrlHandler也是一个回调函数,用户必须为它的服务程序中每一个服务写一个单独的CtrlHandler函数,因此如果有一个程序含有两个服务,那么它至少要拥有5个不同的函数:作为入口点的main()或WinMain(),用于第一个服务的ServiceMain函数和CtrlHandler函数,以及用于第二个服务的ServiceMain函数和CtrlHandler函数。

    SCM调用一个服务的CtrlHandler函数去改变这个服务的状态。例如,当某个管理员用管理工具里的“服务”尝试停止你的服务的时候,你的服务的CtrlHandler函数将收到一个SERVICE_CONTROL_STOP通知。CtrlHandler函数负责执行停止服务所需的一切代码。由于是进程的主线程执行所有的CtrlHandler函数,因而必须尽量优化你的CtrlHandler函数的代码,使它运行起来足够快,以便相同进程中的其它服务的CtrlHandler函数能在适当的时间内收到属于它们的通知。而且基于上述原因,你的CtrlHandler函数必须要能够将想要传达的状态送到服务线程,这个传递过程没有固定的方法,完全取决于你的服务的用途。

  1. 服务的深入讨论
  • 进入点函数
    (1)在进入点函数里面要完成ServiceMain的初始化,准确点说是初始化一个SERVICE_TABLE_ENTRY结构数组,这个结构记录了这个服务程序里面所包含的所有服务的名称和服务的进入点函数,下面是一个SERVICE_TABLE_ENTRY的例子:

    SERVICE_TABLE_ENTRY service_table_entry[] =
    {
    { MyFTPd , FtpdMain },
    { MyHttpd, Httpserv},
    { NULL, NULL },
    };

    第一个成员代表服务的名字,第二个成员是ServiceMain回调函数的地址,上面的服务程序因为拥有两个服务,所以有三个SERVICE_TABLE_ENTRY元素,前两个用于服务,最后的NULL指明数组的结束。
    (2)接下来这个数组的地址被传递到StartServiceCtrlDispatcher函数:

    BOOL StartServiceCtrlDispatcher(
    LPSERVICE_TABLE_ENTRY lpServiceStartTable
    )

    这个Win32函数表明可执行文件的进程怎样通知SCM包含在这个进程中的服务。就像上一章中讲的那样,StartServiceCtrlDispatcher为每一个传递到它的数组中的非空元素产生一个新的线程,每一个进程开始执行由数组元素中的lpServiceStartTable指明的ServiceMain函数。

    SCM启动一个服务程序之后,它会等待该程序的主线程去调StartServiceCtrlDispatcher。如果那个函数在两分钟内没有被调用,SCM将会认为这个服务有问题,并调用TerminateProcess去杀死这个进程。这就要求你的主线程要尽可能快的调用StartServiceCtrlDispatcher。

    StartServiceCtrlDispatcher函数则并不立即返回,相反它会驻留在一个循环内。当在该循环内时,StartServiceCtrlDispatcher悬挂起自己,等待下面两个事件中的一个发生。
      ① 第一,如果SCM要去送一个控制通知给运行在这个进程内一个服务的时候,这个线程就会激活。当控制通知到达后,线程激活并调用相应服务的CtrlHandler函数。CtrlHandler函数处理这个服务控制通知,并返回到StartServiceCtrlDispatcher。StartServiceCtrlDispatcher循环回去后再一次悬挂自己。
      ② 第二,如果服务线程中的一个服务中止,这个线程也将激活。在这种情况下,该进程将运行在它里面的服务数减一。如果服务数为零,StartServiceCtrlDispatcher就会返回到入口点函数,以便能够执行任何与进程有关的清除工作并结束进程。如果还有服务在运行,哪怕只是一个服务,StartServiceCtrlDispatcher也会继续循环下去,继续等待其它的控制通知或者剩下的服务线程中止。

  • ServiceMain函数
    (1)设置一个服务最好的方法就是设置注册表,一般服务在
    HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Service\ServiceName\Parameters
    子键下存放自己的设置,这里的ServiceName是服务的名字。事实上,可能要写一个客户应用程序去进行服务的背景设置,这个客户应用程序将这些信息存在注册表中,以便服务读取。当一个外部应用程序已经改变了某个正在运行中的服务的设置数据的时候,这个服务能够用RegNotifyChangeKeyValue函数去接受一个通知,这样就允许服务快速的重新设置自己。
    (2)StartServiceCtrlDispatcher为每一个传递到它的数组中的非空元素产生一个新的线程。接下来ServiceMain要迅速完成自身工作,首先是必不可少的两项工作:
    ① 第一项是调用RegisterServiceCtrlHandler函数去通知SCM它的CtrlHandler回调函数的地址:

    SERVICE_STATUS_HANDLE RegisterServiceCtrlHandler(
    LPCTSTR lpServiceName, //服务的名字
    LPHANDLER_FUNCTION lpHandlerProc //CtrlHandler函数地址
    )

    第一个参数指明你正在建立的CtrlHandler是为哪一个服务所用,第二个参数是CtrlHandler函数的地址。lpServiceName必须和在SERVICE_TABLE_ENTRY里面被初始化的服务的名字相匹配。RegisterServiceCtrlHandler返回一个SERVICE_STATUS_HANDLE,这是一个32位的句柄。SCM用它来唯一确定这个服务。当这个服务需要把它当时的状态报告给SCM的时候,就必须把这个句柄传给需要它的Win32函数。注意:这个句柄和其他大多数的句柄不同,你无需关闭它。

    ② 第二项在RegisterServiceCtrlHandler函数返回后,ServiceMain线程要立即告诉SCM服务正在继续初始化。具体的方法是通过调用SetServiceStatus函数传递SERVICE_STATUS数据结构。

    BOOL SetServiceStatus(
    SERVICE_STATUS_HANDLE hService, //服务的句柄
    SERVICE_STATUS lpServiceStatus //SERVICE_STATUS结构的地址
    )

    这个函数要求传递给它指明服务的句柄(刚刚通过调用RegisterServiceCtrlHandler得到),和一个初始化的SERVICE_STATUS结构的地址。

  • CtrlHandler函数
    如果你的CtrlHandler函数需要很长的时间执行操作的话,千万要注意:假如CtrlHandler函数在30秒内没有返回的话,SCM将返回一个错误,这不是我们所期望的。所以如果出现上述情况,最好的办法是再建立一个线程,让它去继续执行操作,以便使得CtrlHandler函数能够迅速的返回。

#include <iostream>
#include <windows.h>
#include <tchar.h>
using namespace std;

SERVICE_STATUS  servicestatus;
SERVICE_STATUS_HANDLE hstatus;
bool b_run = false;

void WriteLog(char* str) {
    FILE* fp;
    fopen_s(&fp, "http_test.txt", "a+");
    if (fp != NULL) {
        fprintf_s(fp, "%s\n", str);
        fclose(fp);
    }
}

void WINAPI CtrlHandler(DWORD request) {
    WriteLog("CtrlHandler success!");
    switch (request) {
    case SERVICE_CONTROL_STOP:
        b_run = false;
        servicestatus.dwCurrentState = SERVICE_STOPPED;
        break;
    default:
        break;
    }
    SetServiceStatus(hstatus, &servicestatus);
}

VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR* lpszArgv) {
    servicestatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
    servicestatus.dwCurrentState = SERVICE_START_PENDING;
    servicestatus.dwControlsAccepted = SERVICE_ACCEPT_STOP;
    servicestatus.dwWin32ExitCode = 0;
    servicestatus.dwServiceSpecificExitCode = 0;
    servicestatus.dwCheckPoint = 0;
    servicestatus.dwWaitHint = 0;

    //注册CtrlHandle函数,返回一个操作服务状态函数的句柄
    hstatus =::RegisterServiceCtrlHandler(_T("http_test"), CtrlHandler);
    if (hstatus == 0) {
        WriteLog("RegisterServiceCtrlHandler failed!");
        return;
    }

    WriteLog("RegisterServiceCtrlHandler success!");

    //修改服务状态
    servicestatus.dwCurrentState = SERVICE_RUNNING;
    //向SCM报告运行状态
    SetServiceStatus(hstatus, &servicestatus);

    //任务循环
    b_run = true;
    while (b_run) {
        WriteLog("task continue...");
        Sleep(5 * 1000);
    }
    WriteLog("service stop!");
}


int main() {
    //一个windows服务应用程序包含多个服务
    SERVICE_TABLE_ENTRY service_table_entry[2];
    service_table_entry[0].lpServiceName = _T("http_test");
    service_table_entry[0].lpServiceProc = (LPSERVICE_MAIN_FUNCTION)ServiceMain;;
    service_table_entry[1].lpServiceName = NULL;
    service_table_entry[1].lpServiceProc = NULL;

    //主线程调用该函数,为每个服务分配一个线程
    StartServiceCtrlDispatcher(service_table_entry);

    OutputDebugStringA("this end!");
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值