Win32服务编程模型

我们经常会在Windows的任务管理器中看到一些挂在系统后台中运行的程序,但是我们又无法在任务栏或者是其他位置找到正在执行的这个程序,这种程序在任务管理器里面往往以"齿轮"图标显示出来的(见TaskManager.png),并且运行着的。实际上这就是Windows中所谓的服务。这与Linux系统的守护进程实际上就是同一个东西。只不过Windows对这种"守护进程"的创建方式和管理方式有着一套独立的方式。下面我们就来一起了解一下它的庐山真面目。
1.什么是Windows中的服务?
我们看到的任务管理器中的那些正在运行且带有齿轮符号的程序实际上就是后台服务,这些服务和普通的程序没有什么两样。它们都是按照各自编写好了的业务逻辑运行着的。只不过不会直接或显而易见的显示给用户,它们默默的在后台执行着。完成着它们应该完成的事情。

TaskManager

2.Windows的服务往往被用在软件的哪些领域?
实际上大多数的服务无非挂在后台的,这些服务往往被用于监控对某些软件的更新,或者是通过与Win32的消息共享以实现一些监控设备状态的功能,又或者是被用在看门狗程序中。
3.Windows服务管理机制
实际上Windows的服务的编写流程并不像Linux那样子,我编写完一个程序之后,就可以在Linux的终端中以"程序名称 $"的方式就将程序挂在后台执行了。而Windows采用了一个叫做服务管理器(Service Manager)的东西来完成对服务的创建,添加,删除,启动,停止,暂停或者是从服务管理器中卸载这个服务的机制进行管理的。

SCManager

SCManager_Service

4.通过Win32API编写自己的服务
大致来讲,Win32的服务编程基本都需要经历以下或者是要设计以下的框架。来完成对一个Win32服务程序的基本的编写。
4.1 创建服务的逻辑
4.2 卸载服务的逻辑
4.4 服务的主逻辑(这里面实际上就是编写服务的功能代码[也就是业务代码])
4.3 启动服务的逻辑
4.4 停止服务的逻辑
5.案例
在这个案例中,我们使用Visual Studio来创建一个名叫ServiceDemo的服务,并且里面通过读写文件来看到我们从服务管理器对其进行的交互式操作。(因为服务往往没有标准输出,所以,我们要调试一些细节时,最好通过读写文本的文件的形式来捕获其调试信息)
5.1 创建服务
我们在代码中新建一个ServiceDemo.h和ServiceDemo.cpp的文件,在这里面编写我们创建服务和删除服务的相关逻辑。
创建服务我们主要需要使用到的几个函数就是:
5.1.1 打开服务管理器
SC_HANDLE OpenSCManager(
[in, optional] LPCSTR lpMachineName,//要打开的哪个机器上的服务管理器,我们一般都是打开本地的服务管理器,这里一般都填NULL来表示我想打开的时本地计算机上的服务管理器
[in, optional] LPCSTR lpDatabaseName,//服务管理的数据库名称,这个实际上就填SERVICES_ACTIVE_DATABASE,表示我们就是要打开服务管理器里面目前可用的,活跃的服务的列表(实际上写NULL也表示这个含义)
[in] DWORD dwDesiredAccess//对服务管理器的服务权限,如果我们要创建,那么就必须要具有写的权限,一般填SC_MANAGER_ALL_ACCESS表示我对服务管理器有所有的权限)
);
lpDatabaseName的取值基本上都是SERVICES_ACTIVE_DATABASE
lpDatabaseName可以有以下取值:
SC_MANAGER_ALL_ACCESS 得到对服务管理器的所有的权限(常用)
SC_MANAGER_CREATE_SERVICE 只可以在服务管理器中创建服务
SC_MANAGER_CONNECT 可以连接到在线的服务管理器
SC_MANAGER_ENUMERATE_SERVICE 可以列举出服务管理器中的服务(假设我们想通过EnumServicesStatusEx来获取服务的状态,那么就必须设置这个访问权限)
SC_MANAGER_LOCK 有锁定服务管理器的权限(注意:如果我们会因为某些需要要锁定服务管理器不让别的程序或者是用户来进入到服务管理器(着可能会调用LockServiceDataBase函数),那么这个标志位就需要指定)
SC_MANAGER_MODIFY_BOOT_CONFIG 需要调用NotifyBootConfigStatus函数时,就需要设置这个权限标识
SC_MANAGER_QUERY_LOCK_STATUS 若我们锁定了服务管理器,那么需要调用QueryServiceLockStatus来获取服务管理器的锁定状态时,就需要这个权限标识
若此函数执行成功,那么返回的句柄就非空。否则就失败了(那么就需要在失败的时候调用一次GetLastError来获取错误码。那么对于这个函数,错误码有以下两种,读者可以自行编写对其的处理逻辑:
ERROR_ACCESS_DENIED 存取的权限被拒绝
ERROR_DATABASE_DOES_NOT_EXIST 指定的服务数据库不存在
5.1.2 判断需要创建的服务是否已经在服务管理器中存在了
对于这个步骤,我们需要使用到OpenService来打开一个和我们想要创建的服务名称相同的服务,并通过其返回的服务句柄是否为空来判断是否已经存在了与我们想创建的服务同名称的服务。
SC_HANDLE OpenService(
[in] SC_HANDLE hSCManager, //服务管理器的句柄
[in] LPCSTR lpServiceName, //要打开的服务的名称
[in] DWORD dwDesiredAccess //服务的访问权限
);
hSCManager:从哪个服务管理器的句柄中来打开服务
lpServiceName:要打开的服务的名称是什么
dwDesiredAccess:对服务的存取权限,一般设置为SERVICE_ALL_ACCESS,但还可以有以下取值:
SERVICE_ALL_ACCESS 对打开的服务具有所有的权限
SERVICE_CHANGE_CONFIG 可以配置服务(比如可以改变服务的启动状态这些的,涉及到调用ChangeServiceConfig时,这个权限就是必须的)
SERVICE_ENUMERATE_DEPENDENTS 可以获取服务的依赖项(服务也可能调用一些自定义的dll,也可能依赖其他的服务,着往往是调用EnumDependentServices时必要的权限标识)
SERVICE_INTERROGATE 若需要调用ControlService函数来获取服务状态时,那么这个权限标识就是必要的
SERVICE_PAUSE_CONTINUE 需要调用ControlService来继续执行服务时,这个标识就是必要的
SERVICE_QUERY_CONFIG 若需要读取服务的配置信息,那么这个权限就是必要的
SERVICE_QUERY_STATUS 若需要获取服务的状态信息,这个标识位就是必要的
SERVICE_START 调用ControlService来启动服务,那么此标识位就是必要的
SERVICE_STOP 调用ControlService来停止服务,那么此标识位就是必要的
SERVICE_USER_DEFINED_CONTROL 若ControlService定义了用户自定义的对服务的控制行为,那么这个标识就是必要的

 1 //创建服务
 2 bool CreateServiceDemo()
 3 {
 4     //1.创建服务之前,我们需要打开Windows的服务管理器
 5     SC_HANDLE scHandle = OpenSCManager(NULL, SERVICES_ACTIVE_DATABASE, SC_MANAGER_ALL_ACCESS);
 6     //1.1 判断是否打开了服务管理器
 7     if (scHandle)
 8     {
 9         //2.然后我们需要先打开与我们想创建的服务名称相同的服务,我们需要知道当前的服务管理器里面是否已经有了相同名称的服务,如果有的话,那会导致创建同名的服务失败
10         SC_HANDLE hService = OpenService(scHandle, SERVICE_NAME, SERVICE_ALL_ACCESS);
11         //2.1 若返回的服务的句柄不为空,那说明服务管理器里面已经有了同名的服务,那么我们创建与其同名的服务就会失败
12         if (hService)
13         {
14             CloseServiceHandle(hService);
15             CloseServiceHandle(scHandle);
16             std::cerr << "Create Service " << SERVICE_NAME << " Faild! Because It's Exists In SCManager!" << std::endl;
17             return false;
18         }
19         else
20         {
21             //2.2 说明服务管理器中没有这个服务,那么我们就可以创建它
22             /// 由于我们创建服务的时候,那个服务啊,需要知道这个服务自己的可执行文件的绝对路径才能创建,所以我们安装服务的时候就需要知道这个路径在哪
23             TCHAR serviceFileName[128] = { 0 };
24             DWORD serviceFileNameSize = sizeof(serviceFileName);
25             GetModuleFileName(NULL, serviceFileName, serviceFileNameSize);
26             hService = CreateService(scHandle, SERVICE_NAME, SERVICE_NAME, SERVICE_START | SERVICE_STOP | SERVICE_PAUSE_CONTINUE, SERVICE_WIN32_OWN_PROCESS, SERVICE_AUTO_START, SERVICE_ERROR_IGNORE, serviceFileName, NULL, NULL, NULL, NULL, NULL);
27             if (hService)
28             {
29                 CloseServiceHandle(hService);
30                 CloseServiceHandle(scHandle);
31                 std::cout << "Create Service " << SERVICE_NAME << " Successful!" << std::endl;
32                 return true;
33             }
34             else
35             {
36                 std::cout << "Create Service " << SERVICE_NAME << " Faild!" << std::endl;
37                 return false;
38             }
39         }
40     }
41     else
42     {
43         std::cerr << "Create Service " << SERVICE_NAME << " Faild! Open SCManager Faild!" << std::endl;
44         return false;
45     }
46 }
View Code

若函数执行成功(即返回的服务句柄有效),就说明打开服务成功,在这个步骤的逻辑里面就说明已经存在了相同名称的服务
5.1.3 若不存在服务则创建服务
对于这个步骤我们需要调用的核心API就是CreateService,具体的参数很多,以下就是介绍:
SC_HANDLE CreateServiceA(
[in] SC_HANDLE hSCManager, //服务管理器的句柄
[in] LPCSTR lpServiceName, //服务的名称(这个就是之前打开的服务的名称,服务管理器就是通过这个名称来做区分的)
[in, optional] LPCSTR lpDisplayName, //服务的显示名称(这个就是在任务管理器中看到的服务的显示名称,服务管理器并不会以这个名称作为服务名称的区分)
[in] DWORD dwDesiredAccess, //服务的可控制的权限(用户或者是调用ControlService时,这个权限必须要在这里面有,否则用户在服务管理器中或者是我们调用ContrlService就无法进行这里没有定义的操作)
[in] DWORD dwServiceType, //服务的类型(这个一般是SERVICE_WIN32_OWN_PROCESS,如果是驱动层的服务,那就是其他的标识位)
[in] DWORD dwStartType, //服务的默认启动类型(一般都是SERVICE_AUTO_START,即操作系统启动时,这个服务就已经挂在后台运行了)
[in] DWORD dwErrorControl, //服务出错了怎么控制(一般填SERVICE_ERROR_IGNORE,出错时,继续执行,当然还有其他参数,后面有介绍)
[in, optional] LPCSTR lpBinaryPathName, //服务的二进制文件的绝对路径(如果实际开发过程中,这个路径是随着软件安装了的,那就需要固定为软件的安装路径,否则启动时就无法启动)
[in, optional] LPCSTR lpLoadOrderGroup, //服务属于哪一个用户群组,如果是System的,那么此处就写NULL(而且实际开发时,也一般填NULL)
[out, optional] LPDWORD lpdwTagId, //服务的标记(如果前面的用户群组未指定,那么这里也还是NULL)
[in, optional] LPCSTR lpDependencies, //服务的依赖项目(一般是NULL)
[in, optional] LPCSTR lpServiceStartName,//服务是使用哪个用户名启动的(一般为NULL)
[in, optional] LPCSTR lpPassword //用户名的密码(一般为NULL)
);
hSCManager:往哪个服务管理器对应的句柄里面创建服务
lpServiceName:服务的名称
lpDisplayName:服务的显示名称
dwDesiredAccess:服务的控制权限,我们在服务管理器里面看到,服务可以被启动,可以被停止,可以被暂停,可以被挂起,当然若是通过代码的方式(也就是通过ControlService来启动服务,那么也是一样的概念),有以下控制权限:
SERVICE_ALL_ACCESS 对打开的服务具有所有的权限
SERVICE_CHANGE_CONFIG 可以配置服务(比如可以改变服务的启动状态这些的,涉及到调用ChangeServiceConfig时,这个权限就是必须的)
SERVICE_ENUMERATE_DEPENDENTS 可以获取服务的依赖项(服务也可能调用一些自定义的dll,也可能依赖其他的服务,着往往是调用EnumDependentServices时必要的权限标识)
SERVICE_INTERROGATE 若需要调用ControlService函数来获取服务状态时,那么这个权限标识就是必要的
SERVICE_PAUSE_CONTINUE 需要调用ControlService来继续执行服务时,这个标识就是必要的
SERVICE_QUERY_CONFIG 若需要读取服务的配置信息,那么这个权限就是必要的
SERVICE_QUERY_STATUS 若需要获取服务的状态信息,这个标识位就是必要的
SERVICE_START 调用ControlService来启动服务,那么此标识位就是必要的
SERVICE_STOP 调用ControlService来停止服务,那么此标识位就是必要的
SERVICE_USER_DEFINED_CONTROL 若ControlService定义了用户自定义的对服务的控制行为,那么这个标识就是必要的
dwServiceType:服务的类型,我们一般在用户层开发的服务一般是SERVICE_WIN32_OWN_PROCESS,如果是其他的(例如驱动层的服务),那还有一些其他值:
SERVICE_ADAPTER 保留的服务
SERVICE_FILE_SYSTEM_DRIVER 文件系统驱动层的服务
SERVICE_KERNEL_DRIVER 内核驱动层的服务
SERVICE_RECOGNIZER_DRIVER 保留的服务
SERVICE_WIN32_OWN_PROCESS WIN32自己的进程内的服务(也就是一般的服务),这也是常用的
SERVICE_WIN32_SHARE_PROCESS 与其他程序共用的服务(这个服务可以与其他多个程序共享里面的内容(当然这可能是共享的内存,共享的其他资源)
dwStartType:服务的启动类型(对于大多数的开机即自启的服务,这个标识位一般都是一般都是SERVICE_AUTO_START),当然还可以是下面的其他标识位:
SERVICE_AUTO_START 当服务管理器启动时,会一并让这个服务自动启动
SERVICE_BOOT_START 当操作系统内核加载时,会启动这个服务(这只对驱动类型的服务有效)
SERVICE_DEMAND_START 如果使用了StartService(即通过其他代码来间接启动此服务,那么这个启动类型时必要的)
SERVICE_DISABLED 服务被禁用,不可被启动
SERVICE_SYSTEM_START 由IoInitSystem函数启动的驱动服务,这个只适用于驱动程序的服务
dwErrorControl:服务出错了怎么控制(对于用户层所看到的服务管理器的服务启动失败后的操作(可以是无操作,可以是重新启动,也可以是其他的操作)
SERVICE_ERROR_CRITICAL 若可能,在事件记录器中记录错误,然后以一个较好的状态重新启动,否则就直接以上一次出错之后的最好的状态再重新启动
SERVICE_ERROR_IGNORE 忽略错误继续启动(常用)
SERVICE_ERROR_NORMAL 在事件记录器中记录错误,然后重新启动
SERVICE_ERROR_SEVERE 在事件记录器中记录错误,然后以一个较好的状态重新启动,否则就直接以上一次出错之后的最好的状态再重新启动
lpBinaryPathName:服务的二进制文件路径(这个二进制文件的路径一定要考虑,当我把服务以软件包的形式安装到用户的系统中时,这个路径应该如何获取[强烈建议使用GetModuleFileName获取])
lpLoadOrderGroup:服务属于哪一个用户群组,若不属于哪一个用户群组,那么此参数为NULL
lpdwTagId:服务的标记(如果前面的用户群组未指定,那么这里也还是NULL)
lpDependencies:服务的依赖项目(一般是NULL)
lpServiceStartName:服务是使用哪个用户名启动的(一般为NULL)
lpPassword:用户名的密码(一般为NULL)
5.1.4 卸载服务的逻辑(见代码中的DeleteServiceDemo函数)
若实际开发的软件中包含了服务这些模块,那么卸载软件时,服务也需要被一并卸载,这个卸载肯定不能简单粗暴的直接将软件安装路径中对应的服务直接删除,这是不行的。在执行删除之前,需要先从服务管理器中删除这个服务,然后再将服务从软件中删除。
删除服务中的逻辑实际上就是调用DeleteService,但是在这之前还需要打开服务管理器,打开对应名称的服务,判断服务是不是存在,然后再删除服务。
BOOL DeleteService(
[in] SC_HANDLE hService //参数很简单,就是要删除的服务的句柄
);
5.1.5 服务的主逻辑
在介绍完前面的创建服务和卸载服务的流程之后,便需要知道服务的主逻辑的(实际上就是ServiceMain的编写),我们知道,我们一般的main函数一般都是一个函数,然后我们只需要编写对应的业务函数再在main中调用就行了,但是对于服务则不一样。服务的主逻辑大致需要经过下面的步骤。(这也是所有的Win32服务程序的通用编程框架)
5.1.5.1 创建ServiceMain函数
ServiceMain是服务程序都必须有的,这个和我们的平时日常的main函数有些许的不同,对于服务程序,它里面也有main,但是这个main是我们直接执行的时候,才会进入这个main函数中,那如果我们编写的是服务类程序,那我们实际上启动服务是通过服务管理器来进行启动的,那么服务管理器启动的时候,会通过回调函数机制注册ServiceMain和服务控制的回调函数(也就是后面的RegisterServiceCtrlHandler中的回调函数),我们知道,main函数是有命令行参数的,那么对于服务的ServiceMain函数,也同样有命令行参数,只不过微软把main的int argc和char*argv[]换为了ServiceMain的DWORD dwNumServicesArgs和LPSTR* lpServiceArgVectors而已。实际上这也是ServiceMain函数的格式,如下:
void ServiceMain(
DWORD dwNumServicesArgs,//服务的命令行参数的个数
LPSTR* lpServiceArgVectors//服务的命令行参数字符串数组
);
那么我们就需要在ServiceMain这个函数里面编写我们的服务的主逻辑(也就是你想让这个服务做什么的相关业务代码),但是,我们知道,服务和普通的可执行文件有一些不同的是,服务是可以被停止,可以被挂起,可以被启动的(我们在服务管理器里面随便找一个服务,你看看,是不是服务下面就对应了启动,停止,挂起,继续执行的几个按钮),那么我们编写的时候,就务必要考虑这些事情,也就是我们需要通过必要的代码来知道用户那边执行了什么样的操作(用户点击了启动,或者是点击了停止,那我们代码就需要知道用户点击的是什么)(这也就是5.1.5.2的作用)
5.1.5.2 创建ServiceControlHandler函数
当我们编写完成ServiceMain函数之后,我们需要在该函数的第一行就编写注册服务控制句柄的函数,我们需要用到:
SERVICE_STATUS_HANDLE RegisterServiceCtrlHandlerA(
[in] LPCSTR lpServiceName,//服务的名称
[in] LPHANDLER_FUNCTION lpHandlerProc//服务的控制块回调函数
);
关于lpHandlerProc函数,这个函数的格式如下:
void ServiceControlHandler(DWORD dwControl);
我们先前已经介绍过,用户可以直接通过点击服务管理器里面的一个服务的启动,停止,挂起,继续执行的任意一个按钮,那么实际上呢,当用户点击之后,我们这个ControlHandler就是能接受到这个dwControl的控制码(也就是说,只要用户点击了,那么这个控制服务的回调函数就会被调用,然后这个函数的形参dwControl就能反映用户点击的是哪一个按钮或者是请求了什么样的操作)。那么对于dwControl有以下的取值:
我们有一个快速记忆的方法,既然是Control,那么对应的操作的宏就一定有Control的描述,实际上这一系列控制代码的宏都是SERVICE_CONTROL_XXXXXXX,具体如下:
SERVICE_CONTROL_CONTINUE:通知暂停的服务应恢复。(也就是我们挂起的服务,我们希望再次让他继续执行时) hService 句柄必须具有SERVICE_PAUSE_CONTINUE访问权限。
SERVICE_CONTROL_INTERROGATE:通知服务它应将其当前状态信息报告给服务控制管理器。 hService 句柄必须具有SERVICE_INTERROGATE访问权限。(注,此控件通常并不有用,因为 SCM 知道服务的当前状态。)
SERVICE_CONTROL_NETBINDADD:通知网络服务有用于绑定的新组件。 hService 句柄必须具有SERVICE_PAUSE_CONTINUE访问权限。 但是,此控制代码已弃用;请改用即插即用功能。
SERVICE_CONTROL_NETBINDDISABLE:通知网络服务其其中一个绑定已被禁用。 hService 句柄必须具有SERVICE_PAUSE_CONTINUE访问权限。 但是,此控制代码已弃用;请改用即插即用功能。
SERVICE_CONTROL_NETBINDENABLE:通知网络服务已启用禁用的绑定。 hService 句柄必须具有SERVICE_PAUSE_CONTINUE访问权限。 但是,此控制代码已弃用;请改用即插即用功能。
SERVICE_CONTROL_NETBINDREMOVE:通知网络服务已删除用于绑定的组件。 hService 句柄必须具有SERVICE_PAUSE_CONTINUE访问权限。 但是,此控制代码已弃用;请改用即插即用功能。
SERVICE_CONTROL_PARAMCHANGE:通知服务其启动参数已更改。 hService 句柄必须具有SERVICE_PAUSE_CONTINUE访问权限。
SERVICE_CONTROL_PAUSE:通知服务它应暂停。 hService 句柄必须具有SERVICE_PAUSE_CONTINUE访问权限。
SERVICE_CONTROL_STOP:通知服务它应停止。 hService 句柄必须具有SERVICE_STOP访问权限。将停止请求发送到服务后,不应向服务发送其他控件。
当我们在我们的服务控制的那个回调函数里面,写上对上述相关的语句之后(例如在我们的Code里面的ServiceControlHandler里面编写的针对用户或是系统可能的一切操作的case语句)
5.1.5.2 创建SERVICE_TABLE_ENTRY结构体变量
那么对于这个结构体,实际上就描述的就是服务的名称,服务的入口函数,具体如下:
typedef struct _SERVICE_TABLE_ENTRYA {
LPSTR lpServiceName;//服务的名称
LPSERVICE_MAIN_FUNCTIONA lpServiceProc;//服务的主逻辑入口函数(实际上就是我们前面的ServiceMain)
}
SERVICE_TABLE_ENTRY entry[] = { {(LPSTR)SERVICE_NAME,ServiceMain},{NULL,NULL}};
5.1.5.3 在main中调用StartServiceCtrlDispatcher函数
实际上这个函数的含义就是,将执行的主函数的逻辑,切换到服务的ServiceMain函数中进行执行。我们可以这样子理解,平时我们的exe程序只是在main里面执行,但是如果是服务程序,那么由于服务的这种特殊机制(可被控制,可以和服务管理器交互这些特性),那么就需要将执行者从main(也就是主线程)中切换到由ServiceMain进行托管的线程中进行处理。
这个函数传入的是SERVICE_TABLE_ENTRY的结构体数组变量,
BOOL StartServiceCtrlDispatcherA(
[in] const SERVICE_TABLE_ENTRYA *lpServiceStartTable//服务表格入口点的数组,这个数组的最后一个元素必须是{NULL,NULL},否则进入此函数进行ServiceMain的切换时,会失败
);
运行效果:

Run

 本节代码:https://files.cnblogs.com/files/blogs/792763/ServiceDemo.zip?t=1763265852&download=true

posted @ 2025-11-16 12:05  蜡笔小新Pointer  阅读(7)  评论(0)    收藏  举报